Support showing multiple architectures

This commit is contained in:
Matt Kiazyk 2025-08-23 13:35:49 -06:00
parent a434d26921
commit 4b9d86b22e
31 changed files with 459 additions and 85 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 */; };
@ -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;

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

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

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

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

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

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

View file

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

View file

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

View file

@ -4446,6 +4446,9 @@
}
}
}
},
"AppleSilicon" : {
},
"AppUpdates" : {
"localizations" : {
@ -22102,6 +22105,9 @@
}
}
}
},
"Universal" : {
},
"UnxipExperiment" : {
"localizations" : {

View file

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

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