1010//
1111//===----------------------------------------------------------------------===//
1212
13+
1314import Basics
14- import TSCBasic
1515
16+ import func TSCBasic. tsc_await
17+ import protocol TSCBasic. FileSystem
1618import 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.
1923public 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