From 4b9d86b22eebb7a51e94937271d22396ec86c97c Mon Sep 17 00:00:00 2001 From: Matt Kiazyk Date: Sat, 23 Aug 2025 13:35:49 -0600 Subject: [PATCH 1/8] Support showing multiple architectures --- Xcodes.xcodeproj/project.pbxproj | 17 ------ .../xcshareddata/swiftpm/Package.resolved | 9 --- Xcodes/Backend/AppState+Install.swift | 2 +- Xcodes/Backend/AppState+Update.swift | 8 +-- Xcodes/Backend/AppState.swift | 33 ++++++----- Xcodes/Backend/AvailableXcode.swift | 15 +++-- Xcodes/Backend/InstalledXcode.swift | 10 +++- Xcodes/Backend/SDKs+Xcode.swift | 1 - Xcodes/Backend/Version+XcodeReleases.swift | 4 +- Xcodes/Backend/Xcode.swift | 36 ++++++++--- Xcodes/Frontend/InfoPane/CompilersView.swift | 2 +- Xcodes/Frontend/InfoPane/InfoPane.swift | 4 +- .../InfoPane/InstalledStateButtons.swift | 2 +- .../InfoPane/NotInstalledStateButtons.swift | 4 +- Xcodes/Frontend/InfoPane/SDKsView.swift | 2 +- Xcodes/Frontend/MainWindow.swift | 12 ++-- Xcodes/Frontend/XcodeList/MainToolbar.swift | 25 +++++++- .../XcodeList/XcodeListCategory.swift | 17 ++++++ Xcodes/Frontend/XcodeList/XcodeListView.swift | 22 ++++++- .../Frontend/XcodeList/XcodeListViewRow.swift | 4 +- .../Assets.xcassets/Icons/Contents.json | 6 ++ Xcodes/Resources/Localizable.xcstrings | 6 ++ .../Models/XcodeReleases/Architecture.swift | 20 +++++++ .../Models/XcodeReleases/Checksums.swift | 20 +++++++ .../Models/XcodeReleases/Compilers.swift | 33 +++++++++++ .../XcodesKit/Models/XcodeReleases/Link.swift | 32 ++++++++++ .../Models/XcodeReleases/Release.swift | 59 +++++++++++++++++++ .../XcodesKit/Models/XcodeReleases/SDKs.swift | 57 ++++++++++++++++++ .../Models/XcodeReleases/XcodeRelease.swift | 35 +++++++++++ .../Models/XcodeReleases/XcodeVersion.swift | 24 ++++++++ .../XcodesKit/Models/XcodeReleases/YMD.swift | 23 ++++++++ 31 files changed, 459 insertions(+), 85 deletions(-) create mode 100644 Xcodes/Resources/Assets.xcassets/Icons/Contents.json create mode 100644 Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Architecture.swift create mode 100644 Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Checksums.swift create mode 100644 Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Compilers.swift create mode 100644 Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Link.swift create mode 100644 Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Release.swift create mode 100644 Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/SDKs.swift create mode 100644 Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/XcodeRelease.swift create mode 100644 Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/XcodeVersion.swift create mode 100644 Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/YMD.swift diff --git a/Xcodes.xcodeproj/project.pbxproj b/Xcodes.xcodeproj/project.pbxproj index 7062ad2..6c75ddd 100644 --- a/Xcodes.xcodeproj/project.pbxproj +++ b/Xcodes.xcodeproj/project.pbxproj @@ -48,7 +48,6 @@ CA9FF84E2595079F00E47BAF /* ScrollingTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF84D2595079F00E47BAF /* ScrollingTextView.swift */; }; CA9FF8522595080100E47BAF /* AcknowledgementsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF8512595080100E47BAF /* AcknowledgementsView.swift */; }; CA9FF8662595130600E47BAF /* View+IsHidden.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF8652595130600E47BAF /* View+IsHidden.swift */; }; - CA9FF86D25951C6E00E47BAF /* XCModel in Frameworks */ = {isa = PBXBuildFile; productRef = CA9FF86C25951C6E00E47BAF /* XCModel */; }; CA9FF877259528CC00E47BAF /* Version+XcodeReleases.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF876259528CC00E47BAF /* Version+XcodeReleases.swift */; }; CA9FF87B2595293E00E47BAF /* DataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF87A2595293E00E47BAF /* DataSource.swift */; }; CA9FF88125955C7000E47BAF /* AvailableXcode.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9FF88025955C7000E47BAF /* AvailableXcode.swift */; }; @@ -363,7 +362,6 @@ CABFA9E42592F08E00380FEE /* Version in Frameworks */, CABFA9FD2592F13300380FEE /* LegibleError in Frameworks */, E689540325BE8C64000EBCEA /* DockProgress in Frameworks */, - CA9FF86D25951C6E00E47BAF /* XCModel in Frameworks */, CABFA9F82592F0F900380FEE /* KeychainAccess in Frameworks */, E83FDC442CBB649100679C6B /* Sparkle in Frameworks */, E862D43B2CC8B26F00BAA376 /* SRP in Frameworks */, @@ -726,7 +724,6 @@ CABFA9ED2592F0CC00380FEE /* SwiftSoup */, CABFA9F72592F0F900380FEE /* KeychainAccess */, CABFA9FC2592F13300380FEE /* LegibleError */, - CA9FF86C25951C6E00E47BAF /* XCModel */, CAA858CC25A3D8BC00ACF8C0 /* ErrorHandling */, E689540225BE8C64000EBCEA /* DockProgress */, E8FD5726291EE4AC001E004C /* AsyncNetworkService */, @@ -816,7 +813,6 @@ CABFA9EC2592F0CC00380FEE /* XCRemoteSwiftPackageReference "SwiftSoup" */, CABFA9F62592F0F900380FEE /* XCRemoteSwiftPackageReference "KeychainAccess" */, CABFA9FB2592F13300380FEE /* XCRemoteSwiftPackageReference "LegibleError" */, - CA9FF86B25951C6E00E47BAF /* XCRemoteSwiftPackageReference "data" */, CAA858CB25A3D8BC00ACF8C0 /* XCRemoteSwiftPackageReference "ErrorHandling" */, CAC28186259EE27200B8AB0B /* XCRemoteSwiftPackageReference "CombineExpectations" */, E689540125BE8C64000EBCEA /* XCRemoteSwiftPackageReference "DockProgress" */, @@ -1504,14 +1500,6 @@ minimumVersion = 0.1.4; }; }; - CA9FF86B25951C6E00E47BAF /* XCRemoteSwiftPackageReference "data" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/xcodereleases/data"; - requirement = { - branch = main; - kind = branch; - }; - }; CAA858CB25A3D8BC00ACF8C0 /* XCRemoteSwiftPackageReference "ErrorHandling" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/RobotsAndPencils/ErrorHandling"; @@ -1607,11 +1595,6 @@ isa = XCSwiftPackageProductDependency; productName = LibFido2Swift; }; - CA9FF86C25951C6E00E47BAF /* XCModel */ = { - isa = XCSwiftPackageProductDependency; - package = CA9FF86B25951C6E00E47BAF /* XCRemoteSwiftPackageReference "data" */; - productName = XCModel; - }; CAA1CB2C255A5262003FD669 /* AppleAPI */ = { isa = XCSwiftPackageProductDependency; productName = AppleAPI; diff --git a/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 84e3336..2c9a521 100644 --- a/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Xcodes.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -28,15 +28,6 @@ "version": "0.6.0" } }, - { - "package": "XcodeReleases", - "repositoryURL": "https://github.com/xcodereleases/data", - "state": { - "branch": "main", - "revision": "a43ad89e536d7a3da525fcc23fb182c37b756ecc", - "version": null - } - }, { "package": "DockProgress", "repositoryURL": "https://github.com/sindresorhus/DockProgress", diff --git a/Xcodes/Backend/AppState+Install.swift b/Xcodes/Backend/AppState+Install.swift index 2c9fc84..5e2a074 100644 --- a/Xcodes/Backend/AppState+Install.swift +++ b/Xcodes/Backend/AppState+Install.swift @@ -502,7 +502,7 @@ extension AppState { self.allXcodes[index].installState = .installing(step) let xcode = self.allXcodes[index] - Current.notificationManager.scheduleNotification(title: xcode.id.appleDescription, body: step.description, category: .normal) + Current.notificationManager.scheduleNotification(title: xcode.version.major.description + "." + xcode.version.appleDescription, body: step.description, category: .normal) } } diff --git a/Xcodes/Backend/AppState+Update.swift b/Xcodes/Backend/AppState+Update.swift index 90f531f..52c280e 100644 --- a/Xcodes/Backend/AppState+Update.swift +++ b/Xcodes/Backend/AppState+Update.swift @@ -3,7 +3,6 @@ import Foundation import Path import Version import SwiftSoup -import struct XCModel.Xcode import AppleAPI import XcodesKit @@ -211,8 +210,8 @@ extension AppState { private func xcodeReleases() -> AnyPublisher<[AvailableXcode], Error> { Current.network.dataTask(with: URLRequest(url: URL(string: "https://xcodereleases.com/data.json")!)) .map(\.data) - .decode(type: [XCModel.Xcode].self, decoder: JSONDecoder()) - .map { xcReleasesXcodes in + .decode(type: [XcodeRelease].self, decoder: JSONDecoder()) + .map { xcReleasesXcodes in let xcodes = xcReleasesXcodes.compactMap { xcReleasesXcode -> AvailableXcode? in guard let downloadURL = xcReleasesXcode.links?.download?.url, @@ -233,7 +232,8 @@ extension AppState { requiredMacOSVersion: xcReleasesXcode.requires, releaseNotesURL: xcReleasesXcode.links?.notes?.url, sdks: xcReleasesXcode.sdks, - compilers: xcReleasesXcode.compilers + compilers: xcReleasesXcode.compilers, + architectures: xcReleasesXcode.architectures ) } return xcodes diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index 359c249..dd5ee73 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -26,6 +26,7 @@ enum PreferenceKey: String { case xcodeListCategory case allowedMajorVersions case hideSupportXcodes + case xcodeListArchitectures func isManaged() -> Bool { UserDefaults.standard.objectIsForced(forKey: self.rawValue) } } @@ -146,7 +147,7 @@ class AppState: ObservableObject { // MARK: - Publisher Cancellables var cancellables = Set() - private var installationPublishers: [Version: AnyCancellable] = [:] + private var installationPublishers: [XcodeID: AnyCancellable] = [:] internal var runtimePublishers: [String: Task<(), any Error>] = [:] private var selectPublisher: AnyCancellable? private var uninstallPublisher: AnyCancellable? @@ -523,8 +524,8 @@ class AppState: ObservableObject { // MARK: - Install - func checkMinVersionAndInstall(id: Xcode.ID) { - guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return } + func checkMinVersionAndInstall(id: XcodeID) { + guard let availableXcode = availableXcodes.first(where: { $0.version == id.version }) else { return } // Check to see if users macOS is supported if let requiredMacOSVersion = availableXcode.requiredMacOSVersion { @@ -550,8 +551,8 @@ class AppState: ObservableObject { return !ProcessInfo.processInfo.isOperatingSystemAtLeast(xcodeMinimumMacOSVersion) } - func install(id: Xcode.ID) { - guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return } + func install(id: XcodeID) { + guard let availableXcode = availableXcodes.first(where: { $0.version == id.version }) else { return } installationPublishers[id] = signInIfNeeded() .handleEvents( @@ -626,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 }) else { return } + guard let availableXcode = availableXcodes.first(where: { $0.version == id.version }) else { return } installationPublishers[id] = self.install(.version(availableXcode), downloader: Downloader(rawValue: Current.defaults.string(forKey: "downloader") ?? "aria2") ?? .aria2) .receive(on: DispatchQueue.main) @@ -649,7 +650,7 @@ class AppState: ObservableObject { } func cancelInstall(id: Xcode.ID) { - guard let availableXcode = availableXcodes.first(where: { $0.version == id }) else { return } + guard let availableXcode = availableXcodes.first(where: { $0.version == id.version }) else { return } // Cancel the publisher installationPublishers[id] = nil @@ -767,7 +768,7 @@ class AppState: ObservableObject { config.allowsRunningApplicationSubstitution = false NSWorkspace.shared.openApplication(at: path.url, configuration: config) default: - Logger.appState.error("\(xcode.id) is not installed") + Logger.appState.error("\(xcode.id.version) is not installed") return } } @@ -863,7 +864,7 @@ class AppState: ObservableObject { // If build metadata matches exactly, replace the available version with the installed version. // This should handle Apple versions from /downloads/more which don't have build metadata identifiers. if let index = adjustedAvailableXcodes.map(\.version).firstIndex(where: { $0.buildMetadataIdentifiers == installedXcode.version.buildMetadataIdentifiers }) { - adjustedAvailableXcodes[index].version = installedXcode.version + adjustedAvailableXcodes[index].xcodeID = installedXcode.xcodeID } // If an installed version is the same as one that's listed online which doesn't have build metadata, replace it with the installed version // Not all prerelease Apple versions available online include build metadata @@ -871,7 +872,7 @@ class AppState: ObservableObject { availableXcode.version.isEquivalent(to: installedXcode.version) && availableXcode.version.buildMetadataIdentifiers.isEmpty }) { - adjustedAvailableXcodes[index].version = installedXcode.version + adjustedAvailableXcodes[index].xcodeID = installedXcode.xcodeID } } } @@ -888,14 +889,15 @@ class AppState: ObservableObject { // Include this version if there's only one with this build identifier return availableXcodesWithIdenticalBuildIdentifiers.count == 1 || // Or if there's more than one with this build identifier and this is the release version - availableXcodesWithIdenticalBuildIdentifiers.count > 1 && availableXcode.version.prereleaseIdentifiers.isEmpty - } + + availableXcodesWithIdenticalBuildIdentifiers.count > 1 && (availableXcode.version.prereleaseIdentifiers.isEmpty || availableXcode.architectures?.count ?? 0 != 0) + } .map { availableXcode -> Xcode in let installedXcode = installedXcodes.first(where: { installedXcode in availableXcode.version.isEquivalent(to: installedXcode.version) }) - let identicalBuilds: [Version] + let identicalBuilds: [XcodeID] let prereleaseAvailableXcodesWithIdenticalBuildIdentifiers = availableXcodes .filter { return $0.version.buildMetadataIdentifiers == availableXcode.version.buildMetadataIdentifiers && @@ -905,7 +907,7 @@ class AppState: ObservableObject { } // If this is the release version, add the identical builds to it if !prereleaseAvailableXcodesWithIdenticalBuildIdentifiers.isEmpty, availableXcode.version.prereleaseIdentifiers.isEmpty { - identicalBuilds = [availableXcode.version] + prereleaseAvailableXcodesWithIdenticalBuildIdentifiers.map(\.version) + identicalBuilds = [availableXcode.xcodeID] + prereleaseAvailableXcodesWithIdenticalBuildIdentifiers.map(\.xcodeID) } else { identicalBuilds = [] } @@ -926,7 +928,8 @@ class AppState: ObservableObject { releaseDate: availableXcode.releaseDate, sdks: availableXcode.sdks, compilers: availableXcode.compilers, - downloadFileSize: availableXcode.fileSize + downloadFileSize: availableXcode.fileSize, + architectures: availableXcode.architectures ) } diff --git a/Xcodes/Backend/AvailableXcode.swift b/Xcodes/Backend/AvailableXcode.swift index d0f877e..8415118 100644 --- a/Xcodes/Backend/AvailableXcode.swift +++ b/Xcodes/Backend/AvailableXcode.swift @@ -1,11 +1,12 @@ import Foundation import Version -import struct XCModel.SDKs -import struct XCModel.Compilers +import XcodesKit /// A version of Xcode that's available for installation public struct AvailableXcode: Codable { - public var version: Version + public var version: Version { + return xcodeID.version + } public let url: URL public let filename: String public let releaseDate: Date? @@ -14,9 +15,11 @@ public struct AvailableXcode: Codable { public let sdks: SDKs? public let compilers: Compilers? public let fileSize: Int64? + public let architectures: [Architecture]? public var downloadPath: String { return url.path } + public var xcodeID: XcodeID public init( version: Version, @@ -27,9 +30,9 @@ public struct AvailableXcode: Codable { releaseNotesURL: URL? = nil, sdks: SDKs? = nil, compilers: Compilers? = nil, - fileSize: Int64? = nil + fileSize: Int64? = nil, + architectures: [Architecture]? = nil ) { - self.version = version self.url = url self.filename = filename self.releaseDate = releaseDate @@ -38,5 +41,7 @@ public struct AvailableXcode: Codable { self.sdks = sdks self.compilers = compilers self.fileSize = fileSize + self.architectures = architectures + self.xcodeID = XcodeID(version: version, architectures: architectures) } } diff --git a/Xcodes/Backend/InstalledXcode.swift b/Xcodes/Backend/InstalledXcode.swift index aa3d716..6d31dad 100644 --- a/Xcodes/Backend/InstalledXcode.swift +++ b/Xcodes/Backend/InstalledXcode.swift @@ -5,8 +5,12 @@ import Path /// A version of Xcode that's already installed public struct InstalledXcode: Equatable { public let path: Path + public let xcodeID: XcodeID + /// Composed of the bundle short version from Info.plist and the product build version from version.plist - public let version: Version + public var version: Version { + return xcodeID.version + } public init?(path: Path) { self.path = path @@ -32,11 +36,13 @@ public struct InstalledXcode: Equatable { prereleaseIdentifiers = ["beta"] } - self.version = Version(major: bundleVersion.major, + let version = Version(major: bundleVersion.major, minor: bundleVersion.minor, patch: bundleVersion.patch, prereleaseIdentifiers: prereleaseIdentifiers, buildMetadataIdentifiers: [versionPlist.productBuildVersion].compactMap { $0 }) + + self.xcodeID = XcodeID(version: version, architectures: nil) } } diff --git a/Xcodes/Backend/SDKs+Xcode.swift b/Xcodes/Backend/SDKs+Xcode.swift index e01f3b4..716c7fa 100644 --- a/Xcodes/Backend/SDKs+Xcode.swift +++ b/Xcodes/Backend/SDKs+Xcode.swift @@ -7,7 +7,6 @@ // import Foundation -import struct XCModel.SDKs import XcodesKit import SwiftUI diff --git a/Xcodes/Backend/Version+XcodeReleases.swift b/Xcodes/Backend/Version+XcodeReleases.swift index 4ddd4f3..7c4dd21 100644 --- a/Xcodes/Backend/Version+XcodeReleases.swift +++ b/Xcodes/Backend/Version+XcodeReleases.swift @@ -1,11 +1,11 @@ import Version -import struct XCModel.Xcode +import XcodesKit extension Version { /// Initialize a Version from an XcodeReleases' XCModel.Xcode /// /// This is kinda quick-and-dirty, and it would probably be better for us to adopt something closer to XCModel.Xcode under the hood and map the scraped data to it instead. - init?(xcReleasesXcode: XCModel.Xcode) { + init?(xcReleasesXcode: XcodeRelease) { var versionString = xcReleasesXcode.version.number ?? "" // Append trailing ".0" in order to get a fully-specified version string diff --git a/Xcodes/Backend/Xcode.swift b/Xcodes/Backend/Xcode.swift index 4003ac2..b172149 100644 --- a/Xcodes/Backend/Xcode.swift +++ b/Xcodes/Backend/Xcode.swift @@ -1,14 +1,30 @@ import AppKit import Foundation import Version -import struct XCModel.SDKs -import struct XCModel.Compilers import Path +import XcodesKit + +public struct XcodeID: Codable, Hashable, Identifiable { + public let version: Version + public let architectures: [Architecture]? + + public var id: String { + let architectures = architectures?.map { $0.rawValue}.joined() ?? "" + return version.description + architectures + } + + public init(version: Version, architectures: [Architecture]? = nil) { + self.version = version + self.architectures = architectures + } +} struct Xcode: Identifiable, CustomStringConvertible { - let version: Version + var version: Version { + return id.version + } /// Other Xcode versions that have the same build identifier - let identicalBuilds: [Version] + let identicalBuilds: [XcodeID] var installState: XcodeInstallState let selected: Bool let icon: NSImage? @@ -18,10 +34,12 @@ struct Xcode: Identifiable, CustomStringConvertible { let sdks: SDKs? let compilers: Compilers? let downloadFileSize: Int64? + let architectures: [Architecture]? + let id: XcodeID init( version: Version, - identicalBuilds: [Version] = [], + identicalBuilds: [XcodeID] = [], installState: XcodeInstallState, selected: Bool, icon: NSImage?, @@ -30,9 +48,9 @@ struct Xcode: Identifiable, CustomStringConvertible { releaseDate: Date? = nil, sdks: SDKs? = nil, compilers: Compilers? = nil, - downloadFileSize: Int64? = nil + downloadFileSize: Int64? = nil, + architectures: [Architecture]? = nil ) { - self.version = version self.identicalBuilds = identicalBuilds self.installState = installState self.selected = selected @@ -43,10 +61,10 @@ struct Xcode: Identifiable, CustomStringConvertible { self.sdks = sdks self.compilers = compilers self.downloadFileSize = downloadFileSize + self.architectures = architectures + self.id = XcodeID(version: version, architectures: architectures) } - var id: Version { version } - var description: String { version.appleDescription } diff --git a/Xcodes/Frontend/InfoPane/CompilersView.swift b/Xcodes/Frontend/InfoPane/CompilersView.swift index 962937d..75e3900 100644 --- a/Xcodes/Frontend/InfoPane/CompilersView.swift +++ b/Xcodes/Frontend/InfoPane/CompilersView.swift @@ -7,7 +7,7 @@ // import SwiftUI -import struct XCModel.Compilers +import XcodesKit struct CompilersView: View { let compilers: Compilers? diff --git a/Xcodes/Frontend/InfoPane/InfoPane.swift b/Xcodes/Frontend/InfoPane/InfoPane.swift index f8753cf..b217104 100644 --- a/Xcodes/Frontend/InfoPane/InfoPane.swift +++ b/Xcodes/Frontend/InfoPane/InfoPane.swift @@ -3,8 +3,6 @@ import XcodesKit import Path import SwiftUI import Version -import struct XCModel.Compilers -import struct XCModel.SDKs struct InfoPane: View { let xcode: Xcode @@ -42,7 +40,7 @@ struct InfoPane: View { VStack(alignment: .leading) { ReleaseDateView(date: xcode.releaseDate, url: xcode.releaseNotesURL) CompatibilityView(requiredMacOSVersion: xcode.requiredMacOSVersion) - IdenticalBuildsView(builds: xcode.identicalBuilds) + IdenticalBuildsView(builds: xcode.identicalBuilds.map { $0.version }) SDKandCompilers } .frame(width: 200) diff --git a/Xcodes/Frontend/InfoPane/InstalledStateButtons.swift b/Xcodes/Frontend/InfoPane/InstalledStateButtons.swift index 3c60224..d106872 100644 --- a/Xcodes/Frontend/InfoPane/InstalledStateButtons.swift +++ b/Xcodes/Frontend/InfoPane/InstalledStateButtons.swift @@ -8,7 +8,7 @@ import SwiftUI import Version -import XCModel +import XcodesKit import Path struct InstalledStateButtons: View { diff --git a/Xcodes/Frontend/InfoPane/NotInstalledStateButtons.swift b/Xcodes/Frontend/InfoPane/NotInstalledStateButtons.swift index 28a187e..9600c29 100644 --- a/Xcodes/Frontend/InfoPane/NotInstalledStateButtons.swift +++ b/Xcodes/Frontend/InfoPane/NotInstalledStateButtons.swift @@ -11,7 +11,7 @@ import Version struct NotInstalledStateButtons: View { let downloadFileSizeString: String? - let id: Version + let id: XcodeID @EnvironmentObject var appState: AppState @@ -38,7 +38,7 @@ struct NotInstalledStateButtons: View { #Preview { NotInstalledStateButtons( downloadFileSizeString: "1,19 GB", - id: Version(major: 12, minor: 3, patch: 0) + id: XcodeID(version: Version(major: 12, minor: 3, patch: 0), architectures: nil) ) .padding() } diff --git a/Xcodes/Frontend/InfoPane/SDKsView.swift b/Xcodes/Frontend/InfoPane/SDKsView.swift index 6fa971a..459801c 100644 --- a/Xcodes/Frontend/InfoPane/SDKsView.swift +++ b/Xcodes/Frontend/InfoPane/SDKsView.swift @@ -7,7 +7,7 @@ // import SwiftUI -import struct XCModel.SDKs +import XcodesKit struct SDKsView: View { let content: String diff --git a/Xcodes/Frontend/MainWindow.swift b/Xcodes/Frontend/MainWindow.swift index 698ba96..1907181 100644 --- a/Xcodes/Frontend/MainWindow.swift +++ b/Xcodes/Frontend/MainWindow.swift @@ -15,11 +15,12 @@ struct MainWindow: View { // FB8979533 SceneStorage doesn't restore value after app is quit by user @AppStorage("isShowingInfoPane") private var isShowingInfoPane = false @AppStorage("xcodeListCategory") private var category: XcodeListCategory = .all + @AppStorage("xcodeListArchitecture") private var architecture: XcodeListArchitecture = .universal @AppStorage("isInstalledOnly") private var isInstalledOnly = false var body: some View { NavigationSplitViewWrapper { - XcodeListView(selectedXcodeID: $selectedXcodeID, searchText: searchText, category: category, isInstalledOnly: isInstalledOnly) + XcodeListView(selectedXcodeID: $selectedXcodeID, searchText: searchText, category: category, isInstalledOnly: isInstalledOnly, architecture: architecture) .layoutPriority(1) .alert(item: $appState.xcodeBeingConfirmedForUninstallation) { xcode in Alert(title: Text(String(format: localizeString("Alert.Uninstall.Title"), xcode.description)), @@ -31,7 +32,8 @@ struct MainWindow: View { .mainToolbar( category: $category, isInstalledOnly: $isInstalledOnly, - isShowingInfoPane: $isShowingInfoPane + isShowingInfoPane: $isShowingInfoPane, + architecture: $architecture ) } detail: { Group { @@ -191,11 +193,11 @@ struct MainWindow: View { case let .checkMinSupportedVersion(xcode, deviceVersion): return Alert( title: Text("Alert.MinSupported.Title"), - message: Text(String(format: localizeString("Alert.MinSupported.Message"), xcode.version.descriptionWithoutBuildMetadata, xcode.requiredMacOSVersion ?? "", deviceVersion)), + message: Text(String(format: localizeString("Alert.MinSupported.Message"), xcode.xcodeID.version.descriptionWithoutBuildMetadata, xcode.requiredMacOSVersion ?? "", deviceVersion)), primaryButton: .default( Text("Install"), action: { - self.appState.install(id: xcode.version) + self.appState.install(id: xcode.xcodeID) } ), secondaryButton: .cancel(Text("Cancel")) @@ -223,7 +225,7 @@ struct MainWindow_Previews: PreviewProvider { MainWindow().environmentObject({ () -> AppState in let a = AppState() a.allXcodes = [ - Xcode(version: Version("12.0.0+1234A")!, identicalBuilds: [Version("12.0.0+1234A")!, Version("12.0.0-RC+1234A")!], installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: false, icon: nil), + Xcode(version: Version("12.0.0+1234A")!, identicalBuilds: [XcodeID(version: Version("12.0.0+1234A")!), XcodeID(version: Version("12.0.0-RC+1234A")!)], installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: false, icon: nil), Xcode(version: Version("12.3.0")!, installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: true, icon: nil), Xcode(version: Version("12.2.0")!, installState: .notInstalled, selected: false, icon: nil), Xcode(version: Version("12.1.0")!, installState: .installing(.downloading(progress: configure(Progress(totalUnitCount: 100)) { $0.completedUnitCount = 40 })), selected: false, icon: nil), diff --git a/Xcodes/Frontend/XcodeList/MainToolbar.swift b/Xcodes/Frontend/XcodeList/MainToolbar.swift index 4ba2967..c4511de 100644 --- a/Xcodes/Frontend/XcodeList/MainToolbar.swift +++ b/Xcodes/Frontend/XcodeList/MainToolbar.swift @@ -5,6 +5,7 @@ struct MainToolbarModifier: ViewModifier { @Binding var category: XcodeListCategory @Binding var isInstalledOnly: Bool @Binding var isShowingInfoPane: Bool + @Binding var architectures: XcodeListArchitecture func body(content: Content) -> some View { content @@ -23,6 +24,24 @@ struct MainToolbarModifier: ViewModifier { .help("RefreshDescription") Spacer() + Button(action: { + switch architectures { + case .universal: architectures = .appleSilicon + case .appleSilicon: architectures = .universal + } + }) { + switch architectures { + case .universal: + Label("Universal", systemImage: "cpu.fill") + case .appleSilicon: + Label("Apple Silicon", systemImage: "m4.button.horizontal") + .labelStyle(.trailingIcon) + .foregroundColor(.accentColor) + } + } + .help("FilterAvailableDescription") + .disabled(architectures.isManaged) + Button(action: { switch category { case .all: category = .release @@ -65,13 +84,15 @@ extension View { func mainToolbar( category: Binding, isInstalledOnly: Binding, - isShowingInfoPane: Binding + isShowingInfoPane: Binding, + architecture: Binding ) -> some View { modifier( MainToolbarModifier( category: category, isInstalledOnly: isInstalledOnly, - isShowingInfoPane: isShowingInfoPane + isShowingInfoPane: isShowingInfoPane, + architectures: architecture ) ) } diff --git a/Xcodes/Frontend/XcodeList/XcodeListCategory.swift b/Xcodes/Frontend/XcodeList/XcodeListCategory.swift index 328f636..1d85da9 100644 --- a/Xcodes/Frontend/XcodeList/XcodeListCategory.swift +++ b/Xcodes/Frontend/XcodeList/XcodeListCategory.swift @@ -1,4 +1,5 @@ import Foundation +import XcodesKit enum XcodeListCategory: String, CaseIterable, Identifiable, CustomStringConvertible { case all @@ -17,3 +18,19 @@ enum XcodeListCategory: String, CaseIterable, Identifiable, CustomStringConverti var isManaged: Bool { PreferenceKey.xcodeListCategory.isManaged() } } + +enum XcodeListArchitecture: String, CaseIterable, Identifiable, CustomStringConvertible { + case universal + case appleSilicon + + var id: Self { self } + + var description: String { + switch self { + case .universal: return localizeString("Universal") + case .appleSilicon: return localizeString("Apple Silicon") + } + } + + var isManaged: Bool { PreferenceKey.xcodeListCategory.isManaged() } +} diff --git a/Xcodes/Frontend/XcodeList/XcodeListView.swift b/Xcodes/Frontend/XcodeList/XcodeListView.swift index 44be598..3a802ff 100644 --- a/Xcodes/Frontend/XcodeList/XcodeListView.swift +++ b/Xcodes/Frontend/XcodeList/XcodeListView.swift @@ -7,14 +7,16 @@ struct XcodeListView: View { @Binding var selectedXcodeID: Xcode.ID? private let searchText: String private let category: XcodeListCategory + private let architecture: XcodeListArchitecture private let isInstalledOnly: Bool @AppStorage(PreferenceKey.allowedMajorVersions.rawValue) private var allowedMajorVersions = Int.max - init(selectedXcodeID: Binding, searchText: String, category: XcodeListCategory, isInstalledOnly: Bool) { + init(selectedXcodeID: Binding, searchText: String, category: XcodeListCategory, isInstalledOnly: Bool, architecture: XcodeListArchitecture) { self._selectedXcodeID = selectedXcodeID self.searchText = searchText self.category = category self.isInstalledOnly = isInstalledOnly + self.architecture = architecture } var visibleXcodes: [Xcode] { @@ -28,6 +30,20 @@ struct XcodeListView: View { xcodes = appState.allXcodes.filter { $0.version.isPrerelease } } + switch architecture { + case .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 + } + } + } + + let latestMajor = xcodes.sorted(\.version) .filter { $0.version.isNotPrerelease } .last? @@ -95,11 +111,11 @@ struct PlatformsPocket: View { struct XcodeListView_Previews: PreviewProvider { static var previews: some View { Group { - XcodeListView(selectedXcodeID: .constant(nil), searchText: "", category: .all, isInstalledOnly: false) + XcodeListView(selectedXcodeID: .constant(nil), searchText: "", category: .all, isInstalledOnly: false, architecture: .appleSilicon) .environmentObject({ () -> AppState in let a = AppState() a.allXcodes = [ - Xcode(version: Version("12.0.0+1234A")!, identicalBuilds: [Version("12.0.0+1234A")!, Version("12.0.0-RC+1234A")!], installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: false, icon: nil), + Xcode(version: Version("12.0.0+1234A")!, identicalBuilds: [XcodeID(version: Version("12.0.0+1234A")!), XcodeID(version: Version("12.0.0-RC+1234A")!)], installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: false, icon: nil), Xcode(version: Version("12.3.0")!, installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: true, icon: nil), Xcode(version: Version("12.2.0")!, installState: .notInstalled, selected: false, icon: nil), Xcode(version: Version("12.1.0")!, installState: .installing(.downloading(progress: configure(Progress(totalUnitCount: 100)) { $0.completedUnitCount = 40 })), selected: false, icon: nil), diff --git a/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift b/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift index 60777d7..f25403d 100644 --- a/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift +++ b/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift @@ -21,7 +21,7 @@ struct XcodeListViewRow: View { .font(.subheadline) .foregroundColor(.secondary) .accessibility(label: Text("IdenticalBuilds")) - .accessibility(value: Text(xcode.identicalBuilds.map(\.appleDescription).joined(separator: ", "))) + .accessibility(value: Text(xcode.identicalBuilds.map(\.version.appleDescription).joined(separator: ", "))) .help("IdenticalBuilds.help") } } @@ -156,7 +156,7 @@ struct XcodeListViewRow_Previews: PreviewProvider { ) XcodeListViewRow( - xcode: Xcode(version: Version("12.0.0+1234A")!, identicalBuilds: [Version("12.0.0-RC+1234A")!], installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: false, icon: nil), + xcode: Xcode(version: Version("12.0.0+1234A")!, identicalBuilds: [XcodeID(version: Version("12.0.0-RC+1234A")!)], installState: .installed(Path("/Applications/Xcode-12.3.0.app")!), selected: false, icon: nil), selected: false, appState: AppState() ) diff --git a/Xcodes/Resources/Assets.xcassets/Icons/Contents.json b/Xcodes/Resources/Assets.xcassets/Icons/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Xcodes/Resources/Assets.xcassets/Icons/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Xcodes/Resources/Localizable.xcstrings b/Xcodes/Resources/Localizable.xcstrings index daac9c0..ff577ab 100644 --- a/Xcodes/Resources/Localizable.xcstrings +++ b/Xcodes/Resources/Localizable.xcstrings @@ -4446,6 +4446,9 @@ } } } + }, + "AppleSilicon" : { + }, "AppUpdates" : { "localizations" : { @@ -22102,6 +22105,9 @@ } } } + }, + "Universal" : { + }, "UnxipExperiment" : { "localizations" : { diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Architecture.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Architecture.swift new file mode 100644 index 0000000..2504620 --- /dev/null +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Architecture.swift @@ -0,0 +1,20 @@ +// +// Architecture.swift +// XcodesKit +// +// Created by Matt Kiazyk on 2025-08-23. +// + +import Foundation + +/// The name of an Architecture. +public enum Architecture: String, Codable, Equatable, Hashable { + /// The Arm64 architecture (Apple Silicon) + case arm64 = "arm64" + /// The X86\_64 architecture (64-bit Intel) + case x86_64 = "x86_64" + /// The i386 architecture (32-bit Intel) + case i386 = "i386" + /// The PowerPC architecture (Motorola) + case powerPC = "ppc" +} diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Checksums.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Checksums.swift new file mode 100644 index 0000000..b944c69 --- /dev/null +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Checksums.swift @@ -0,0 +1,20 @@ +// +// Checksums.swift +// xcodereleases +// +// Created by Xcode Releases on 9/17/20. +// Copyright © 2020 Xcode Releases. All rights reserved. +// + + +import Foundation + +public struct Checksums: Codable { + + public let sha1: String? + + public init(sha1: String? = nil) { + self.sha1 = sha1 + } + +} diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Compilers.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Compilers.swift new file mode 100644 index 0000000..3f012f0 --- /dev/null +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Compilers.swift @@ -0,0 +1,33 @@ +// +// Compiler.swift +// xcodereleases +// +// Created by Xcode Releases on 4/4/18. +// Copyright © 2018 Xcode Releases. All rights reserved. +// + +import Foundation + +public struct Compilers: Codable { + public let gcc: Array? + public let llvm_gcc: Array? + public let llvm: Array? + public let clang: Array? + public let swift: Array? + + public init(gcc: XcodeVersion? = nil, llvm_gcc: XcodeVersion? = nil, llvm: XcodeVersion? = nil, clang: XcodeVersion? = nil, swift: XcodeVersion? = nil) { + self.gcc = gcc.map { [$0] } + self.llvm_gcc = llvm_gcc.map { [$0] } + self.llvm = llvm.map { [$0] } + self.clang = clang.map { [$0] } + self.swift = swift.map { [$0] } + } + + public init(gcc: Array?, llvm_gcc: Array?, llvm: Array?, clang: Array?, swift: Array?) { + self.gcc = gcc?.isEmpty == true ? nil : gcc + self.llvm_gcc = llvm_gcc?.isEmpty == true ? nil : llvm_gcc + self.llvm = llvm?.isEmpty == true ? nil : llvm + self.clang = clang?.isEmpty == true ? nil : clang + self.swift = swift?.isEmpty == true ? nil : swift + } +} diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Link.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Link.swift new file mode 100644 index 0000000..67e5736 --- /dev/null +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Link.swift @@ -0,0 +1,32 @@ +// +// Link.swift +// xcodereleases +// +// Created by Xcode Releases on 4/5/18. +// Copyright © 2018 Xcode Releases. All rights reserved. +// + +import Foundation + +public struct Link: Codable { + public let url: URL + public let sizeMB: Int? + /// The platforms supported by this link, if applicable. + public var architectures: [Architecture]? + +// public init(_ string: String, _ size: Int? = nil, _ architectures: [Architecture]? = nil) { +// self.url = URL(string: string)! +// self.sizeMB = size +// self.architectures = architectures +// } +} + +public struct Links: Codable { + public let download: Link? + public let notes: Link? + + public init(download: Link? = nil, notes: Link? = nil) { + self.download = download + self.notes = notes + } +} diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Release.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Release.swift new file mode 100644 index 0000000..9c83e62 --- /dev/null +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Release.swift @@ -0,0 +1,59 @@ +// +// Release.swift +// xcodereleases +// +// Created by Xcode Releases on 4/4/18. +// Copyright © 2018 Xcode Releases. All rights reserved. +// + +import Foundation + +public enum Release: Codable { + + public enum CodingKeys: String, CodingKey { + case gm, gmSeed, rc, beta, dp, release + } + + public var isGM: Bool { + guard case .gm = self else { return false } + return true + } + + case gm + case gmSeed(Int) + case rc(Int) + case beta(Int) + case dp(Int) + case release + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + if let _ = try container.decodeIfPresent(Bool.self, forKey: .gm) { + self = .gm + } else if let v = try container.decodeIfPresent(Int.self, forKey: .gmSeed) { + self = .gmSeed(v) + } else if let v = try container.decodeIfPresent(Int.self, forKey: .rc) { + self = .rc(v) + } else if let v = try container.decodeIfPresent(Int.self, forKey: .beta) { + self = .beta(v) + } else if let v = try container.decodeIfPresent(Int.self, forKey: .dp) { + self = .dp(v) + } else if let _ = try container.decodeIfPresent(Bool.self, forKey: .release) { + self = .release + } else { + fatalError("Unreachable") + } + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case .gm: try container.encode(true, forKey: .gm) + case .gmSeed(let v): try container.encode(v, forKey: .gmSeed) + case .rc(let v): try container.encode(v, forKey: .rc) + case .beta(let v): try container.encode(v, forKey: .beta) + case .dp(let v): try container.encode(v, forKey: .dp) + case .release: try container.encode(true, forKey: .release) + } + } +} diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/SDKs.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/SDKs.swift new file mode 100644 index 0000000..1dcffd6 --- /dev/null +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/SDKs.swift @@ -0,0 +1,57 @@ +// +// SDKs.swift +// xcodereleases +// +// Created by Xcode Releases on 4/4/18. +// Copyright © 2018 Xcode Releases. All rights reserved. +// + +import Foundation + +public struct SDKs: Codable { + public let macOS: Array? + public let iOS: Array? + public let watchOS: Array? + public let tvOS: Array? + public let visionOS: Array? + + public init(macOS: XcodeVersion? = nil, iOS: XcodeVersion? = nil, watchOS: XcodeVersion? = nil, tvOS: XcodeVersion? = nil, visionOS: XcodeVersion? = nil) { + self.macOS = macOS.map { [$0] } + self.iOS = iOS.map { [$0] } + self.watchOS = watchOS.map { [$0] } + self.tvOS = tvOS.map { [$0] } + self.visionOS = visionOS.map { [$0] } + } + + public init(macOS: Array?, iOS: XcodeVersion? = nil, watchOS: XcodeVersion? = nil, tvOS: XcodeVersion? = nil, visionOS: XcodeVersion? = nil) { + self.macOS = macOS?.isEmpty == true ? nil : macOS + self.iOS = iOS.map { [$0] } + self.watchOS = watchOS.map { [$0] } + self.tvOS = tvOS.map { [$0] } + self.visionOS = visionOS.map { [$0] } + } + + public init(macOS: Array?, iOS: Array?, watchOS: XcodeVersion? = nil, tvOS: XcodeVersion? = nil, visionOS: XcodeVersion? = nil) { + self.macOS = macOS?.isEmpty == true ? nil : macOS + self.iOS = iOS?.isEmpty == true ? nil : iOS + self.watchOS = watchOS.map { [$0] } + self.tvOS = tvOS.map { [$0] } + self.visionOS = visionOS.map { [$0] } + } + + public init(macOS: Array?, iOS: Array?, watchOS: Array?, tvOS: XcodeVersion? = nil, visionOS: XcodeVersion? = nil) { + self.macOS = macOS?.isEmpty == true ? nil : macOS + self.iOS = iOS?.isEmpty == true ? nil : iOS + self.watchOS = watchOS?.isEmpty == true ? nil : watchOS + self.tvOS = tvOS.map { [$0] } + self.visionOS = visionOS.map { [$0] } + } + + public init(macOS: Array?, iOS: Array?, watchOS: Array?, tvOS: Array?, visionOS: Array?) { + self.macOS = macOS?.isEmpty == true ? nil : macOS + self.iOS = iOS?.isEmpty == true ? nil : iOS + self.watchOS = watchOS?.isEmpty == true ? nil : watchOS + self.tvOS = tvOS?.isEmpty == true ? nil : tvOS + self.visionOS = visionOS?.isEmpty == true ? nil : visionOS + } +} diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/XcodeRelease.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/XcodeRelease.swift new file mode 100644 index 0000000..4df4b30 --- /dev/null +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/XcodeRelease.swift @@ -0,0 +1,35 @@ +// +// Xcode.swift +// xcodereleases +// +// Created by Xcode Releases on 4/3/18. +// Copyright © 2018 Xcode Releases. All rights reserved. +// + +import Foundation + +public struct XcodeRelease: Codable { + public let name: String + public let version: XcodeVersion + public let date: YMD + public let requires: String + public let sdks: SDKs? + public let compilers: Compilers? + public let links: Links? + public let checksums: Checksums? + + public var architectures: [Architecture]? { + return links.flatMap { $0.download?.architectures } + } + + public init(name: String = "Xcode", version: XcodeVersion, date: (Int, Int, Int), requires: String, sdks: SDKs? = nil, compilers: Compilers? = nil, links: Links? = nil, checksums: Checksums? = nil) { + self.name = name + self.version = version; + self.date = YMD(date); + self.requires = requires; + self.sdks = sdks; + self.compilers = compilers + self.links = links + self.checksums = checksums + } +} diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/XcodeVersion.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/XcodeVersion.swift new file mode 100644 index 0000000..0913839 --- /dev/null +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/XcodeVersion.swift @@ -0,0 +1,24 @@ +// +// Version.swift +// xcodereleases +// +// Created by Xcode Releases on 4/4/18. +// Copyright © 2018 Xcode Releases. All rights reserved. +// + +import Foundation + +public typealias V = XcodeVersion +public struct XcodeVersion: Codable { + public let number: String? + public let build: String? + public let release: Release + + public init(_ build: String, _ number: String? = nil, _ release: Release = .release) { + self.number = number; self.build = build; self.release = release + } + + public init(number: String, _ build: String? = nil, _ release: Release = .release) { + self.number = number; self.build = build; self.release = release + } +} diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/YMD.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/YMD.swift new file mode 100644 index 0000000..a97bd4d --- /dev/null +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/YMD.swift @@ -0,0 +1,23 @@ +// +// YMD.swift +// xcodereleases +// +// Created by Xcode Releases on 4/4/18. +// Copyright © 2018 Xcode Releases. All rights reserved. +// + +import Foundation + +public struct YMD: Codable { + public let year: Int + public let month: Int + public let day: Int + + public init(_ ymd: (Int, Int, Int)) { + self.year = ymd.0; self.month = ymd.1; self.day = ymd.2 + } + + public init(_ year: Int, _ month: Int, _ day: Int) { + self.year = year; self.month = month; self.day = day + } +} From debc41f6883c1a236c86592c388d2c06411b3803 Mon Sep 17 00:00:00 2001 From: Matt Kiazyk Date: Sat, 23 Aug 2025 14:56:40 -0600 Subject: [PATCH 2/8] 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] + } +} From ceae881d9a4f000323a17ef8e681900641ffc5a0 Mon Sep 17 00:00:00 2001 From: Matt Kiazyk Date: Sat, 23 Aug 2025 20:11:57 -0600 Subject: [PATCH 3/8] show installed xcodes specific with architectures --- Xcodes/Backend/AppState.swift | 8 ++++++-- Xcodes/Backend/InstalledXcode.swift | 13 +++++++++---- Xcodes/Backend/Xcode.swift | 12 ------------ .../XcodesKit/Sources/XcodesKit/Shell/Process.swift | 8 ++++++-- .../Sources/XcodesKit/Shell/XcodesShell.swift | 4 ++++ 5 files changed, 25 insertions(+), 20 deletions(-) diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index 423e780..f2bcf46 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -894,7 +894,11 @@ class AppState: ObservableObject { } .map { availableXcode -> Xcode in let installedXcode = installedXcodes.first(where: { installedXcode in - availableXcode.version.isEquivalent(to: installedXcode.version) + if availableXcode.architectures == nil { + return availableXcode.version.isEquivalent(to: installedXcode.version) + } else { + return availableXcode.xcodeID == installedXcode.xcodeID + } }) let identicalBuilds: [XcodeID] @@ -913,7 +917,7 @@ class AppState: ObservableObject { } // If the existing install state is "installing", keep it - let existingXcodeInstallState = allXcodes.first { $0.version == availableXcode.version && $0.installState.installing }?.installState + let existingXcodeInstallState = allXcodes.first { $0.id == availableXcode.xcodeID && $0.installState.installing }?.installState // Otherwise, determine it from whether there's an installed Xcode let defaultXcodeInstallState: XcodeInstallState = installedXcode.map { .installed($0.path) } ?? .notInstalled diff --git a/Xcodes/Backend/InstalledXcode.swift b/Xcodes/Backend/InstalledXcode.swift index 87ec7f1..bc2ddd3 100644 --- a/Xcodes/Backend/InstalledXcode.swift +++ b/Xcodes/Backend/InstalledXcode.swift @@ -1,6 +1,7 @@ import Foundation import Version import Path +import XcodesKit /// A version of Xcode that's already installed public struct InstalledXcode: Equatable { @@ -36,16 +37,20 @@ public struct InstalledXcode: Equatable { prereleaseIdentifiers = ["beta"] } - // need: - // lipo -archs /Applications/Xcode-26.0.0-Beta.3.app/Contents/MacOS/Xcode - + let archsString = try? XcodesKit.Current.shell.archs(path.url.appending(path: "Contents/MacOS/Xcode")).out + + let architectures = archsString? + .trimmingCharacters(in: .whitespacesAndNewlines) + .split(separator: " ") + .compactMap { Architecture(rawValue: String($0)) } + let version = Version(major: bundleVersion.major, minor: bundleVersion.minor, patch: bundleVersion.patch, prereleaseIdentifiers: prereleaseIdentifiers, buildMetadataIdentifiers: [versionPlist.productBuildVersion].compactMap { $0 }) - self.xcodeID = XcodeID(version: version, architectures: nil) + self.xcodeID = XcodeID(version: version, architectures: architectures) } } diff --git a/Xcodes/Backend/Xcode.swift b/Xcodes/Backend/Xcode.swift index 20177a0..b172149 100644 --- a/Xcodes/Backend/Xcode.swift +++ b/Xcodes/Backend/Xcode.swift @@ -17,18 +17,6 @@ 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/XcodesKit/Sources/XcodesKit/Shell/Process.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Shell/Process.swift index b83a079..daf28e9 100644 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Shell/Process.swift +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Shell/Process.swift @@ -6,10 +6,14 @@ public typealias ProcessOutput = (status: Int32, out: String, err: String) extension Process { static func run(_ executable: Path, workingDirectory: URL? = nil, input: String? = nil, _ arguments: String...) async throws -> ProcessOutput { - return try await run(executable.url, workingDirectory: workingDirectory, input: input, arguments) + return try run(executable.url, workingDirectory: workingDirectory, input: input, arguments) } - static func run(_ executable: URL, workingDirectory: URL? = nil, input: String? = nil, _ arguments: [String]) async throws -> ProcessOutput { + static func run(_ executable: Path, workingDirectory: URL? = nil, input: String? = nil, _ arguments: String...) throws -> ProcessOutput { + return try run(executable.url, workingDirectory: workingDirectory, input: input, arguments) + } + + static func run(_ executable: URL, workingDirectory: URL? = nil, input: String? = nil, _ arguments: [String]) throws -> ProcessOutput { let process = Process() process.currentDirectoryURL = workingDirectory ?? executable.deletingLastPathComponent() diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Shell/XcodesShell.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Shell/XcodesShell.swift index 4c72795..ef091f2 100644 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Shell/XcodesShell.swift +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Shell/XcodesShell.swift @@ -26,4 +26,8 @@ public struct XcodesShell { public var deleteRuntime: (String) async throws -> ProcessOutput = { try await Process.run(Path.root.usr.bin.join("xcrun"), "simctl", "runtime", "delete", $0) } + + public var archs: (URL) throws -> ProcessOutput = { + try Process.run(Path.root.usr.bin.join("lipo"), "-archs", $0.path) + } } From 0bc8e42a9b18f8368ea2f933ca13af27d471b56c Mon Sep 17 00:00:00 2001 From: Matt Kiazyk Date: Sat, 23 Aug 2025 21:51:13 -0600 Subject: [PATCH 4/8] have both arch versions as the same version on list --- Xcodes/Backend/AppState.swift | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index f2bcf46..9a70e1f 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -894,11 +894,13 @@ class AppState: ObservableObject { } .map { availableXcode -> Xcode in let installedXcode = installedXcodes.first(where: { installedXcode in - if availableXcode.architectures == nil { - return availableXcode.version.isEquivalent(to: installedXcode.version) - } else { - return availableXcode.xcodeID == installedXcode.xcodeID - } + // if we want to have only specific Xcodes as selected instead of the Architecture Equivalent. + // if availableXcode.architectures == nil { +// return availableXcode.version.isEquivalent(to: installedXcode.version) +// } else { +// return availableXcode.xcodeID == installedXcode.xcodeID +// } + return availableXcode.version.isEquivalent(to: installedXcode.version) }) let identicalBuilds: [XcodeID] From 2e2b16e759131e33ee67da06e84c3d8a77d16419 Mon Sep 17 00:00:00 2001 From: Matt Kiazyk Date: Mon, 25 Aug 2025 22:26:41 -0500 Subject: [PATCH 5/8] warning if users download silicon runtime without xcode 26 selected --- Xcodes.xcodeproj/project.pbxproj | 4 --- Xcodes/Backend/AppState+Runtimes.swift | 25 ++++++++++++-- Xcodes/Backend/Environment.swift | 11 ++++-- Xcodes/Frontend/InfoPane/PlatformsView.swift | 34 +++++++++---------- .../InfoPane/RuntimeArchitecture.swift | 17 ---------- Xcodes/Resources/Localizable.xcstrings | 11 ++++++ .../Models/Runtimes/CoreSimulatorImage.swift | 4 ++- .../XcodesKit/Models/Runtimes/Runtimes.swift | 3 +- .../Models/XcodeReleases/Architecture.swift | 4 --- 9 files changed, 64 insertions(+), 49 deletions(-) delete mode 100644 Xcodes/Frontend/InfoPane/RuntimeArchitecture.swift diff --git a/Xcodes.xcodeproj/project.pbxproj b/Xcodes.xcodeproj/project.pbxproj index 6c75ddd..93f4952 100644 --- a/Xcodes.xcodeproj/project.pbxproj +++ b/Xcodes.xcodeproj/project.pbxproj @@ -139,7 +139,6 @@ E8DA461125FAF7FB002E85EF /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8DA461025FAF7FB002E85EF /* NotificationsView.swift */; }; E8E98A9025D8631800EC89A0 /* InstallationStepRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAFBC3FF259AC17F00E2A3D8 /* InstallationStepRowView.swift */; }; E8E98A9625D863D700EC89A0 /* InstallationStepDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8E98A9525D863D700EC89A0 /* InstallationStepDetailView.swift */; }; - E8EE58C02E1CC2A50003FA9F /* RuntimeArchitecture.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8EE58BF2E1CC2A50003FA9F /* RuntimeArchitecture.swift */; }; E8F44A1E296B4CD7002D6592 /* Path in Frameworks */ = {isa = PBXBuildFile; productRef = E8F44A1D296B4CD7002D6592 /* Path */; }; E8FA00542B5B109800769CE0 /* com.xcodesorg.xcodesapp.Helper in Copy Helper */ = {isa = PBXBuildFile; fileRef = CA9FF8AE2595967A00E47BAF /* com.xcodesorg.xcodesapp.Helper */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; E8FD5727291EE4AC001E004C /* AsyncNetworkService in Frameworks */ = {isa = PBXBuildFile; productRef = E8FD5726291EE4AC001E004C /* AsyncNetworkService */; }; @@ -342,7 +341,6 @@ E8D655BF288DD04700A139C2 /* SelectedActionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedActionType.swift; sourceTree = ""; }; E8DA461025FAF7FB002E85EF /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = ""; }; E8E98A9525D863D700EC89A0 /* InstallationStepDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallationStepDetailView.swift; sourceTree = ""; }; - E8EE58BF2E1CC2A50003FA9F /* RuntimeArchitecture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeArchitecture.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -660,7 +658,6 @@ E8E98A9425D863B100EC89A0 /* InfoPane */ = { isa = PBXGroup; children = ( - E8EE58BF2E1CC2A50003FA9F /* RuntimeArchitecture.swift */, B0403CEF2AD92D7B00137C09 /* ReleaseNotesView.swift */, B0403CF32AD9381D00137C09 /* SDKsView.swift */, B0403CF52AD9849E00137C09 /* CompilersView.swift */, @@ -938,7 +935,6 @@ 53CBAB2C263DCC9100410495 /* XcodesAlert.swift in Sources */, 332807412CA5EA820036F691 /* SignInSecurityKeyTouchView.swift in Sources */, CA61A6E0259835580008926E /* Xcode.swift in Sources */, - E8EE58C02E1CC2A50003FA9F /* RuntimeArchitecture.swift in Sources */, CAE4247F259A666100B8B246 /* MainWindow.swift in Sources */, CA452BB0259FD9770072DFA4 /* ProgressIndicator.swift in Sources */, B0403CF02AD92D7B00137C09 /* ReleaseNotesView.swift in Sources */, diff --git a/Xcodes/Backend/AppState+Runtimes.swift b/Xcodes/Backend/AppState+Runtimes.swift index e2b1149..f9a9864 100644 --- a/Xcodes/Backend/AppState+Runtimes.swift +++ b/Xcodes/Backend/AppState+Runtimes.swift @@ -61,7 +61,22 @@ extension AppState { // only selected xcodes > 16.1 beta 3 can download runtimes via a xcodebuild -downloadPlatform version // only Runtimes coming from cryptexDiskImage can be downloaded via xcodebuild if selectedXcode.version > Version(major: 16, minor: 0, patch: 0) { - downloadRuntimeViaXcodeBuild(runtime: runtime) + + if runtime.architectures?.isAppleSilicon ?? false { + if selectedXcode.version > Version(major: 26, minor: 0, patch: 0) { + downloadRuntimeViaXcodeBuild(runtime: runtime) + } else { + // not supported + Logger.appState.error("Trying to download a runtime we can't download") + DispatchQueue.main.async { + self.presentedAlert = .generic(title: localizeString("Alert.Install.Error.Title"), message: localizeString("Alert.Install.Error.Need.Xcode26")) + } + return + } + + } else { + downloadRuntimeViaXcodeBuild(runtime: runtime) + } } else { // not supported Logger.appState.error("Trying to download a runtime we can't download") @@ -77,7 +92,8 @@ extension AppState { func downloadRuntimeViaXcodeBuild(runtime: DownloadableRuntime) { - let downloadRuntimeTask = Current.shell.downloadRuntime(runtime.platform.shortName, runtime.simulatorVersion.buildUpdate) + let downloadRuntimeTask = Current.shell.downloadRuntime(runtime.platform.shortName, runtime.simulatorVersion.buildUpdate, runtime.architectures?.isAppleSilicon ?? false ? Architecture.arm64.rawValue : nil) + runtimePublishers[runtime.identifier] = Task { [weak self] in guard let self = self else { return } do { @@ -258,7 +274,10 @@ extension AppState { } func coreSimulatorInfo(runtime: DownloadableRuntime) -> CoreSimulatorImage? { - return installedRuntimes.filter({ $0.runtimeInfo.build == runtime.simulatorVersion.buildUpdate }).first + return installedRuntimes.filter({ + $0.runtimeInfo.build == runtime.simulatorVersion.buildUpdate && + ((runtime.architectures ?? []).isEmpty ? true : + $0.runtimeInfo.supportedArchitectures == runtime.architectures )}).first } func deleteRuntime(runtime: DownloadableRuntime) async throws { diff --git a/Xcodes/Backend/Environment.swift b/Xcodes/Backend/Environment.swift index 61574ce..b515e11 100644 --- a/Xcodes/Backend/Environment.swift +++ b/Xcodes/Backend/Environment.swift @@ -196,7 +196,7 @@ public struct Shell { return Process.run(unxipPath.url, workingDirectory: url.deletingLastPathComponent(), ["\(url.path)"]) } - public var downloadRuntime: (String, String) -> AsyncThrowingStream = { platform, version in + public var downloadRuntime: (String, String, String?) -> AsyncThrowingStream = { platform, version, architecture in return AsyncThrowingStream { continuation in Task { // Assume progress will not have data races, so we manually opt-out isolation checks. @@ -204,7 +204,7 @@ public struct Shell { progress.kind = .file progress.fileOperationKind = .downloading - let process = Process() + var process = Process() let xcodeBuildPath = Path.root.usr.bin.join("xcodebuild").url process.executableURL = xcodeBuildPath @@ -215,6 +215,13 @@ public struct Shell { "\(version)" ] + if let architecture { + process.arguments?.append(contentsOf: [ + "-architectureVariant", + "\(architecture)" + ]) + } + let stdOutPipe = Pipe() process.standardOutput = stdOutPipe let stdErrPipe = Pipe() diff --git a/Xcodes/Frontend/InfoPane/PlatformsView.swift b/Xcodes/Frontend/InfoPane/PlatformsView.swift index 6e0b9a1..cf69c7e 100644 --- a/Xcodes/Frontend/InfoPane/PlatformsView.swift +++ b/Xcodes/Frontend/InfoPane/PlatformsView.swift @@ -11,7 +11,7 @@ import XcodesKit struct PlatformsView: View { @EnvironmentObject var appState: AppState - @AppStorage("selectedRuntimeArchitecture") private var selectedRuntimeArchitecture: RuntimeArchitecture = .arm64 + @AppStorage("selectedRuntimeArchitecture") private var selectedRuntimeArchitecture: Architecture = .arm64 let xcode: Xcode @@ -22,7 +22,7 @@ struct PlatformsView: View { appState.downloadableRuntimes.filter { $0.sdkBuildUpdate?.contains(sdkBuild) ?? false && ($0.architectures?.isEmpty ?? true || - $0.architectures?.contains(selectedRuntimeArchitecture.rawValue) ?? false) + $0.architectures?.contains(selectedRuntimeArchitecture) ?? false) } } @@ -43,10 +43,10 @@ struct PlatformsView: View { } label: { switch selectedRuntimeArchitecture { case .arm64: - Label(selectedRuntimeArchitecture.displayValue, systemImage: "m4.button.horizontal") + Label(selectedRuntimeArchitecture.rawValue, systemImage: "m4.button.horizontal") .labelStyle(.trailingIcon) case .x86_64: - Label(selectedRuntimeArchitecture.displayValue, systemImage: "cpu.fill") + Label(selectedRuntimeArchitecture.rawValue, systemImage: "cpu.fill") .labelStyle(.trailingIcon) } } @@ -74,21 +74,21 @@ struct PlatformsView: View { Text("\(runtime.visibleIdentifier)") .font(.headline) ForEach(runtime.architectures ?? [], id: \.self) { architecture in - TagView(text: architecture) + TagView(text: architecture.rawValue) } pathIfAvailable(xcode: xcode, runtime: runtime) - - if runtime.installState == .notInstalled { - // TODO: Update the downloadableRuntimes with the appropriate installState so we don't have to check path awkwardly - if appState.runtimeInstallPath(xcode: xcode, runtime: runtime) != nil { - EmptyView() - } else { - HStack { - Spacer() - DownloadRuntimeButton(runtime: runtime) - } - } - } + + if runtime.installState == .notInstalled { + // TODO: Update the downloadableRuntimes with the appropriate installState so we don't have to check path awkwardly + if appState.runtimeInstallPath(xcode: xcode, runtime: runtime) != nil { + EmptyView() + } else { + HStack { + Spacer() + DownloadRuntimeButton(runtime: runtime) + } + } + } Spacer() Text(runtime.downloadFileSizeString) diff --git a/Xcodes/Frontend/InfoPane/RuntimeArchitecture.swift b/Xcodes/Frontend/InfoPane/RuntimeArchitecture.swift deleted file mode 100644 index abbdf45..0000000 --- a/Xcodes/Frontend/InfoPane/RuntimeArchitecture.swift +++ /dev/null @@ -1,17 +0,0 @@ -// -// RuntimeArchitecture.swift -// Xcodes -// -// Created by Matt Kiazyk on 2025-07-07. -// - -enum RuntimeArchitecture: String, CaseIterable, Identifiable { - case arm64 - case x86_64 - - var id: Self { self } - - var displayValue: String { - return rawValue - } -} diff --git a/Xcodes/Resources/Localizable.xcstrings b/Xcodes/Resources/Localizable.xcstrings index 1f6e687..93196fa 100644 --- a/Xcodes/Resources/Localizable.xcstrings +++ b/Xcodes/Resources/Localizable.xcstrings @@ -2083,6 +2083,17 @@ } } }, + "Alert.Install.Error.Need.Xcode26" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple supports downloading Apple Silicon runtimes only when Xcode 26+ is selected. Please Select and try downloading again or download the universal build." + } + } + } + }, "Alert.Install.Error.Title" : { "comment" : "Install", "extractionState" : "manual", diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/CoreSimulatorImage.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/CoreSimulatorImage.swift index cf537d4..3ea12db 100644 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/CoreSimulatorImage.swift +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/CoreSimulatorImage.swift @@ -37,8 +37,10 @@ public struct CoreSimulatorImage: Decodable, Identifiable, Equatable { public struct CoreSimulatorRuntimeInfo: Decodable { public let build: String + public let supportedArchitectures: [Architecture]? - public init(build: String) { + public init(build: String, supportedArchitectures: [Architecture]? = nil) { self.build = build + self.supportedArchitectures = supportedArchitectures } } diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/Runtimes.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/Runtimes.swift index 357fa23..581b6c0 100644 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/Runtimes.swift +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/Runtimes/Runtimes.swift @@ -12,7 +12,7 @@ public struct DownloadableRuntime: Codable, Identifiable, Hashable { public let category: Category public let simulatorVersion: SimulatorVersion public let source: String? - public let architectures: [String]? + public let architectures: [Architecture]? public let dictionaryVersion: Int public let contentType: ContentType public let platform: Platform @@ -170,6 +170,7 @@ public struct InstalledRuntime: Decodable { let state: String let version: String let sizeBytes: Int? + let supportedArchitectures: [Architecture]? } extension InstalledRuntime { diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Architecture.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Architecture.swift index 2721bc6..78ac5e2 100644 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Architecture.swift +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Architecture.swift @@ -15,10 +15,6 @@ public enum Architecture: String, Codable, Equatable, Hashable, Identifiable { case arm64 = "arm64" /// The X86\_64 architecture (64-bit Intel) case x86_64 = "x86_64" - /// The i386 architecture (32-bit Intel) - case i386 = "i386" - /// The PowerPC architecture (Motorola) - case powerPC = "ppc" } extension Array where Element == Architecture { From a7b3f78813d9e1f7e539270ff38caef5b7da90a8 Mon Sep 17 00:00:00 2001 From: Matt Kiazyk Date: Mon, 25 Aug 2025 22:37:51 -0500 Subject: [PATCH 6/8] clean up localizable strings --- .../Frontend/XcodeList/XcodeListViewRow.swift | 4 +- Xcodes/Resources/Localizable.xcstrings | 471 +++++++++++++++++- 2 files changed, 464 insertions(+), 11 deletions(-) diff --git a/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift b/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift index 2fe75e9..cc7f311 100644 --- a/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift +++ b/Xcodes/Frontend/XcodeList/XcodeListViewRow.swift @@ -29,8 +29,8 @@ struct XcodeListViewRow: View { Image(systemName: "m4.button.horizontal") .font(.subheadline) .foregroundColor(.secondary) - .accessibility(label: Text("AppleSilicon")) - .help("AppleSilicon.help") + .accessibility(label: Text("Apple Silicon")) + .help("Apple Silicon") } } diff --git a/Xcodes/Resources/Localizable.xcstrings b/Xcodes/Resources/Localizable.xcstrings index 93196fa..386905f 100644 --- a/Xcodes/Resources/Localizable.xcstrings +++ b/Xcodes/Resources/Localizable.xcstrings @@ -2086,11 +2086,125 @@ "Alert.Install.Error.Need.Xcode26" : { "extractionState" : "manual", "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple supports downloading Apple Silicon runtimes only when Xcode 26+ is selected. Please Select and try downloading again or download the universal build." + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple supports downloading Apple Silicon runtimes only when Xcode 26+ is selected. Please Select and try downloading again or download the universal build." + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple supports downloading Apple Silicon runtimes only when Xcode 26+ is selected. Please Select and try downloading again or download the universal build." + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple supports downloading Apple Silicon runtimes only when Xcode 26+ is selected. Please Select and try downloading again or download the universal build." + } + }, "en" : { "stringUnit" : { "state" : "translated", "value" : "Apple supports downloading Apple Silicon runtimes only when Xcode 26+ is selected. Please Select and try downloading again or download the universal build." } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple supports downloading Apple Silicon runtimes only when Xcode 26+ is selected. Please Select and try downloading again or download the universal build." + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple supports downloading Apple Silicon runtimes only when Xcode 26+ is selected. Please Select and try downloading again or download the universal build." + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple supports downloading Apple Silicon runtimes only when Xcode 26+ is selected. Please Select and try downloading again or download the universal build." + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple supports downloading Apple Silicon runtimes only when Xcode 26+ is selected. Please Select and try downloading again or download the universal build." + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple supports downloading Apple Silicon runtimes only when Xcode 26+ is selected. Please Select and try downloading again or download the universal build." + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple supports downloading Apple Silicon runtimes only when Xcode 26+ is selected. Please Select and try downloading again or download the universal build." + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple supports downloading Apple Silicon runtimes only when Xcode 26+ is selected. Please Select and try downloading again or download the universal build." + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple supports downloading Apple Silicon runtimes only when Xcode 26+ is selected. Please Select and try downloading again or download the universal build." + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple supports downloading Apple Silicon runtimes only when Xcode 26+ is selected. Please Select and try downloading again or download the universal build." + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple supports downloading Apple Silicon runtimes only when Xcode 26+ is selected. Please Select and try downloading again or download the universal build." + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple supports downloading Apple Silicon runtimes only when Xcode 26+ is selected. Please Select and try downloading again or download the universal build." + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple supports downloading Apple Silicon runtimes only when Xcode 26+ is selected. Please Select and try downloading again or download the universal build." + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple supports downloading Apple Silicon runtimes only when Xcode 26+ is selected. Please Select and try downloading again or download the universal build." + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple supports downloading Apple Silicon runtimes only when Xcode 26+ is selected. Please Select and try downloading again or download the universal build." + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apple supports downloading Apple Silicon runtimes only when Xcode 26+ is selected. Please Select and try downloading again or download the universal build." + } } } }, @@ -4457,12 +4571,6 @@ } } } - }, - "AppleSilicon" : { - - }, - "AppleSilicon.help" : { - }, "AppUpdates" : { "localizations" : { @@ -10496,10 +10604,240 @@ } }, "Install Apple Silicon" : { - + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Apple Silicon" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Apple Silicon" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Apple Silicon" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Apple Silicon" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Apple Silicon" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Apple Silicon" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Apple Silicon" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Apple Silicon" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Apple Silicon" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Apple Silicon" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Apple Silicon" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Apple Silicon" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Apple Silicon" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Apple Silicon" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Apple Silicon" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Apple Silicon" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Apple Silicon" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Apple Silicon" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Apple Silicon" + } + } + } }, "Install Universal" : { - + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Universal" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Universal" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Universal" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Universal" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Universal" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Universal" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Universal" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Universal" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Universal" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Universal" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Universal" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Universal" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Universal" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Universal" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Universal" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Universal" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Universal" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Universal" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install Universal" + } + } + } }, "InstallationError.CodesignVerifyFailed" : { "extractionState" : "manual", @@ -22127,7 +22465,122 @@ } }, "Universal" : { - + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "Universal" + } + }, + "ca" : { + "stringUnit" : { + "state" : "translated", + "value" : "Universal" + } + }, + "de" : { + "stringUnit" : { + "state" : "translated", + "value" : "Universal" + } + }, + "el" : { + "stringUnit" : { + "state" : "translated", + "value" : "Universal" + } + }, + "es" : { + "stringUnit" : { + "state" : "translated", + "value" : "Universal" + } + }, + "fi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Universal" + } + }, + "fr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Universal" + } + }, + "hi" : { + "stringUnit" : { + "state" : "translated", + "value" : "Universal" + } + }, + "it" : { + "stringUnit" : { + "state" : "translated", + "value" : "Universal" + } + }, + "ja" : { + "stringUnit" : { + "state" : "translated", + "value" : "Universal" + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "Universal" + } + }, + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Universal" + } + }, + "pl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Universal" + } + }, + "pt-BR" : { + "stringUnit" : { + "state" : "translated", + "value" : "Universal" + } + }, + "ru" : { + "stringUnit" : { + "state" : "translated", + "value" : "Universal" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Universal" + } + }, + "uk" : { + "stringUnit" : { + "state" : "translated", + "value" : "Universal" + } + }, + "zh-Hans" : { + "stringUnit" : { + "state" : "translated", + "value" : "Universal" + } + }, + "zh-Hant" : { + "stringUnit" : { + "state" : "translated", + "value" : "Universal" + } + } + } }, "UnxipExperiment" : { "localizations" : { From bfb8c2cbb876d2a628d398bc1775dece4c4b8ce2 Mon Sep 17 00:00:00 2001 From: Matt Kiazyk Date: Mon, 25 Aug 2025 22:45:21 -0500 Subject: [PATCH 7/8] fix tests --- XcodesTests/AppStateUpdateTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/XcodesTests/AppStateUpdateTests.swift b/XcodesTests/AppStateUpdateTests.swift index b510652..a2f501e 100644 --- a/XcodesTests/AppStateUpdateTests.swift +++ b/XcodesTests/AppStateUpdateTests.swift @@ -149,7 +149,7 @@ class AppStateUpdateTests: XCTestCase { ) XCTAssertEqual(subject.allXcodes.map(\.version), [Version("12.4.0+12D4e")!]) - XCTAssertEqual(subject.allXcodes.map(\.identicalBuilds), [[Version("12.4.0+12D4e")!, Version("12.4.0-RC+12D4e")!]]) + XCTAssertEqual(subject.allXcodes.map(\.identicalBuilds), [[XcodeID(version: Version("12.4.0+12D4e")!), XcodeID(version: Version("12.4.0-RC+12D4e")!)]]) } func testIdenticalBuilds_DoNotMergeReleaseVersions() { @@ -234,7 +234,7 @@ class AppStateUpdateTests: XCTestCase { ) XCTAssertEqual(subject.allXcodes.map(\.version), [Version("12.4.0+12D4e")!]) - XCTAssertEqual(subject.allXcodes.map(\.identicalBuilds), [[Version("12.4.0+12D4e")!, Version("12.4.0-RC+12D4e")!]]) + XCTAssertEqual(subject.allXcodes.map(\.identicalBuilds), [[XcodeID(version: Version("12.4.0+12D4e")!), XcodeID(version: Version("12.4.0-RC+12D4e")!)]]) } func testIdenticalBuilds_AppleDataSource_DoNotMergeVersionsWithoutBuildIdentifiers() { From 472e36ed0f13b9377a5d70e4b9dd3977cfe296dc Mon Sep 17 00:00:00 2001 From: Matt Kiazyk Date: Mon, 25 Aug 2025 22:59:28 -0500 Subject: [PATCH 8/8] adjust names --- Xcodes/Frontend/InfoPane/PlatformsView.swift | 6 +++--- .../XcodesKit/Models/XcodeReleases/Architecture.swift | 9 +++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Xcodes/Frontend/InfoPane/PlatformsView.swift b/Xcodes/Frontend/InfoPane/PlatformsView.swift index cf69c7e..9235870 100644 --- a/Xcodes/Frontend/InfoPane/PlatformsView.swift +++ b/Xcodes/Frontend/InfoPane/PlatformsView.swift @@ -43,10 +43,10 @@ struct PlatformsView: View { } label: { switch selectedRuntimeArchitecture { case .arm64: - Label(selectedRuntimeArchitecture.rawValue, systemImage: "m4.button.horizontal") + Label(selectedRuntimeArchitecture.displayString, systemImage: "m4.button.horizontal") .labelStyle(.trailingIcon) case .x86_64: - Label(selectedRuntimeArchitecture.rawValue, systemImage: "cpu.fill") + Label(selectedRuntimeArchitecture.displayString, systemImage: "cpu.fill") .labelStyle(.trailingIcon) } } @@ -74,7 +74,7 @@ struct PlatformsView: View { Text("\(runtime.visibleIdentifier)") .font(.headline) ForEach(runtime.architectures ?? [], id: \.self) { architecture in - TagView(text: architecture.rawValue) + TagView(text: architecture.displayString) } pathIfAvailable(xcode: xcode, runtime: runtime) diff --git a/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Architecture.swift b/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Architecture.swift index 78ac5e2..eb3ab40 100644 --- a/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Architecture.swift +++ b/Xcodes/XcodesKit/Sources/XcodesKit/Models/XcodeReleases/Architecture.swift @@ -15,6 +15,15 @@ public enum Architecture: String, Codable, Equatable, Hashable, Identifiable { case arm64 = "arm64" /// The X86\_64 architecture (64-bit Intel) case x86_64 = "x86_64" + + public var displayString: String { + switch self { + case .arm64: + return "Apple Silicon" + case .x86_64: + return "Intel" + } + } } extension Array where Element == Architecture {