Skip to content

Commit a2aa745

Browse files
Implement --checksum option on swift sdk install (#7722)
### Motivation: This option [was specified in the corresponding proposal for Swift SDKs](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0387-cross-compilation-destinations.md#swift-sdk-installation-and-configuration): > For Swift SDKs installed from remote URLs an additional `--checksum` option is required, through which users of a Swift SDK can specify a checksum provided by a publisher of the SDK. The latter can produce a checksum by running `swift package compute-checksum` command (introduced in [SE-0272](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0272-swiftpm-binary-dependencies.md)) with the Swift SDK bundle archive as an argument. ### Modifications: Added `isFileSupported` method on `Archiver` to unify archive extensions matching logic between `swift package compute-checksum` and `swift sdk install`. Refactored checksum computation logic into `static func checksum` on `Workspace.BinaryArtifactsManager` to also unify it between the two commands. Added error and output handling for checksums in `func SwiftSDKBundleStore.installIfValid`, updated corresponding unit tests. ### Result: Resolves rdar://130590711 --------- Co-authored-by: Alastair Houghton <[email protected]>
1 parent 3d08211 commit a2aa745

File tree

9 files changed

+77
-26
lines changed

9 files changed

+77
-26
lines changed

Sources/Basics/Archiver/Archiver.swift

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
import _Concurrency
14+
import struct Foundation.URL
1415

1516
/// The `Archiver` protocol abstracts away the different operations surrounding archives.
1617
public protocol Archiver: Sendable {

Sources/PackageModel/SwiftSDKs/SwiftSDK.swift

+17
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ public enum SwiftSDKError: Swift.Error {
2525
/// A passed argument is neither a valid file system path nor a URL.
2626
case invalidPathOrURL(String)
2727

28+
/// Bundles installed from remote URLs require a checksum to be provided.
29+
case checksumNotProvided(URL)
30+
31+
/// Computed archive checksum does not match the provided checksum.
32+
case checksumInvalid(computed: String, provided: String)
33+
2834
/// Couldn't find the Xcode installation.
2935
case invalidInstallation(String)
3036

@@ -64,6 +70,17 @@ public enum SwiftSDKError: Swift.Error {
6470
extension SwiftSDKError: CustomStringConvertible {
6571
public var description: String {
6672
switch self {
73+
case let .checksumInvalid(computed, provided):
74+
return """
75+
Computed archive checksum `\(computed)` does not match the provided checksum `\(provided)`.
76+
"""
77+
78+
case .checksumNotProvided(let url):
79+
return """
80+
Bundles installed from remote URLs (`\(url)`) require their checksum passed via `--checksum` option.
81+
The distributor of the bundle must compute it with the `swift package compute-checksum` \
82+
command and provide it with their Swift SDK installation instructions.
83+
"""
6784
case .invalidBundleArchive(let archivePath):
6885
return """
6986
Swift SDK archive at `\(archivePath)` does not contain at least one directory with the \

Sources/PackageModel/SwiftSDKs/SwiftSDKBundleStore.swift

+23-4
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ public final class SwiftSDKBundleStore {
2222
public enum Output: Equatable, CustomStringConvertible {
2323
case downloadStarted(URL)
2424
case downloadFinishedSuccessfully(URL)
25+
case verifyingChecksum
26+
case checksumValid
2527
case unpackingArchive(bundlePathOrURL: String)
2628
case installationSuccessful(bundlePathOrURL: String, bundleName: String)
2729

@@ -31,6 +33,10 @@ public final class SwiftSDKBundleStore {
3133
return "Downloading a Swift SDK bundle archive from `\(url)`..."
3234
case let .downloadFinishedSuccessfully(url):
3335
return "Swift SDK bundle archive successfully downloaded from `\(url)`."
36+
case .verifyingChecksum:
37+
return "Verifying if checksum of the downloaded archive is valid..."
38+
case .checksumValid:
39+
return "Downloaded archive has a valid checksum."
3440
case let .installationSuccessful(bundlePathOrURL, bundleName):
3541
return "Swift SDK bundle at `\(bundlePathOrURL)` successfully installed as \(bundleName)."
3642
case let .unpackingArchive(bundlePathOrURL):
@@ -145,8 +151,10 @@ public final class SwiftSDKBundleStore {
145151
/// - archiver: Archiver instance to use for extracting bundle archives.
146152
public func install(
147153
bundlePathOrURL: String,
154+
checksum: String? = nil,
148155
_ archiver: any Archiver,
149-
_ httpClient: HTTPClient = .init()
156+
_ httpClient: HTTPClient = .init(),
157+
hasher: ((_ archivePath: AbsolutePath) throws -> String)? = nil
150158
) async throws {
151159
let bundleName = try await withTemporaryDirectory(fileSystem: self.fileSystem, removeTreeOnDeinit: true) { temporaryDirectory in
152160
let bundlePath: AbsolutePath
@@ -156,9 +164,13 @@ public final class SwiftSDKBundleStore {
156164
let scheme = bundleURL.scheme,
157165
scheme == "http" || scheme == "https"
158166
{
167+
guard let checksum, let hasher else {
168+
throw SwiftSDKError.checksumNotProvided(bundleURL)
169+
}
170+
159171
let bundleName: String
160172
let fileNameComponent = bundleURL.lastPathComponent
161-
if archiver.supportedExtensions.contains(where: { fileNameComponent.hasSuffix($0) }) {
173+
if archiver.isFileSupported(fileNameComponent) {
162174
bundleName = fileNameComponent
163175
} else {
164176
// Assume that the bundle is a tarball if it doesn't have a recognized extension.
@@ -193,9 +205,16 @@ public final class SwiftSDKBundleStore {
193205
)
194206
self.downloadProgressAnimation?.complete(success: true)
195207

196-
bundlePath = downloadedBundlePath
197-
198208
self.outputHandler(.downloadFinishedSuccessfully(bundleURL))
209+
210+
self.outputHandler(.verifyingChecksum)
211+
let computedChecksum = try hasher(downloadedBundlePath)
212+
guard computedChecksum == checksum else {
213+
throw SwiftSDKError.checksumInvalid(computed: computedChecksum, provided: checksum)
214+
}
215+
self.outputHandler(.checksumValid)
216+
217+
bundlePath = downloadedBundlePath
199218
} else if
200219
let cwd: AbsolutePath = self.fileSystem.currentWorkingDirectory,
201220
let originalBundlePath = try? AbsolutePath(validating: bundlePathOrURL, relativeTo: cwd)

Sources/SwiftSDKCommand/Configuration/DeprecatedSwiftSDKConfigurationCommand.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ import ArgumentParser
1414
import Basics
1515
import PackageModel
1616

17-
public struct DeprecatedSwiftSDKConfigurationCommand: ParsableCommand {
18-
public static let configuration = CommandConfiguration(
17+
package struct DeprecatedSwiftSDKConfigurationCommand: ParsableCommand {
18+
package static let configuration = CommandConfiguration(
1919
commandName: "configuration",
2020
abstract: """
2121
Deprecated: use `swift sdk configure` instead.
@@ -29,5 +29,5 @@ public struct DeprecatedSwiftSDKConfigurationCommand: ParsableCommand {
2929
]
3030
)
3131

32-
public init() {}
32+
package init() {}
3333
}

Sources/SwiftSDKCommand/InstallSwiftSDK.swift

+14-4
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ import CoreCommands
1717
import Foundation
1818
import PackageModel
1919

20+
import class Workspace.Workspace
2021
import var TSCBasic.stdoutStream
2122

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

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

3941
func run(
4042
hostTriple: Triple,
@@ -53,10 +55,18 @@ public struct InstallSwiftSDK: SwiftSDKSubcommand {
5355
.percent(stream: stdoutStream, verbose: false, header: "Downloading")
5456
.throttled(interval: .milliseconds(300))
5557
)
58+
5659
try await store.install(
5760
bundlePathOrURL: bundlePathOrURL,
61+
checksum: self.checksum,
5862
UniversalArchiver(self.fileSystem, cancellator),
59-
HTTPClient()
63+
HTTPClient(),
64+
hasher: {
65+
try Workspace.BinaryArtifactsManager.checksum(
66+
forBinaryArtifactAt: $0,
67+
fileSystem: self.fileSystem
68+
)
69+
}
6070
)
6171
}
6272
}

Sources/SwiftSDKCommand/ListSwiftSDKs.swift

+3-4
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ import CoreCommands
1616
import PackageModel
1717
import SPMBuildCore
1818

19-
public struct ListSwiftSDKs: SwiftSDKSubcommand {
20-
public static let configuration = CommandConfiguration(
19+
package struct ListSwiftSDKs: SwiftSDKSubcommand {
20+
package static let configuration = CommandConfiguration(
2121
commandName: "list",
2222
abstract:
2323
"""
@@ -28,8 +28,7 @@ public struct ListSwiftSDKs: SwiftSDKSubcommand {
2828
@OptionGroup()
2929
var locations: LocationOptions
3030

31-
32-
public init() {}
31+
package init() {}
3332

3433
func run(
3534
hostTriple: Triple,

Sources/SwiftSDKCommand/RemoveSwiftSDK.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ import Basics
1515
import CoreCommands
1616
import PackageModel
1717

18-
public struct RemoveSwiftSDK: SwiftSDKSubcommand {
19-
public static let configuration = CommandConfiguration(
18+
package struct RemoveSwiftSDK: SwiftSDKSubcommand {
19+
package static let configuration = CommandConfiguration(
2020
commandName: "remove",
2121
abstract: """
2222
Removes a previously installed Swift SDK bundle from the filesystem.

Sources/SwiftSDKCommand/SwiftSDKCommand.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
import ArgumentParser
1414
import Basics
1515

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

32-
public init() {}
32+
package init() {}
3333
}

Tests/PackageModelTests/SwiftSDKBundleTests.swift

+11-6
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import XCTest
1818

1919
import struct TSCBasic.ByteString
2020
import protocol TSCBasic.FileSystem
21+
import class Workspace.Workspace
2122

2223
private let testArtifactID = "test-artifact"
2324

@@ -145,13 +146,13 @@ final class SwiftSDKBundleTests: XCTestCase {
145146
let cancellator = Cancellator(observabilityScope: observabilityScope)
146147
let archiver = UniversalArchiver(localFileSystem, cancellator)
147148

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

154-
for (bundleURLString, fixture) in fixtureAndURLs {
155+
for (bundleURLString, fixture, checksum) in fixtureAndURLs {
155156
let httpClient = HTTPClient { request, _ in
156157
guard case let .download(_, downloadPath) = request.kind else {
157158
XCTFail("Unexpected HTTPClient.Request.Kind")
@@ -172,12 +173,16 @@ final class SwiftSDKBundleTests: XCTestCase {
172173
output.append($0)
173174
}
174175
)
175-
try await store.install(bundlePathOrURL: bundleURLString, archiver, httpClient)
176+
try await store.install(bundlePathOrURL: bundleURLString, checksum: checksum, archiver, httpClient) {
177+
try Workspace.BinaryArtifactsManager.checksum(forBinaryArtifactAt: $0, fileSystem: localFileSystem)
178+
}
176179

177180
let bundleURL = URL(string: bundleURLString)!
178181
XCTAssertEqual(output, [
179182
.downloadStarted(bundleURL),
180183
.downloadFinishedSuccessfully(bundleURL),
184+
.verifyingChecksum,
185+
.checksumValid,
181186
.unpackingArchive(bundlePathOrURL: bundleURLString),
182187
.installationSuccessful(
183188
bundlePathOrURL: bundleURLString,

0 commit comments

Comments
 (0)