support downloading individual xcode architecture versions

This commit is contained in:
Matt Kiazyk 2025-08-23 14:56:40 -06:00
parent 4b9d86b22e
commit debc41f688
8 changed files with 52 additions and 17 deletions

View file

@ -525,7 +525,7 @@ class AppState: ObservableObject {
// MARK: - Install
func checkMinVersionAndInstall(id: XcodeID) {
guard let availableXcode = availableXcodes.first(where: { $0.version == id.version }) else { return }
guard let availableXcode = availableXcodes.first(where: { $0.xcodeID == id }) else { return }
// Check to see if users macOS is supported
if let requiredMacOSVersion = availableXcode.requiredMacOSVersion {
@ -552,7 +552,7 @@ class AppState: ObservableObject {
}
func install(id: XcodeID) {
guard let availableXcode = availableXcodes.first(where: { $0.version == id.version }) else { return }
guard let availableXcode = availableXcodes.first(where: { $0.xcodeID == id }) else { return }
installationPublishers[id] = signInIfNeeded()
.handleEvents(
@ -627,7 +627,7 @@ class AppState: ObservableObject {
/// Skips using the username/password to log in to Apple, and simply gets a Auth Cookie used in downloading
/// As of Nov 2022 this was returning a 403 forbidden
func installWithoutLogin(id: Xcode.ID) {
guard let availableXcode = availableXcodes.first(where: { $0.version == id.version }) else { return }
guard let availableXcode = availableXcodes.first(where: { $0.xcodeID == id }) else { return }
installationPublishers[id] = self.install(.version(availableXcode), downloader: Downloader(rawValue: Current.defaults.string(forKey: "downloader") ?? "aria2") ?? .aria2)
.receive(on: DispatchQueue.main)
@ -650,7 +650,7 @@ class AppState: ObservableObject {
}
func cancelInstall(id: Xcode.ID) {
guard let availableXcode = availableXcodes.first(where: { $0.version == id.version }) else { return }
guard let availableXcode = availableXcodes.first(where: { $0.xcodeID == id }) else { return }
// Cancel the publisher
installationPublishers[id] = nil
@ -894,7 +894,7 @@ class AppState: ObservableObject {
}
.map { availableXcode -> Xcode in
let installedXcode = installedXcodes.first(where: { installedXcode in
availableXcode.version.isEquivalent(to: installedXcode.version)
availableXcode.version.isEquivalent(to: installedXcode.version)
})
let identicalBuilds: [XcodeID]

View file

@ -35,6 +35,9 @@ public struct InstalledXcode: Equatable {
else if infoPlist.bundleIconName == "XcodeBeta", !prereleaseIdentifiers.contains("beta") {
prereleaseIdentifiers = ["beta"]
}
// need:
// lipo -archs /Applications/Xcode-26.0.0-Beta.3.app/Contents/MacOS/Xcode
let version = Version(major: bundleVersion.major,
minor: bundleVersion.minor,

View file

@ -17,6 +17,18 @@ public struct XcodeID: Codable, Hashable, Identifiable {
self.version = version
self.architectures = architectures
}
public var architectureString: String {
switch architectures {
case .some(let architectures):
if architectures.isAppleSilicon {
return "Apple Silicon"
} else {
return "Universal"
}
default: return "Universal"
}
}
}
struct Xcode: Identifiable, CustomStringConvertible {

View file

@ -20,7 +20,11 @@ struct NotInstalledStateButtons: View {
Button {
appState.checkMinVersionAndInstall(id: id)
} label: {
Text("Install") .help("Install")
if id.architectures?.isAppleSilicon ?? false {
Text("Install Apple Silicon").help("Install")
} else {
Text("Install Universal").help("Install")
}
}
if let size = downloadFileSizeString {

View file

@ -30,17 +30,8 @@ struct XcodeListView: View {
xcodes = appState.allXcodes.filter { $0.version.isPrerelease }
}
switch architecture {
case .appleSilicon:
if architecture == .appleSilicon {
xcodes = xcodes.filter { $0.architectures == [.arm64] }
case .universal:
xcodes = xcodes.filter {
if let architectures = $0.architectures {
return architectures.contains(.x86_64) // we're assuming that if architectures contains x86 then it's universal
} else {
return true
}
}
}

View file

@ -24,6 +24,14 @@ struct XcodeListViewRow: View {
.accessibility(value: Text(xcode.identicalBuilds.map(\.version.appleDescription).joined(separator: ", ")))
.help("IdenticalBuilds.help")
}
if xcode.architectures?.isAppleSilicon ?? false {
Image(systemName: "m4.button.horizontal")
.font(.subheadline)
.foregroundColor(.secondary)
.accessibility(label: Text("AppleSilicon"))
.help("AppleSilicon.help")
}
}
if case let .installed(path) = xcode.installState {

View file

@ -4449,6 +4449,9 @@
},
"AppleSilicon" : {
},
"AppleSilicon.help" : {
},
"AppUpdates" : {
"localizations" : {
@ -10480,6 +10483,12 @@
}
}
}
},
"Install Apple Silicon" : {
},
"Install Universal" : {
},
"InstallationError.CodesignVerifyFailed" : {
"extractionState" : "manual",

View file

@ -8,7 +8,9 @@
import Foundation
/// The name of an Architecture.
public enum Architecture: String, Codable, Equatable, Hashable {
public enum Architecture: String, Codable, Equatable, Hashable, Identifiable {
public var id: Self { self }
/// The Arm64 architecture (Apple Silicon)
case arm64 = "arm64"
/// The X86\_64 architecture (64-bit Intel)
@ -18,3 +20,9 @@ public enum Architecture: String, Codable, Equatable, Hashable {
/// The PowerPC architecture (Motorola)
case powerPC = "ppc"
}
extension Array where Element == Architecture {
public var isAppleSilicon: Bool {
self == [.arm64]
}
}