11
11
//===----------------------------------------------------------------------===//
12
12
13
13
import Basics
14
- import TSCBasic
15
14
15
+ import func TSCBasic. tsc_await
16
+ import protocol TSCBasic. FileSystem
16
17
import struct Foundation. URL
18
+ import struct TSCBasic. AbsolutePath
19
+ import struct TSCBasic. RegEx
17
20
18
21
/// Represents an `.artifactbundle` on the filesystem that contains cross-compilation destinations.
19
22
public struct DestinationBundle {
@@ -126,57 +129,110 @@ public struct DestinationBundle {
126
129
/// - Parameters:
127
130
/// - bundlePathOrURL: A string passed on the command line, which is either an absolute or relative to a current
128
131
/// 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.
132
135
public static func install(
133
136
bundlePathOrURL: String ,
134
137
destinationsDirectory: AbsolutePath ,
135
138
_ fileSystem: some FileSystem ,
139
+ _ archiver: some Archiver ,
136
140
_ 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,
149
165
observabilityScope: observabilityScope,
150
- progress: nil ,
151
- completion: completion
166
+ progress: nil
152
167
)
153
- }
154
168
155
- guard let body = response. body else {
156
- throw StringError ( " No downloadable data available at URL ` \( bundleURL) `. " )
157
- }
169
+ bundlePath = downloadedBundlePath
158
170
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
+ }
161
180
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,
169
183
destinationsDirectory: destinationsDirectory,
184
+ temporaryDirectory: temporaryDirectory,
170
185
fileSystem,
186
+ archiver,
171
187
observabilityScope
172
188
)
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)
175
213
}
176
214
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)
178
234
}
179
-
235
+
180
236
/// Installs an unpacked destination bundle to a destinations installation directory.
181
237
/// - Parameters:
182
238
/// - bundlePath: absolute path to an unpacked destination bundle directory.
@@ -186,23 +242,30 @@ public struct DestinationBundle {
186
242
private static func installIfValid(
187
243
bundlePath: AbsolutePath ,
188
244
destinationsDirectory: AbsolutePath ,
245
+ temporaryDirectory: AbsolutePath ,
189
246
_ fileSystem: some FileSystem ,
247
+ _ archiver: some Archiver ,
190
248
_ 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
+
192
258
guard
193
- fileSystem. isDirectory ( bundlePath ) ,
194
- let bundleName = bundlePath . components. last
259
+ fileSystem. isDirectory ( unpackedBundlePath ) ,
260
+ let bundleName = unpackedBundlePath . components. last
195
261
else {
196
262
throw DestinationError . pathIsNotDirectory ( bundlePath)
197
263
}
198
264
199
265
let installedBundlePath = destinationsDirectory. appending ( component: bundleName)
200
- guard !fileSystem. exists ( installedBundlePath) else {
201
- throw DestinationError . destinationBundleAlreadyInstalled ( bundleName: bundleName)
202
- }
203
266
204
267
let validatedBundle = try Self . parseAndValidate (
205
- bundlePath: bundlePath ,
268
+ bundlePath: unpackedBundlePath ,
206
269
fileSystem: fileSystem,
207
270
observabilityScope: observabilityScope
208
271
)
@@ -226,7 +289,7 @@ public struct DestinationBundle {
226
289
}
227
290
}
228
291
229
- try fileSystem. copy ( from: bundlePath , to: installedBundlePath)
292
+ try fileSystem. copy ( from: unpackedBundlePath , to: installedBundlePath)
230
293
}
231
294
232
295
/// Parses metadata of an `.artifactbundle` and validates it as a bundle containing
0 commit comments