Skip to content
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

Implement --checksum option on swift sdk install #7722

Merged
merged 7 commits into from
Jul 27, 2024
Merged
5 changes: 5 additions & 0 deletions Sources/Basics/Archiver/Archiver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
//===----------------------------------------------------------------------===//

import _Concurrency
import struct Foundation.URL

/// The `Archiver` protocol abstracts away the different operations surrounding archives.
public protocol Archiver: Sendable {
Expand Down Expand Up @@ -95,4 +96,8 @@ extension Archiver {
self.validate(path: path, completion: { continuation.resume(with: $0) })
}
}

package func isFileSupported(_ lastPathComponent: String) -> Bool {
self.supportedExtensions.contains(where: { lastPathComponent.hasSuffix($0) })
}
}
13 changes: 3 additions & 10 deletions Sources/Commands/PackageCommands/ComputeChecksum.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,10 @@ struct ComputeChecksum: SwiftCommand {
var path: AbsolutePath

func run(_ swiftCommandState: SwiftCommandState) throws {
let binaryArtifactsManager = try Workspace.BinaryArtifactsManager(
fileSystem: swiftCommandState.fileSystem,
authorizationProvider: swiftCommandState.getAuthorizationProvider(),
hostToolchain: swiftCommandState.getHostToolchain(),
checksumAlgorithm: SHA256(),
cachePath: .none,
customHTTPClient: .none,
customArchiver: .none,
delegate: .none
let checksum = try Workspace.BinaryArtifactsManager.checksum(
forBinaryArtifactAt: self.path,
fileSystem: swiftCommandState.fileSystem
)
let checksum = try binaryArtifactsManager.checksum(forBinaryArtifactAt: path)
print(checksum)
}
}
17 changes: 17 additions & 0 deletions Sources/PackageModel/SwiftSDKs/SwiftSDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ public enum SwiftSDKError: Swift.Error {
/// A passed argument is neither a valid file system path nor a URL.
case invalidPathOrURL(String)

/// Bundles installed from remote URLs require a checksum to be provided.
case checksumNotProvided(URL)

/// Computed archive checksum does not match the provided checksum.
case checksumInvalid(computed: String, provided: String)

/// Couldn't find the Xcode installation.
case invalidInstallation(String)

Expand Down Expand Up @@ -64,6 +70,17 @@ public enum SwiftSDKError: Swift.Error {
extension SwiftSDKError: CustomStringConvertible {
public var description: String {
switch self {
case let .checksumInvalid(computed, provided):
return """
Computed archive checksum `\(computed) does not match the provided checksum `\(provided)`.
"""

case .checksumNotProvided(let url):
return """
Bundles installed from remote URLs (such as \(url)) require their checksum passed via `--checksum` option.
The distributor of the bundle must compute it with `swift package compute-checksum` \
MaxDesiatov marked this conversation as resolved.
Show resolved Hide resolved
command and provide it with their Swift SDK installation instructions.
"""
case .invalidBundleArchive(let archivePath):
return """
Swift SDK archive at `\(archivePath)` does not contain at least one directory with the \
Expand Down
27 changes: 23 additions & 4 deletions Sources/PackageModel/SwiftSDKs/SwiftSDKBundleStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public final class SwiftSDKBundleStore {
public enum Output: Equatable, CustomStringConvertible {
case downloadStarted(URL)
case downloadFinishedSuccessfully(URL)
case verifyingChecksum
case checksumValid
case unpackingArchive(bundlePathOrURL: String)
case installationSuccessful(bundlePathOrURL: String, bundleName: String)

Expand All @@ -31,6 +33,10 @@ public final class SwiftSDKBundleStore {
return "Downloading a Swift SDK bundle archive from `\(url)`..."
case let .downloadFinishedSuccessfully(url):
return "Swift SDK bundle archive successfully downloaded from `\(url)`."
case .verifyingChecksum:
return "Verifying if checksum of the downloaded archive is valid..."
case .checksumValid:
return "Downloaded archive has a valid checksum."
case let .installationSuccessful(bundlePathOrURL, bundleName):
return "Swift SDK bundle at `\(bundlePathOrURL)` successfully installed as \(bundleName)."
case let .unpackingArchive(bundlePathOrURL):
Expand Down Expand Up @@ -145,8 +151,10 @@ public final class SwiftSDKBundleStore {
/// - archiver: Archiver instance to use for extracting bundle archives.
public func install(
bundlePathOrURL: String,
checksum: String? = nil,
_ archiver: any Archiver,
_ httpClient: HTTPClient = .init()
_ httpClient: HTTPClient = .init(),
hasher: ((_ archivePath: AbsolutePath) throws -> String)? = nil
) async throws {
let bundleName = try await withTemporaryDirectory(fileSystem: self.fileSystem, removeTreeOnDeinit: true) { temporaryDirectory in
let bundlePath: AbsolutePath
Expand All @@ -156,9 +164,13 @@ public final class SwiftSDKBundleStore {
let scheme = bundleURL.scheme,
scheme == "http" || scheme == "https"
{
guard let checksum, let hasher else {
throw SwiftSDKError.checksumNotProvided(bundleURL)
}

let bundleName: String
let fileNameComponent = bundleURL.lastPathComponent
if archiver.supportedExtensions.contains(where: { fileNameComponent.hasSuffix($0) }) {
if archiver.isFileSupported(fileNameComponent) {
bundleName = fileNameComponent
} else {
// Assume that the bundle is a tarball if it doesn't have a recognized extension.
Expand Down Expand Up @@ -193,9 +205,16 @@ public final class SwiftSDKBundleStore {
)
self.downloadProgressAnimation?.complete(success: true)

bundlePath = downloadedBundlePath

self.outputHandler(.downloadFinishedSuccessfully(bundleURL))

self.outputHandler(.verifyingChecksum)
let computedChecksum = try hasher(downloadedBundlePath)
guard computedChecksum == checksum else {
throw SwiftSDKError.checksumInvalid(computed: computedChecksum, provided: checksum)
}
self.outputHandler(.checksumValid)

bundlePath = downloadedBundlePath
} else if
let cwd: AbsolutePath = self.fileSystem.currentWorkingDirectory,
let originalBundlePath = try? AbsolutePath(validating: bundlePathOrURL, relativeTo: cwd)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import ArgumentParser
import Basics
import PackageModel

public struct DeprecatedSwiftSDKConfigurationCommand: ParsableCommand {
public static let configuration = CommandConfiguration(
package struct DeprecatedSwiftSDKConfigurationCommand: ParsableCommand {
package static let configuration = CommandConfiguration(
commandName: "configuration",
abstract: """
Deprecated: use `swift sdk configure` instead.
Expand All @@ -29,5 +29,5 @@ public struct DeprecatedSwiftSDKConfigurationCommand: ParsableCommand {
]
)

public init() {}
package init() {}
}
18 changes: 14 additions & 4 deletions Sources/SwiftSDKCommand/InstallSwiftSDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ import CoreCommands
import Foundation
import PackageModel

import class Workspace.Workspace
import var TSCBasic.stdoutStream

public struct InstallSwiftSDK: SwiftSDKSubcommand {
public static let configuration = CommandConfiguration(
struct InstallSwiftSDK: SwiftSDKSubcommand {
static let configuration = CommandConfiguration(
commandName: "install",
abstract: """
Installs a given Swift SDK bundle to a location discoverable by SwiftPM. If the artifact bundle \
Expand All @@ -34,7 +35,8 @@ public struct InstallSwiftSDK: SwiftSDKSubcommand {
@Argument(help: "A local filesystem path or a URL of a Swift SDK bundle to install.")
var bundlePathOrURL: String

public init() {}
@Option(help: "The checksum of the bundle generated with `swift package compute-checksum`.")
var checksum: String? = nil

func run(
hostTriple: Triple,
Expand All @@ -53,10 +55,18 @@ public struct InstallSwiftSDK: SwiftSDKSubcommand {
.percent(stream: stdoutStream, verbose: false, header: "Downloading")
.throttled(interval: .milliseconds(300))
)

try await store.install(
bundlePathOrURL: bundlePathOrURL,
checksum: self.checksum,
UniversalArchiver(self.fileSystem, cancellator),
HTTPClient()
HTTPClient(),
hasher: {
try Workspace.BinaryArtifactsManager.checksum(
forBinaryArtifactAt: $0,
fileSystem: self.fileSystem
)
}
)
}
}
7 changes: 3 additions & 4 deletions Sources/SwiftSDKCommand/ListSwiftSDKs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import CoreCommands
import PackageModel
import SPMBuildCore

public struct ListSwiftSDKs: SwiftSDKSubcommand {
public static let configuration = CommandConfiguration(
package struct ListSwiftSDKs: SwiftSDKSubcommand {
package static let configuration = CommandConfiguration(
commandName: "list",
abstract:
"""
Expand All @@ -28,8 +28,7 @@ public struct ListSwiftSDKs: SwiftSDKSubcommand {
@OptionGroup()
var locations: LocationOptions


public init() {}
package init() {}

func run(
hostTriple: Triple,
Expand Down
4 changes: 2 additions & 2 deletions Sources/SwiftSDKCommand/RemoveSwiftSDK.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import Basics
import CoreCommands
import PackageModel

public struct RemoveSwiftSDK: SwiftSDKSubcommand {
public static let configuration = CommandConfiguration(
package struct RemoveSwiftSDK: SwiftSDKSubcommand {
package static let configuration = CommandConfiguration(
commandName: "remove",
abstract: """
Removes a previously installed Swift SDK bundle from the filesystem.
Expand Down
6 changes: 3 additions & 3 deletions Sources/SwiftSDKCommand/SwiftSDKCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
import ArgumentParser
import Basics

public struct SwiftSDKCommand: AsyncParsableCommand {
public static let configuration = CommandConfiguration(
package struct SwiftSDKCommand: AsyncParsableCommand {
package static let configuration = CommandConfiguration(
commandName: "sdk",
_superCommandName: "swift",
abstract: "Perform operations on Swift SDKs.",
Expand All @@ -29,5 +29,5 @@ public struct SwiftSDKCommand: AsyncParsableCommand {
helpNames: [.short, .long, .customLong("help", withSingleDash: true)]
)

public init() {}
package init() {}
}
29 changes: 22 additions & 7 deletions Sources/Workspace/Workspace+BinaryArtifacts.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import SPMBuildCore

import struct TSCBasic.ByteString
import protocol TSCBasic.HashAlgorithm

import struct TSCBasic.SHA256
import enum TSCUtility.Diagnostics

extension Workspace {
Expand Down Expand Up @@ -537,20 +537,35 @@ extension Workspace {
return result.get()
}

public func checksum(forBinaryArtifactAt path: AbsolutePath) throws -> String {
package static func checksum(
forBinaryArtifactAt path: AbsolutePath,
hashAlgorithm: HashAlgorithm = SHA256(),
archiver: (any Archiver)? = nil,
fileSystem: any FileSystem
) throws -> String {
let archiver = archiver ?? UniversalArchiver(fileSystem)
// Validate the path has a supported extension.
guard let pathExtension = path.extension, self.archiver.supportedExtensions.contains(pathExtension) else {
let supportedExtensionList = self.archiver.supportedExtensions.joined(separator: ", ")
guard let lastPathComponent = path.components.last, archiver.isFileSupported(lastPathComponent) else {
let supportedExtensionList = archiver.supportedExtensions.joined(separator: ", ")
throw StringError("unexpected file type; supported extensions are: \(supportedExtensionList)")
}

// Ensure that the path with the accepted extension is a file.
guard self.fileSystem.isFile(path) else {
guard fileSystem.isFile(path) else {
throw StringError("file not found at path: \(path.pathString)")
}

let contents = try self.fileSystem.readFileContents(path)
return self.checksumAlgorithm.hash(contents).hexadecimalRepresentation
let contents = try fileSystem.readFileContents(path)
return hashAlgorithm.hash(contents).hexadecimalRepresentation
}

public func checksum(forBinaryArtifactAt path: AbsolutePath) throws -> String {
try Self.checksum(
forBinaryArtifactAt: path,
hashAlgorithm: self.checksumAlgorithm,
archiver: self.archiver,
fileSystem: self.fileSystem
)
}

public func cancel(deadline: DispatchTime) throws {
Expand Down
17 changes: 11 additions & 6 deletions Tests/PackageModelTests/SwiftSDKBundleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import XCTest
import struct TSCBasic.ByteString
import protocol TSCBasic.FileSystem
import class TSCBasic.InMemoryFileSystem
import class Workspace.Workspace

private let testArtifactID = "test-artifact"

Expand Down Expand Up @@ -146,13 +147,13 @@ final class SwiftSDKBundleTests: XCTestCase {
let cancellator = Cancellator(observabilityScope: observabilityScope)
let archiver = UniversalArchiver(localFileSystem, cancellator)

let fixtureAndURLs: [(url: String, fixture: String)] = [
("https://localhost/archive?test=foo", "test-sdk.artifactbundle.tar.gz"),
("https://localhost/archive.tar.gz", "test-sdk.artifactbundle.tar.gz"),
("https://localhost/archive.zip", "test-sdk.artifactbundle.zip"),
let fixtureAndURLs: [(url: String, fixture: String, checksum: String)] = [
("https://localhost/archive?test=foo", "test-sdk.artifactbundle.tar.gz", "724b5abf125287517dbc5be9add055d4755dfca679e163b249ea1045f5800c6e"),
("https://localhost/archive.tar.gz", "test-sdk.artifactbundle.tar.gz", "724b5abf125287517dbc5be9add055d4755dfca679e163b249ea1045f5800c6e"),
("https://localhost/archive.zip", "test-sdk.artifactbundle.zip", "74f6df5aa91c582c12e3a6670ff95973e463dd3266aabbc52ad13c3cd27e2793"),
]

for (bundleURLString, fixture) in fixtureAndURLs {
for (bundleURLString, fixture, checksum) in fixtureAndURLs {
let httpClient = HTTPClient { request, _ in
guard case let .download(_, downloadPath) = request.kind else {
XCTFail("Unexpected HTTPClient.Request.Kind")
Expand All @@ -173,12 +174,16 @@ final class SwiftSDKBundleTests: XCTestCase {
output.append($0)
}
)
try await store.install(bundlePathOrURL: bundleURLString, archiver, httpClient)
try await store.install(bundlePathOrURL: bundleURLString, checksum: checksum, archiver, httpClient) {
try Workspace.BinaryArtifactsManager.checksum(forBinaryArtifactAt: $0, fileSystem: localFileSystem)
}

let bundleURL = URL(string: bundleURLString)!
XCTAssertEqual(output, [
.downloadStarted(bundleURL),
.downloadFinishedSuccessfully(bundleURL),
.verifyingChecksum,
.checksumValid,
.unpackingArchive(bundlePathOrURL: bundleURLString),
.installationSuccessful(
bundlePathOrURL: bundleURLString,
Expand Down