Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
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 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 \
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