11import Foundation
22import XUtils
3+ import XCAssetCompiler
34
45public struct Packer : Sendable {
56 public let buildSettings : BuildSettings
67 public let plan : Plan
8+ public let diagnostics : Diagnostics
79
8- public init ( buildSettings: BuildSettings , plan: Plan ) {
10+ public init ( buildSettings: BuildSettings , plan: Plan , diagnostics : Diagnostics = Diagnostics ( ) ) {
911 self . plan = plan
1012 self . buildSettings = buildSettings
13+ self . diagnostics = diagnostics
1114 }
1215
1316 private func build( ) async throws {
@@ -86,12 +89,14 @@ public struct Packer: Sendable {
8689
8790 try await withThrowingTaskGroup ( of: Void . self) { group in
8891 for product in plan. allProducts {
89- try pack (
90- product: product,
91- binDir: binDir,
92- outputURL: product. directory ( inApp: outputURL) ,
93- & group
94- )
92+ let productOutputURL = product. directory ( inApp: outputURL)
93+ group. addTask {
94+ try await pack (
95+ product: product,
96+ binDir: binDir,
97+ outputURL: productOutputURL
98+ )
99+ }
95100 }
96101
97102 while !group. isEmpty {
@@ -112,103 +117,147 @@ public struct Packer: Sendable {
112117 return dest
113118 }
114119
120+ // swiftlint:disable:next function_body_length
115121 @Sendable private func pack(
116122 product: Plan . Product ,
117123 binDir: URL ,
118- outputURL: URL ,
119- _ group: inout ThrowingTaskGroup < Void , Error >
120- ) throws {
121- @Sendable func packFileToRoot( srcName: String ) async throws {
122- let srcURL = URL ( fileURLWithPath: srcName)
123- let destURL = outputURL. appendingPathComponent ( srcURL. lastPathComponent)
124- try FileManager . default. copyItem ( at: srcURL, to: destURL)
125-
126- try Task . checkCancellation ( )
127- }
124+ outputURL: URL
125+ ) async throws {
126+ try ? FileManager . default. createDirectory ( at: outputURL, withIntermediateDirectories: true )
128127
129- @Sendable func packFile( srcName: String , dstName: String ? = nil , sign: Bool = false ) async throws {
130- let srcURL = URL ( fileURLWithPath: srcName, relativeTo: binDir)
131- let dstURL = URL ( fileURLWithPath: dstName ?? srcURL. lastPathComponent, relativeTo: outputURL)
132- try ? FileManager . default. createDirectory ( at: dstURL. deletingLastPathComponent ( ) , withIntermediateDirectories: true )
133- try FileManager . default. copyItem ( at: srcURL, to: dstURL)
128+ let compiled : CompiledCatalog ?
129+ if let catalogPath = product. assetCatalogPath {
130+ let compiler = XCAssetCompiler (
131+ deploymentTarget: product. deploymentTarget,
132+ diagnostics: diagnostics
133+ )
134+ compiled = try await compiler. compile ( catalog: URL ( fileURLWithPath: catalogPath) )
135+ } else {
136+ compiled = nil
137+ }
134138
135- try Task . checkCancellation ( )
139+ let effectiveIconPath : String ?
140+ if let compiled, compiled. primaryIconName != nil {
141+ if product. iconPath != nil {
142+ await diagnostics. warn (
143+ " xtool.yml: iconPath is ignored because the asset catalog supplies an AppIcon. "
144+ )
145+ }
146+ effectiveIconPath = nil
147+ } else {
148+ effectiveIconPath = product. iconPath
136149 }
137150
138- // Ensure output directory is available
139- try ? FileManager . default. createDirectory ( at: outputURL, withIntermediateDirectories: true )
151+ try await withThrowingTaskGroup ( of: Void . self) { group in
152+ @Sendable func packFileToRoot( srcName: String ) async throws {
153+ let srcURL = URL ( fileURLWithPath: srcName)
154+ let destURL = outputURL. appendingPathComponent ( srcURL. lastPathComponent)
155+ try FileManager . default. copyItem ( at: srcURL, to: destURL)
140156
141- for command in product. resources {
142- group. addTask {
143- switch command {
144- case . bundle( let package , let target) :
145- try await packFile ( srcName: " \( package ) _ \( target) .bundle " )
146- case . binaryTarget( let name) :
147- let src = URL ( fileURLWithPath: " \( name) .framework/ \( name) " , relativeTo: binDir)
148- let magic = Data ( " !<arch> \n " . utf8)
149- let thinMagic = Data ( " !<thin> \n " . utf8)
150- guard let bytes = try ? FileHandle ( forReadingFrom: src) . read ( upToCount: magic. count) else {
151- // if we can't find the binary, it might be a static framework that SwiftPM
152- // did not copy into the .build directory. we don't need to pack it anyway.
153- break
154- }
155- // if the magic matches one of these it's a static archive; don't embed it.
156- // https://github.com/apple/llvm-project/blob/e716ff14c46490d2da6b240806c04e2beef01f40/llvm/include/llvm/Object/Archive.h#L33
157- // swiftlint:disable:previous line_length
158- if bytes != magic && bytes != thinMagic {
159- try await packFile ( srcName: " \( name) .framework " , dstName: " Frameworks/ \( name) .framework " , sign: true )
157+ try Task . checkCancellation ( )
158+ }
159+
160+ @Sendable func packFile( srcName: String , dstName: String ? = nil , sign: Bool = false ) async throws {
161+ let srcURL = URL ( fileURLWithPath: srcName, relativeTo: binDir)
162+ let dstURL = URL ( fileURLWithPath: dstName ?? srcURL. lastPathComponent, relativeTo: outputURL)
163+ try ? FileManager . default. createDirectory (
164+ at: dstURL. deletingLastPathComponent ( ) ,
165+ withIntermediateDirectories: true
166+ )
167+ try FileManager . default. copyItem ( at: srcURL, to: dstURL)
168+
169+ try Task . checkCancellation ( )
170+ }
171+
172+ for command in product. resources {
173+ group. addTask {
174+ switch command {
175+ case . bundle( let package , let target) :
176+ try await packFile ( srcName: " \( package ) _ \( target) .bundle " )
177+ case . binaryTarget( let name) :
178+ let src = URL ( fileURLWithPath: " \( name) .framework/ \( name) " , relativeTo: binDir)
179+ let magic = Data ( " !<arch> \n " . utf8)
180+ let thinMagic = Data ( " !<thin> \n " . utf8)
181+ guard let bytes = try ? FileHandle ( forReadingFrom: src) . read ( upToCount: magic. count) else {
182+ // if we can't find the binary, it might be a static framework that SwiftPM
183+ // did not copy into the .build directory. we don't need to pack it anyway.
184+ break
185+ }
186+ // if the magic matches one of these it's a static archive; don't embed it.
187+ // https://github.com/apple/llvm-project/blob/e716ff14c46490d2da6b240806c04e2beef01f40/llvm/include/llvm/Object/Archive.h#L33
188+ // swiftlint:disable:previous line_length
189+ if bytes != magic && bytes != thinMagic {
190+ try await packFile ( srcName: " \( name) .framework " , dstName: " Frameworks/ \( name) .framework " , sign: true )
191+ }
192+ case . library( let name) :
193+ try await packFile ( srcName: " lib \( name) .dylib " , dstName: " Frameworks/lib \( name) .dylib " , sign: true )
194+ case . root( let source) :
195+ try await packFileToRoot ( srcName: source)
160196 }
161- case . library( let name) :
162- try await packFile ( srcName: " lib \( name) .dylib " , dstName: " Frameworks/lib \( name) .dylib " , sign: true )
163- case . root( let source) :
164- try await packFileToRoot ( srcName: source)
165197 }
166198 }
167- }
168- if let iconPath = product. iconPath {
169- group. addTask {
170- try await packFileToRoot ( srcName: iconPath)
171- }
172- }
173- for compiledAsset in product. compiledAssets {
174- let bytes = compiledAsset. carData
175- group. addTask {
176- let destURL = outputURL. appendingPathComponent ( " Assets.car " )
177- try bytes. write ( to: destURL)
178- try Task . checkCancellation ( )
199+ if let iconPath = effectiveIconPath {
200+ group. addTask {
201+ try await packFileToRoot ( srcName: iconPath)
202+ }
179203 }
180- for emittedFile in compiledAsset. emittedFiles {
204+ if let compiled {
205+ let carData = compiled. carData
181206 group. addTask {
182- let destURL = outputURL. appendingPathComponent ( emittedFile . name )
183- try emittedFile . data . write ( to: destURL)
207+ let destURL = outputURL. appendingPathComponent ( " Assets.car " )
208+ try carData . write ( to: destURL)
184209 try Task . checkCancellation ( )
185210 }
211+ for emittedFile in compiled. emittedFiles {
212+ let name = emittedFile. name
213+ let data = emittedFile. data
214+ group. addTask {
215+ let destURL = outputURL. appendingPathComponent ( name)
216+ try data. write ( to: destURL)
217+ try Task . checkCancellation ( )
218+ }
219+ }
186220 }
187- }
188- group. addTask {
189- try await packFile ( srcName: product. targetName, dstName: product. product)
190- }
191- group. addTask {
192- var info = product. infoPlist
193-
194- if product. type == . application {
195- info [ " UIRequiredDeviceCapabilities " ] = [ " arm64 " ]
196- info [ " LSRequiresIPhoneOS " ] = true
197- info [ " CFBundleSupportedPlatforms " ] = [ " iPhoneOS " ]
221+ group. addTask {
222+ try await packFile ( srcName: product. targetName, dstName: product. product)
198223 }
224+ group. addTask {
225+ var info = product. infoPlist
226+
227+ if product. type == . application {
228+ info [ " UIRequiredDeviceCapabilities " ] = [ " arm64 " ]
229+ info [ " LSRequiresIPhoneOS " ] = true
230+ info [ " CFBundleSupportedPlatforms " ] = [ " iPhoneOS " ]
231+ }
199232
200- if let iconPath = product. iconPath {
201- let iconName = URL ( fileURLWithPath: iconPath) . deletingPathExtension ( ) . lastPathComponent
202- info [ " CFBundleIconFile " ] = iconName
233+ if let compiled {
234+ info. merge ( compiled. infoPlistAdditions, uniquingKeysWith: { _, new in new } )
235+ }
236+
237+ if let iconPath = effectiveIconPath {
238+ let iconName = URL ( fileURLWithPath: iconPath) . deletingPathExtension ( ) . lastPathComponent
239+ info [ " CFBundleIconFile " ] = iconName
240+ }
241+
242+ let infoPath = outputURL. appendingPathComponent ( " Info.plist " )
243+ let encodedPlist = try PropertyListSerialization . data (
244+ fromPropertyList: info,
245+ format: . xml,
246+ options: 0
247+ )
248+ try encodedPlist. write ( to: infoPath)
203249 }
204250
205- let infoPath = outputURL. appendingPathComponent ( " Info.plist " )
206- let encodedPlist = try PropertyListSerialization . data (
207- fromPropertyList: info,
208- format: . xml,
209- options: 0
210- )
211- try encodedPlist. write ( to: infoPath)
251+ while !group. isEmpty {
252+ do {
253+ try await group. next ( )
254+ } catch is CancellationError {
255+ // continue
256+ } catch {
257+ group. cancelAll ( )
258+ throw error
259+ }
260+ }
212261 }
213262 }
214263}
0 commit comments