From 4b9d86b22eebb7a51e94937271d22396ec86c97c Mon Sep 17 00:00:00 2001 From: Matt Kiazyk Date: Sat, 23 Aug 2025 13:35:49 -0600 Subject: [PATCH] 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 + } +}