diff --git a/Sources/Basics/Archiver/Archiver.swift b/Sources/Basics/Archiver/Archiver.swift index 6a2502158f3..1af017e3837 100644 --- a/Sources/Basics/Archiver/Archiver.swift +++ b/Sources/Basics/Archiver/Archiver.swift @@ -11,6 +11,7 @@ //===----------------------------------------------------------------------===// import _Concurrency +import struct Foundation.URL /// The `Archiver` protocol abstracts away the different operations surrounding archives. public protocol Archiver: Sendable { diff --git a/Sources/PackageModel/SwiftSDKs/SwiftSDK.swift b/Sources/PackageModel/SwiftSDKs/SwiftSDK.swift index 63b404d098f..faa55dbdc0e 100644 --- a/Sources/PackageModel/SwiftSDKs/SwiftSDK.swift +++ b/Sources/PackageModel/SwiftSDKs/SwiftSDK.swift @@ -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) @@ -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 (`\(url)`) require their checksum passed via `--checksum` option. + The distributor of the bundle must compute it with the `swift package compute-checksum` \ + 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 \ diff --git a/Sources/PackageModel/SwiftSDKs/SwiftSDKBundleStore.swift b/Sources/PackageModel/SwiftSDKs/SwiftSDKBundleStore.swift index 4a6e83da01b..7810717e683 100644 --- a/Sources/PackageModel/SwiftSDKs/SwiftSDKBundleStore.swift +++ b/Sources/PackageModel/SwiftSDKs/SwiftSDKBundleStore.swift @@ -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) @@ -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): @@ -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 @@ -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. @@ -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) diff --git a/Sources/SwiftSDKCommand/Configuration/DeprecatedSwiftSDKConfigurationCommand.swift b/Sources/SwiftSDKCommand/Configuration/DeprecatedSwiftSDKConfigurationCommand.swift index cdbebc5fce9..696a25983b6 100644 --- a/Sources/SwiftSDKCommand/Configuration/DeprecatedSwiftSDKConfigurationCommand.swift +++ b/Sources/SwiftSDKCommand/Configuration/DeprecatedSwiftSDKConfigurationCommand.swift @@ -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. @@ -29,5 +29,5 @@ public struct DeprecatedSwiftSDKConfigurationCommand: ParsableCommand { ] ) - public init() {} + package init() {} } diff --git a/Sources/SwiftSDKCommand/InstallSwiftSDK.swift b/Sources/SwiftSDKCommand/InstallSwiftSDK.swift index 39b8b4f2471..6b56a23446d 100644 --- a/Sources/SwiftSDKCommand/InstallSwiftSDK.swift +++ b/Sources/SwiftSDKCommand/InstallSwiftSDK.swift @@ -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 \ @@ -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, @@ -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 + ) + } ) } } diff --git a/Sources/SwiftSDKCommand/ListSwiftSDKs.swift b/Sources/SwiftSDKCommand/ListSwiftSDKs.swift index 4b7701034b6..062b43b9e75 100644 --- a/Sources/SwiftSDKCommand/ListSwiftSDKs.swift +++ b/Sources/SwiftSDKCommand/ListSwiftSDKs.swift @@ -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: """ @@ -28,8 +28,7 @@ public struct ListSwiftSDKs: SwiftSDKSubcommand { @OptionGroup() var locations: LocationOptions - - public init() {} + package init() {} func run( hostTriple: Triple, diff --git a/Sources/SwiftSDKCommand/RemoveSwiftSDK.swift b/Sources/SwiftSDKCommand/RemoveSwiftSDK.swift index 2c0a735c257..78bc3248a0d 100644 --- a/Sources/SwiftSDKCommand/RemoveSwiftSDK.swift +++ b/Sources/SwiftSDKCommand/RemoveSwiftSDK.swift @@ -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. diff --git a/Sources/SwiftSDKCommand/SwiftSDKCommand.swift b/Sources/SwiftSDKCommand/SwiftSDKCommand.swift index 1441f37f15b..e167cc548ac 100644 --- a/Sources/SwiftSDKCommand/SwiftSDKCommand.swift +++ b/Sources/SwiftSDKCommand/SwiftSDKCommand.swift @@ -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.", @@ -29,5 +29,5 @@ public struct SwiftSDKCommand: AsyncParsableCommand { helpNames: [.short, .long, .customLong("help", withSingleDash: true)] ) - public init() {} + package init() {} } diff --git a/Tests/PackageModelTests/SwiftSDKBundleTests.swift b/Tests/PackageModelTests/SwiftSDKBundleTests.swift index 1dd9ed4c2ab..ab9da497dd2 100644 --- a/Tests/PackageModelTests/SwiftSDKBundleTests.swift +++ b/Tests/PackageModelTests/SwiftSDKBundleTests.swift @@ -18,6 +18,7 @@ import XCTest import struct TSCBasic.ByteString import protocol TSCBasic.FileSystem +import class Workspace.Workspace private let testArtifactID = "test-artifact" @@ -145,13 +146,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") @@ -172,12 +173,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,