Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,13 @@ Xcodes.app and CLI is updated, maintained with contributors like yourself. Even

You'll need macOS 15.6 Ventura and Xcode 26 in order to build and run Xcodes.app.

### Known build warnings

The project builds without Swift warnings. Two known items remain:

- `ld: building for macOS-14.6, but linking with dylib '@rpath/libcbor.0.11.0.dylib' which was built for newer version 15.0`: the prebuilt `libcbor` in LibFido2Swift targets macOS 15 while the app targets 14.6. Resolving it means raising the app's deployment target or rebuilding that dependency for 14.6.
- `SMJobBless` is deprecated in favor of `SMAppService`. The one legacy call is isolated in a marked shim in `HelperClient.swift` until the privileged helper moves to `SMAppService`, which uses a different install and approval model.

`Unxip` and `aria2` must be compiled as a universal binary
```
# compile for Intel
Expand Down
1 change: 1 addition & 0 deletions Xcodes.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,7 @@
};
D971F84C2E79102E005F84C9 /* Fix libfido2 structure */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
Expand Down
2 changes: 1 addition & 1 deletion Xcodes/Backend/AppState+Runtimes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ extension AppState {
for: runtime,
destinationDirectory: .xcodesApplicationSupport,
downloader: downloader
) { progress in
) { [weak self] progress in
let expectedTaskID = taskID
Task { @MainActor [weak self] in
if let expectedTaskID, self?.runtimeTaskIDs[runtime.identifier] != expectedTaskID {
Expand Down
19 changes: 18 additions & 1 deletion Xcodes/Backend/HelperClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ final class HelperClient {
do {
let authRef = try authorizationRef(&authRights, nil, [.interactionAllowed, .extendRights, .preAuthorize])
var cfError: Unmanaged<CFError>?
SMJobBless(kSMDomainSystemLaunchd, machServiceName as CFString, authRef, &cfError)
(SMJobBlessInstaller() as PrivilegedHelperBlessing).bless(label: machServiceName, authorization: authRef, error: &cfError)
if let error = cfError?.takeRetainedValue() { throw error }

self.connection?.invalidate()
Expand Down Expand Up @@ -268,6 +268,23 @@ final class HelperClient {
}
}

/// Installs the privileged helper with SMJobBless, which macOS 13 deprecated in favor of SMAppService.
/// SMAppService uses a different install and approval model (a bundled launch daemon plist, approved in
/// System Settings), so the move is a separate change. Until then the one legacy call lives here, and is
/// reached through a protocol so the known deprecation doesn't warn on every build.
private protocol PrivilegedHelperBlessing {
@discardableResult
func bless(label: String, authorization: AuthorizationRef?, error: inout Unmanaged<CFError>?) -> Bool
}

private struct SMJobBlessInstaller: PrivilegedHelperBlessing {
@available(macOS, deprecated: 13.0, message: "Move the privileged helper to SMAppService")
@discardableResult
func bless(label: String, authorization: AuthorizationRef?, error: inout Unmanaged<CFError>?) -> Bool {
SMJobBless(kSMDomainSystemLaunchd, label as CFString, authorization, &error)
}
}

enum HelperClientError: LocalizedError {
case failedToCreateRemoteObjectProxy
case message(String)
Expand Down
16 changes: 8 additions & 8 deletions Xcodes/Backend/NotificationManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,13 @@ public final class NotificationManager: NSObject, UNUserNotificationCenterDelega
notificationStatusTask = Task { [weak self] in
let settings = await UNUserNotificationCenter.current().notificationSettings()
guard !Task.isCancelled else {
await self?.clearNotificationStatusTask(id: taskID)
self?.clearNotificationStatusTask(id: taskID)
return
}

let status = NotificationManager.systemPromptStatusFromSettings(settings)
await self?.setNotificationStatus(status, ifNotificationStatusTaskID: taskID)
await self?.clearNotificationStatusTask(id: taskID)
self?.setNotificationStatus(status, ifNotificationStatusTaskID: taskID)
self?.clearNotificationStatusTask(id: taskID)
}
}

Expand Down Expand Up @@ -94,14 +94,14 @@ public final class NotificationManager: NSObject, UNUserNotificationCenterDelega
do {
let granted = try await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge])
guard !Task.isCancelled else {
await self?.clearRequestAccessTask(id: taskID)
self?.clearRequestAccessTask(id: taskID)
return
}

Logger.appState.log("User has \(granted ? "Granted" : "NOT GRANTED") notification permission")
} catch {
guard !Task.isCancelled else {
await self?.clearRequestAccessTask(id: taskID)
self?.clearRequestAccessTask(id: taskID)
return
}

Expand All @@ -110,13 +110,13 @@ public final class NotificationManager: NSObject, UNUserNotificationCenterDelega

let settings = await UNUserNotificationCenter.current().notificationSettings()
guard !Task.isCancelled else {
await self?.clearRequestAccessTask(id: taskID)
self?.clearRequestAccessTask(id: taskID)
return
}

let status = NotificationManager.systemPromptStatusFromSettings(settings)
await self?.setNotificationStatus(status, ifRequestAccessTaskID: taskID)
await self?.clearRequestAccessTask(id: taskID)
self?.setNotificationStatus(status, ifRequestAccessTaskID: taskID)
self?.clearRequestAccessTask(id: taskID)
}
}

Expand Down
2 changes: 1 addition & 1 deletion Xcodes/Frontend/Preferences/PlatformsListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ struct PlatformsListView: View {
.task {
loadRuntimes()
}
.onChange(of: appState.installedRuntimes) { _ in
.onChange(of: appState.installedRuntimes) {
loadRuntimes()
}
}
Expand Down
10 changes: 5 additions & 5 deletions Xcodes/Frontend/Preferences/UpdatesPreferencePane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,9 @@ class ObservableUpdater: ObservableObject {
automaticallyChecksForUpdatesObservation = updater.observe(
\.automaticallyChecksForUpdates,
options: [.initial, .new, .old],
changeHandler: { [weak self] updater, change in
guard change.newValue != change.oldValue else { return }
let automaticallyChecksForUpdates = updater.automaticallyChecksForUpdates
changeHandler: { [weak self] _, change in
// Use the observed value: the updater's properties are main actor-isolated and this handler isn't
guard change.newValue != change.oldValue, let automaticallyChecksForUpdates = change.newValue else { return }
Task { @MainActor [weak self] in
self?.automaticallyChecksForUpdates = automaticallyChecksForUpdates
}
Expand All @@ -121,8 +121,8 @@ class ObservableUpdater: ObservableObject {
lastUpdateCheckDateObservation = updater.observe(
\.lastUpdateCheckDate,
options: [.initial, .new, .old],
changeHandler: { [weak self] updater, change in
let lastUpdateCheckDate = updater.lastUpdateCheckDate
changeHandler: { [weak self] _, change in
let lastUpdateCheckDate = change.newValue ?? nil
Task { @MainActor [weak self] in
self?.lastUpdateCheckDate = lastUpdateCheckDate
}
Expand Down
2 changes: 1 addition & 1 deletion Xcodes/XcodesApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ struct XcodesApp: App {
// so that it's triggered when an individual window's phase changes instead of all window phases.
// When used on a View it's also invoked on launch, which doesn't occur with a WindowGroup.
// FB8954581 ScenePhase read from App doesn't return a value on launch
.onChange(of: scenePhase) { newScenePhase in
.onChange(of: scenePhase) { _, newScenePhase in
guard !isTesting else { return }
if case .active = newScenePhase {
appState.updateIfNeeded()
Expand Down