Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
4 changes: 3 additions & 1 deletion Sources/LinuxPlatform/Linux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,9 @@ public struct Linux: Platform {
[]
}

public func selfUpdate() async throws {}
public func getExecutableName(forArch: String) -> String {
"swiftly-\(forArch)-unknown-linux-gnu"
}

public func currentToolchain() throws -> ToolchainVersion? { nil }

Expand Down
4 changes: 4 additions & 0 deletions Sources/Swiftly/Config.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ public struct Config: Codable, Equatable {

/// The CPU architecture of the platform. If omitted, assumed to be x86_64.
public let architecture: String?

public func getArchitecture() -> String {
self.architecture ?? "x86_64"
}
}

public var inUse: ToolchainVersion?
Expand Down
9 changes: 6 additions & 3 deletions Sources/Swiftly/Install.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ struct Install: SwiftlyCommand {
url += "\(snapshotString)-\(release.date)-a-\(platformFullString).\(Swiftly.currentPlatform.toolchainFileExtension)"
}

guard let url = URL(string: url) else {
throw Error(message: "Invalid toolchain URL: \(url)")
}

let animation = PercentProgressAnimation(
stream: stdoutStream,
header: "Downloading \(version)"
Expand All @@ -134,10 +138,9 @@ struct Install: SwiftlyCommand {
var lastUpdate = Date()

do {
try await httpClient.downloadToolchain(
version,
try await httpClient.downloadFile(
url: url,
to: tmpFile.path,
to: tmpFile,
reportProgress: { progress in
let now = Date()

Expand Down
70 changes: 68 additions & 2 deletions Sources/Swiftly/SelfUpdate.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,78 @@
import ArgumentParser
import Foundation
import TSCBasic
import TSCUtility

import SwiftlyCore

internal struct SelfUpdate: SwiftlyCommand {
public static var configuration = CommandConfiguration(
abstract: "Update the version of swiftly itself."
)

internal var httpClient = SwiftlyHTTPClient()

private enum CodingKeys: CodingKey {}

internal mutating func run() async throws {
print("updating swiftly")
try await Swiftly.currentPlatform.selfUpdate()
SwiftlyCore.print("Checking for swiftly updates...")

let release: SwiftlyGitHubRelease = try await self.httpClient.getFromGitHub(
url: "https://api.github.com/repos/swift-server/swiftly/releases/latest"
)

let version = try SwiftlyVersion(parsing: release.tag)

guard version > Swiftly.version else {
SwiftlyCore.print("Already up to date.")
return
}

SwiftlyCore.print("A new version is available: \(version)")

let config = try Config.load()
let executableName = Swiftly.currentPlatform.getExecutableName(forArch: config.platform.getArchitecture())
let urlString = "https://github.com/swift-server/swiftly/versions/latest/download/\(executableName)"
guard let downloadURL = URL(string: urlString) else {
throw Error(message: "Invalid download url: \(urlString)")
}

let tmpFile = Swiftly.currentPlatform.getTempFilePath()
FileManager.default.createFile(atPath: tmpFile.path, contents: nil)
defer {
try? FileManager.default.removeItem(at: tmpFile)
}

let animation = PercentProgressAnimation(
stream: stdoutStream,
header: "Downloading swiftly \(version)"
)
do {
try await self.httpClient.downloadFile(
url: downloadURL,
to: tmpFile,
reportProgress: { progress in
let downloadedMiB = Double(progress.receivedBytes) / (1024.0 * 1024.0)
let totalMiB = Double(progress.totalBytes!) / (1024.0 * 1024.0)

animation.update(
step: progress.receivedBytes,
total: progress.totalBytes!,
text: "Downloaded \(String(format: "%.1f", downloadedMiB)) MiB of \(String(format: "%.1f", totalMiB)) MiB"
)
}
)
} catch {
animation.complete(success: false)
throw error
}
animation.complete(success: true)

let swiftlyExecutable = Swiftly.currentPlatform.swiftlyBinDir.appendingPathComponent("swiftly", isDirectory: false)
try FileManager.default.removeItem(at: swiftlyExecutable)
try FileManager.default.moveItem(at: tmpFile, to: swiftlyExecutable)
try FileManager.default.setAttributes([.posixPermissions: 0o755], ofItemAtPath: swiftlyExecutable.path)

SwiftlyCore.print("Successfully updated swiftly to \(version) (was \(Swiftly.version))")
}
}
5 changes: 4 additions & 1 deletion Sources/Swiftly/Swiftly.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@ import SwiftlyCore
@main
@available(macOS 10.15, *)
public struct Swiftly: SwiftlyCommand {
public static let version = SwiftlyVersion(major: 0, minor: 1, patch: 0)

public static var configuration = CommandConfiguration(
abstract: "A utility for installing and managing Swift toolchains.",

version: "0.1.0",
version: String(describing: Self.version),

subcommands: [
Install.self,
Use.self,
Uninstall.self,
List.self,
Update.self,
SelfUpdate.self,
]
)

Expand Down
62 changes: 62 additions & 0 deletions Sources/Swiftly/SwiftlyVersion.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import _StringProcessing
import Foundation
import SwiftlyCore

/// Struct modeling a version of swiftly itself.
public struct SwiftlyVersion: Equatable, Comparable, CustomStringConvertible {
/// Regex matching versions like "a.b.c", "a.b.c-alpha", and "a.b.c-alpha2".
static let regex: Regex<(Substring, Substring, Substring, Substring, Substring?)> =
try! Regex("^(\\d+)\\.(\\d+)\\.(\\d+)(?:-([a-zA-Z0-9]+))?$")

public let major: Int
public let minor: Int
public let patch: Int
public let suffix: String?

public init(major: Int, minor: Int, patch: Int, suffix: String? = nil) {
self.major = major
self.minor = minor
self.patch = patch
self.suffix = suffix
}

public init(parsing tag: String) throws {
guard let match = try Self.regex.wholeMatch(in: tag) else {
throw Error(message: "unable to parse release tag: \"\(tag)\"")
}

self.major = Int(match.output.1)!
self.minor = Int(match.output.2)!
self.patch = Int(match.output.3)!
self.suffix = match.output.4.flatMap(String.init)
}

public static func < (lhs: Self, rhs: Self) -> Bool {
if lhs.major != rhs.major {
return lhs.major < rhs.major
} else if lhs.minor != rhs.minor {
return lhs.minor < rhs.minor
} else if lhs.patch != rhs.patch {
return lhs.patch < rhs.patch
} else {
switch (lhs.suffix, rhs.suffix) {
case (.none, .some):
return false
case (.some, .none):
return true
case let (.some(lhsSuffix), .some(rhsSuffix)):
return lhsSuffix < rhsSuffix
case (.none, .none):
return false
}
}
}

public var description: String {
var base = "\(self.major).\(self.minor).\(self.patch)"
if let suffix = self.suffix {
base += "-\(suffix)"
}
return base
}
}
6 changes: 5 additions & 1 deletion Sources/SwiftlyCore/HTTPClient+GitHubAPI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,14 @@ import _StringProcessing
import AsyncHTTPClient
import Foundation

public struct SwiftlyGitHubRelease: Codable {
public let tag: String
}

extension SwiftlyHTTPClient {
/// Get a JSON response from the GitHub REST API.
/// This will use the authorization token set, if any.
private func getFromGitHub<T: Decodable>(url: String) async throws -> T {
public func getFromGitHub<T: Decodable>(url: String) async throws -> T {
var headers: [String: String] = [:]
if let token = self.githubToken ?? ProcessInfo.processInfo.environment["SWIFTLY_GITHUB_TOKEN"] {
headers["Authorization"] = "Bearer \(token)"
Expand Down
Loading