Skip to content

Commit f4567bd

Browse files
committed
Merge branch 'main' into security-key-auth
# Conflicts: # Xcodes.xcodeproj/project.pbxproj
2 parents 27bbf6f + c1670b2 commit f4567bd

15 files changed

Lines changed: 359 additions & 39 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ on:
88

99
jobs:
1010
test:
11-
runs-on: macos-13
11+
runs-on: macos-14
1212
steps:
1313
- uses: actions/checkout@v4
1414
- name: Run tests
1515
env:
16-
DEVELOPER_DIR: /Applications/Xcode_15.0.1.app
16+
DEVELOPER_DIR: /Applications/Xcode_16.app
1717
run: xcodebuild test -scheme Xcodes

Xcodes.xcodeproj/project.pbxproj

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -117,14 +117,14 @@
117117
E689540325BE8C64000EBCEA /* DockProgress in Frameworks */ = {isa = PBXBuildFile; productRef = E689540225BE8C64000EBCEA /* DockProgress */; };
118118
E81D7EA02805250100A205FC /* Collection+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E81D7E9F2805250100A205FC /* Collection+.swift */; };
119119
E832EAF82B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E832EAF72B0FBCF4001B570D /* RuntimeInstallationStepDetailView.swift */; };
120+
E83FDC442CBB649100679C6B /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = E83FDC432CBB649100679C6B /* Sparkle */; };
120121
E84B7D0D2B296A8900DBDA2B /* NavigationSplitViewWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84B7D0C2B296A8900DBDA2B /* NavigationSplitViewWrapper.swift */; };
121122
E84E4F522B323A5F003F3959 /* CornerRadiusModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84E4F512B323A5F003F3959 /* CornerRadiusModifier.swift */; };
122123
E84E4F542B333864003F3959 /* PlatformsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E84E4F532B333864003F3959 /* PlatformsListView.swift */; };
123124
E84E4F572B335094003F3959 /* OrderedCollections in Frameworks */ = {isa = PBXBuildFile; productRef = E84E4F562B335094003F3959 /* OrderedCollections */; };
124125
E86671272B309D2F0048559A /* PlatformsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E86671262B309D2F0048559A /* PlatformsView.swift */; };
125126
E87AB3C52939B65E00D72F43 /* Hardware.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87AB3C42939B65E00D72F43 /* Hardware.swift */; };
126127
E87DD6EB25D053FA00D86808 /* Progress+.swift in Sources */ = {isa = PBXBuildFile; fileRef = E87DD6EA25D053FA00D86808 /* Progress+.swift */; };
127-
E891A1C42B43ACF900A1B9D1 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = E891A1C32B43ACF900A1B9D1 /* Sparkle */; };
128128
E89342FA25EDCC17007CF557 /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E89342F925EDCC17007CF557 /* NotificationManager.swift */; };
129129
E8977EA325C11E1500835F80 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8977EA225C11E1500835F80 /* PreferencesView.swift */; };
130130
E8B20CBF2A2EDEC20057D816 /* SDKs+Xcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8B20CBE2A2EDEC20057D816 /* SDKs+Xcode.swift */; };
@@ -357,7 +357,7 @@
357357
E689540325BE8C64000EBCEA /* DockProgress in Frameworks */,
358358
CA9FF86D25951C6E00E47BAF /* XCModel in Frameworks */,
359359
CABFA9F82592F0F900380FEE /* KeychainAccess in Frameworks */,
360-
E891A1C42B43ACF900A1B9D1 /* Sparkle in Frameworks */,
360+
E83FDC442CBB649100679C6B /* Sparkle in Frameworks */,
361361
CAA858CD25A3D8BC00ACF8C0 /* ErrorHandling in Frameworks */,
362362
E8C0EB1A291EF43E0081528A /* XcodesKit in Frameworks */,
363363
E8FD5727291EE4AC001E004C /* AsyncNetworkService in Frameworks */,
@@ -721,7 +721,7 @@
721721
E8C0EB19291EF43E0081528A /* XcodesKit */,
722722
E8F44A1D296B4CD7002D6592 /* Path */,
723723
E84E4F562B335094003F3959 /* OrderedCollections */,
724-
E891A1C32B43ACF900A1B9D1 /* Sparkle */,
724+
E83FDC432CBB649100679C6B /* Sparkle */,
725725
334A932B2CA885A400A5E079 /* LibFido2Swift */,
726726
);
727727
productName = XcodesMac;
@@ -810,7 +810,7 @@
810810
E8FD5725291EE4AC001E004C /* XCRemoteSwiftPackageReference "AsyncHTTPNetworkService" */,
811811
E8F44A1C296B4CD7002D6592 /* XCRemoteSwiftPackageReference "Path" */,
812812
E84E4F552B335094003F3959 /* XCRemoteSwiftPackageReference "swift-collections" */,
813-
E891A1C22B43ACA400A1B9D1 /* XCRemoteSwiftPackageReference "Sparkle" */,
813+
E83FDC422CBB649100679C6B /* XCRemoteSwiftPackageReference "Sparkle" */,
814814
33027E282CA8BB5800CB387C /* XCRemoteSwiftPackageReference "LibFido2Swift" */,
815815
);
816816
productRefGroup = CAD2E79F2449574E00113D76 /* Products */;
@@ -1091,7 +1091,7 @@
10911091
CODE_SIGN_IDENTITY = "-";
10921092
CODE_SIGN_STYLE = Manual;
10931093
COMBINE_HIDPI_IMAGES = YES;
1094-
CURRENT_PROJECT_VERSION = 26;
1094+
CURRENT_PROJECT_VERSION = 27;
10951095
DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\"";
10961096
DEVELOPMENT_TEAM = "";
10971097
ENABLE_HARDENED_RUNTIME = NO;
@@ -1103,7 +1103,7 @@
11031103
"@executable_path/../Frameworks",
11041104
);
11051105
MACOSX_DEPLOYMENT_TARGET = 13.0;
1106-
MARKETING_VERSION = 2.1.2;
1106+
MARKETING_VERSION = 2.2.0;
11071107
PRODUCT_BUNDLE_IDENTIFIER = com.xcodesorg.xcodesapp;
11081108
PRODUCT_NAME = Xcodes;
11091109
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -1343,7 +1343,7 @@
13431343
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
13441344
CODE_SIGN_STYLE = Automatic;
13451345
COMBINE_HIDPI_IMAGES = YES;
1346-
CURRENT_PROJECT_VERSION = 26;
1346+
CURRENT_PROJECT_VERSION = 27;
13471347
DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\"";
13481348
DEVELOPMENT_TEAM = ZU6GR6B2FY;
13491349
ENABLE_HARDENED_RUNTIME = YES;
@@ -1355,7 +1355,7 @@
13551355
"@executable_path/../Frameworks",
13561356
);
13571357
MACOSX_DEPLOYMENT_TARGET = 13.0;
1358-
MARKETING_VERSION = 2.1.2;
1358+
MARKETING_VERSION = 2.2.0;
13591359
PRODUCT_BUNDLE_IDENTIFIER = com.xcodesorg.xcodesapp;
13601360
PRODUCT_NAME = Xcodes;
13611361
SWIFT_VERSION = 5.0;
@@ -1371,7 +1371,7 @@
13711371
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
13721372
CODE_SIGN_STYLE = Automatic;
13731373
COMBINE_HIDPI_IMAGES = YES;
1374-
CURRENT_PROJECT_VERSION = 26;
1374+
CURRENT_PROJECT_VERSION = 27;
13751375
DEVELOPMENT_ASSET_PATHS = "\"Xcodes/Preview Content\"";
13761376
DEVELOPMENT_TEAM = ZU6GR6B2FY;
13771377
ENABLE_HARDENED_RUNTIME = YES;
@@ -1383,7 +1383,7 @@
13831383
"@executable_path/../Frameworks",
13841384
);
13851385
MACOSX_DEPLOYMENT_TARGET = 13.0;
1386-
MARKETING_VERSION = 2.1.2;
1386+
MARKETING_VERSION = 2.2.0;
13871387
PRODUCT_BUNDLE_IDENTIFIER = com.xcodesorg.xcodesapp;
13881388
PRODUCT_NAME = Xcodes;
13891389
SWIFT_VERSION = 5.0;
@@ -1553,20 +1553,20 @@
15531553
minimumVersion = 4.3.1;
15541554
};
15551555
};
1556-
E84E4F552B335094003F3959 /* XCRemoteSwiftPackageReference "swift-collections" */ = {
1556+
E83FDC422CBB649100679C6B /* XCRemoteSwiftPackageReference "Sparkle" */ = {
15571557
isa = XCRemoteSwiftPackageReference;
1558-
repositoryURL = "https://github.com/apple/swift-collections.git";
1558+
repositoryURL = "https://github.com/sparkle-project/Sparkle";
15591559
requirement = {
15601560
kind = upToNextMajorVersion;
1561-
minimumVersion = 1.0.5;
1561+
minimumVersion = 2.6.4;
15621562
};
15631563
};
1564-
E891A1C22B43ACA400A1B9D1 /* XCRemoteSwiftPackageReference "Sparkle" */ = {
1564+
E84E4F552B335094003F3959 /* XCRemoteSwiftPackageReference "swift-collections" */ = {
15651565
isa = XCRemoteSwiftPackageReference;
1566-
repositoryURL = "https://github.com/sparkle-project/Sparkle";
1566+
repositoryURL = "https://github.com/apple/swift-collections.git";
15671567
requirement = {
15681568
kind = upToNextMajorVersion;
1569-
minimumVersion = 2.5.2;
1569+
minimumVersion = 1.0.5;
15701570
};
15711571
};
15721572
E8F44A1C296B4CD7002D6592 /* XCRemoteSwiftPackageReference "Path" */ = {
@@ -1636,16 +1636,16 @@
16361636
package = E689540125BE8C64000EBCEA /* XCRemoteSwiftPackageReference "DockProgress" */;
16371637
productName = DockProgress;
16381638
};
1639+
E83FDC432CBB649100679C6B /* Sparkle */ = {
1640+
isa = XCSwiftPackageProductDependency;
1641+
package = E83FDC422CBB649100679C6B /* XCRemoteSwiftPackageReference "Sparkle" */;
1642+
productName = Sparkle;
1643+
};
16391644
E84E4F562B335094003F3959 /* OrderedCollections */ = {
16401645
isa = XCSwiftPackageProductDependency;
16411646
package = E84E4F552B335094003F3959 /* XCRemoteSwiftPackageReference "swift-collections" */;
16421647
productName = OrderedCollections;
16431648
};
1644-
E891A1C32B43ACF900A1B9D1 /* Sparkle */ = {
1645-
isa = XCSwiftPackageProductDependency;
1646-
package = E891A1C22B43ACA400A1B9D1 /* XCRemoteSwiftPackageReference "Sparkle" */;
1647-
productName = Sparkle;
1648-
};
16491649
E8C0EB19291EF43E0081528A /* XcodesKit */ = {
16501650
isa = XCSwiftPackageProductDependency;
16511651
productName = XcodesKit;

Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Xcodes/Backend/AppState+Runtimes.swift

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import OSLog
44
import Combine
55
import Path
66
import AppleAPI
7+
import Version
78

89
extension AppState {
910
func updateDownloadableRuntimes() {
@@ -48,6 +49,69 @@ extension AppState {
4849
}
4950

5051
func downloadRuntime(runtime: DownloadableRuntime) {
52+
guard let selectedXcode = self.allXcodes.first(where: { $0.selected }) else {
53+
Logger.appState.error("No selected Xcode")
54+
DispatchQueue.main.async {
55+
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: "No selected Xcode. Please make an Xcode active")
56+
}
57+
return
58+
}
59+
// new runtimes
60+
if runtime.contentType == .cryptexDiskImage {
61+
// only selected xcodes > 16.1 beta 3 can download runtimes via a xcodebuild -downloadPlatform version
62+
// only Runtimes coming from cryptexDiskImage can be downloaded via xcodebuild
63+
if selectedXcode.version > Version(major: 16, minor: 0, patch: 0) {
64+
downloadRuntimeViaXcodeBuild(runtime: runtime)
65+
} else {
66+
// not supported
67+
Logger.appState.error("Trying to download a runtime we can't download")
68+
DispatchQueue.main.async {
69+
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: "Sorry. Apple only supports downloading runtimes iOS 18+, tvOS 18+, watchOS 11+, visionOS 2+ with Xcode 16.1+. Please download and make active.")
70+
}
71+
return
72+
}
73+
} else {
74+
downloadRuntimeObseleteWay(runtime: runtime)
75+
}
76+
}
77+
78+
func downloadRuntimeViaXcodeBuild(runtime: DownloadableRuntime) {
79+
runtimePublishers[runtime.identifier] = Task {
80+
do {
81+
for try await progress in Current.shell.downloadRuntime(runtime.platform.shortName, runtime.simulatorVersion.buildUpdate) {
82+
if progress.isIndeterminate {
83+
DispatchQueue.main.async {
84+
self.setInstallationStep(of: runtime, to: .installing, postNotification: false)
85+
}
86+
} else {
87+
DispatchQueue.main.async {
88+
self.setInstallationStep(of: runtime, to: .downloading(progress: progress), postNotification: false)
89+
}
90+
}
91+
92+
}
93+
Logger.appState.debug("Done downloading runtime - \(runtime.name)")
94+
DispatchQueue.main.async {
95+
guard let index = self.downloadableRuntimes.firstIndex(where: { $0.identifier == runtime.identifier }) else { return }
96+
self.downloadableRuntimes[index].installState = .installed
97+
self.update()
98+
}
99+
100+
} catch {
101+
Logger.appState.error("Error downloading runtime: \(error.localizedDescription)")
102+
DispatchQueue.main.async {
103+
self.error = error
104+
if let error = error as? String {
105+
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error)
106+
} else {
107+
self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: error.legibleLocalizedDescription)
108+
}
109+
}
110+
}
111+
}
112+
}
113+
114+
func downloadRuntimeObseleteWay(runtime: DownloadableRuntime) {
51115
runtimePublishers[runtime.identifier] = Task {
52116
do {
53117
let downloadedURL = try await downloadRunTimeFull(runtime: runtime)

Xcodes/Backend/AppState.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,11 @@ class AppState: ObservableObject {
498498
guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return }
499499

500500
installationPublishers[id] = signInIfNeeded()
501+
.handleEvents(
502+
receiveSubscription: { [unowned self] _ in
503+
self.setInstallationStep(of: availableXcode.version, to: .authenticating)
504+
}
505+
)
501506
.flatMap { [unowned self] in
502507
// signInIfNeeded might finish before the user actually authenticates if UI is involved.
503508
// This publisher will wait for the @Published authentication state to change to authenticated or unauthenticated before finishing,

Xcodes/Backend/Environment.swift

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,77 @@ public struct Shell {
196196
return Process.run(unxipPath.url, workingDirectory: url.deletingLastPathComponent(), ["\(url.path)"])
197197
}
198198

199+
public var downloadRuntime: (String, String) -> AsyncThrowingStream<Progress, Error> = { platform, version in
200+
return AsyncThrowingStream<Progress, Error> { continuation in
201+
Task {
202+
// Assume progress will not have data races, so we manually opt-out isolation checks.
203+
nonisolated(unsafe) var progress = Progress()
204+
progress.kind = .file
205+
progress.fileOperationKind = .downloading
206+
207+
let process = Process()
208+
let xcodeBuildPath = Path.root.usr.bin.join("xcodebuild").url
209+
210+
process.executableURL = xcodeBuildPath
211+
process.arguments = [
212+
"-downloadPlatform",
213+
"\(platform)",
214+
"-buildVersion",
215+
"\(version)"
216+
]
217+
218+
let stdOutPipe = Pipe()
219+
process.standardOutput = stdOutPipe
220+
let stdErrPipe = Pipe()
221+
process.standardError = stdErrPipe
222+
223+
let observer = NotificationCenter.default.addObserver(
224+
forName: .NSFileHandleDataAvailable,
225+
object: nil,
226+
queue: OperationQueue.main
227+
) { note in
228+
guard
229+
// This should always be the case for Notification.Name.NSFileHandleDataAvailable
230+
let handle = note.object as? FileHandle,
231+
handle === stdOutPipe.fileHandleForReading || handle === stdErrPipe.fileHandleForReading
232+
else { return }
233+
234+
defer { handle.waitForDataInBackgroundAndNotify() }
235+
236+
let string = String(decoding: handle.availableData, as: UTF8.self)
237+
238+
// TODO: fix warning. ObservingProgressView is currently tied to an updating progress
239+
progress.updateFromXcodebuild(text: string)
240+
241+
continuation.yield(progress)
242+
}
243+
244+
stdOutPipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
245+
stdErrPipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
246+
247+
continuation.onTermination = { @Sendable _ in
248+
process.terminate()
249+
NotificationCenter.default.removeObserver(observer, name: .NSFileHandleDataAvailable, object: nil)
250+
}
251+
252+
do {
253+
try process.run()
254+
} catch {
255+
continuation.finish(throwing: error)
256+
}
257+
258+
process.waitUntilExit()
259+
260+
NotificationCenter.default.removeObserver(observer, name: .NSFileHandleDataAvailable, object: nil)
261+
262+
guard process.terminationReason == .exit, process.terminationStatus == 0 else {
263+
continuation.finish(throwing: ProcessExecutionError(process: process, standardOutput: "", standardError: ""))
264+
return
265+
}
266+
continuation.finish()
267+
}
268+
}
269+
}
199270
}
200271

201272
public struct Files {

Xcodes/Backend/Progress+.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,5 +70,38 @@ extension Progress {
7070
}
7171

7272
}
73+
74+
func updateFromXcodebuild(text: String) {
75+
self.totalUnitCount = 100
76+
self.completedUnitCount = 0
77+
self.localizedAdditionalDescription = "" // to not show the addtional
78+
79+
do {
80+
81+
let downloadPattern = #"(\d+\.\d+)% \(([\d.]+ (?:MB|GB)) of ([\d.]+ GB)\)"#
82+
let downloadRegex = try NSRegularExpression(pattern: downloadPattern)
83+
84+
// Search for matches in the text
85+
if let match = downloadRegex.firstMatch(in: text, range: NSRange(text.startIndex..., in: text)) {
86+
// Extract the percentage - simpler then trying to extract size MB/GB and convert to bytes.
87+
if let percentRange = Range(match.range(at: 1), in: text), let percentDouble = Double(text[percentRange]) {
88+
let percent = Int64(percentDouble.rounded())
89+
self.completedUnitCount = percent
90+
}
91+
}
92+
93+
// "Downloading tvOS 18.1 Simulator (22J5567a): Installing..." or
94+
// "Downloading tvOS 18.1 Simulator (22J5567a): Installing (registering download)..."
95+
if text.range(of: "Installing") != nil {
96+
// sets the progress to indeterminite to show animating progress
97+
self.totalUnitCount = 0
98+
self.completedUnitCount = 0
99+
}
100+
101+
} catch {
102+
Logger.appState.error("Invalid regular expression")
103+
}
104+
105+
}
73106
}
74107

Xcodes/Backend/XcodeCommands.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,11 @@ struct XcodeCommands: Commands {
3535

3636
struct InstallButton: View {
3737
@EnvironmentObject var appState: AppState
38-
@State private var isLoading = false
3938

4039
let xcode: Xcode?
4140

4241
var body: some View {
43-
ProgressButton(isInProgress: isLoading) {
42+
Button {
4443
install()
4544
} label: {
4645
Text("Install")
@@ -49,7 +48,6 @@ struct InstallButton: View {
4948
}
5049

5150
private func install() {
52-
isLoading = true
5351
guard let xcode = xcode else { return }
5452
appState.checkMinVersionAndInstall(id: xcode.id)
5553
}

0 commit comments

Comments
 (0)