From debc41f6883c1a236c86592c388d2c06411b3803 Mon Sep 17 00:00:00 2001 From: Matt Kiazyk Date: Sat, 23 Aug 2025 14:56:40 -0600 Subject: [PATCH] support downloading individual xcode architecture versions --- Xcodes/Backend/AppState.swift | 10 +++++----- Xcodes/Backend/InstalledXcode.swift | 3 +++ Xcodes/Backend/Xcode.swift | 12 ++++++++++++ .../Frontend/InfoPane/NotInstalledStateButtons.swift | 6 +++++- Xcodes/Frontend/XcodeList/XcodeListView.swift | 11 +---------- Xcodes/Frontend/XcodeList/XcodeListViewRow.swift | 8 ++++++++ Xcodes/Resources/Localizable.xcstrings | 9 +++++++++ .../Models/XcodeReleases/Architecture.swift | 10 +++++++++- 8 files changed, 52 insertions(+), 17 deletions(-) diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index dd5ee73..423e780 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -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] diff --git a/Xcodes/Backend/InstalledXcode.swift b/Xcodes/Backend/InstalledXcode.swift index 6d31dad..87ec7f1 100644 --- a/Xcodes/Backend/InstalledXcode.swift +++ b/Xcodes/Backend/InstalledXcode.swift @@ -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, diff --git a/Xcodes/Backend/Xcode.swift b/Xcodes/Backend/Xcode.swift index b172149..20177a0 100644 --- a/Xcodes/Backend/Xcode.swift +++ b/Xcodes/Backend/Xcode.swift @@ -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 { diff --git a/Xcodes/Frontend/InfoPane/NotInstalledStateButtons.swift b/Xcodes/Frontend/InfoPane/NotInstalledStateButtons.swift index 9600c29..9e6bf5c 100644 --- a/Xcodes/Frontend/InfoPane/NotInstalledStateButtons.swift +++ b/Xcodes/Frontend/InfoPane/NotInstalledStateButtons.swift @@ -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 { diff --git a/Xcodes/Frontend/XcodeList/XcodeListView.swift b/Xcodes/Frontend/XcodeList/XcodeListView.swift index 3a802ff..9a8c9c4 100644 --- a/Xcodes/Frontend/XcodeList/XcodeListView.swift +++ b/Xcodes/Frontend/XcodeList/XcodeListView.swift @@ -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 - } - } } diff --git a/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift b/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift index f25403d..2fe75e9 100644 --- a/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift +++ b/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift @@ -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 { diff --git a/Xcodes/Resources/Localizable.xcstrings b/Xcodes/Resources/Localizable.xcstrings index ff577ab..1f6e687 100644 --- a/Xcodes/Resources/Localizable.xcstrings +++ b/Xcodes/Resources/Localizable.xcstrings @@ -4449,6 +4449,9 @@ }, "AppleSilicon" : { + }, + "AppleSilicon.help" : { + }, "AppUpdates" : { "localizations" : { @@ -10480,6 +10483,12 @@ } } } + }, + "Install Apple Silicon" : { + + }, + "Install Universal" : { + }, "InstallationError.CodesignVerifyFailed" : { "extractionState" : "manual", diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Architecture.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Architecture.swift index 2504620..2721bc6 100644 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Architecture.swift +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Architecture.swift @@ -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] + } +}