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