Merge pull request #743 from XcodesOrg/XcodeArchitectures

Support Showing and Downloading Multiple Xcode Architectures.
This commit is contained in:
Matt Kiazyk 2025-08-26 20:49:45 -05:00 committed by GitHub
commit 08738d6912
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 1031 additions and 138 deletions

View file

@ -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 */; };
@ -140,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 */; };
@ -343,7 +341,6 @@
E8D655BF288DD04700A139C2 /* SelectedActionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectedActionType.swift; sourceTree = "<group>"; };
E8DA461025FAF7FB002E85EF /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = "<group>"; };
E8E98A9525D863D700EC89A0 /* InstallationStepDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstallationStepDetailView.swift; sourceTree = "<group>"; };
E8EE58BF2E1CC2A50003FA9F /* RuntimeArchitecture.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuntimeArchitecture.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@ -363,7 +360,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 */,
@ -662,7 +658,6 @@
E8E98A9425D863B100EC89A0 /* InfoPane */ = {
isa = PBXGroup;
children = (
E8EE58BF2E1CC2A50003FA9F /* RuntimeArchitecture.swift */,
B0403CEF2AD92D7B00137C09 /* ReleaseNotesView.swift */,
B0403CF32AD9381D00137C09 /* SDKsView.swift */,
B0403CF52AD9849E00137C09 /* CompilersView.swift */,
@ -726,7 +721,6 @@
CABFA9ED2592F0CC00380FEE /* SwiftSoup */,
CABFA9F72592F0F900380FEE /* KeychainAccess */,
CABFA9FC2592F13300380FEE /* LegibleError */,
CA9FF86C25951C6E00E47BAF /* XCModel */,
CAA858CC25A3D8BC00ACF8C0 /* ErrorHandling */,
E689540225BE8C64000EBCEA /* DockProgress */,
E8FD5726291EE4AC001E004C /* AsyncNetworkService */,
@ -816,7 +810,6 @@
CABFA9EC2592F0CC00380FEE /* XCRemoteSwiftPackageReference "SwiftSoup" */,
CABFA9F62592F0F900380FEE /* XCRemoteSwiftPackageReference "KeychainAccess" */,
CABFA9FB2592F13300380FEE /* XCRemoteSwiftPackageReference "LegibleError" */,
CA9FF86B25951C6E00E47BAF /* XCRemoteSwiftPackageReference "data" */,
CAA858CB25A3D8BC00ACF8C0 /* XCRemoteSwiftPackageReference "ErrorHandling" */,
CAC28186259EE27200B8AB0B /* XCRemoteSwiftPackageReference "CombineExpectations" */,
E689540125BE8C64000EBCEA /* XCRemoteSwiftPackageReference "DockProgress" */,
@ -942,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 */,
@ -1504,14 +1496,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 +1591,6 @@
isa = XCSwiftPackageProductDependency;
productName = LibFido2Swift;
};
CA9FF86C25951C6E00E47BAF /* XCModel */ = {
isa = XCSwiftPackageProductDependency;
package = CA9FF86B25951C6E00E47BAF /* XCRemoteSwiftPackageReference "data" */;
productName = XCModel;
};
CAA1CB2C255A5262003FD669 /* AppleAPI */ = {
isa = XCSwiftPackageProductDependency;
productName = AppleAPI;

View file

@ -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",

View file

@ -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)
}
}

View file

@ -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 {

View file

@ -3,7 +3,6 @@ import Foundation
import Path
import Version
import SwiftSoup
import struct XCModel.Xcode
import AppleAPI
import XcodesKit
@ -211,7 +210,7 @@ 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())
.decode(type: [XcodeRelease].self, decoder: JSONDecoder())
.map { xcReleasesXcodes in
let xcodes = xcReleasesXcodes.compactMap { xcReleasesXcode -> AvailableXcode? in
guard
@ -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

View file

@ -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<AnyCancellable>()
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.xcodeID == id }) 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.xcodeID == id }) 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.xcodeID == id }) 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.xcodeID == id }) 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,21 @@ 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)
// 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: [Version]
let identicalBuilds: [XcodeID]
let prereleaseAvailableXcodesWithIdenticalBuildIdentifiers = availableXcodes
.filter {
return $0.version.buildMetadataIdentifiers == availableXcode.version.buildMetadataIdentifiers &&
@ -905,13 +913,13 @@ 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 = []
}
// 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
@ -926,7 +934,8 @@ class AppState: ObservableObject {
releaseDate: availableXcode.releaseDate,
sdks: availableXcode.sdks,
compilers: availableXcode.compilers,
downloadFileSize: availableXcode.fileSize
downloadFileSize: availableXcode.fileSize,
architectures: availableXcode.architectures
)
}

View file

@ -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)
}
}

View file

@ -196,7 +196,7 @@ public struct Shell {
return Process.run(unxipPath.url, workingDirectory: url.deletingLastPathComponent(), ["\(url.path)"])
}
public var downloadRuntime: (String, String) -> AsyncThrowingStream<Progress, Error> = { platform, version in
public var downloadRuntime: (String, String, String?) -> AsyncThrowingStream<Progress, Error> = { platform, version, architecture in
return AsyncThrowingStream<Progress, Error> { 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()

View file

@ -1,12 +1,17 @@
import Foundation
import Version
import Path
import XcodesKit
/// 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 +37,20 @@ public struct InstalledXcode: Equatable {
prereleaseIdentifiers = ["beta"]
}
self.version = Version(major: bundleVersion.major,
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: architectures)
}
}

View file

@ -7,7 +7,6 @@
//
import Foundation
import struct XCModel.SDKs
import XcodesKit
import SwiftUI

View file

@ -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

View file

@ -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
}

View file

@ -7,7 +7,7 @@
//
import SwiftUI
import struct XCModel.Compilers
import XcodesKit
struct CompilersView: View {
let compilers: Compilers?

View file

@ -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)

View file

@ -8,7 +8,7 @@
import SwiftUI
import Version
import XCModel
import XcodesKit
import Path
struct InstalledStateButtons: View {

View file

@ -11,7 +11,7 @@ import Version
struct NotInstalledStateButtons: View {
let downloadFileSizeString: String?
let id: Version
let id: XcodeID
@EnvironmentObject var appState: AppState
@ -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 {
@ -38,7 +42,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()
}

View file

@ -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.displayString, systemImage: "m4.button.horizontal")
.labelStyle(.trailingIcon)
case .x86_64:
Label(selectedRuntimeArchitecture.displayValue, systemImage: "cpu.fill")
Label(selectedRuntimeArchitecture.displayString, 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.displayString)
}
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)

View file

@ -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
}
}

View file

@ -7,7 +7,7 @@
//
import SwiftUI
import struct XCModel.SDKs
import XcodesKit
struct SDKsView: View {
let content: String

View file

@ -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),

View file

@ -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<XcodeListCategory>,
isInstalledOnly: Binding<Bool>,
isShowingInfoPane: Binding<Bool>
isShowingInfoPane: Binding<Bool>,
architecture: Binding<XcodeListArchitecture>
) -> some View {
modifier(
MainToolbarModifier(
category: category,
isInstalledOnly: isInstalledOnly,
isShowingInfoPane: isShowingInfoPane
isShowingInfoPane: isShowingInfoPane,
architectures: architecture
)
)
}

View file

@ -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() }
}

View file

@ -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<Xcode.ID?>, searchText: String, category: XcodeListCategory, isInstalledOnly: Bool) {
init(selectedXcodeID: Binding<Xcode.ID?>, 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,11 @@ struct XcodeListView: View {
xcodes = appState.allXcodes.filter { $0.version.isPrerelease }
}
if architecture == .appleSilicon {
xcodes = xcodes.filter { $0.architectures == [.arm64] }
}
let latestMajor = xcodes.sorted(\.version)
.filter { $0.version.isNotPrerelease }
.last?
@ -95,11 +102,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),

View file

@ -21,9 +21,17 @@ 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")
}
if xcode.architectures?.isAppleSilicon ?? false {
Image(systemName: "m4.button.horizontal")
.font(.subheadline)
.foregroundColor(.secondary)
.accessibility(label: Text("Apple Silicon"))
.help("Apple Silicon")
}
}
if case let .installed(path) = xcode.installState {
@ -156,7 +164,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()
)

View file

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View file

@ -2083,6 +2083,131 @@
}
}
},
"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."
}
}
}
},
"Alert.Install.Error.Title" : {
"comment" : "Install",
"extractionState" : "manual",
@ -10478,6 +10603,242 @@
}
}
},
"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",
"localizations" : {
@ -22103,6 +22464,124 @@
}
}
},
"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" : {
"ar" : {

View file

@ -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
}
}

View file

@ -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 {

View file

@ -0,0 +1,33 @@
//
// 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, Identifiable {
public var id: Self { self }
/// The Arm64 architecture (Apple Silicon)
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 {
public var isAppleSilicon: Bool {
self == [.arm64]
}
}

View file

@ -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
}
}

View file

@ -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<XcodeVersion>?
public let llvm_gcc: Array<XcodeVersion>?
public let llvm: Array<XcodeVersion>?
public let clang: Array<XcodeVersion>?
public let swift: Array<XcodeVersion>?
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<XcodeVersion>?, llvm_gcc: Array<XcodeVersion>?, llvm: Array<XcodeVersion>?, clang: Array<XcodeVersion>?, swift: Array<XcodeVersion>?) {
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
}
}

View file

@ -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
}
}

View file

@ -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)
}
}
}

View file

@ -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<XcodeVersion>?
public let iOS: Array<XcodeVersion>?
public let watchOS: Array<XcodeVersion>?
public let tvOS: Array<XcodeVersion>?
public let visionOS: Array<XcodeVersion>?
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<XcodeVersion>?, 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<XcodeVersion>?, iOS: Array<XcodeVersion>?, 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<XcodeVersion>?, iOS: Array<XcodeVersion>?, watchOS: Array<XcodeVersion>?, 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<XcodeVersion>?, iOS: Array<XcodeVersion>?, watchOS: Array<XcodeVersion>?, tvOS: Array<XcodeVersion>?, visionOS: Array<XcodeVersion>?) {
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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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()

View file

@ -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)
}
}

View file

@ -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() {