Skip to content

Use swift.org API for getting releases and some snapshot toolchains #153

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Aug 23, 2024
48 changes: 46 additions & 2 deletions Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,7 @@ Likewise, the latest snapshot associated with a given development branch can be

**--token=\<token\>:**

*A GitHub authentiation token to use for any GitHub API requests.*

*A GitHub authentication token to use for any GitHub API requests.*

This is useful to avoid GitHub's low rate limits. If an installation
fails with an "unauthorized" status code, it likely means the rate limit has been hit.
Expand Down Expand Up @@ -93,6 +92,51 @@ written to this file as commands that can be run after the installation.



## list-available

List toolchains available for install.

```
swiftly list-available [<toolchain-selector>] [--version] [--help]
```

**toolchain-selector:**

*A filter to use when listing toolchains.*


The toolchain selector determines which toolchains to list. If no selector is provided, all available release toolchains will be listed:

$ swiftly list-available

The available toolchains associated with a given major version can be listed by specifying the major version as the selector:

$ swiftly list-available 5

Likewise, the available toolchains associated with a given minor version can be listed by specifying the minor version as the selector:

$ swiftly list-available 5.2

The installed snapshots for a given devlopment branch can be listed by specifying the branch as the selector:

$ swiftly list-available main-snapshot
$ swiftly list-available 6.0-snapshot

Note that listing available snapshots before 6.0 is unsupported.


**--version:**

*Show the version.*


**--help:**

*Show help information.*




## use

Set the active toolchain. If no toolchain is provided, print the currently in-use toolchain, if any.
Expand Down
1 change: 0 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ let package = Package(
dependencies: ["Swiftly"],
exclude: ghApiCacheExcludedResources,
resources: ghApiCacheResources + [
.embedInCode("gh-api-cache/swift-releases-page1.json"),
.embedInCode("mock-signing-key-private.pgp"),
]
),
Expand Down
20 changes: 10 additions & 10 deletions Sources/LinuxPlatform/Linux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -436,15 +436,15 @@ public struct Linux: Platform {

switch choice {
case "1":
return PlatformDefinition(name: "ubuntu2204", nameFull: "ubuntu22.04", namePretty: "Ubuntu 22.04")
return PlatformDefinition.ubuntu2204
case "2":
return PlatformDefinition(name: "ubuntu2004", nameFull: "ubuntu20.04", namePretty: "Ubuntu 20.04")
return PlatformDefinition.ubuntu2004
case "3":
return PlatformDefinition(name: "ubuntu1804", nameFull: "ubuntu18.04", namePretty: "Ubuntu 18.04")
return PlatformDefinition.ubuntu1804
case "4":
return PlatformDefinition(name: "ubi9", nameFull: "ubi9", namePretty: "RHEL 9")
return PlatformDefinition.rhel9
case "5":
return PlatformDefinition(name: "amazonlinux2", nameFull: "amazonlinux2", namePretty: "Amazon Linux 2")
return PlatformDefinition.amazonlinux2
default:
fatalError("Installation canceled")
}
Expand All @@ -455,15 +455,15 @@ public struct Linux: Platform {
if let platform = platform {
switch platform {
case "ubuntu22.04":
return PlatformDefinition(name: "ubuntu2204", nameFull: "ubuntu22.04", namePretty: "Ubuntu 22.04")
return PlatformDefinition.ubuntu2204
case "ubuntu20.04":
return PlatformDefinition(name: "ubuntu2004", nameFull: "ubuntu20.04", namePretty: "Ubuntu 20.04")
return PlatformDefinition.ubuntu2004
case "ubuntu18.04":
return PlatformDefinition(name: "ubuntu1804", nameFull: "ubuntu18.04", namePretty: "Ubuntu 18.04")
return PlatformDefinition.ubuntu1804
case "amazonlinux2":
return PlatformDefinition(name: "amazonlinux2", nameFull: "amazonlinux2", namePretty: "Amazon Linux 2")
return PlatformDefinition.amazonlinux2
case "rhel9":
return PlatformDefinition(name: "ubi9", nameFull: "ubi9", namePretty: "RHEL 9")
return PlatformDefinition.rhel9
default:
fatalError("Unrecognized platform \(platform)")
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/MacOSPlatform/MacOS.swift
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ public struct MacOS: Platform {

public func detectPlatform(disableConfirmation _: Bool, platform _: String?) async -> PlatformDefinition {
// No special detection required on macOS platform
PlatformDefinition(name: "xcode", nameFull: "osx", namePretty: "macOS")
PlatformDefinition.macOS
}

public func getShell() async throws -> String {
Expand Down
15 changes: 7 additions & 8 deletions Sources/Swiftly/Install.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,8 @@ struct Install: SwiftlyCommand {
var use: Bool = false

@Option(help: ArgumentHelp(
"A GitHub authentiation token to use for any GitHub API requests.",
"A GitHub authentication token to use for any GitHub API requests.",
discussion: """

This is useful to avoid GitHub's low rate limits. If an installation
fails with an \"unauthorized\" status code, it likely means the rate limit has been hit.
"""
Expand All @@ -76,9 +75,9 @@ struct Install: SwiftlyCommand {
try validateSwiftly()

let selector = try ToolchainSelector(parsing: self.version)
SwiftlyCore.httpClient.githubToken = self.token
let toolchainVersion = try await self.resolve(selector: selector)
var config = try Config.load()
SwiftlyCore.httpClient.githubToken = self.token
let toolchainVersion = try await self.resolve(config: config, selector: selector)
let postInstallScript = try await Self.execute(
version: toolchainVersion,
&config,
Expand Down Expand Up @@ -227,12 +226,12 @@ struct Install: SwiftlyCommand {

/// Utilize the GitHub API along with the provided selector to select a toolchain for install.
/// TODO: update this to use an official swift.org API
func resolve(selector: ToolchainSelector) async throws -> ToolchainVersion {
func resolve(config: Config, selector: ToolchainSelector) async throws -> ToolchainVersion {
switch selector {
case .latest:
SwiftlyCore.print("Fetching the latest stable Swift release...")

guard let release = try await SwiftlyCore.httpClient.getReleaseToolchains(limit: 1).first else {
guard let release = try await SwiftlyCore.httpClient.getReleaseToolchains(platform: config.platform, limit: 1).first else {
throw Error(message: "couldn't get latest releases")
}
return .stable(release)
Expand All @@ -251,7 +250,7 @@ struct Install: SwiftlyCommand {
SwiftlyCore.print("Fetching the latest stable Swift \(major).\(minor) release...")
// If a patch was not provided, perform a lookup to get the latest patch release
// of the provided major/minor version pair.
let toolchain = try await SwiftlyCore.httpClient.getReleaseToolchains(limit: 1) { release in
let toolchain = try await SwiftlyCore.httpClient.getReleaseToolchains(platform: config.platform, limit: 1) { release in
release.major == major && release.minor == minor
}.first

Expand All @@ -269,7 +268,7 @@ struct Install: SwiftlyCommand {
SwiftlyCore.print("Fetching the latest \(branch) branch snapshot...")
// If a date was not provided, perform a lookup to find the most recent snapshot
// for the given branch.
let snapshot = try await SwiftlyCore.httpClient.getSnapshotToolchains(limit: 1) { snapshot in
let snapshot = try await SwiftlyCore.httpClient.getSnapshotToolchains(platform: config.platform, branch: branch, limit: 1) { snapshot in
snapshot.branch == branch
}.first

Expand Down
37 changes: 18 additions & 19 deletions Sources/Swiftly/ListAvailable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ struct ListAvailable: SwiftlyCommand {
discussion: """

The toolchain selector determines which toolchains to list. If no selector \
is provided, all available toolchains will be listed:
is provided, all available release toolchains will be listed:

$ swiftly list-available

Expand All @@ -28,9 +28,9 @@ struct ListAvailable: SwiftlyCommand {
The installed snapshots for a given devlopment branch can be listed by specifying the branch as the selector:

$ swiftly list-available main-snapshot
$ swiftly list-available 5.7-snapshot
$ swiftly list-available 6.0-snapshot

Note that listing available snapshots is currently unsupported. It will be introduced in a future release.
Note that listing available snapshots before 6.0 is unsupported.
Copy link
Contributor

@adam-fowler adam-fowler Aug 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assumed this is because the data isn't available at the moment. Could this change?

"""
))
var toolchainSelector: String?
Expand All @@ -45,18 +45,24 @@ struct ListAvailable: SwiftlyCommand {
try ToolchainSelector(parsing: input)
}

if let selector {
guard !selector.isSnapshotSelector() else {
SwiftlyCore.print("Listing available snapshots is not currently supported.")
return
let config = try Config.load()

let tc: [ToolchainVersion]

switch selector {
case let .snapshot(branch, _):
if case let .release(major, _) = branch, major < 6 {
throw Error(message: "Listing available snapshots previous to 6.0 is not supported.")
}
}

let toolchains = try await SwiftlyCore.httpClient.getReleaseToolchains()
.map(ToolchainVersion.stable)
.filter { selector?.matches(toolchain: $0) ?? true }
tc = try await SwiftlyCore.httpClient.getSnapshotToolchains(platform: config.platform, branch: branch).map { ToolchainVersion.snapshot($0) }
case .stable, .latest:
tc = try await SwiftlyCore.httpClient.getReleaseToolchains(platform: config.platform).map { ToolchainVersion.stable($0) }
default:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than using default here, can we explicitly check for .release? It reads a bit clearer that way.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I've changed this to add explicit stable/latest case, and a default to catch no selector provided.

tc = try await SwiftlyCore.httpClient.getReleaseToolchains(platform: config.platform).map { ToolchainVersion.stable($0) }
}

let config = try Config.load()
let toolchains = tc.filter { selector?.matches(toolchain: $0) ?? true }

let installedToolchains = Set(config.listInstalledToolchains(selector: selector))
let activeToolchain = config.inUse
Expand Down Expand Up @@ -100,13 +106,6 @@ struct ListAvailable: SwiftlyCommand {
for toolchain in toolchains where toolchain.isStableRelease() {
printToolchain(toolchain)
}

// print("")
// print("Available snapshot toolchains")
// print("-----------------------------")
// for toolchain in toolchains where toolchain.isSnapshot() {
// printToolchain(toolchain)
// }
}
}
}
1 change: 1 addition & 0 deletions Sources/Swiftly/Swiftly.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public struct Swiftly: SwiftlyCommand {

subcommands: [
Install.self,
ListAvailable.self,
Use.self,
Uninstall.self,
List.self,
Expand Down
8 changes: 4 additions & 4 deletions Sources/Swiftly/Update.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ struct Update: SwiftlyCommand {
return
}

guard let newToolchain = try await self.lookupNewToolchain(parameters) else {
guard let newToolchain = try await self.lookupNewToolchain(config, parameters) else {
SwiftlyCore.print("\(parameters.oldToolchain) is already up to date")
return
}
Expand Down Expand Up @@ -181,10 +181,10 @@ struct Update: SwiftlyCommand {

/// Tries to find a toolchain version that meets the provided parameters, if one exists.
/// This does not download the toolchain, but it does query the GitHub API to find the suitable toolchain.
private func lookupNewToolchain(_ bounds: UpdateParameters) async throws -> ToolchainVersion? {
private func lookupNewToolchain(_ config: Config, _ bounds: UpdateParameters) async throws -> ToolchainVersion? {
switch bounds {
case let .stable(old, range):
return try await SwiftlyCore.httpClient.getReleaseToolchains(limit: 1) { release in
return try await SwiftlyCore.httpClient.getReleaseToolchains(platform: config.platform, limit: 1) { release in
switch range {
case .latest:
return release > old
Expand All @@ -195,7 +195,7 @@ struct Update: SwiftlyCommand {
}
}.first.map(ToolchainVersion.stable)
case let .snapshot(old):
return try await SwiftlyCore.httpClient.getSnapshotToolchains(limit: 1) { snapshot in
return try await SwiftlyCore.httpClient.getSnapshotToolchains(platform: config.platform, branch: old.branch, limit: 1) { snapshot in
snapshot.branch == old.branch && snapshot.date > old.date
}.first.map(ToolchainVersion.snapshot)
}
Expand Down
22 changes: 0 additions & 22 deletions Sources/SwiftlyCore/HTTPClient+GitHubAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,6 @@ extension SwiftlyHTTPClient {
}
}

/// Get a list of releases on the apple/swift GitHub repository.
/// The releases are returned in "pages" of `perPage` releases (default 100). The page argument specifies the
/// page number.
///
/// The results are returned in lexicographic order.
public func getReleases(page: Int, perPage: Int = 100) async throws -> [GitHubTag] {
let url = "https://api.github.com/repos/apple/swift/releases?per_page=\(perPage)&page=\(page)"
let releases: [GitHubRelease] = try await self.getFromGitHub(url: url)
return releases.filter { !$0.prerelease }.map { $0.toGitHubTag() }
}

/// Get a list of tags on the apple/swift GitHub repository.
/// The tags are returned in pages of 100. The page argument specifies the page number.
///
Expand All @@ -78,17 +67,6 @@ extension SwiftlyHTTPClient {
}
}

/// Model of a GitHub REST API release object.
/// See: https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#list-releases
public struct GitHubRelease: Decodable {
fileprivate let name: String
fileprivate let prerelease: Bool

fileprivate func toGitHubTag() -> GitHubTag {
GitHubTag(name: self.name, commit: nil)
}
}

/// Model of a GitHub REST API tag/release object.
public struct GitHubTag: Decodable {
internal struct Commit: Decodable {
Expand Down
Loading