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
17 changes: 17 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,28 @@ let package = Package(
.target(
name: "SwiftlyCore",
dependencies: [
"SwiftlyDownloadAPI",
"SwiftlyWebsiteAPI",
.product(name: "AsyncHTTPClient", package: "async-http-client"),
.product(name: "NIOFoundationCompat", package: "swift-nio"),
.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
.product(name: "OpenAPIAsyncHTTPClient", package: "swift-openapi-async-http-client"),
],
),
.target(
name: "SwiftlyDownloadAPI",
dependencies: [
.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
],
plugins: [
.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator"),
]
),
.target(
name: "SwiftlyWebsiteAPI",
dependencies: [
.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
],
plugins: [
.plugin(name: "OpenAPIGenerator", package: "swift-openapi-generator"),
]
Expand Down
142 changes: 106 additions & 36 deletions Sources/LinuxPlatform/Linux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ public struct Linux: Platform {
"tar.gz"
}

private static let skipVerificationMessage: String = "To skip signature verification, specify the --no-verify flag."
private static let skipVerificationMessage: String =
"To skip signature verification, specify the --no-verify flag."

public func verifySwiftlySystemPrerequisites() throws {
// Check if the root CA certificates are installed on this system for NIOSSL to use.
Expand All @@ -67,10 +68,15 @@ public struct Linux: Platform {
}
}

public func verifySystemPrerequisitesForInstall(_ ctx: SwiftlyCoreContext, platformName: String, version _: ToolchainVersion, requireSignatureValidation: Bool) async throws -> String? {
public func verifySystemPrerequisitesForInstall(
_ ctx: SwiftlyCoreContext, platformName: String, version _: ToolchainVersion,
requireSignatureValidation: Bool
) async throws -> String? {
// TODO: these are hard-coded until we have a place to query for these based on the toolchain version
// These lists were copied from the dockerfile sources here: https://github.com/apple/swift-docker/tree/ea035798755cce4ec41e0c6dbdd320904cef0421/5.10
let packages: [String] = switch platformName {
let packages: [String] =
switch platformName
{
case "ubuntu1804":
[
"libatomic1",
Expand Down Expand Up @@ -221,7 +227,9 @@ public struct Linux: Platform {
[]
}

let manager: String? = switch platformName {
let manager: String? =
switch platformName
{
case "ubuntu1804":
"apt-get"
case "ubuntu2004":
Expand Down Expand Up @@ -259,18 +267,19 @@ public struct Linux: Platform {
}

let tmpFile = self.getTempFilePath()
let _ = FileManager.default.createFile(atPath: tmpFile.path, contents: nil, attributes: [.posixPermissions: 0o600])
let _ = FileManager.default.createFile(
atPath: tmpFile.path, contents: nil, attributes: [.posixPermissions: 0o600]
)
defer {
try? FileManager.default.removeItem(at: tmpFile)
}

guard let url = URL(string: "https://www.swift.org/keys/all-keys.asc") else {
throw SwiftlyError(message: "malformed URL to the swift gpg keys")
}

try await ctx.httpClient.downloadFile(url: url, to: tmpFile)
try await ctx.httpClient.getGpgKeys().download(to: tmpFile)
if let mockedHomeDir = ctx.mockedHomeDir {
try self.runProgram("gpg", "--import", tmpFile.path, quiet: true, env: ["GNUPGHOME": mockedHomeDir.appendingPathComponent(".gnupg").path])
try self.runProgram(
"gpg", "--import", tmpFile.path, quiet: true,
env: ["GNUPGHOME": mockedHomeDir.appendingPathComponent(".gnupg").path]
)
} else {
try self.runProgram("gpg", "--import", tmpFile.path, quiet: true)
}
Expand Down Expand Up @@ -323,13 +332,17 @@ public struct Linux: Platform {
}
}

public func install(_ ctx: SwiftlyCoreContext, from tmpFile: URL, version: ToolchainVersion, verbose: Bool) throws {
public func install(
_ ctx: SwiftlyCoreContext, from tmpFile: URL, version: ToolchainVersion, verbose: Bool
) throws {
guard tmpFile.fileExists() else {
throw SwiftlyError(message: "\(tmpFile) doesn't exist")
}

if !self.swiftlyToolchainsDir(ctx).fileExists() {
try FileManager.default.createDirectory(at: self.swiftlyToolchainsDir(ctx), withIntermediateDirectories: false)
try FileManager.default.createDirectory(
at: self.swiftlyToolchainsDir(ctx), withIntermediateDirectories: false
)
}

ctx.print("Extracting toolchain...")
Expand Down Expand Up @@ -375,7 +388,9 @@ public struct Linux: Platform {
try self.runProgram(tmpDir.appendingPathComponent("swiftly").path, "init")
}

public func uninstall(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion, verbose _: Bool) throws {
public func uninstall(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion, verbose _: Bool)
throws
{
let toolchainDir = self.swiftlyToolchainsDir(ctx).appendingPathComponent(toolchain.name)
try FileManager.default.removeItem(at: toolchainDir)
}
Expand All @@ -390,7 +405,9 @@ public struct Linux: Platform {
FileManager.default.temporaryDirectory.appendingPathComponent("swiftly-\(UUID())")
}

public func verifySignature(_ ctx: SwiftlyCoreContext, archiveDownloadURL: URL, archive: URL, verbose: Bool) async throws {
public func verifyToolchainSignature(
_ ctx: SwiftlyCoreContext, toolchainFile: ToolchainFile, archive: URL, verbose: Bool
) async throws {
if verbose {
ctx.print("Downloading toolchain signature...")
}
Expand All @@ -401,15 +418,15 @@ public struct Linux: Platform {
try? FileManager.default.removeItem(at: sigFile)
}

try await ctx.httpClient.downloadFile(
url: archiveDownloadURL.appendingPathExtension("sig"),
to: sigFile
)
try await ctx.httpClient.getSwiftToolchainFileSignature(toolchainFile).download(to: sigFile)

ctx.print("Verifying toolchain signature...")
do {
if let mockedHomeDir = ctx.mockedHomeDir {
try self.runProgram("gpg", "--verify", sigFile.path, archive.path, quiet: false, env: ["GNUPGHOME": mockedHomeDir.appendingPathComponent(".gnupg").path])
try self.runProgram(
"gpg", "--verify", sigFile.path, archive.path, quiet: false,
env: ["GNUPGHOME": mockedHomeDir.appendingPathComponent(".gnupg").path]
)
} else {
try self.runProgram("gpg", "--verify", sigFile.path, archive.path, quiet: !verbose)
}
Expand All @@ -418,23 +435,65 @@ public struct Linux: Platform {
}
}

private func manualSelectPlatform(_ ctx: SwiftlyCoreContext, _ platformPretty: String?) async -> PlatformDefinition {
public func verifySwiftlySignature(
_ ctx: SwiftlyCoreContext, archiveDownloadURL: URL, archive: URL, verbose: Bool
) async throws {
if verbose {
ctx.print("Downloading swiftly signature...")
}

let sigFile = self.getTempFilePath()
let _ = FileManager.default.createFile(atPath: sigFile.path, contents: nil)
defer {
try? FileManager.default.removeItem(at: sigFile)
}

try await ctx.httpClient.getSwiftlyReleaseSignature(
url: archiveDownloadURL.appendingPathExtension("sig")
).download(to: sigFile)

ctx.print("Verifying swiftly signature...")
do {
if let mockedHomeDir = ctx.mockedHomeDir {
try self.runProgram(
"gpg", "--verify", sigFile.path, archive.path, quiet: false,
env: ["GNUPGHOME": mockedHomeDir.appendingPathComponent(".gnupg").path]
)
} else {
try self.runProgram("gpg", "--verify", sigFile.path, archive.path, quiet: !verbose)
}
} catch {
throw SwiftlyError(message: "Signature verification failed: \(error).")
}
}

private func manualSelectPlatform(_ ctx: SwiftlyCoreContext, _ platformPretty: String?) async
-> PlatformDefinition
{
if let platformPretty {
print("\(platformPretty) is not an officially supported platform, but the toolchains for another platform may still work on it.")
print(
"\(platformPretty) is not an officially supported platform, but the toolchains for another platform may still work on it."
)
} else {
print("This platform could not be detected, but a toolchain for one of the supported platforms may work on it.")
print(
"This platform could not be detected, but a toolchain for one of the supported platforms may work on it."
)
}

let selections = self.linuxPlatforms.enumerated().map { "\($0 + 1)) \($1.namePretty)" }.joined(separator: "\n")
let selections = self.linuxPlatforms.enumerated().map { "\($0 + 1)) \($1.namePretty)" }.joined(
separator: "\n")

print("""
Please select the platform to use for toolchain downloads:
print(
"""
Please select the platform to use for toolchain downloads:

0) Cancel
\(selections)
""")
0) Cancel
\(selections)
""")

let choice = ctx.readLine(prompt: "Pick one of the available selections [0-\(self.linuxPlatforms.count)] ") ?? "0"
let choice =
ctx.readLine(prompt: "Pick one of the available selections [0-\(self.linuxPlatforms.count)] ")
?? "0"

guard let choiceNum = Int(choice) else {
fatalError("Installation canceled")
Expand All @@ -447,11 +506,15 @@ public struct Linux: Platform {
return self.linuxPlatforms[choiceNum - 1]
}

public func detectPlatform(_ ctx: SwiftlyCoreContext, disableConfirmation: Bool, platform: String?) async throws -> PlatformDefinition {
public func detectPlatform(
_ ctx: SwiftlyCoreContext, disableConfirmation: Bool, platform: String?
) async throws -> PlatformDefinition {
// We've been given a hint to use
if let platform {
guard let pd = linuxPlatforms.first(where: { $0.nameFull == platform }) else {
fatalError("Unrecognized platform \(platform). Recognized values: \(self.linuxPlatforms.map(\.nameFull).joined(separator: ", ")).")
fatalError(
"Unrecognized platform \(platform). Recognized values: \(self.linuxPlatforms.map(\.nameFull).joined(separator: ", "))."
)
}

return pd
Expand Down Expand Up @@ -489,9 +552,13 @@ public struct Linux: Platform {
} else if info.hasPrefix("ID_LIKE=") {
idlike = String(info.dropFirst("ID_LIKE=".count)).replacingOccurrences(of: "\"", with: "")
} else if info.hasPrefix("VERSION_ID=") {
versionID = String(info.dropFirst("VERSION_ID=".count)).replacingOccurrences(of: "\"", with: "").replacingOccurrences(of: ".", with: "")
versionID = String(info.dropFirst("VERSION_ID=".count)).replacingOccurrences(
of: "\"", with: ""
).replacingOccurrences(of: ".", with: "")
} else if info.hasPrefix("PRETTY_NAME=") {
platformPretty = String(info.dropFirst("PRETTY_NAME=".count)).replacingOccurrences(of: "\"", with: "")
platformPretty = String(info.dropFirst("PRETTY_NAME=".count)).replacingOccurrences(
of: "\"", with: ""
)
}
}

Expand Down Expand Up @@ -529,7 +596,9 @@ public struct Linux: Platform {
}

return .rhel9
} else if let pd = [PlatformDefinition.ubuntu1804, .ubuntu2004, .ubuntu2204, .ubuntu2404, .debian12, .fedora39].first(where: { $0.name == id + versionID }) {
} else if let pd = [
PlatformDefinition.ubuntu1804, .ubuntu2004, .ubuntu2204, .ubuntu2404, .debian12, .fedora39,
].first(where: { $0.name == id + versionID }) {
return pd
}

Expand Down Expand Up @@ -559,7 +628,8 @@ public struct Linux: Platform {
return "/bin/bash"
}

public func findToolchainLocation(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) -> URL {
public func findToolchainLocation(_ ctx: SwiftlyCoreContext, _ toolchain: ToolchainVersion) -> URL
{
self.swiftlyToolchainsDir(ctx).appendingPathComponent("\(toolchain.name)")
}

Expand Down
Loading