Skip to content

Commit 150ec01

Browse files
authored
Cross-compilation: fix bundles not unpacked on installation (#6361)
`swift experimental-destination install` subcommand works with bundle directories, but fails to unpack bundle archives. We should pass an instance of an archiver to the installation function to unpack destination bundle archives if needed. Since `withTemporaryDirectory` is `async`, `SwiftDestinationTool` had to be converted to `AsyncParsableCommand` and also gain `@main` attribute for that to work. Additionally, `@main` attribute requires `-parse-as-library` passed in CMake. rdar://107367895
1 parent a6fde25 commit 150ec01

File tree

14 files changed

+232
-98
lines changed

14 files changed

+232
-98
lines changed

Sources/Basics/Archiver.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
import _Concurrency
1314
import TSCBasic
1415

1516
/// The `Archiver` protocol abstracts away the different operations surrounding archives.
@@ -51,3 +52,14 @@ public protocol Archiver {
5152
completion: @escaping (Result<Bool, Error>) -> Void
5253
)
5354
}
55+
56+
extension Archiver {
57+
public func extract(
58+
from archivePath: AbsolutePath,
59+
to destinationPath: AbsolutePath
60+
) async throws {
61+
try await withCheckedThrowingContinuation {
62+
self.extract(from: archivePath, to: destinationPath, completion: $0.resume(with:))
63+
}
64+
}
65+
}

Sources/Basics/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ add_library(Basics
2626
FileSystem/AsyncFileSystem.swift
2727
FileSystem/FileSystem+Extensions.swift
2828
FileSystem/Path+Extensions.swift
29+
FileSystem/TemporaryFile.swift
2930
FileSystem/VFSOverlay.swift
3031
HTTPClient/HTTPClient.swift
3132
HTTPClient/HTTPClientConfiguration.swift

Sources/CrossCompilationDestinationsTool/Configuration/ConfigureDestination.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212

1313
import ArgumentParser
1414

15-
struct ConfigureDestination: ParsableCommand {
16-
static let configuration = CommandConfiguration(
15+
public struct ConfigureDestination: ParsableCommand {
16+
public static let configuration = CommandConfiguration(
1717
commandName: "configuration",
1818
abstract: """
1919
Manages configuration options for installed cross-compilation destinations.
@@ -24,4 +24,6 @@ struct ConfigureDestination: ParsableCommand {
2424
ShowConfiguration.self,
2525
]
2626
)
27+
28+
public init() {}
2729
}

Sources/CrossCompilationDestinationsTool/DestinationCommand.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import var TSCBasic.localFileSystem
2222
import var TSCBasic.stdoutStream
2323

2424
/// A protocol for functions and properties common to all destination subcommands.
25-
protocol DestinationCommand: ParsableCommand {
25+
protocol DestinationCommand: AsyncParsableCommand {
2626
/// Common locations options provided by ArgumentParser.
2727
var locations: LocationOptions { get }
2828

@@ -35,7 +35,7 @@ protocol DestinationCommand: ParsableCommand {
3535
buildTimeTriple: Triple,
3636
_ destinationsDirectory: AbsolutePath,
3737
_ observabilityScope: ObservabilityScope
38-
) throws
38+
) async throws
3939
}
4040

4141
extension DestinationCommand {
@@ -62,7 +62,7 @@ extension DestinationCommand {
6262
return destinationsDirectory
6363
}
6464

65-
public func run() throws {
65+
public func run() async throws {
6666
let observabilityHandler = SwiftToolObservabilityHandler(outputStream: stdoutStream, logLevel: .info)
6767
let observabilitySystem = ObservabilitySystem(observabilityHandler)
6868
let observabilityScope = observabilitySystem.topScope
@@ -73,7 +73,7 @@ extension DestinationCommand {
7373

7474
var commandError: Error? = nil
7575
do {
76-
try self.run(buildTimeTriple: triple, destinationsDirectory, observabilityScope)
76+
try await self.run(buildTimeTriple: triple, destinationsDirectory, observabilityScope)
7777
if observabilityScope.errorsReported {
7878
throw ExitCode.failure
7979
}

Sources/CrossCompilationDestinationsTool/InstallDestination.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ import var TSCBasic.localFileSystem
2121
import var TSCBasic.stdoutStream
2222
import func TSCBasic.tsc_await
2323

24-
struct InstallDestination: DestinationCommand {
25-
static let configuration = CommandConfiguration(
24+
public struct InstallDestination: DestinationCommand {
25+
public static let configuration = CommandConfiguration(
2626
commandName: "install",
2727
abstract: """
2828
Installs a given destination artifact bundle to a location discoverable by SwiftPM. If the artifact bundle \
@@ -36,15 +36,18 @@ struct InstallDestination: DestinationCommand {
3636
@Argument(help: "A local filesystem path or a URL of an artifact bundle to install.")
3737
var bundlePathOrURL: String
3838

39+
public init() {}
40+
3941
func run(
4042
buildTimeTriple: Triple,
4143
_ destinationsDirectory: AbsolutePath,
4244
_ observabilityScope: ObservabilityScope
43-
) throws {
44-
try DestinationBundle.install(
45+
) async throws {
46+
try await DestinationBundle.install(
4547
bundlePathOrURL: bundlePathOrURL,
4648
destinationsDirectory: destinationsDirectory,
4749
self.fileSystem,
50+
ZipArchiver(fileSystem: self.fileSystem),
4851
observabilityScope
4952
)
5053
}

Sources/CrossCompilationDestinationsTool/ListDestinations.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import SPMBuildCore
1818

1919
import struct TSCBasic.AbsolutePath
2020

21-
struct ListDestinations: DestinationCommand {
21+
public struct ListDestinations: DestinationCommand {
2222
public static let configuration = CommandConfiguration(
2323
commandName: "list",
2424
abstract:
@@ -30,6 +30,8 @@ struct ListDestinations: DestinationCommand {
3030
@OptionGroup()
3131
var locations: LocationOptions
3232

33+
public init() {}
34+
3335
func run(
3436
buildTimeTriple: Triple,
3537
_ destinationsDirectory: AbsolutePath,

Sources/CrossCompilationDestinationsTool/RemoveDestination.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import PackageModel
1717

1818
import struct TSCBasic.AbsolutePath
1919

20-
struct RemoveDestination: DestinationCommand {
21-
static let configuration = CommandConfiguration(
20+
public struct RemoveDestination: DestinationCommand {
21+
public static let configuration = CommandConfiguration(
2222
commandName: "remove",
2323
abstract: """
2424
Removes a previously installed destination artifact bundle from the filesystem.
@@ -31,6 +31,8 @@ struct RemoveDestination: DestinationCommand {
3131
@Argument(help: "Name of the destination artifact bundle or ID of the destination to remove from the filesystem.")
3232
var destinationIDOrBundleName: String
3333

34+
public init() {}
35+
3436
func run(
3537
buildTimeTriple: Triple,
3638
_ destinationsDirectory: AbsolutePath,

Sources/CrossCompilationDestinationsTool/SwiftDestinationTool.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import ArgumentParser
1414
import Basics
1515

16-
public struct SwiftDestinationTool: ParsableCommand {
16+
public struct SwiftDestinationTool: AsyncParsableCommand {
1717
public static let configuration = CommandConfiguration(
1818
commandName: "experimental-destination",
1919
_superCommandName: "swift",

Sources/PackageModel/Destination.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ public enum DestinationError: Swift.Error {
2727
/// The schema version is invalid.
2828
case invalidSchemaVersion
2929

30+
/// Name of the destination bundle is not valid.
31+
case invalidBundleName(String)
32+
3033
/// No valid destinations were decoded from a destination file.
3134
case noDestinationsDecoded(AbsolutePath)
3235

@@ -58,6 +61,10 @@ extension DestinationError: CustomStringConvertible {
5861
return "unsupported destination file schema version"
5962
case .invalidInstallation(let problem):
6063
return problem
64+
case .invalidBundleName(let name):
65+
return """
66+
invalid bundle name `\(name)`, unpacked destination bundles are expected to have `.artifactbundle` extension
67+
"""
6168
case .noDestinationsDecoded(let path):
6269
return "no valid destinations were decoded from a destination file at path `\(path)`"
6370
case .pathIsNotDirectory(let path):

Sources/PackageModel/DestinationBundle.swift

Lines changed: 107 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,14 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
1314
import Basics
14-
import TSCBasic
1515

16+
import func TSCBasic.tsc_await
17+
import protocol TSCBasic.FileSystem
1618
import struct Foundation.URL
19+
import struct TSCBasic.AbsolutePath
20+
import struct TSCBasic.RegEx
1721

1822
/// Represents an `.artifactbundle` on the filesystem that contains cross-compilation destinations.
1923
public struct DestinationBundle {
@@ -126,57 +130,110 @@ public struct DestinationBundle {
126130
/// - Parameters:
127131
/// - bundlePathOrURL: A string passed on the command line, which is either an absolute or relative to a current
128132
/// working directory path, or a URL to a destination artifact bundle.
129-
/// - destinationsDirectory: a directory where the destination artifact bundle should be installed.
130-
/// - fileSystem: file system on which all of the file operations should run.
131-
/// - observabilityScope: observability scope for reporting warnings and errors.
133+
/// - destinationsDirectory: A directory where the destination artifact bundle should be installed.
134+
/// - fileSystem: File system on which all of the file operations should run.
135+
/// - observabilityScope: Observability scope for reporting warnings and errors.
132136
public static func install(
133137
bundlePathOrURL: String,
134138
destinationsDirectory: AbsolutePath,
135139
_ fileSystem: some FileSystem,
140+
_ archiver: some Archiver,
136141
_ observabilityScope: ObservabilityScope
137-
) throws {
138-
let installedBundlePath: AbsolutePath
139-
140-
if
141-
let bundleURL = URL(string: bundlePathOrURL),
142-
let scheme = bundleURL.scheme,
143-
scheme == "http" || scheme == "https"
144-
{
145-
let response = try tsc_await { (completion: @escaping (Result<HTTPClientResponse, Error>) -> Void) in
146-
let client = LegacyHTTPClient()
147-
client.execute(
148-
.init(method: .get, url: bundleURL),
142+
) async throws {
143+
_ = try await withTemporaryDirectory(
144+
fileSystem: fileSystem,
145+
removeTreeOnDeinit: true
146+
) { temporaryDirectory in
147+
let bundlePath: AbsolutePath
148+
149+
if
150+
let bundleURL = URL(string: bundlePathOrURL),
151+
let scheme = bundleURL.scheme,
152+
scheme == "http" || scheme == "https"
153+
{
154+
let bundleName = bundleURL.lastPathComponent
155+
let downloadedBundlePath = temporaryDirectory.appending(component: bundleName)
156+
157+
let client = HTTPClient()
158+
var request = HTTPClientRequest.download(
159+
url: bundleURL,
160+
fileSystem: AsyncFileSystem { fileSystem },
161+
destination: downloadedBundlePath
162+
)
163+
request.options.validResponseCodes = [200]
164+
_ = try await client.execute(
165+
request,
149166
observabilityScope: observabilityScope,
150-
progress: nil,
151-
completion: completion
167+
progress: nil
152168
)
153-
}
154169

155-
guard let body = response.body else {
156-
throw StringError("No downloadable data available at URL `\(bundleURL)`.")
157-
}
170+
bundlePath = downloadedBundlePath
158171

159-
let fileName = bundleURL.lastPathComponent
160-
installedBundlePath = destinationsDirectory.appending(component: fileName)
172+
print("Destination artifact bundle successfully downloaded from `\(bundleURL)`.")
173+
} else if
174+
let cwd = fileSystem.currentWorkingDirectory,
175+
let originalBundlePath = try? AbsolutePath(validating: bundlePathOrURL, relativeTo: cwd)
176+
{
177+
bundlePath = originalBundlePath
178+
} else {
179+
throw DestinationError.invalidPathOrURL(bundlePathOrURL)
180+
}
161181

162-
try fileSystem.writeFileContents(installedBundlePath, data: body)
163-
} else if
164-
let cwd = fileSystem.currentWorkingDirectory,
165-
let originalBundlePath = try? AbsolutePath(validating: bundlePathOrURL, relativeTo: cwd)
166-
{
167-
try installIfValid(
168-
bundlePath: originalBundlePath,
182+
try await installIfValid(
183+
bundlePath: bundlePath,
169184
destinationsDirectory: destinationsDirectory,
185+
temporaryDirectory: temporaryDirectory,
170186
fileSystem,
187+
archiver,
171188
observabilityScope
172189
)
173-
} else {
174-
throw DestinationError.invalidPathOrURL(bundlePathOrURL)
190+
}.value
191+
192+
print("Destination artifact bundle at `\(bundlePathOrURL)` successfully installed.")
193+
}
194+
195+
/// Unpacks a destination bundle if it has an archive extension in its filename.
196+
/// - Parameters:
197+
/// - bundlePath: Absolute path to a destination bundle to unpack if needed.
198+
/// - temporaryDirectory: Absolute path to a temporary directory in which the bundle can be unpacked if needed.
199+
/// - fileSystem: A file system to operate on that contains the given paths.
200+
/// - archiver: Archiver to use for unpacking.
201+
/// - Returns: Path to an unpacked destination bundle if unpacking is needed, value of `bundlePath` is returned
202+
/// otherwise.
203+
private static func unpackIfNeeded(
204+
bundlePath: AbsolutePath,
205+
destinationsDirectory: AbsolutePath,
206+
temporaryDirectory: AbsolutePath,
207+
_ fileSystem: some FileSystem,
208+
_ archiver: some Archiver
209+
) async throws -> AbsolutePath {
210+
let regex = try RegEx(pattern: "(.+\\.artifactbundle).*")
211+
212+
guard let bundleName = bundlePath.components.last else {
213+
throw DestinationError.invalidPathOrURL(bundlePath.pathString)
214+
}
215+
216+
guard let unpackedBundleName = regex.matchGroups(in: bundleName).first?.first else {
217+
throw DestinationError.invalidBundleName(bundleName)
218+
}
219+
220+
let installedBundlePath = destinationsDirectory.appending(component: unpackedBundleName)
221+
guard !fileSystem.exists(installedBundlePath) else {
222+
throw DestinationError.destinationBundleAlreadyInstalled(bundleName: unpackedBundleName)
175223
}
176224

177-
observabilityScope.emit(info: "Destination artifact bundle at `\(bundlePathOrURL)` successfully installed.")
225+
print("\(bundleName) is assumed to be an archive, unpacking...")
226+
227+
// If there's no archive extension on the bundle name, assuming it's not archived and returning the same path.
228+
guard unpackedBundleName != bundleName else {
229+
return bundlePath
230+
}
231+
232+
try await archiver.extract(from: bundlePath, to: temporaryDirectory)
233+
234+
return temporaryDirectory.appending(component: unpackedBundleName)
178235
}
179-
236+
180237
/// Installs an unpacked destination bundle to a destinations installation directory.
181238
/// - Parameters:
182239
/// - bundlePath: absolute path to an unpacked destination bundle directory.
@@ -186,23 +243,30 @@ public struct DestinationBundle {
186243
private static func installIfValid(
187244
bundlePath: AbsolutePath,
188245
destinationsDirectory: AbsolutePath,
246+
temporaryDirectory: AbsolutePath,
189247
_ fileSystem: some FileSystem,
248+
_ archiver: some Archiver,
190249
_ observabilityScope: ObservabilityScope
191-
) throws {
250+
) async throws {
251+
let unpackedBundlePath = try await unpackIfNeeded(
252+
bundlePath: bundlePath,
253+
destinationsDirectory: destinationsDirectory,
254+
temporaryDirectory: temporaryDirectory,
255+
fileSystem,
256+
archiver
257+
)
258+
192259
guard
193-
fileSystem.isDirectory(bundlePath),
194-
let bundleName = bundlePath.components.last
260+
fileSystem.isDirectory(unpackedBundlePath),
261+
let bundleName = unpackedBundlePath.components.last
195262
else {
196263
throw DestinationError.pathIsNotDirectory(bundlePath)
197264
}
198265

199266
let installedBundlePath = destinationsDirectory.appending(component: bundleName)
200-
guard !fileSystem.exists(installedBundlePath) else {
201-
throw DestinationError.destinationBundleAlreadyInstalled(bundleName: bundleName)
202-
}
203267

204268
let validatedBundle = try Self.parseAndValidate(
205-
bundlePath: bundlePath,
269+
bundlePath: unpackedBundlePath,
206270
fileSystem: fileSystem,
207271
observabilityScope: observabilityScope
208272
)
@@ -226,7 +290,7 @@ public struct DestinationBundle {
226290
}
227291
}
228292

229-
try fileSystem.copy(from: bundlePath, to: installedBundlePath)
293+
try fileSystem.copy(from: unpackedBundlePath, to: installedBundlePath)
230294
}
231295

232296
/// Parses metadata of an `.artifactbundle` and validates it as a bundle containing

0 commit comments

Comments
 (0)