From ab87ffd539520c82fdca361e4b38ff68eec4d924 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Thu, 13 Mar 2025 15:22:56 -0400 Subject: [PATCH 01/12] DO NOT MERGE: initial conversion of swift toolchain release to OpenAPI --- Sources/SwiftlyCore/HTTPClient.swift | 139 ++++++++++-------- Sources/SwiftlyCore/SwiftlyCore.swift | 4 +- .../SwiftlyCore/openapi-generator-config.yaml | 2 +- Tests/SwiftlyTests/HTTPClientTests.swift | 12 +- Tests/SwiftlyTests/SwiftlyTests.swift | 18 +++ 5 files changed, 105 insertions(+), 70 deletions(-) diff --git a/Sources/SwiftlyCore/HTTPClient.swift b/Sources/SwiftlyCore/HTTPClient.swift index 85ee37be..ab0b7df4 100644 --- a/Sources/SwiftlyCore/HTTPClient.swift +++ b/Sources/SwiftlyCore/HTTPClient.swift @@ -53,6 +53,12 @@ extension Components.Schemas.SwiftlyReleasePlatformArtifacts { public protocol HTTPRequestExecutor { func execute(_ request: HTTPClientRequest, timeout: TimeAmount) async throws -> HTTPClientResponse func getCurrentSwiftlyRelease() async throws -> Components.Schemas.SwiftlyRelease + func getReleaseToolchains( + platform: PlatformDefinition, + arch a: Components.Schemas.Architecture?, + limit: Int?, + filter: ((ToolchainVersion.StableRelease) -> Bool)? + ) async throws -> [ToolchainVersion.StableRelease] } internal struct SwiftlyUserAgentMiddleware: ClientMiddleware { @@ -128,6 +134,61 @@ internal class HTTPRequestExecutorImpl: HTTPRequestExecutor { let response = try await client.getCurrentSwiftlyRelease() return try response.ok.body.json } + + public func getReleaseToolchains( + platform: PlatformDefinition, + arch a: Components.Schemas.Architecture? = nil, + limit: Int? = nil, + filter: ((ToolchainVersion.StableRelease) -> Bool)? = nil + ) async throws -> [ToolchainVersion.StableRelease] { + let arch = a ?? cpuArch + + let config = AsyncHTTPClientTransport.Configuration(client: self.httpClient, timeout: .seconds(30)) + let swiftlyUserAgent = SwiftlyUserAgentMiddleware() + + let client = Client( + serverURL: try Servers.Server1.url(), + transport: AsyncHTTPClientTransport(configuration: config), + middlewares: [swiftlyUserAgent] + ) + + let response = try await client.listReleases() + + var swiftOrgFiltered: [ToolchainVersion.StableRelease] = try response.ok.body.json.compactMap { swiftOrgRelease in + if platform.name != PlatformDefinition.macOS.name { + // If the platform isn't xcode then verify that there is an offering for this platform name and arch + guard let swiftOrgPlatform = swiftOrgRelease.platforms.first(where: { $0.matches(platform) }) else { + return nil + } + + guard case let archs = swiftOrgPlatform.archs, archs.contains(arch) else { + return nil + } + } + + guard let version = try? ToolchainVersion(parsing: swiftOrgRelease.stableName), + case let .stable(release) = version + else { + throw SwiftlyError(message: "error parsing swift.org release version: \(swiftOrgRelease.stableName)") + } + + if let filter { + guard filter(release) else { + return nil + } + } + + return release + } + + swiftOrgFiltered.sort(by: >) + + return if let limit = limit { + Array(swiftOrgFiltered.prefix(limit)) + } else { + swiftOrgFiltered + } + } } private func makeRequest(url: String) -> HTTPClientRequest { @@ -136,13 +197,21 @@ private func makeRequest(url: String) -> HTTPClientRequest { return request } -struct SwiftOrgPlatform: Codable { - var name: String - var archs: [String]? +extension Components.Schemas.Release { + var stableName: String { + let components = self.name.components(separatedBy: ".") + if components.count == 2 { + return self.name + ".0" + } else { + return self.name + } + } +} - /// platform is a mapping from the 'name' field of the swift.org platform object +extension Components.Schemas.Platform { + /// platformDef is a mapping from the 'name' field of the swift.org platform object /// to swiftly's PlatformDefinition, if possible. - var platform: PlatformDefinition? { + var platformDef: PlatformDefinition? { // NOTE: some of these platforms are represented on swift.org metadata, but not supported by swiftly and so they don't have constants in PlatformDefinition switch self.name { case "Ubuntu 14.04": @@ -181,7 +250,7 @@ struct SwiftOrgPlatform: Codable { } func matches(_ platform: PlatformDefinition) -> Bool { - guard let myPlatform = self.platform else { + guard let myPlatform = self.platformDef else { return false } @@ -189,20 +258,6 @@ struct SwiftOrgPlatform: Codable { } } -public struct SwiftOrgRelease: Codable { - var name: String - var platforms: [SwiftOrgPlatform] - - var stableName: String { - let components = self.name.components(separatedBy: ".") - if components.count == 2 { - return self.name + ".0" - } else { - return self.name - } - } -} - public struct SwiftOrgSnapshotList: Codable { var aarch64: [SwiftOrgSnapshot]? var x86_64: [SwiftOrgSnapshot]? @@ -291,49 +346,11 @@ public struct SwiftlyHTTPClient { /// limit (default unlimited). public func getReleaseToolchains( platform: PlatformDefinition, - arch a: String? = nil, + arch a: Components.Schemas.Architecture? = nil, limit: Int? = nil, filter: ((ToolchainVersion.StableRelease) -> Bool)? = nil ) async throws -> [ToolchainVersion.StableRelease] { - let arch = a ?? cpuArch - - let url = "https://www.swift.org/api/v1/install/releases.json" - let swiftOrgReleases: [SwiftOrgRelease] = try await self.getFromJSON(url: url, type: [SwiftOrgRelease].self) - - var swiftOrgFiltered: [ToolchainVersion.StableRelease] = try swiftOrgReleases.compactMap { swiftOrgRelease in - if platform.name != PlatformDefinition.macOS.name { - // If the platform isn't xcode then verify that there is an offering for this platform name and arch - guard let swiftOrgPlatform = swiftOrgRelease.platforms.first(where: { $0.matches(platform) }) else { - return nil - } - - guard let archs = swiftOrgPlatform.archs, archs.contains(arch) else { - return nil - } - } - - guard let version = try? ToolchainVersion(parsing: swiftOrgRelease.stableName), - case let .stable(release) = version - else { - throw SwiftlyError(message: "error parsing swift.org release version: \(swiftOrgRelease.stableName)") - } - - if let filter { - guard filter(release) else { - return nil - } - } - - return release - } - - swiftOrgFiltered.sort(by: >) - - return if let limit = limit { - Array(swiftOrgFiltered.prefix(limit)) - } else { - swiftOrgFiltered - } + try await SwiftlyCore.httpRequestExecutor.getReleaseToolchains(platform: platform, arch: a, limit: limit, filter: filter) } public struct SnapshotBranchNotFoundError: LocalizedError { @@ -349,7 +366,7 @@ public struct SwiftlyHTTPClient { limit: Int? = nil, filter: ((ToolchainVersion.Snapshot) -> Bool)? = nil ) async throws -> [ToolchainVersion.Snapshot] { - let arch = a ?? cpuArch + let arch = a ?? String(describing: cpuArch) let platformName = if platform.name == PlatformDefinition.macOS.name { "macos" diff --git a/Sources/SwiftlyCore/SwiftlyCore.swift b/Sources/SwiftlyCore/SwiftlyCore.swift index f5b1b685..686e8177 100644 --- a/Sources/SwiftlyCore/SwiftlyCore.swift +++ b/Sources/SwiftlyCore/SwiftlyCore.swift @@ -53,9 +53,9 @@ public func readLine(prompt: String) -> String? { } #if arch(x86_64) -public let cpuArch = "x86_64" +public let cpuArch = Components.Schemas.Architecture(value1: Components.Schemas.KnownArchitecture.x8664) #elseif arch(arm64) -public let cpuArch = "aarch64" +public let cpuArch = Components.Schemas.Architecture(value1: Components.Schemas.KnownArchitecture.aarch64) #else #error("Unsupported processor architecture") #endif diff --git a/Sources/SwiftlyCore/openapi-generator-config.yaml b/Sources/SwiftlyCore/openapi-generator-config.yaml index c8f6e7a7..15dbf7f5 100644 --- a/Sources/SwiftlyCore/openapi-generator-config.yaml +++ b/Sources/SwiftlyCore/openapi-generator-config.yaml @@ -4,4 +4,4 @@ generate: namingStrategy: idiomatic accessModifier: public filter: - tags: ["Swiftly"] + tags: ["Swiftly", "Toolchains"] diff --git a/Tests/SwiftlyTests/HTTPClientTests.swift b/Tests/SwiftlyTests/HTTPClientTests.swift index 645875d5..b71eb4ac 100644 --- a/Tests/SwiftlyTests/HTTPClientTests.swift +++ b/Tests/SwiftlyTests/HTTPClientTests.swift @@ -6,9 +6,9 @@ final class HTTPClientTests: SwiftlyTests { func testGet() async throws { // GIVEN: we have a swiftly http client // WHEN: we make get request for a particular type of JSON - var releases: [SwiftOrgRelease] = try await SwiftlyCore.httpClient.getFromJSON( + var releases: [Components.Schemas.Release] = try await SwiftlyCore.httpClient.getFromJSON( url: "https://www.swift.org/api/v1/install/releases.json", - type: [SwiftOrgRelease].self, + type: [Components.Schemas.Release].self, headers: [:] ) // THEN: we get a decoded JSON response @@ -20,7 +20,7 @@ final class HTTPClientTests: SwiftlyTests { do { releases = try await SwiftlyCore.httpClient.getFromJSON( url: "https://www.swift.org/api/v1/install/releases-invalid.json", - type: [SwiftOrgRelease].self, + type: [Components.Schemas.Release].self, headers: [:] ) } catch { @@ -35,7 +35,7 @@ final class HTTPClientTests: SwiftlyTests { do { releases = try await SwiftlyCore.httpClient.getFromJSON( url: "https://invalid.swift.org/api/v1/install/releases.json", - type: [SwiftOrgRelease].self, + type: [Components.Schemas.Release].self, headers: [:] ) } catch { @@ -74,7 +74,7 @@ final class HTTPClientTests: SwiftlyTests { .release(major: 6, minor: 0), // This is available in swift.org API ] - for arch in ["x86_64", "aarch64"] { + for arch in [Components.Schemas.Architecture(value1: Components.Schemas.KnownArchitecture.x8664), Components.Schemas.Architecture(value1: Components.Schemas.KnownArchitecture.aarch64)] { for platform in supportedPlatforms { // GIVEN: we have a swiftly http client with swift.org metadata capability // WHEN: we ask for the first five releases of a supported platform in a supported arch @@ -87,7 +87,7 @@ final class HTTPClientTests: SwiftlyTests { for branch in branches { // GIVEN: we have a swiftly http client with swift.org metadata capability // WHEN: we ask for the first five snapshots on a branch for a supported platform and arch - let snapshots = try await SwiftlyCore.httpClient.getSnapshotToolchains(platform: platform, arch: arch, branch: branch, limit: 5) + let snapshots = try await SwiftlyCore.httpClient.getSnapshotToolchains(platform: platform, arch: String(describing: arch), branch: branch, limit: 5) // THEN: we get at least 3 releases XCTAssertTrue(3 <= snapshots.count) } diff --git a/Tests/SwiftlyTests/SwiftlyTests.swift b/Tests/SwiftlyTests/SwiftlyTests.swift index 3f1fefad..22819674 100644 --- a/Tests/SwiftlyTests/SwiftlyTests.swift +++ b/Tests/SwiftlyTests/SwiftlyTests.swift @@ -467,6 +467,15 @@ private struct MockHTTPRequestExecutor: HTTPRequestExecutor { public func getCurrentSwiftlyRelease() async throws -> Components.Schemas.SwiftlyRelease { throw SwiftlyTestError(message: "Mocking of fetching the current swiftly release is not implemented in MockHTTPRequestExecutor.") } + + public func getReleaseToolchains( + platform _: PlatformDefinition, + arch _: Components.Schemas.Architecture?, + limit _: Int?, + filter _: ((ToolchainVersion.StableRelease) -> Bool)? + ) async throws -> [ToolchainVersion.StableRelease] { + throw SwiftlyTestError(message: "Mocking of fetching the release toolchains is not implemented in MockHTTPRequestExecutor.") + } } /// An `HTTPRequestExecutor` which will return a mocked response to any toolchain download requests. @@ -506,6 +515,15 @@ public class MockToolchainDownloader: HTTPRequestExecutor { return release } + public func getReleaseToolchains( + platform _: PlatformDefinition, + arch _: Components.Schemas.Architecture?, + limit _: Int?, + filter _: ((ToolchainVersion.StableRelease) -> Bool)? + ) async throws -> [ToolchainVersion.StableRelease] { + throw SwiftlyTestError(message: "Mocking of the fetching of release toolchains is not implemented by the MockToolchainDownloader") + } + public func execute(_ request: HTTPClientRequest, timeout: TimeAmount) async throws -> HTTPClientResponse { guard let url = URL(string: request.url) else { throw SwiftlyTestError(message: "invalid request URL: \(request.url)") From 354321d941ee6293ccd989777effb095a6a6d3fe Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Fri, 14 Mar 2025 10:40:33 -0400 Subject: [PATCH 02/12] Fix HTTPClientTests failures --- Sources/SwiftlyCore/HTTPClient.swift | 5 +++++ Sources/SwiftlyCore/SwiftlyCore.swift | 4 ++-- Tests/SwiftlyTests/HTTPClientTests.swift | 4 ++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Sources/SwiftlyCore/HTTPClient.swift b/Sources/SwiftlyCore/HTTPClient.swift index ab0b7df4..3488f96e 100644 --- a/Sources/SwiftlyCore/HTTPClient.swift +++ b/Sources/SwiftlyCore/HTTPClient.swift @@ -208,6 +208,11 @@ extension Components.Schemas.Release { } } +extension Components.Schemas.Architecture { + static var x8664: Components.Schemas.Architecture = .init(value1: Components.Schemas.KnownArchitecture.x8664, value2: "x86_64") + static var aarch64: Components.Schemas.Architecture = .init(value1: Components.Schemas.KnownArchitecture.aarch64, value2: "aarch64") +} + extension Components.Schemas.Platform { /// platformDef is a mapping from the 'name' field of the swift.org platform object /// to swiftly's PlatformDefinition, if possible. diff --git a/Sources/SwiftlyCore/SwiftlyCore.swift b/Sources/SwiftlyCore/SwiftlyCore.swift index 686e8177..76a104b8 100644 --- a/Sources/SwiftlyCore/SwiftlyCore.swift +++ b/Sources/SwiftlyCore/SwiftlyCore.swift @@ -53,9 +53,9 @@ public func readLine(prompt: String) -> String? { } #if arch(x86_64) -public let cpuArch = Components.Schemas.Architecture(value1: Components.Schemas.KnownArchitecture.x8664) +public let cpuArch = Components.Schemas.Architecture.x8664 #elseif arch(arm64) -public let cpuArch = Components.Schemas.Architecture(value1: Components.Schemas.KnownArchitecture.aarch64) +public let cpuArch = Components.Schemas.Architecture.aarch64 #else #error("Unsupported processor architecture") #endif diff --git a/Tests/SwiftlyTests/HTTPClientTests.swift b/Tests/SwiftlyTests/HTTPClientTests.swift index b71eb4ac..275e9ab2 100644 --- a/Tests/SwiftlyTests/HTTPClientTests.swift +++ b/Tests/SwiftlyTests/HTTPClientTests.swift @@ -74,7 +74,7 @@ final class HTTPClientTests: SwiftlyTests { .release(major: 6, minor: 0), // This is available in swift.org API ] - for arch in [Components.Schemas.Architecture(value1: Components.Schemas.KnownArchitecture.x8664), Components.Schemas.Architecture(value1: Components.Schemas.KnownArchitecture.aarch64)] { + for arch in [Components.Schemas.Architecture.x8664, Components.Schemas.Architecture.aarch64] { for platform in supportedPlatforms { // GIVEN: we have a swiftly http client with swift.org metadata capability // WHEN: we ask for the first five releases of a supported platform in a supported arch @@ -87,7 +87,7 @@ final class HTTPClientTests: SwiftlyTests { for branch in branches { // GIVEN: we have a swiftly http client with swift.org metadata capability // WHEN: we ask for the first five snapshots on a branch for a supported platform and arch - let snapshots = try await SwiftlyCore.httpClient.getSnapshotToolchains(platform: platform, arch: String(describing: arch), branch: branch, limit: 5) + let snapshots = try await SwiftlyCore.httpClient.getSnapshotToolchains(platform: platform, arch: arch.value2!, branch: branch, limit: 5) // THEN: we get at least 3 releases XCTAssertTrue(3 <= snapshots.count) } From a72ce702c8a0c47cdfd563ae26a7d8fdae406799 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Fri, 14 Mar 2025 12:55:46 -0400 Subject: [PATCH 03/12] Rework the layer that does the filtering of releases Add release mocking to the toolchain downloader with a mocked set of releases --- Sources/SwiftlyCore/HTTPClient.swift | 90 ++++++++++++------------- Tests/SwiftlyTests/InstallTests.swift | 1 + Tests/SwiftlyTests/SwiftlyTests.swift | 94 ++++++++++++++++++++------- 3 files changed, 113 insertions(+), 72 deletions(-) diff --git a/Sources/SwiftlyCore/HTTPClient.swift b/Sources/SwiftlyCore/HTTPClient.swift index 3488f96e..f081b9ef 100644 --- a/Sources/SwiftlyCore/HTTPClient.swift +++ b/Sources/SwiftlyCore/HTTPClient.swift @@ -53,12 +53,7 @@ extension Components.Schemas.SwiftlyReleasePlatformArtifacts { public protocol HTTPRequestExecutor { func execute(_ request: HTTPClientRequest, timeout: TimeAmount) async throws -> HTTPClientResponse func getCurrentSwiftlyRelease() async throws -> Components.Schemas.SwiftlyRelease - func getReleaseToolchains( - platform: PlatformDefinition, - arch a: Components.Schemas.Architecture?, - limit: Int?, - filter: ((ToolchainVersion.StableRelease) -> Bool)? - ) async throws -> [ToolchainVersion.StableRelease] + func getReleaseToolchains() async throws -> [Components.Schemas.Release] } internal struct SwiftlyUserAgentMiddleware: ClientMiddleware { @@ -135,14 +130,7 @@ internal class HTTPRequestExecutorImpl: HTTPRequestExecutor { return try response.ok.body.json } - public func getReleaseToolchains( - platform: PlatformDefinition, - arch a: Components.Schemas.Architecture? = nil, - limit: Int? = nil, - filter: ((ToolchainVersion.StableRelease) -> Bool)? = nil - ) async throws -> [ToolchainVersion.StableRelease] { - let arch = a ?? cpuArch - + public func getReleaseToolchains() async throws -> [Components.Schemas.Release] { let config = AsyncHTTPClientTransport.Configuration(client: self.httpClient, timeout: .seconds(30)) let swiftlyUserAgent = SwiftlyUserAgentMiddleware() @@ -154,40 +142,7 @@ internal class HTTPRequestExecutorImpl: HTTPRequestExecutor { let response = try await client.listReleases() - var swiftOrgFiltered: [ToolchainVersion.StableRelease] = try response.ok.body.json.compactMap { swiftOrgRelease in - if platform.name != PlatformDefinition.macOS.name { - // If the platform isn't xcode then verify that there is an offering for this platform name and arch - guard let swiftOrgPlatform = swiftOrgRelease.platforms.first(where: { $0.matches(platform) }) else { - return nil - } - - guard case let archs = swiftOrgPlatform.archs, archs.contains(arch) else { - return nil - } - } - - guard let version = try? ToolchainVersion(parsing: swiftOrgRelease.stableName), - case let .stable(release) = version - else { - throw SwiftlyError(message: "error parsing swift.org release version: \(swiftOrgRelease.stableName)") - } - - if let filter { - guard filter(release) else { - return nil - } - } - - return release - } - - swiftOrgFiltered.sort(by: >) - - return if let limit = limit { - Array(swiftOrgFiltered.prefix(limit)) - } else { - swiftOrgFiltered - } + return try response.ok.body.json } } @@ -355,7 +310,44 @@ public struct SwiftlyHTTPClient { limit: Int? = nil, filter: ((ToolchainVersion.StableRelease) -> Bool)? = nil ) async throws -> [ToolchainVersion.StableRelease] { - try await SwiftlyCore.httpRequestExecutor.getReleaseToolchains(platform: platform, arch: a, limit: limit, filter: filter) + let arch = a ?? cpuArch + + let releases = try await SwiftlyCore.httpRequestExecutor.getReleaseToolchains() + + var swiftOrgFiltered: [ToolchainVersion.StableRelease] = try releases.compactMap { swiftOrgRelease in + if platform.name != PlatformDefinition.macOS.name { + // If the platform isn't xcode then verify that there is an offering for this platform name and arch + guard let swiftOrgPlatform = swiftOrgRelease.platforms.first(where: { $0.matches(platform) }) else { + return nil + } + + guard case let archs = swiftOrgPlatform.archs, archs.contains(arch) else { + return nil + } + } + + guard let version = try? ToolchainVersion(parsing: swiftOrgRelease.stableName), + case let .stable(release) = version + else { + throw SwiftlyError(message: "error parsing swift.org release version: \(swiftOrgRelease.stableName)") + } + + if let filter { + guard filter(release) else { + return nil + } + } + + return release + } + + swiftOrgFiltered.sort(by: >) + + return if let limit = limit { + Array(swiftOrgFiltered.prefix(limit)) + } else { + swiftOrgFiltered + } } public struct SnapshotBranchNotFoundError: LocalizedError { diff --git a/Tests/SwiftlyTests/InstallTests.swift b/Tests/SwiftlyTests/InstallTests.swift index b73bf40e..9634e085 100644 --- a/Tests/SwiftlyTests/InstallTests.swift +++ b/Tests/SwiftlyTests/InstallTests.swift @@ -30,6 +30,7 @@ final class InstallTests: SwiftlyTests { } // As of writing this, 5.8.0 is the latest stable release. Assert it is at least that new. + print("RELEASE IS \(release)") XCTAssertTrue(release >= ToolchainVersion.StableRelease(major: 5, minor: 8, patch: 0)) try await validateInstalledToolchains([installedToolchain], description: "install latest") diff --git a/Tests/SwiftlyTests/SwiftlyTests.swift b/Tests/SwiftlyTests/SwiftlyTests.swift index 22819674..968651b3 100644 --- a/Tests/SwiftlyTests/SwiftlyTests.swift +++ b/Tests/SwiftlyTests/SwiftlyTests.swift @@ -17,8 +17,8 @@ struct SwiftlyTestError: LocalizedError { let message: String } -class SwiftlyTests: XCTestCase { - override class func tearDown() { +public class SwiftlyTests: XCTestCase { + override public class func tearDown() { #if os(Linux) let deleteTestGPGKeys = Process() deleteTestGPGKeys.executableURL = URL(fileURLWithPath: "/usr/bin/env") @@ -36,13 +36,13 @@ class SwiftlyTests: XCTestCase { } // Below are some constants that can be used to write test cases. - static let oldStable = ToolchainVersion(major: 5, minor: 6, patch: 0) - static let oldStableNewPatch = ToolchainVersion(major: 5, minor: 6, patch: 3) - static let newStable = ToolchainVersion(major: 5, minor: 7, patch: 0) - static let oldMainSnapshot = ToolchainVersion(snapshotBranch: .main, date: "2022-09-10") - static let newMainSnapshot = ToolchainVersion(snapshotBranch: .main, date: "2022-10-22") - static let oldReleaseSnapshot = ToolchainVersion(snapshotBranch: .release(major: 5, minor: 7), date: "2022-08-27") - static let newReleaseSnapshot = ToolchainVersion(snapshotBranch: .release(major: 5, minor: 7), date: "2022-08-30") + public static let oldStable = ToolchainVersion(major: 5, minor: 6, patch: 0) + public static let oldStableNewPatch = ToolchainVersion(major: 5, minor: 6, patch: 3) + public static let newStable = ToolchainVersion(major: 5, minor: 7, patch: 0) + public static let oldMainSnapshot = ToolchainVersion(snapshotBranch: .main, date: "2022-09-10") + public static let newMainSnapshot = ToolchainVersion(snapshotBranch: .main, date: "2022-10-22") + public static let oldReleaseSnapshot = ToolchainVersion(snapshotBranch: .release(major: 5, minor: 7), date: "2022-08-27") + public static let newReleaseSnapshot = ToolchainVersion(snapshotBranch: .release(major: 5, minor: 7), date: "2022-08-30") static let allToolchains: Set = [ oldStable, @@ -468,12 +468,7 @@ private struct MockHTTPRequestExecutor: HTTPRequestExecutor { throw SwiftlyTestError(message: "Mocking of fetching the current swiftly release is not implemented in MockHTTPRequestExecutor.") } - public func getReleaseToolchains( - platform _: PlatformDefinition, - arch _: Components.Schemas.Architecture?, - limit _: Int?, - filter _: ((ToolchainVersion.StableRelease) -> Bool)? - ) async throws -> [ToolchainVersion.StableRelease] { + public func getReleaseToolchains() async throws -> [Components.Schemas.Release] { throw SwiftlyTestError(message: "Mocking of fetching the release toolchains is not implemented in MockHTTPRequestExecutor.") } } @@ -494,13 +489,31 @@ public class MockToolchainDownloader: HTTPRequestExecutor { private let latestSwiftlyVersion: SwiftlyVersion - public init(executables: [String]? = nil, latestSwiftlyVersion: SwiftlyVersion = SwiftlyCore.version, delegate: HTTPRequestExecutor) { + private let releaseToolchains: [ToolchainVersion.StableRelease] + + public init( + executables: [String]? = nil, + latestSwiftlyVersion: SwiftlyVersion = SwiftlyCore.version, + releaseToolchains: [ToolchainVersion.StableRelease] = [ + SwiftlyTests.oldStable.asStableRelease!, + SwiftlyTests.newStable.asStableRelease!, + SwiftlyTests.oldStableNewPatch.asStableRelease!, + ToolchainVersion.StableRelease(major: 5, minor: 7, patch: 4), // Some tests look for a patch in the 5.7.x series larger than 5.0.3 + ToolchainVersion.StableRelease(major: 5, minor: 9, patch: 0), // Some tests try to update from 5.9.0 + ToolchainVersion.StableRelease(major: 5, minor: 9, patch: 1), + ToolchainVersion.StableRelease(major: 6, minor: 0, patch: 0), // Some tests check for a release larger than 5.8.0 to be present + ToolchainVersion.StableRelease(major: 6, minor: 0, patch: 1), // Some tests try to update from 6.0.0 + ToolchainVersion.StableRelease(major: 6, minor: 0, patch: 2), // Some tests try to update from 6.0.1 + ], + delegate: HTTPRequestExecutor + ) { self.executables = executables ?? ["swift"] #if os(Linux) self.signatures = [:] #endif self.delegate = delegate self.latestSwiftlyVersion = latestSwiftlyVersion + self.releaseToolchains = releaseToolchains } public func getCurrentSwiftlyRelease() async throws -> Components.Schemas.SwiftlyRelease { @@ -515,13 +528,48 @@ public class MockToolchainDownloader: HTTPRequestExecutor { return release } - public func getReleaseToolchains( - platform _: PlatformDefinition, - arch _: Components.Schemas.Architecture?, - limit _: Int?, - filter _: ((ToolchainVersion.StableRelease) -> Bool)? - ) async throws -> [ToolchainVersion.StableRelease] { - throw SwiftlyTestError(message: "Mocking of the fetching of release toolchains is not implemented by the MockToolchainDownloader") + public func getReleaseToolchains() async throws -> [Components.Schemas.Release] { + let currentPlatform = try await Swiftly.currentPlatform.detectPlatform(disableConfirmation: true, platform: nil) + + let platformName = switch currentPlatform { + case PlatformDefinition.ubuntu2004: + "Ubuntu 20.04" + case PlatformDefinition.amazonlinux2: + "Amazon Linux 2" + case PlatformDefinition.ubuntu2204: + "Ubuntu 22.04" + case PlatformDefinition.rhel9: + "Red Hat Universal Base Image 9" + case PlatformDefinition(name: "ubuntu2404", nameFull: "ubuntu24.04", namePretty: "Ubuntu 24.04"): + "Ubuntu 24.04" + case PlatformDefinition(name: "debian12", nameFull: "debian12", namePretty: "Debian 12"): + "Debian 12" + case PlatformDefinition(name: "fedora39", nameFull: "fedora39", namePretty: "Fedora 39"): + "Fedora 39" + case PlatformDefinition.macOS: + "Xcode" // NOTE: this is not actually a platform that gets added in the swift.org API for macos/xcode + default: + String?.none + } + + guard let platformName else { + throw SwiftlyTestError(message: "Could not detect the current platform in test") + } + + return self.releaseToolchains.map { releaseToolchain in + Components.Schemas.Release( + name: String(describing: releaseToolchain), + date: "", + platforms: [.init( + name: platformName, + platform: .init(value1: .linux, value2: "Linux"), + archs: [cpuArch] + )], + tag: "", + xcode: "", + xcodeRelease: true + ) + } } public func execute(_ request: HTTPClientRequest, timeout: TimeAmount) async throws -> HTTPClientResponse { From 429a13f711d0f87c4b99f212fb28c1b2fac38d3e Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Fri, 14 Mar 2025 13:22:44 -0400 Subject: [PATCH 04/12] Remove print statement --- Tests/SwiftlyTests/InstallTests.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/SwiftlyTests/InstallTests.swift b/Tests/SwiftlyTests/InstallTests.swift index 9634e085..b73bf40e 100644 --- a/Tests/SwiftlyTests/InstallTests.swift +++ b/Tests/SwiftlyTests/InstallTests.swift @@ -30,7 +30,6 @@ final class InstallTests: SwiftlyTests { } // As of writing this, 5.8.0 is the latest stable release. Assert it is at least that new. - print("RELEASE IS \(release)") XCTAssertTrue(release >= ToolchainVersion.StableRelease(major: 5, minor: 8, patch: 0)) try await validateInstalledToolchains([installedToolchain], description: "install latest") From e82393c1d8cea8bcaa1504d205f8d2f93672b21c Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Fri, 14 Mar 2025 13:25:51 -0400 Subject: [PATCH 05/12] Fix regression in get snapshot toolchains --- Sources/SwiftlyCore/HTTPClient.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/SwiftlyCore/HTTPClient.swift b/Sources/SwiftlyCore/HTTPClient.swift index f081b9ef..581ffe7e 100644 --- a/Sources/SwiftlyCore/HTTPClient.swift +++ b/Sources/SwiftlyCore/HTTPClient.swift @@ -363,7 +363,7 @@ public struct SwiftlyHTTPClient { limit: Int? = nil, filter: ((ToolchainVersion.Snapshot) -> Bool)? = nil ) async throws -> [ToolchainVersion.Snapshot] { - let arch = a ?? String(describing: cpuArch) + let arch = a ?? cpuArch.value2 let platformName = if platform.name == PlatformDefinition.macOS.name { "macos" From 797c287cd405fc5168ddb1f803b4e328d5739745 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Fri, 14 Mar 2025 13:40:27 -0400 Subject: [PATCH 06/12] Fix test failure with Debian 12 --- Sources/SwiftlyCore/HTTPClient.swift | 2 +- Tests/SwiftlyTests/SwiftlyTests.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/SwiftlyCore/HTTPClient.swift b/Sources/SwiftlyCore/HTTPClient.swift index 581ffe7e..845b7ae1 100644 --- a/Sources/SwiftlyCore/HTTPClient.swift +++ b/Sources/SwiftlyCore/HTTPClient.swift @@ -201,7 +201,7 @@ extension Components.Schemas.Platform { case "Ubuntu 24.04": PlatformDefinition(name: "ubuntu2404", nameFull: "ubuntu24.04", namePretty: "Ubuntu 24.04") case "Debian 12": - PlatformDefinition(name: "debian12", nameFull: "debian12", namePretty: "Debian 12") + PlatformDefinition(name: "debian12", nameFull: "debian12", namePretty: "Debian GNU/Linux 12") case "Fedora 39": PlatformDefinition(name: "fedora39", nameFull: "fedora39", namePretty: "Fedora 39") default: diff --git a/Tests/SwiftlyTests/SwiftlyTests.swift b/Tests/SwiftlyTests/SwiftlyTests.swift index 968651b3..ab9442a9 100644 --- a/Tests/SwiftlyTests/SwiftlyTests.swift +++ b/Tests/SwiftlyTests/SwiftlyTests.swift @@ -542,7 +542,7 @@ public class MockToolchainDownloader: HTTPRequestExecutor { "Red Hat Universal Base Image 9" case PlatformDefinition(name: "ubuntu2404", nameFull: "ubuntu24.04", namePretty: "Ubuntu 24.04"): "Ubuntu 24.04" - case PlatformDefinition(name: "debian12", nameFull: "debian12", namePretty: "Debian 12"): + case PlatformDefinition(name: "debian12", nameFull: "debian12", namePretty: "Debian GNU/Linux 12"): "Debian 12" case PlatformDefinition(name: "fedora39", nameFull: "fedora39", namePretty: "Fedora 39"): "Fedora 39" @@ -553,7 +553,7 @@ public class MockToolchainDownloader: HTTPRequestExecutor { } guard let platformName else { - throw SwiftlyTestError(message: "Could not detect the current platform in test") + throw SwiftlyTestError(message: "Could not detect the current platform in test: \(currentPlatform)") } return self.releaseToolchains.map { releaseToolchain in From 8bfc70b0d38521ab90566768112670918fdbd1b9 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Fri, 14 Mar 2025 13:44:42 -0400 Subject: [PATCH 07/12] Increase accuracy of release objects for macOS/xcode in mock toolchain downloader --- Tests/SwiftlyTests/SwiftlyTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/SwiftlyTests/SwiftlyTests.swift b/Tests/SwiftlyTests/SwiftlyTests.swift index ab9442a9..4d6c2b80 100644 --- a/Tests/SwiftlyTests/SwiftlyTests.swift +++ b/Tests/SwiftlyTests/SwiftlyTests.swift @@ -560,11 +560,11 @@ public class MockToolchainDownloader: HTTPRequestExecutor { Components.Schemas.Release( name: String(describing: releaseToolchain), date: "", - platforms: [.init( + platforms: platformName != "Xcode" ? [.init( name: platformName, platform: .init(value1: .linux, value2: "Linux"), archs: [cpuArch] - )], + )] : [], tag: "", xcode: "", xcodeRelease: true From c0a30a3610b4570fa91264efa4d0116233fcb9cd Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Fri, 14 Mar 2025 16:08:02 -0400 Subject: [PATCH 08/12] Use OpenAPI for retrieving the snapshot releases --- Sources/SwiftlyCore/HTTPClient.swift | 78 +++++++++++++++++---------- Tests/SwiftlyTests/SwiftlyTests.swift | 57 ++++++++++++++++++-- Tests/SwiftlyTests/UpdateTests.swift | 2 +- 3 files changed, 104 insertions(+), 33 deletions(-) diff --git a/Sources/SwiftlyCore/HTTPClient.swift b/Sources/SwiftlyCore/HTTPClient.swift index 845b7ae1..f0122042 100644 --- a/Sources/SwiftlyCore/HTTPClient.swift +++ b/Sources/SwiftlyCore/HTTPClient.swift @@ -54,6 +54,7 @@ public protocol HTTPRequestExecutor { func execute(_ request: HTTPClientRequest, timeout: TimeAmount) async throws -> HTTPClientResponse func getCurrentSwiftlyRelease() async throws -> Components.Schemas.SwiftlyRelease func getReleaseToolchains() async throws -> [Components.Schemas.Release] + func getSnapshotToolchains(branch: Components.Schemas.KnownSourceBranch, platform: Components.Schemas.KnownPlatformIdentifier) async throws -> Components.Schemas.DevToolchains } internal struct SwiftlyUserAgentMiddleware: ClientMiddleware { @@ -144,6 +145,21 @@ internal class HTTPRequestExecutorImpl: HTTPRequestExecutor { return try response.ok.body.json } + + public func getSnapshotToolchains(branch: Components.Schemas.KnownSourceBranch, platform: Components.Schemas.KnownPlatformIdentifier) async throws -> Components.Schemas.DevToolchains { + let config = AsyncHTTPClientTransport.Configuration(client: self.httpClient, timeout: .seconds(30)) + let swiftlyUserAgent = SwiftlyUserAgentMiddleware() + + let client = Client( + serverURL: try Servers.Server1.url(), + transport: AsyncHTTPClientTransport(configuration: config), + middlewares: [swiftlyUserAgent] + ) + + let response = try await client.listDevToolchains(.init(path: .init(branch: branch, platform: platform))) + + return try response.ok.body.json + } } private func makeRequest(url: String) -> HTTPClientRequest { @@ -218,15 +234,7 @@ extension Components.Schemas.Platform { } } -public struct SwiftOrgSnapshotList: Codable { - var aarch64: [SwiftOrgSnapshot]? - var x86_64: [SwiftOrgSnapshot]? - var universal: [SwiftOrgSnapshot]? -} - -public struct SwiftOrgSnapshot: Codable { - var dir: String - +extension Components.Schemas.DevToolchainForArch { private static let snapshotRegex: Regex<(Substring, Substring?, Substring?, Substring)> = try! Regex("swift(?:-(\\d+)\\.(\\d+))?-DEVELOPMENT-SNAPSHOT-(\\d{4}-\\d{2}-\\d{2})") @@ -363,35 +371,49 @@ public struct SwiftlyHTTPClient { limit: Int? = nil, filter: ((ToolchainVersion.Snapshot) -> Bool)? = nil ) async throws -> [ToolchainVersion.Snapshot] { - let arch = a ?? cpuArch.value2 + let platformId: Components.Schemas.KnownPlatformIdentifier = switch platform.name { + // case PlatformDefinition.ubuntu2404.name: + // .ubuntu2404 + // case PlatformDefinition.debian12.name: + // .debian12 + // case PlatformDefinition.fedora39.name: + // .fedora39 + case PlatformDefinition.ubuntu2204.name: + .ubuntu2204 + case PlatformDefinition.ubuntu2004.name: + .ubuntu2004 + case PlatformDefinition.rhel9.name: + .ubi9 + case PlatformDefinition.amazonlinux2.name: + .amazonlinux2 + case PlatformDefinition.macOS.name: + .macos + default: + throw SwiftlyError(message: "No snapshot toolchains available for platform \(platform.name)") + } - let platformName = if platform.name == PlatformDefinition.macOS.name { - "macos" - } else { - platform.name + let sourceBranch: Components.Schemas.KnownSourceBranch = switch branch { + case .main: + .main + case .release(major: 6, minor: 0): + ._6_0 + default: + throw SwiftlyError(message: "Unknown snapshot branch: \(branch)") } - let url = "https://www.swift.org/api/v1/install/dev/\(branch.name)/\(platformName).json" + let devToolchains = try await SwiftlyCore.httpRequestExecutor.getSnapshotToolchains(branch: sourceBranch, platform: platformId) - // For a particular branch and platform the snapshots are listed underneath their architecture - let swiftOrgSnapshotArchs: SwiftOrgSnapshotList - do { - swiftOrgSnapshotArchs = try await self.getFromJSON(url: url, type: SwiftOrgSnapshotList.self) - } catch is JSONNotFoundError { - throw SnapshotBranchNotFoundError(branch: branch) - } catch { - throw error - } + let arch = a ?? cpuArch.value2 // These are the available snapshots for the branch, platform, and architecture let swiftOrgSnapshots = if platform.name == PlatformDefinition.macOS.name { - swiftOrgSnapshotArchs.universal ?? [SwiftOrgSnapshot]() + devToolchains.universal ?? [Components.Schemas.DevToolchainForArch]() } else if arch == "aarch64" { - swiftOrgSnapshotArchs.aarch64 ?? [SwiftOrgSnapshot]() + devToolchains.aarch64 ?? [Components.Schemas.DevToolchainForArch]() } else if arch == "x86_64" { - swiftOrgSnapshotArchs.x86_64 ?? [SwiftOrgSnapshot]() + devToolchains.x8664 ?? [Components.Schemas.DevToolchainForArch]() } else { - [SwiftOrgSnapshot]() + [Components.Schemas.DevToolchainForArch]() } // Convert these into toolchain snapshot versions that match the filter diff --git a/Tests/SwiftlyTests/SwiftlyTests.swift b/Tests/SwiftlyTests/SwiftlyTests.swift index 4d6c2b80..dc3c4942 100644 --- a/Tests/SwiftlyTests/SwiftlyTests.swift +++ b/Tests/SwiftlyTests/SwiftlyTests.swift @@ -39,10 +39,10 @@ public class SwiftlyTests: XCTestCase { public static let oldStable = ToolchainVersion(major: 5, minor: 6, patch: 0) public static let oldStableNewPatch = ToolchainVersion(major: 5, minor: 6, patch: 3) public static let newStable = ToolchainVersion(major: 5, minor: 7, patch: 0) - public static let oldMainSnapshot = ToolchainVersion(snapshotBranch: .main, date: "2022-09-10") - public static let newMainSnapshot = ToolchainVersion(snapshotBranch: .main, date: "2022-10-22") - public static let oldReleaseSnapshot = ToolchainVersion(snapshotBranch: .release(major: 5, minor: 7), date: "2022-08-27") - public static let newReleaseSnapshot = ToolchainVersion(snapshotBranch: .release(major: 5, minor: 7), date: "2022-08-30") + public static let oldMainSnapshot = ToolchainVersion(snapshotBranch: .main, date: "2025-03-10") + public static let newMainSnapshot = ToolchainVersion(snapshotBranch: .main, date: "2025-03-14") + public static let oldReleaseSnapshot = ToolchainVersion(snapshotBranch: .release(major: 6, minor: 0), date: "2025-02-09") + public static let newReleaseSnapshot = ToolchainVersion(snapshotBranch: .release(major: 6, minor: 0), date: "2025-02-11") static let allToolchains: Set = [ oldStable, @@ -471,6 +471,10 @@ private struct MockHTTPRequestExecutor: HTTPRequestExecutor { public func getReleaseToolchains() async throws -> [Components.Schemas.Release] { throw SwiftlyTestError(message: "Mocking of fetching the release toolchains is not implemented in MockHTTPRequestExecutor.") } + + public func getSnapshotToolchains(branch _: Components.Schemas.KnownSourceBranch, platform _: Components.Schemas.KnownPlatformIdentifier) async throws -> Components.Schemas.DevToolchains { + throw SwiftlyTestError(message: "Mocking of fetching the snapshot toolchains is not implemented in MockHTTPRequestExecutor.") + } } /// An `HTTPRequestExecutor` which will return a mocked response to any toolchain download requests. @@ -491,6 +495,8 @@ public class MockToolchainDownloader: HTTPRequestExecutor { private let releaseToolchains: [ToolchainVersion.StableRelease] + private let snapshotToolchains: [ToolchainVersion.Snapshot] + public init( executables: [String]? = nil, latestSwiftlyVersion: SwiftlyVersion = SwiftlyCore.version, @@ -505,6 +511,12 @@ public class MockToolchainDownloader: HTTPRequestExecutor { ToolchainVersion.StableRelease(major: 6, minor: 0, patch: 1), // Some tests try to update from 6.0.0 ToolchainVersion.StableRelease(major: 6, minor: 0, patch: 2), // Some tests try to update from 6.0.1 ], + snapshotToolchains: [ToolchainVersion.Snapshot] = [ + SwiftlyTests.oldMainSnapshot.asSnapshot!, + SwiftlyTests.newMainSnapshot.asSnapshot!, + SwiftlyTests.oldReleaseSnapshot.asSnapshot!, + SwiftlyTests.newReleaseSnapshot.asSnapshot!, + ], delegate: HTTPRequestExecutor ) { self.executables = executables ?? ["swift"] @@ -514,6 +526,7 @@ public class MockToolchainDownloader: HTTPRequestExecutor { self.delegate = delegate self.latestSwiftlyVersion = latestSwiftlyVersion self.releaseToolchains = releaseToolchains + self.snapshotToolchains = snapshotToolchains } public func getCurrentSwiftlyRelease() async throws -> Components.Schemas.SwiftlyRelease { @@ -572,6 +585,42 @@ public class MockToolchainDownloader: HTTPRequestExecutor { } } + public func getSnapshotToolchains(branch: Components.Schemas.KnownSourceBranch, platform _: Components.Schemas.KnownPlatformIdentifier) async throws -> Components.Schemas.DevToolchains { + let currentPlatform = try await Swiftly.currentPlatform.detectPlatform(disableConfirmation: true, platform: nil) + + let releasesForBranch = self.snapshotToolchains.filter { snapshotVersion in + switch snapshotVersion.branch { + case .main: + branch == .main + case let .release(major, minor): + major == 6 && minor == 0 && branch == ._6_0 + } + } + + let devToolchainsForArch = releasesForBranch.map { branchSnapshot in + Components.Schemas.DevToolchainForArch( + name: Components.Schemas.DevToolchainKind?.none, + date: "", + dir: branch == .main ? + "swift-DEVELOPMENT-SNAPSHOT-\(branchSnapshot.date)" : + "swift-6.0-DEVELOPMENT-SNAPSHOT-\(branchSnapshot.date)", + download: "", + downloadSignature: nil, + debugInfo: nil + ) + } + + if currentPlatform == PlatformDefinition.macOS { + return Components.Schemas.DevToolchains(universal: devToolchainsForArch) + } else if cpuArch == Components.Schemas.Architecture.x8664 { + return Components.Schemas.DevToolchains(x8664: devToolchainsForArch) + } else if cpuArch == Components.Schemas.Architecture.aarch64 { + return Components.Schemas.DevToolchains(aarch64: devToolchainsForArch) + } else { + return Components.Schemas.DevToolchains() + } + } + public func execute(_ request: HTTPClientRequest, timeout: TimeAmount) async throws -> HTTPClientResponse { guard let url = URL(string: request.url) else { throw SwiftlyTestError(message: "invalid request URL: \(request.url)") diff --git a/Tests/SwiftlyTests/UpdateTests.swift b/Tests/SwiftlyTests/UpdateTests.swift index f442b29b..1df791cc 100644 --- a/Tests/SwiftlyTests/UpdateTests.swift +++ b/Tests/SwiftlyTests/UpdateTests.swift @@ -177,7 +177,7 @@ final class UpdateTests: SwiftlyTests { for branch in branches { try await self.withTestHome { try await self.withMockedToolchain { - let date = "2024-06-18" + let date = branch == .main ? SwiftlyTests.oldMainSnapshot.asSnapshot!.date : SwiftlyTests.oldReleaseSnapshot.asSnapshot!.date try await self.installMockedToolchain(selector: .snapshot(branch: branch, date: date)) var update = try self.parseCommand( From ed6dc64775a47f27f34c372c5c0d93707ef7a874 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Fri, 14 Mar 2025 17:01:55 -0400 Subject: [PATCH 09/12] Fix fedora39 --- Sources/SwiftlyCore/HTTPClient.swift | 2 +- Tests/SwiftlyTests/SwiftlyTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/SwiftlyCore/HTTPClient.swift b/Sources/SwiftlyCore/HTTPClient.swift index f0122042..476261de 100644 --- a/Sources/SwiftlyCore/HTTPClient.swift +++ b/Sources/SwiftlyCore/HTTPClient.swift @@ -219,7 +219,7 @@ extension Components.Schemas.Platform { case "Debian 12": PlatformDefinition(name: "debian12", nameFull: "debian12", namePretty: "Debian GNU/Linux 12") case "Fedora 39": - PlatformDefinition(name: "fedora39", nameFull: "fedora39", namePretty: "Fedora 39") + PlatformDefinition(name: "fedora39", nameFull: "fedora39", namePretty: "Fedora Linux 39") default: nil } diff --git a/Tests/SwiftlyTests/SwiftlyTests.swift b/Tests/SwiftlyTests/SwiftlyTests.swift index dc3c4942..1819f806 100644 --- a/Tests/SwiftlyTests/SwiftlyTests.swift +++ b/Tests/SwiftlyTests/SwiftlyTests.swift @@ -557,7 +557,7 @@ public class MockToolchainDownloader: HTTPRequestExecutor { "Ubuntu 24.04" case PlatformDefinition(name: "debian12", nameFull: "debian12", namePretty: "Debian GNU/Linux 12"): "Debian 12" - case PlatformDefinition(name: "fedora39", nameFull: "fedora39", namePretty: "Fedora 39"): + case PlatformDefinition(name: "fedora39", nameFull: "fedora39", namePretty: "Fedora Linux 39"): "Fedora 39" case PlatformDefinition.macOS: "Xcode" // NOTE: this is not actually a platform that gets added in the swift.org API for macos/xcode From e706c33957e152f3aa7409cb4bd14568cafaa313 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Fri, 21 Mar 2025 10:34:26 -0400 Subject: [PATCH 10/12] Decouple from the known list of platforms and branches so that swiftly can operate independently of its current schema version --- Sources/SwiftlyCore/HTTPClient.swift | 36 ++++++++++++--------------- Sources/SwiftlyCore/openapi.yaml | 6 ++--- Tests/SwiftlyTests/SwiftlyTests.swift | 10 ++++---- 3 files changed, 24 insertions(+), 28 deletions(-) diff --git a/Sources/SwiftlyCore/HTTPClient.swift b/Sources/SwiftlyCore/HTTPClient.swift index 476261de..757daf5e 100644 --- a/Sources/SwiftlyCore/HTTPClient.swift +++ b/Sources/SwiftlyCore/HTTPClient.swift @@ -54,7 +54,7 @@ public protocol HTTPRequestExecutor { func execute(_ request: HTTPClientRequest, timeout: TimeAmount) async throws -> HTTPClientResponse func getCurrentSwiftlyRelease() async throws -> Components.Schemas.SwiftlyRelease func getReleaseToolchains() async throws -> [Components.Schemas.Release] - func getSnapshotToolchains(branch: Components.Schemas.KnownSourceBranch, platform: Components.Schemas.KnownPlatformIdentifier) async throws -> Components.Schemas.DevToolchains + func getSnapshotToolchains(branch: Components.Schemas.SourceBranch, platform: Components.Schemas.PlatformIdentifier) async throws -> Components.Schemas.DevToolchains } internal struct SwiftlyUserAgentMiddleware: ClientMiddleware { @@ -146,7 +146,7 @@ internal class HTTPRequestExecutorImpl: HTTPRequestExecutor { return try response.ok.body.json } - public func getSnapshotToolchains(branch: Components.Schemas.KnownSourceBranch, platform: Components.Schemas.KnownPlatformIdentifier) async throws -> Components.Schemas.DevToolchains { + public func getSnapshotToolchains(branch: Components.Schemas.SourceBranch, platform: Components.Schemas.PlatformIdentifier) async throws -> Components.Schemas.DevToolchains { let config = AsyncHTTPClientTransport.Configuration(client: self.httpClient, timeout: .seconds(30)) let swiftlyUserAgent = SwiftlyUserAgentMiddleware() @@ -371,34 +371,30 @@ public struct SwiftlyHTTPClient { limit: Int? = nil, filter: ((ToolchainVersion.Snapshot) -> Bool)? = nil ) async throws -> [ToolchainVersion.Snapshot] { - let platformId: Components.Schemas.KnownPlatformIdentifier = switch platform.name { - // case PlatformDefinition.ubuntu2404.name: - // .ubuntu2404 - // case PlatformDefinition.debian12.name: - // .debian12 - // case PlatformDefinition.fedora39.name: - // .fedora39 + let platformId: Components.Schemas.PlatformIdentifier = switch platform.name { + // These are new platforms that aren't yet in the list of known platforms in the OpenAPI schema + case PlatformDefinition.ubuntu2404.name, PlatformDefinition.debian12.name, PlatformDefinition.fedora39.name: + .init(value2: platform.name) + case PlatformDefinition.ubuntu2204.name: - .ubuntu2204 + .init(value1: .ubuntu2204) case PlatformDefinition.ubuntu2004.name: - .ubuntu2004 + .init(value1: .ubuntu2004) case PlatformDefinition.rhel9.name: - .ubi9 + .init(value1: .ubi9) case PlatformDefinition.amazonlinux2.name: - .amazonlinux2 + .init(value1: .amazonlinux2) case PlatformDefinition.macOS.name: - .macos + .init(value1: .macos) default: throw SwiftlyError(message: "No snapshot toolchains available for platform \(platform.name)") } - let sourceBranch: Components.Schemas.KnownSourceBranch = switch branch { + let sourceBranch: Components.Schemas.SourceBranch = switch branch { case .main: - .main - case .release(major: 6, minor: 0): - ._6_0 - default: - throw SwiftlyError(message: "Unknown snapshot branch: \(branch)") + .init(value1: .main) + case let .release(major, minor): + .init(value2: "\(major).\(minor)") } let devToolchains = try await SwiftlyCore.httpRequestExecutor.getSnapshotToolchains(branch: sourceBranch, platform: platformId) diff --git a/Sources/SwiftlyCore/openapi.yaml b/Sources/SwiftlyCore/openapi.yaml index 46eaebe0..61d3efea 100644 --- a/Sources/SwiftlyCore/openapi.yaml +++ b/Sources/SwiftlyCore/openapi.yaml @@ -37,12 +37,12 @@ paths: in: path required: true schema: - $ref: '#/components/schemas/KnownSourceBranch' + $ref: '#/components/schemas/SourceBranch' - name: platform in: path required: true schema: - $ref: '#/components/schemas/KnownPlatformIdentifier' + $ref: '#/components/schemas/PlatformIdentifier' get: operationId: listDevToolchains summary: Fetch all development toolchains @@ -61,7 +61,7 @@ paths: in: path required: true schema: - $ref: '#/components/schemas/KnownSourceBranch' + $ref: '#/components/schemas/SourceBranch' get: operationId: listStaticSDKDevToolchains summary: Fetch all static SDK development toolchains diff --git a/Tests/SwiftlyTests/SwiftlyTests.swift b/Tests/SwiftlyTests/SwiftlyTests.swift index 1819f806..f8dbe02d 100644 --- a/Tests/SwiftlyTests/SwiftlyTests.swift +++ b/Tests/SwiftlyTests/SwiftlyTests.swift @@ -472,7 +472,7 @@ private struct MockHTTPRequestExecutor: HTTPRequestExecutor { throw SwiftlyTestError(message: "Mocking of fetching the release toolchains is not implemented in MockHTTPRequestExecutor.") } - public func getSnapshotToolchains(branch _: Components.Schemas.KnownSourceBranch, platform _: Components.Schemas.KnownPlatformIdentifier) async throws -> Components.Schemas.DevToolchains { + public func getSnapshotToolchains(branch _: Components.Schemas.SourceBranch, platform _: Components.Schemas.PlatformIdentifier) async throws -> Components.Schemas.DevToolchains { throw SwiftlyTestError(message: "Mocking of fetching the snapshot toolchains is not implemented in MockHTTPRequestExecutor.") } } @@ -585,15 +585,15 @@ public class MockToolchainDownloader: HTTPRequestExecutor { } } - public func getSnapshotToolchains(branch: Components.Schemas.KnownSourceBranch, platform _: Components.Schemas.KnownPlatformIdentifier) async throws -> Components.Schemas.DevToolchains { + public func getSnapshotToolchains(branch: Components.Schemas.SourceBranch, platform _: Components.Schemas.PlatformIdentifier) async throws -> Components.Schemas.DevToolchains { let currentPlatform = try await Swiftly.currentPlatform.detectPlatform(disableConfirmation: true, platform: nil) let releasesForBranch = self.snapshotToolchains.filter { snapshotVersion in switch snapshotVersion.branch { case .main: - branch == .main + branch.value1 == .main || branch.value1?.rawValue == "main" case let .release(major, minor): - major == 6 && minor == 0 && branch == ._6_0 + branch.value2 == "\(major).\(minor)" || branch.value1?.rawValue == "\(major).\(minor)" } } @@ -601,7 +601,7 @@ public class MockToolchainDownloader: HTTPRequestExecutor { Components.Schemas.DevToolchainForArch( name: Components.Schemas.DevToolchainKind?.none, date: "", - dir: branch == .main ? + dir: branch.value1 == .main || branch.value2 == "main" ? "swift-DEVELOPMENT-SNAPSHOT-\(branchSnapshot.date)" : "swift-6.0-DEVELOPMENT-SNAPSHOT-\(branchSnapshot.date)", download: "", From 6e1ff4d4c08cfdc1ef0044b60ecfa86f71a7c477 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Fri, 21 Mar 2025 14:02:43 -0400 Subject: [PATCH 11/12] Add convenience constructors so that clients do not have to know about the value field of the anyOf to assign. --- Sources/SwiftlyCore/HTTPClient.swift | 60 +++++++++++++++++++++------ Tests/SwiftlyTests/SwiftlyTests.swift | 4 +- 2 files changed, 50 insertions(+), 14 deletions(-) diff --git a/Sources/SwiftlyCore/HTTPClient.swift b/Sources/SwiftlyCore/HTTPClient.swift index 757daf5e..1a61db52 100644 --- a/Sources/SwiftlyCore/HTTPClient.swift +++ b/Sources/SwiftlyCore/HTTPClient.swift @@ -8,7 +8,7 @@ import NIOHTTP1 import OpenAPIAsyncHTTPClient import OpenAPIRuntime -extension Components.Schemas.SwiftlyRelease { +public extension Components.Schemas.SwiftlyRelease { public var swiftlyVersion: SwiftlyVersion { get throws { guard let releaseVersion = try? SwiftlyVersion(parsing: self.version) else { @@ -20,7 +20,7 @@ extension Components.Schemas.SwiftlyRelease { } } -extension Components.Schemas.SwiftlyReleasePlatformArtifacts { +public extension Components.Schemas.SwiftlyReleasePlatformArtifacts { public var isDarwin: Bool { self.platform.value1 == .darwin } @@ -50,6 +50,12 @@ extension Components.Schemas.SwiftlyReleasePlatformArtifacts { } } +public extension Components.Schemas.SwiftlyPlatformIdentifier { + init(_ knownSwiftlyPlatformIdentifier: Components.Schemas.KnownSwiftlyPlatformIdentifier) { + self.init(value1: knownSwiftlyPlatformIdentifier) + } +} + public protocol HTTPRequestExecutor { func execute(_ request: HTTPClientRequest, timeout: TimeAmount) async throws -> HTTPClientResponse func getCurrentSwiftlyRelease() async throws -> Components.Schemas.SwiftlyRelease @@ -179,9 +185,39 @@ extension Components.Schemas.Release { } } +public extension Components.Schemas.Architecture { + init(_ knownArchitecture: Components.Schemas.KnownArchitecture) { + self.init(value1: knownArchitecture, value2: knownArchitecture.rawValue) + } + + init(_ string: String) { + self.init(value2: string) + } +} + +public extension Components.Schemas.PlatformIdentifier { + init(_ knownPlatformIdentifier: Components.Schemas.KnownPlatformIdentifier) { + self.init(value1: knownPlatformIdentifier) + } + + init(_ string: String) { + self.init(value2: string) + } +} + +public extension Components.Schemas.SourceBranch { + init(_ knownSourceBranch: Components.Schemas.KnownSourceBranch) { + self.init(value1: knownSourceBranch) + } + + init(_ string: String) { + self.init(value2: string) + } +} + extension Components.Schemas.Architecture { - static var x8664: Components.Schemas.Architecture = .init(value1: Components.Schemas.KnownArchitecture.x8664, value2: "x86_64") - static var aarch64: Components.Schemas.Architecture = .init(value1: Components.Schemas.KnownArchitecture.aarch64, value2: "aarch64") + static var x8664: Components.Schemas.Architecture = .init(Components.Schemas.KnownArchitecture.x8664) + static var aarch64: Components.Schemas.Architecture = .init(Components.Schemas.KnownArchitecture.aarch64) } extension Components.Schemas.Platform { @@ -374,27 +410,27 @@ public struct SwiftlyHTTPClient { let platformId: Components.Schemas.PlatformIdentifier = switch platform.name { // These are new platforms that aren't yet in the list of known platforms in the OpenAPI schema case PlatformDefinition.ubuntu2404.name, PlatformDefinition.debian12.name, PlatformDefinition.fedora39.name: - .init(value2: platform.name) + .init(platform.name) case PlatformDefinition.ubuntu2204.name: - .init(value1: .ubuntu2204) + .init(.ubuntu2204) case PlatformDefinition.ubuntu2004.name: - .init(value1: .ubuntu2004) + .init(.ubuntu2004) case PlatformDefinition.rhel9.name: - .init(value1: .ubi9) + .init(.ubi9) case PlatformDefinition.amazonlinux2.name: - .init(value1: .amazonlinux2) + .init(.amazonlinux2) case PlatformDefinition.macOS.name: - .init(value1: .macos) + .init(.macos) default: throw SwiftlyError(message: "No snapshot toolchains available for platform \(platform.name)") } let sourceBranch: Components.Schemas.SourceBranch = switch branch { case .main: - .init(value1: .main) + .init(.main) case let .release(major, minor): - .init(value2: "\(major).\(minor)") + .init("\(major).\(minor)") } let devToolchains = try await SwiftlyCore.httpRequestExecutor.getSnapshotToolchains(branch: sourceBranch, platform: platformId) diff --git a/Tests/SwiftlyTests/SwiftlyTests.swift b/Tests/SwiftlyTests/SwiftlyTests.swift index f8dbe02d..385d59f2 100644 --- a/Tests/SwiftlyTests/SwiftlyTests.swift +++ b/Tests/SwiftlyTests/SwiftlyTests.swift @@ -533,8 +533,8 @@ public class MockToolchainDownloader: HTTPRequestExecutor { let release = Components.Schemas.SwiftlyRelease( version: self.latestSwiftlyVersion.description, platforms: [ - .init(platform: .init(value1: .darwin), arm64: "https://download.swift.org/swiftly-darwin.pkg", x8664: "https://download.swift.org/swiftly-darwin.pkg"), - .init(platform: .init(value1: .linux), arm64: "https://download.swift.org/swiftly-linux.tar.gz", x8664: "https://download.swift.org/swiftly-linux.tar.gz"), + .init(platform: .init(.darwin), arm64: "https://download.swift.org/swiftly-darwin.pkg", x8664: "https://download.swift.org/swiftly-darwin.pkg"), + .init(platform: .init(.linux), arm64: "https://download.swift.org/swiftly-linux.tar.gz", x8664: "https://download.swift.org/swiftly-linux.tar.gz"), ] ) From fc916f30970be3e46c50c007f454a8a8912c1405 Mon Sep 17 00:00:00 2001 From: Chris McGee Date: Fri, 21 Mar 2025 14:20:47 -0400 Subject: [PATCH 12/12] Reformat --- Sources/SwiftlyCore/HTTPClient.swift | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Sources/SwiftlyCore/HTTPClient.swift b/Sources/SwiftlyCore/HTTPClient.swift index 1a61db52..c3644348 100644 --- a/Sources/SwiftlyCore/HTTPClient.swift +++ b/Sources/SwiftlyCore/HTTPClient.swift @@ -8,7 +8,7 @@ import NIOHTTP1 import OpenAPIAsyncHTTPClient import OpenAPIRuntime -public extension Components.Schemas.SwiftlyRelease { +extension Components.Schemas.SwiftlyRelease { public var swiftlyVersion: SwiftlyVersion { get throws { guard let releaseVersion = try? SwiftlyVersion(parsing: self.version) else { @@ -20,7 +20,7 @@ public extension Components.Schemas.SwiftlyRelease { } } -public extension Components.Schemas.SwiftlyReleasePlatformArtifacts { +extension Components.Schemas.SwiftlyReleasePlatformArtifacts { public var isDarwin: Bool { self.platform.value1 == .darwin } @@ -50,8 +50,8 @@ public extension Components.Schemas.SwiftlyReleasePlatformArtifacts { } } -public extension Components.Schemas.SwiftlyPlatformIdentifier { - init(_ knownSwiftlyPlatformIdentifier: Components.Schemas.KnownSwiftlyPlatformIdentifier) { +extension Components.Schemas.SwiftlyPlatformIdentifier { + public init(_ knownSwiftlyPlatformIdentifier: Components.Schemas.KnownSwiftlyPlatformIdentifier) { self.init(value1: knownSwiftlyPlatformIdentifier) } } @@ -185,32 +185,32 @@ extension Components.Schemas.Release { } } -public extension Components.Schemas.Architecture { - init(_ knownArchitecture: Components.Schemas.KnownArchitecture) { +extension Components.Schemas.Architecture { + public init(_ knownArchitecture: Components.Schemas.KnownArchitecture) { self.init(value1: knownArchitecture, value2: knownArchitecture.rawValue) } - init(_ string: String) { + public init(_ string: String) { self.init(value2: string) } } -public extension Components.Schemas.PlatformIdentifier { - init(_ knownPlatformIdentifier: Components.Schemas.KnownPlatformIdentifier) { +extension Components.Schemas.PlatformIdentifier { + public init(_ knownPlatformIdentifier: Components.Schemas.KnownPlatformIdentifier) { self.init(value1: knownPlatformIdentifier) } - init(_ string: String) { + public init(_ string: String) { self.init(value2: string) } } -public extension Components.Schemas.SourceBranch { - init(_ knownSourceBranch: Components.Schemas.KnownSourceBranch) { +extension Components.Schemas.SourceBranch { + public init(_ knownSourceBranch: Components.Schemas.KnownSourceBranch) { self.init(value1: knownSourceBranch) } - init(_ string: String) { + public init(_ string: String) { self.init(value2: string) } }