Skip to content

Commit c18b8c4

Browse files
authored
[5.9] Cross-compilation: fix bundles not unpacked on installation (#6362)
`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 6f2bce2 commit c18b8c4

File tree

14 files changed

+231
-98
lines changed

14 files changed

+231
-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: 106 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@
1111
//===----------------------------------------------------------------------===//
1212

1313
import Basics
14-
import TSCBasic
1514

15+
import func TSCBasic.tsc_await
16+
import protocol TSCBasic.FileSystem
1617
import struct Foundation.URL
18+
import struct TSCBasic.AbsolutePath
19+
import struct TSCBasic.RegEx
1720

1821
/// Represents an `.artifactbundle` on the filesystem that contains cross-compilation destinations.
1922
public struct DestinationBundle {
@@ -126,57 +129,110 @@ public struct DestinationBundle {
126129
/// - Parameters:
127130
/// - bundlePathOrURL: A string passed on the command line, which is either an absolute or relative to a current
128131
/// 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.
132+
/// - destinationsDirectory: A directory where the destination artifact bundle should be installed.
133+
/// - fileSystem: File system on which all of the file operations should run.
134+
/// - observabilityScope: Observability scope for reporting warnings and errors.
132135
public static func install(
133136
bundlePathOrURL: String,
134137
destinationsDirectory: AbsolutePath,
135138
_ fileSystem: some FileSystem,
139+
_ archiver: some Archiver,
136140
_ 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),
141+
) async throws {
142+
_ = try await withTemporaryDirectory(
143+
fileSystem: fileSystem,
144+
removeTreeOnDeinit: true
145+
) { temporaryDirectory in
146+
let bundlePath: AbsolutePath
147+
148+
if
149+
let bundleURL = URL(string: bundlePathOrURL),
150+
let scheme = bundleURL.scheme,
151+
scheme == "http" || scheme == "https"
152+
{
153+
let bundleName = bundleURL.lastPathComponent
154+
let downloadedBundlePath = temporaryDirectory.appending(component: bundleName)
155+
156+
let client = HTTPClient()
157+
var request = HTTPClientRequest.download(
158+
url: bundleURL,
159+
fileSystem: AsyncFileSystem { fileSystem },
160+
destination: downloadedBundlePath
161+
)
162+
request.options.validResponseCodes = [200]
163+
_ = try await client.execute(
164+
request,
149165
observabilityScope: observabilityScope,
150-
progress: nil,
151-
completion: completion
166+
progress: nil
152167
)
153-
}
154168

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

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

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,
181+
try await installIfValid(
182+
bundlePath: bundlePath,
169183
destinationsDirectory: destinationsDirectory,
184+
temporaryDirectory: temporaryDirectory,
170185
fileSystem,
186+
archiver,
171187
observabilityScope
172188
)
173-
} else {
174-
throw DestinationError.invalidPathOrURL(bundlePathOrURL)
189+
}.value
190+
191+
print("Destination artifact bundle at `\(bundlePathOrURL)` successfully installed.")
192+
}
193+
194+
/// Unpacks a destination bundle if it has an archive extension in its filename.
195+
/// - Parameters:
196+
/// - bundlePath: Absolute path to a destination bundle to unpack if needed.
197+
/// - temporaryDirectory: Absolute path to a temporary directory in which the bundle can be unpacked if needed.
198+
/// - fileSystem: A file system to operate on that contains the given paths.
199+
/// - archiver: Archiver to use for unpacking.
200+
/// - Returns: Path to an unpacked destination bundle if unpacking is needed, value of `bundlePath` is returned
201+
/// otherwise.
202+
private static func unpackIfNeeded(
203+
bundlePath: AbsolutePath,
204+
destinationsDirectory: AbsolutePath,
205+
temporaryDirectory: AbsolutePath,
206+
_ fileSystem: some FileSystem,
207+
_ archiver: some Archiver
208+
) async throws -> AbsolutePath {
209+
let regex = try RegEx(pattern: "(.+\\.artifactbundle).*")
210+
211+
guard let bundleName = bundlePath.components.last else {
212+
throw DestinationError.invalidPathOrURL(bundlePath.pathString)
175213
}
176214

177-
observabilityScope.emit(info: "Destination artifact bundle at `\(bundlePathOrURL)` successfully installed.")
215+
guard let unpackedBundleName = regex.matchGroups(in: bundleName).first?.first else {
216+
throw DestinationError.invalidBundleName(bundleName)
217+
}
218+
219+
let installedBundlePath = destinationsDirectory.appending(component: unpackedBundleName)
220+
guard !fileSystem.exists(installedBundlePath) else {
221+
throw DestinationError.destinationBundleAlreadyInstalled(bundleName: unpackedBundleName)
222+
}
223+
224+
print("\(bundleName) is assumed to be an archive, unpacking...")
225+
226+
// If there's no archive extension on the bundle name, assuming it's not archived and returning the same path.
227+
guard unpackedBundleName != bundleName else {
228+
return bundlePath
229+
}
230+
231+
try await archiver.extract(from: bundlePath, to: temporaryDirectory)
232+
233+
return temporaryDirectory.appending(component: unpackedBundleName)
178234
}
179-
235+
180236
/// Installs an unpacked destination bundle to a destinations installation directory.
181237
/// - Parameters:
182238
/// - bundlePath: absolute path to an unpacked destination bundle directory.
@@ -186,23 +242,30 @@ public struct DestinationBundle {
186242
private static func installIfValid(
187243
bundlePath: AbsolutePath,
188244
destinationsDirectory: AbsolutePath,
245+
temporaryDirectory: AbsolutePath,
189246
_ fileSystem: some FileSystem,
247+
_ archiver: some Archiver,
190248
_ observabilityScope: ObservabilityScope
191-
) throws {
249+
) async throws {
250+
let unpackedBundlePath = try await unpackIfNeeded(
251+
bundlePath: bundlePath,
252+
destinationsDirectory: destinationsDirectory,
253+
temporaryDirectory: temporaryDirectory,
254+
fileSystem,
255+
archiver
256+
)
257+
192258
guard
193-
fileSystem.isDirectory(bundlePath),
194-
let bundleName = bundlePath.components.last
259+
fileSystem.isDirectory(unpackedBundlePath),
260+
let bundleName = unpackedBundlePath.components.last
195261
else {
196262
throw DestinationError.pathIsNotDirectory(bundlePath)
197263
}
198264

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

204267
let validatedBundle = try Self.parseAndValidate(
205-
bundlePath: bundlePath,
268+
bundlePath: unpackedBundlePath,
206269
fileSystem: fileSystem,
207270
observabilityScope: observabilityScope
208271
)
@@ -226,7 +289,7 @@ public struct DestinationBundle {
226289
}
227290
}
228291

229-
try fileSystem.copy(from: bundlePath, to: installedBundlePath)
292+
try fileSystem.copy(from: unpackedBundlePath, to: installedBundlePath)
230293
}
231294

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

0 commit comments

Comments
 (0)