From 8369b521e3c554b8840978c1411a876298cf4c00 Mon Sep 17 00:00:00 2001 From: Brandon Evans Date: Tue, 23 Jun 2020 23:03:22 -0600 Subject: [PATCH 1/4] Add install flags for --latest and --latest-prerelease --- Sources/XcodesKit/Foundation.swift | 6 ++ Sources/XcodesKit/Models.swift | 3 + Sources/XcodesKit/XcodeInstaller.swift | 91 +++++++++++++++++------ Sources/xcodes/main.swift | 19 ++++- Tests/XcodesKitTests/XcodesKitTests.swift | 2 +- 5 files changed, 97 insertions(+), 24 deletions(-) diff --git a/Sources/XcodesKit/Foundation.swift b/Sources/XcodesKit/Foundation.swift index b9fe659..8ff82ae 100644 --- a/Sources/XcodesKit/Foundation.swift +++ b/Sources/XcodesKit/Foundation.swift @@ -20,3 +20,9 @@ public extension NumberFormatter { return string(from: number as! NSNumber) } } + +extension Sequence { + func sorted(_ keyPath: KeyPath) -> [Element] { + sorted(by: { $0[keyPath: keyPath] < $1[keyPath: keyPath] }) + } +} diff --git a/Sources/XcodesKit/Models.swift b/Sources/XcodesKit/Models.swift index 5a367b7..51edd44 100644 --- a/Sources/XcodesKit/Models.swift +++ b/Sources/XcodesKit/Models.swift @@ -43,6 +43,9 @@ public struct Xcode: Codable { public let version: Version public let url: URL public let filename: String + + var isPrerelease: Bool { version.prereleaseIdentifiers.isEmpty == false } + var isNotPrerelease: Bool { version.prereleaseIdentifiers.isEmpty == true } public init(version: Version, url: URL, filename: String) { self.version = version diff --git a/Sources/XcodesKit/XcodeInstaller.swift b/Sources/XcodesKit/XcodeInstaller.swift index d1d1985..5626a65 100644 --- a/Sources/XcodesKit/XcodeInstaller.swift +++ b/Sources/XcodesKit/XcodeInstaller.swift @@ -18,6 +18,8 @@ public final class XcodeInstaller { case unsupportedFileFormat(extension: String) case missingSudoerPassword case unavailableVersion(Version) + case noNonPrereleaseVersionAvailable + case noPrereleaseVersionAvailable case missingUsernameOrPassword case versionAlreadyInstalled(InstalledXcode) case invalidVersion(String) @@ -54,6 +56,10 @@ public final class XcodeInstaller { return "Missing password. Please try again." case let .unavailableVersion(version): return "Could not find version \(version.xcodeDescription)." + case .noNonPrereleaseVersionAvailable: + return "No non-prerelease versions available." + case .noPrereleaseVersionAvailable: + return "No prerelease versions available." case .missingUsernameOrPassword: return "Missing username or a password. Please try again." case let .versionAlreadyInstalled(installedXcode): @@ -117,23 +123,62 @@ public final class XcodeInstaller { self.configuration = configuration self.xcodeList = xcodeList } + + public enum InstallationType { + case version(String) + case url(String, Path) + case latest + case latestPrerelease + } - public func install(_ versionString: String, _ urlString: String?) -> Promise { + public func install(_ installationType: InstallationType) -> Promise { return firstly { () -> Promise<(Xcode, URL)> in - guard let version = Version(xcodeVersion: versionString) ?? versionFromXcodeVersionFile() else { - throw Error.invalidVersion(versionString) - } - - if let installedXcode = Current.files.installedXcodes().first(where: { $0.version.isEqualWithoutBuildMetadataIdentifiers(to: version) }) { - throw Error.versionAlreadyInstalled(installedXcode) - } + switch installationType { + case .latest: + Current.logging.log("Updating...") + + return update() + .then { availableXcodes -> Promise<(Xcode, URL)> in + guard let latestNonPrereleaseXcode = availableXcodes.filter(\.isNotPrerelease).sorted(\.version).last else { + throw Error.noNonPrereleaseVersionAvailable + } + Current.logging.log("Latest non-prerelease version available is \(latestNonPrereleaseXcode.version.xcodeDescription)") + + if let installedXcode = Current.files.installedXcodes().first(where: { $0.version.isEqualWithoutBuildMetadataIdentifiers(to: latestNonPrereleaseXcode.version) }) { + throw Error.versionAlreadyInstalled(installedXcode) + } - if let urlString = urlString { - let url = URL(fileURLWithPath: urlString, relativeTo: nil) - let xcode = Xcode(version: version, url: url, filename: String(url.path.suffix(fromLast: "/"))) - return Promise.value((xcode, url)) - } - else { + return self.downloadXcode(version: latestNonPrereleaseXcode.version) + } + case .latestPrerelease: + Current.logging.log("Updating...") + + return update() + .then { availableXcodes -> Promise<(Xcode, URL)> in + guard let latestPrereleaseXcode = availableXcodes.filter(\.isPrerelease).sorted(\.version).last else { + throw Error.noNonPrereleaseVersionAvailable + } + Current.logging.log("Latest prerelease version available is \(latestPrereleaseXcode.version.xcodeDescription)") + + if let installedXcode = Current.files.installedXcodes().first(where: { $0.version.isEqualWithoutBuildMetadataIdentifiers(to: latestPrereleaseXcode.version) }) { + throw Error.versionAlreadyInstalled(installedXcode) + } + + return self.downloadXcode(version: latestPrereleaseXcode.version) + } + case .url(let versionString, let path): + guard let version = Version(xcodeVersion: versionString) ?? versionFromXcodeVersionFile() else { + throw Error.invalidVersion(versionString) + } + let xcode = Xcode(version: version, url: path.url, filename: String(path.string.suffix(fromLast: "/"))) + return Promise.value((xcode, path.url)) + case .version(let versionString): + guard let version = Version(xcodeVersion: versionString) ?? versionFromXcodeVersionFile() else { + throw Error.invalidVersion(versionString) + } + if let installedXcode = Current.files.installedXcodes().first(where: { $0.version.isEqualWithoutBuildMetadataIdentifiers(to: version) }) { + throw Error.versionAlreadyInstalled(installedXcode) + } return self.downloadXcode(version: version) } } @@ -383,19 +428,23 @@ public final class XcodeInstaller { } } - public func updateAndPrint() -> Promise { + func update() -> Promise<[Xcode]> { return firstly { () -> Promise in loginIfNeeded() } .then { () -> Promise<[Xcode]> in self.xcodeList.update() } - .then { xcodes -> Promise in - self.printAvailableXcodes(xcodes, installed: Current.files.installedXcodes()) - } - .done { - Current.shell.exit(0) - } + } + + public func updateAndPrint() -> Promise { + update() + .then { xcodes -> Promise in + self.printAvailableXcodes(xcodes, installed: Current.files.installedXcodes()) + } + .done { + Current.shell.exit(0) + } } public func printAvailableXcodes(_ xcodes: [Xcode], installed installedXcodes: [InstalledXcode]) -> Promise { diff --git a/Sources/xcodes/main.swift b/Sources/xcodes/main.swift index 0e3e334..4f41486 100644 --- a/Sources/xcodes/main.swift +++ b/Sources/xcodes/main.swift @@ -93,17 +93,32 @@ let update = Command(usage: "update", app.add(subCommand: update) let urlFlag = Flag(longName: "url", type: String.self, description: "Local path to Xcode .xip") +let latestFlag = Flag(longName: "latest", value: false, description: "Update and then install the latest non-prerelease version available.") +let latestPrereleaseFlag = Flag(longName: "latest-prerelease", value: false, description: "Update and then install the latest prerelease version available, including GM seeds and GMs.") let install = Command(usage: "install ", shortMessage: "Download and install a specific version of Xcode", - flags: [urlFlag], + flags: [urlFlag, latestFlag, latestPrereleaseFlag], example: """ xcodes install 10.2.1 xcodes install 11 Beta 7 xcodes install 11.2 GM seed xcodes install 9.0 --url ~/Archive/Xcode_9.xip + xcodes install --latest-prerelease """) { flags, args in let versionString = args.joined(separator: " ") - installer.install(versionString, flags.getString(name: "url")) + + let installation: XcodeInstaller.InstallationType + if flags.getBool(name: "latest") == true { + installation = .latest + } else if flags.getBool(name: "latest-prerelease") == true { + installation = .latestPrerelease + } else if let url = flags.getString(name: "url"), let path = Path(url) { + installation = .url(versionString, path) + } else { + installation = .version(versionString) + } + + installer.install(installation) .catch { error in switch error { case Process.PMKError.execution(let process, let standardOutput, let standardError): diff --git a/Tests/XcodesKitTests/XcodesKitTests.swift b/Tests/XcodesKitTests/XcodesKitTests.swift index 7dc2203..6fea753 100644 --- a/Tests/XcodesKitTests/XcodesKitTests.swift +++ b/Tests/XcodesKitTests/XcodesKitTests.swift @@ -195,7 +195,7 @@ final class XcodesKitTests: XCTestCase { let expectation = self.expectation(description: "Finished") - installer.install("0.0.0", nil) + installer.install(.version("0.0.0")) .ensure { let url = URL(fileURLWithPath: "LogOutput-FullHappyPath.txt", relativeTo: URL(fileURLWithPath: #file).deletingLastPathComponent()) From 4bb7d75ea816bdf8713e67efdd316cef940006f2 Mon Sep 17 00:00:00 2001 From: Brandon Evans Date: Thu, 6 Aug 2020 14:35:39 -0600 Subject: [PATCH 2/4] Parse Xcode release dates --- Sources/XcodesKit/DateFormatter+.swift | 17 +++++++++++++++++ Sources/XcodesKit/Models.swift | 5 ++++- Sources/XcodesKit/XcodeInstaller.swift | 2 +- Sources/XcodesKit/XcodeList.swift | 9 ++++++--- Tests/XcodesKitTests/XcodesKitTests.swift | 18 ++++++++++-------- 5 files changed, 38 insertions(+), 13 deletions(-) create mode 100644 Sources/XcodesKit/DateFormatter+.swift diff --git a/Sources/XcodesKit/DateFormatter+.swift b/Sources/XcodesKit/DateFormatter+.swift new file mode 100644 index 0000000..a9eb59e --- /dev/null +++ b/Sources/XcodesKit/DateFormatter+.swift @@ -0,0 +1,17 @@ +import Foundation + +extension DateFormatter { + /// Date format used in JSON returned from `URL.downloads` + static let downloadsDateModified: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "MM/dd/yy HH:mm" + return formatter + }() + + /// Date format used in HTML returned from `URL.download` + static let downloadsReleaseDate: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "MMMM d, yyyy" + return formatter + }() +} diff --git a/Sources/XcodesKit/Models.swift b/Sources/XcodesKit/Models.swift index 51edd44..2670373 100644 --- a/Sources/XcodesKit/Models.swift +++ b/Sources/XcodesKit/Models.swift @@ -43,14 +43,16 @@ public struct Xcode: Codable { public let version: Version public let url: URL public let filename: String + public let releaseDate: Date? var isPrerelease: Bool { version.prereleaseIdentifiers.isEmpty == false } var isNotPrerelease: Bool { version.prereleaseIdentifiers.isEmpty == true } - public init(version: Version, url: URL, filename: String) { + public init(version: Version, url: URL, filename: String, releaseDate: Date?) { self.version = version self.url = url self.filename = filename + self.releaseDate = releaseDate } } @@ -61,6 +63,7 @@ struct Downloads: Codable { public struct Download: Codable { public let name: String public let files: [File] + public let dateModified: Date public struct File: Codable { public let remotePath: String diff --git a/Sources/XcodesKit/XcodeInstaller.swift b/Sources/XcodesKit/XcodeInstaller.swift index 5626a65..927b579 100644 --- a/Sources/XcodesKit/XcodeInstaller.swift +++ b/Sources/XcodesKit/XcodeInstaller.swift @@ -170,7 +170,7 @@ public final class XcodeInstaller { guard let version = Version(xcodeVersion: versionString) ?? versionFromXcodeVersionFile() else { throw Error.invalidVersion(versionString) } - let xcode = Xcode(version: version, url: path.url, filename: String(path.string.suffix(fromLast: "/"))) + let xcode = Xcode(version: version, url: path.url, filename: String(path.string.suffix(fromLast: "/")), releaseDate: nil) return Promise.value((xcode, path.url)) case .version(let versionString): guard let version = Version(xcodeVersion: versionString) ?? versionFromXcodeVersionFile() else { diff --git a/Sources/XcodesKit/XcodeList.swift b/Sources/XcodesKit/XcodeList.swift index b9feb3d..3cc35ba 100644 --- a/Sources/XcodesKit/XcodeList.swift +++ b/Sources/XcodesKit/XcodeList.swift @@ -54,7 +54,9 @@ extension XcodeList { Current.network.dataTask(with: URLRequest.downloads) } .map { (data, response) -> [Xcode] in - let downloads = try JSONDecoder().decode(Downloads.self, from: data) + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .formatted(.downloadsDateModified) + let downloads = try decoder.decode(Downloads.self, from: data) let xcodes = downloads .downloads .filter { $0.name.range(of: "^Xcode [0-9]", options: .regularExpression) != nil } @@ -66,7 +68,7 @@ extension XcodeList { let version = Version(xcodeVersion: download.name) else { return nil } - return Xcode(version: version, url: url, filename: String(xcodeFile.remotePath.suffix(fromLast: "/"))) + return Xcode(version: version, url: url, filename: String(xcodeFile.remotePath.suffix(fromLast: "/")), releaseDate: download.dateModified) } return xcodes } @@ -88,6 +90,7 @@ extension XcodeList { guard let xcodeHeader = try document.select("h2:containsOwn(Xcode)").first(), let productBuildVersion = try xcodeHeader.parent()?.select("li:contains(Build)").text().replacingOccurrences(of: "Build", with: ""), + let releaseDateString = try xcodeHeader.parent()?.select("li:contains(Released)").text().replacingOccurrences(of: "Released", with: ""), let version = Version(xcodeVersion: try xcodeHeader.text(), buildMetadataIdentifier: productBuildVersion), let path = try document.select(".direct-download[href*=xip]").first()?.attr("href"), let url = URL(string: "https://developer.apple.com" + path) @@ -95,6 +98,6 @@ extension XcodeList { let filename = String(path.suffix(fromLast: "/")) - return [Xcode(version: version, url: url, filename: filename)] + return [Xcode(version: version, url: url, filename: filename, releaseDate: DateFormatter.downloadsReleaseDate.date(from: releaseDateString))] } } diff --git a/Tests/XcodesKitTests/XcodesKitTests.swift b/Tests/XcodesKitTests/XcodesKitTests.swift index 6fea753..726d0ad 100644 --- a/Tests/XcodesKitTests/XcodesKitTests.swift +++ b/Tests/XcodesKitTests/XcodesKitTests.swift @@ -50,7 +50,7 @@ final class XcodesKitTests: XCTestCase { return (Progress(), Promise(error: PMKError.invalidCallingConvention)) } - let xcode = Xcode(version: Version("0.0.0")!, url: URL(string: "https://apple.com/xcode.xip")!, filename: "mock.xip") + let xcode = Xcode(version: Version("0.0.0")!, url: URL(string: "https://apple.com/xcode.xip")!, filename: "mock.xip", releaseDate: nil) installer.downloadOrUseExistingArchive(for: xcode, progressChanged: { _ in }) .tap { result in guard case .fulfilled(let value) = result else { XCTFail("downloadOrUseExistingArchive rejected."); return } @@ -68,7 +68,7 @@ final class XcodesKitTests: XCTestCase { return (Progress(), Promise.value((destination, HTTPURLResponse(url: url.pmkRequest.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!))) } - let xcode = Xcode(version: Version("0.0.0")!, url: URL(string: "https://apple.com/xcode.xip")!, filename: "mock.xip") + let xcode = Xcode(version: Version("0.0.0")!, url: URL(string: "https://apple.com/xcode.xip")!, filename: "mock.xip", releaseDate: nil) installer.downloadOrUseExistingArchive(for: xcode, progressChanged: { _ in }) .tap { result in guard case .fulfilled(let value) = result else { XCTFail("downloadOrUseExistingArchive rejected."); return } @@ -81,7 +81,7 @@ final class XcodesKitTests: XCTestCase { func test_InstallArchivedXcode_SecurityAssessmentFails_Throws() { Current.shell.spctlAssess = { _ in return Promise(error: Process.PMKError.execution(process: Process(), standardOutput: nil, standardError: nil)) } - let xcode = Xcode(version: Version("0.0.0")!, url: URL(fileURLWithPath: "/"), filename: "mock") + let xcode = Xcode(version: Version("0.0.0")!, url: URL(fileURLWithPath: "/"), filename: "mock", releaseDate: nil) let installedXcode = InstalledXcode(path: Path("/Applications/Xcode-0.0.0.app")!)! installer.installArchivedXcode(xcode, at: URL(fileURLWithPath: "/Xcode-0.0.0.xip")) .catch { error in XCTAssertEqual(error as! XcodeInstaller.Error, XcodeInstaller.Error.failedSecurityAssessment(xcode: installedXcode, output: "")) } @@ -90,7 +90,7 @@ final class XcodesKitTests: XCTestCase { func test_InstallArchivedXcode_VerifySigningCertificateFails_Throws() { Current.shell.codesignVerify = { _ in return Promise(error: Process.PMKError.execution(process: Process(), standardOutput: nil, standardError: nil)) } - let xcode = Xcode(version: Version("0.0.0")!, url: URL(fileURLWithPath: "/"), filename: "mock") + let xcode = Xcode(version: Version("0.0.0")!, url: URL(fileURLWithPath: "/"), filename: "mock", releaseDate: nil) installer.installArchivedXcode(xcode, at: URL(fileURLWithPath: "/Xcode-0.0.0.xip")) .catch { error in XCTAssertEqual(error as! XcodeInstaller.Error, XcodeInstaller.Error.codesignVerifyFailed(output: "")) } } @@ -98,7 +98,7 @@ final class XcodesKitTests: XCTestCase { func test_InstallArchivedXcode_VerifySigningCertificateDoesntMatch_Throws() { Current.shell.codesignVerify = { _ in return Promise.value((0, "", "")) } - let xcode = Xcode(version: Version("0.0.0")!, url: URL(fileURLWithPath: "/"), filename: "mock") + let xcode = Xcode(version: Version("0.0.0")!, url: URL(fileURLWithPath: "/"), filename: "mock", releaseDate: nil) installer.installArchivedXcode(xcode, at: URL(fileURLWithPath: "/Xcode-0.0.0.xip")) .catch { error in XCTAssertEqual(error as! XcodeInstaller.Error, XcodeInstaller.Error.unexpectedCodeSigningIdentity(identifier: "", certificateAuthority: [])) } } @@ -110,7 +110,7 @@ final class XcodesKitTests: XCTestCase { return URL(fileURLWithPath: "\(NSHomeDirectory())/.Trash/\(itemURL.lastPathComponent)") } - let xcode = Xcode(version: Version("0.0.0")!, url: URL(fileURLWithPath: "/"), filename: "mock") + let xcode = Xcode(version: Version("0.0.0")!, url: URL(fileURLWithPath: "/"), filename: "mock", releaseDate: nil) let xipURL = URL(fileURLWithPath: "/Xcode-0.0.0.xip") installer.installArchivedXcode(xcode, at: xipURL) .ensure { XCTAssertEqual(trashedItemAtURL, xipURL) } @@ -135,8 +135,10 @@ final class XcodesKitTests: XCTestCase { // It's an available release version Current.network.dataTask = { url in if url.pmkRequest.url! == URLRequest.downloads.url! { - let downloads = Downloads(downloads: [Download(name: "Xcode 0.0.0", files: [Download.File(remotePath: "https://apple.com/xcode.xip")])]) - let downloadsData = try! JSONEncoder().encode(downloads) + let downloads = Downloads(downloads: [Download(name: "Xcode 0.0.0", files: [Download.File(remotePath: "https://apple.com/xcode.xip")], dateModified: Date())]) + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .formatted(.downloadsDateModified) + let downloadsData = try! encoder.encode(downloads) return Promise.value((data: downloadsData, response: HTTPURLResponse(url: url.pmkRequest.url!, statusCode: 200, httpVersion: nil, headerFields: nil)!)) } From 82fc7efc3aa8b7923765dcfb5a4374137386a568 Mon Sep 17 00:00:00 2001 From: Brandon Evans Date: Thu, 6 Aug 2020 15:24:34 -0600 Subject: [PATCH 3/4] Sort prerelease versions by release date --- Sources/XcodesKit/Models.swift | 3 -- Sources/XcodesKit/Version+.swift | 3 ++ Sources/XcodesKit/XcodeInstaller.swift | 46 +++++++++++++++++--------- 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/Sources/XcodesKit/Models.swift b/Sources/XcodesKit/Models.swift index 2670373..fe0fbb8 100644 --- a/Sources/XcodesKit/Models.swift +++ b/Sources/XcodesKit/Models.swift @@ -44,9 +44,6 @@ public struct Xcode: Codable { public let url: URL public let filename: String public let releaseDate: Date? - - var isPrerelease: Bool { version.prereleaseIdentifiers.isEmpty == false } - var isNotPrerelease: Bool { version.prereleaseIdentifiers.isEmpty == true } public init(version: Version, url: URL, filename: String, releaseDate: Date?) { self.version = version diff --git a/Sources/XcodesKit/Version+.swift b/Sources/XcodesKit/Version+.swift index d5cd1ee..c4ecec3 100644 --- a/Sources/XcodesKit/Version+.swift +++ b/Sources/XcodesKit/Version+.swift @@ -45,4 +45,7 @@ public extension Version { } return base } + + var isPrerelease: Bool { prereleaseIdentifiers.isEmpty == false } + var isNotPrerelease: Bool { prereleaseIdentifiers.isEmpty == true } } diff --git a/Sources/XcodesKit/XcodeInstaller.swift b/Sources/XcodesKit/XcodeInstaller.swift index 927b579..a57d40d 100644 --- a/Sources/XcodesKit/XcodeInstaller.swift +++ b/Sources/XcodesKit/XcodeInstaller.swift @@ -139,7 +139,7 @@ public final class XcodeInstaller { return update() .then { availableXcodes -> Promise<(Xcode, URL)> in - guard let latestNonPrereleaseXcode = availableXcodes.filter(\.isNotPrerelease).sorted(\.version).last else { + guard let latestNonPrereleaseXcode = availableXcodes.filter(\.version.isNotPrerelease).sorted(\.version).last else { throw Error.noNonPrereleaseVersionAvailable } Current.logging.log("Latest non-prerelease version available is \(latestNonPrereleaseXcode.version.xcodeDescription)") @@ -155,7 +155,12 @@ public final class XcodeInstaller { return update() .then { availableXcodes -> Promise<(Xcode, URL)> in - guard let latestPrereleaseXcode = availableXcodes.filter(\.isPrerelease).sorted(\.version).last else { + guard let latestPrereleaseXcode = availableXcodes + .filter({ $0.version.isPrerelease }) + .filter({ $0.releaseDate != nil }) + .sorted(by: { $0.releaseDate! < $1.releaseDate! }) + .last + else { throw Error.noNonPrereleaseVersionAvailable } Current.logging.log("Latest prerelease version available is \(latestPrereleaseXcode.version.xcodeDescription)") @@ -448,20 +453,25 @@ public final class XcodeInstaller { } public func printAvailableXcodes(_ xcodes: [Xcode], installed installedXcodes: [InstalledXcode]) -> Promise { - var allXcodeVersions = xcodes.map { $0.version } + struct ReleasedVersion { + let version: Version + let releaseDate: Date? + } + + var allXcodeVersions = xcodes.map { ReleasedVersion(version: $0.version, releaseDate: $0.releaseDate) } for installedXcode in installedXcodes { // If an installed version isn't listed online, add the installed version - if !allXcodeVersions.contains(where: { version in - version.isEquivalentForDeterminingIfInstalled(toInstalled: installedXcode.version) + if !allXcodeVersions.contains(where: { releasedVersion in + releasedVersion.version.isEquivalentForDeterminingIfInstalled(toInstalled: installedXcode.version) }) { - allXcodeVersions.append(installedXcode.version) + allXcodeVersions.append(ReleasedVersion(version: installedXcode.version, releaseDate: nil)) } // 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 with build metadata - else if let index = allXcodeVersions.firstIndex(where: { version in - version.isEquivalentForDeterminingIfInstalled(toInstalled: installedXcode.version) && - version.buildMetadataIdentifiers.isEmpty + else if let index = allXcodeVersions.firstIndex(where: { releasedVersion in + releasedVersion.version.isEquivalentForDeterminingIfInstalled(toInstalled: installedXcode.version) && + releasedVersion.version.buildMetadataIdentifiers.isEmpty }) { - allXcodeVersions[index] = installedXcode.version + allXcodeVersions[index] = ReleasedVersion(version: installedXcode.version, releaseDate: nil) } } @@ -470,11 +480,17 @@ public final class XcodeInstaller { let selectedInstalledXcodeVersion = installedXcodes.first { output.out.hasPrefix($0.path.string) }.map { $0.version } allXcodeVersions - .sorted { $0 < $1 } - .forEach { xcodeVersion in - var output = xcodeVersion.xcodeDescription - if installedXcodes.contains(where: { xcodeVersion.isEquivalentForDeterminingIfInstalled(toInstalled: $0.version) }) { - if xcodeVersion == selectedInstalledXcodeVersion { + .sorted { first, second -> Bool in + // Sort prereleases by release date, otherwise sort by version + if first.version.isPrerelease, second.version.isPrerelease, let firstDate = first.releaseDate, let secondDate = second.releaseDate { + return firstDate < secondDate + } + return first.version < second.version + } + .forEach { releasedVersion in + var output = releasedVersion.version.xcodeDescription + if installedXcodes.contains(where: { releasedVersion.version.isEquivalentForDeterminingIfInstalled(toInstalled: $0.version) }) { + if releasedVersion.version == selectedInstalledXcodeVersion { output += " (Installed, Selected)" } else { From 2b802c0237e8d496bfdb80a9df1a7cc17ccce105 Mon Sep 17 00:00:00 2001 From: Brandon Evans Date: Tue, 25 Aug 2020 21:48:05 -0600 Subject: [PATCH 4/4] Save Guaka patch update --- Package.resolved | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Package.resolved b/Package.resolved index db38534..9e562b2 100644 --- a/Package.resolved +++ b/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/nsomar/Guaka.git", "state": { "branch": null, - "revision": "90c03a9e4c894808a8882a16ccf56743b4b2d40c", - "version": "0.4.0" + "revision": "6fb29b2378166a30d72120980e1c099c664598de", + "version": "0.4.1" } }, { @@ -60,8 +60,8 @@ "repositoryURL": "https://github.com/getGuaka/StringScanner.git", "state": { "branch": null, - "revision": "39f9b77e37c69ab6fec14aadf79a066468ce63e3", - "version": "0.4.0" + "revision": "de1685ad202cb586d626ed52d6de904dd34189f3", + "version": "0.4.1" } }, {