Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 123 additions & 55 deletions Sources/SwiftlyCore/HTTPClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,17 @@ extension Components.Schemas.SwiftlyReleasePlatformArtifacts {
}
}

extension Components.Schemas.SwiftlyPlatformIdentifier {
public 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
func getReleaseToolchains() async throws -> [Components.Schemas.Release]
func getSnapshotToolchains(branch: Components.Schemas.SourceBranch, platform: Components.Schemas.PlatformIdentifier) async throws -> Components.Schemas.DevToolchains
}

internal struct SwiftlyUserAgentMiddleware: ClientMiddleware {
Expand Down Expand Up @@ -128,6 +136,36 @@ internal class HTTPRequestExecutorImpl: HTTPRequestExecutor {
let response = try await client.getCurrentSwiftlyRelease()
return try response.ok.body.json
}

public func getReleaseToolchains() async throws -> [Components.Schemas.Release] {
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()

return try response.ok.body.json
}

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

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 {
Expand All @@ -136,13 +174,56 @@ 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.Architecture {
public init(_ knownArchitecture: Components.Schemas.KnownArchitecture) {
self.init(value1: knownArchitecture, value2: knownArchitecture.rawValue)
}

public init(_ string: String) {
self.init(value2: string)
}
}

extension Components.Schemas.PlatformIdentifier {
public init(_ knownPlatformIdentifier: Components.Schemas.KnownPlatformIdentifier) {
self.init(value1: knownPlatformIdentifier)
}

public init(_ string: String) {
self.init(value2: string)
}
}

extension Components.Schemas.SourceBranch {
public init(_ knownSourceBranch: Components.Schemas.KnownSourceBranch) {
self.init(value1: knownSourceBranch)
}

public init(_ string: String) {
self.init(value2: string)
}
}

extension Components.Schemas.Architecture {
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 {
/// 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":
Expand Down Expand Up @@ -172,46 +253,24 @@ struct SwiftOrgPlatform: Codable {
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")
PlatformDefinition(name: "fedora39", nameFull: "fedora39", namePretty: "Fedora Linux 39")
default:
nil
}
}

func matches(_ platform: PlatformDefinition) -> Bool {
guard let myPlatform = self.platform else {
guard let myPlatform = self.platformDef else {
return false
}

return myPlatform.name == platform.name
}
}

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]?
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})")

Expand Down Expand Up @@ -291,23 +350,22 @@ 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)
let releases = try await SwiftlyCore.httpRequestExecutor.getReleaseToolchains()

var swiftOrgFiltered: [ToolchainVersion.StableRelease] = try swiftOrgReleases.compactMap { swiftOrgRelease in
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 let archs = swiftOrgPlatform.archs, archs.contains(arch) else {
guard case let archs = swiftOrgPlatform.archs, archs.contains(arch) else {
return nil
}
}
Expand Down Expand Up @@ -349,35 +407,45 @@ public struct SwiftlyHTTPClient {
limit: Int? = nil,
filter: ((ToolchainVersion.Snapshot) -> Bool)? = nil
) async throws -> [ToolchainVersion.Snapshot] {
let arch = a ?? cpuArch
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(platform.name)

case PlatformDefinition.ubuntu2204.name:
.init(.ubuntu2204)
case PlatformDefinition.ubuntu2004.name:
.init(.ubuntu2004)
case PlatformDefinition.rhel9.name:
.init(.ubi9)
case PlatformDefinition.amazonlinux2.name:
.init(.amazonlinux2)
case PlatformDefinition.macOS.name:
.init(.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.SourceBranch = switch branch {
case .main:
.init(.main)
case let .release(major, minor):
.init("\(major).\(minor)")
}

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
Expand Down
4 changes: 2 additions & 2 deletions Sources/SwiftlyCore/SwiftlyCore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.x8664
#elseif arch(arm64)
public let cpuArch = "aarch64"
public let cpuArch = Components.Schemas.Architecture.aarch64
#else
#error("Unsupported processor architecture")
#endif
2 changes: 1 addition & 1 deletion Sources/SwiftlyCore/openapi-generator-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ generate:
namingStrategy: idiomatic
accessModifier: public
filter:
tags: ["Swiftly"]
tags: ["Swiftly", "Toolchains"]
6 changes: 3 additions & 3 deletions Sources/SwiftlyCore/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
12 changes: 6 additions & 6 deletions Tests/SwiftlyTests/HTTPClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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.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
Expand All @@ -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: arch.value2!, branch: branch, limit: 5)
// THEN: we get at least 3 releases
XCTAssertTrue(3 <= snapshots.count)
}
Expand Down
Loading