@@ -87,21 +87,10 @@ struct QuickPkg: AsyncParsableCommand {
8787 mutating func run( ) async throws {
8888 let logger = Logger ( verbosity: verbose)
8989
90- // Normalize path
91- var path = itemPath
92- if path. hasPrefix ( " ~ " ) {
93- path = NSString ( string: path) . expandingTildeInPath
94- }
95- path = ( path as NSString ) . standardizingPath
96-
97- // Remove trailing slash
98- if path. hasSuffix ( " / " ) {
99- path = String ( path. dropLast ( ) )
100- }
101-
90+ // Normalize path and determine input type
91+ let path = normalizePath ( itemPath)
10292 let url = URL ( filePath: path)
10393
104- // Determine input type
10594 guard let inputType = InputType . from ( path: path) else {
10695 throw QuickPkgError . unsupportedExtension ( url. pathExtension)
10796 }
@@ -126,67 +115,21 @@ struct QuickPkg: AsyncParsableCommand {
126115 // Find the application
127116 let appURL : URL
128117 do {
129- switch inputType {
130- case . app:
131- guard FileManager . default. fileExists ( atPath: path) else {
132- throw QuickPkgError . fileNotFound ( path)
133- }
134- appURL = url
135-
136- case . dmg:
137- guard FileManager . default. fileExists ( atPath: path) else {
138- throw QuickPkgError . fileNotFound ( path)
139- }
140- let mountPoints = try await dmgManager. attach ( url)
141- let apps = findApplications ( in: mountPoints)
142- guard !apps. isEmpty else {
143- throw QuickPkgError . noApplicationFound
144- }
145- guard apps. count == 1 else {
146- throw QuickPkgError . multipleApplicationsFound ( apps. map ( \. path) )
147- }
148- appURL = apps [ 0 ]
149-
150- case . zip:
151- guard FileManager . default. fileExists ( atPath: path) else {
152- throw QuickPkgError . fileNotFound ( path)
153- }
154- let extractDir = tempDir. path. appendingPathComponent ( " unarchive " )
155- try FileManager . default. createDirectory ( at: extractDir, withIntermediateDirectories: true )
156- try await archiveExtractor. extractZip ( url, to: extractDir)
157- let apps = findApplications ( in: [ extractDir] )
158- guard !apps. isEmpty else {
159- throw QuickPkgError . noApplicationFound
160- }
161- guard apps. count == 1 else {
162- throw QuickPkgError . multipleApplicationsFound ( apps. map ( \. path) )
163- }
164- appURL = apps [ 0 ]
165-
166- case . xip:
167- guard FileManager . default. fileExists ( atPath: path) else {
168- throw QuickPkgError . fileNotFound ( path)
169- }
170- let extractDir = tempDir. path. appendingPathComponent ( " unarchive " )
171- try FileManager . default. createDirectory ( at: extractDir, withIntermediateDirectories: true )
172- try await archiveExtractor. extractXip ( url, to: extractDir)
173- let apps = findApplications ( in: [ extractDir] )
174- guard !apps. isEmpty else {
175- throw QuickPkgError . noApplicationFound
176- }
177- guard apps. count == 1 else {
178- throw QuickPkgError . multipleApplicationsFound ( apps. map ( \. path) )
179- }
180- appURL = apps [ 0 ]
181- }
118+ appURL = try await findApplication (
119+ at: url,
120+ inputType: inputType,
121+ tempDir: tempDir,
122+ dmgManager: dmgManager,
123+ archiveExtractor: archiveExtractor
124+ )
182125 } catch {
183126 if shouldClean { await dmgManager. detachAll ( ) }
184127 throw error
185128 }
186129
187130 logger. log ( " Found application: \( appURL. path) " , level: 1 )
188131
189- // Copy app to payload directory (needed for dmg/zip/xip, and for apps to avoid modifying original)
132+ // Copy app to payload directory
190133 let payloadDir = tempDir. path. appendingPathComponent ( " payload " )
191134 try FileManager . default. createDirectory ( at: payloadDir, withIntermediateDirectories: true )
192135 let payloadAppURL = payloadDir. appendingPathComponent ( appURL. lastPathComponent)
@@ -203,63 +146,10 @@ struct QuickPkg: AsyncParsableCommand {
203146 }
204147
205148 // Prepare scripts if needed
206- var scriptsDir : URL ?
207- if let scriptsPath = scripts {
208- let scriptsURL = URL ( filePath: scriptsPath)
209- guard FileManager . default. fileExists ( atPath: scriptsPath) else {
210- throw QuickPkgError . scriptNotFound ( scriptsPath)
211- }
212- scriptsDir = scriptsURL
213- }
214-
215- if preinstall != nil || postinstall != nil {
216- let tmpScriptsDir = tempDir. path. appendingPathComponent ( " scripts " )
217- try FileManager . default. createDirectory ( at: tmpScriptsDir, withIntermediateDirectories: true )
218-
219- // Copy existing scripts folder if provided
220- if let existingScripts = scriptsDir {
221- for item in try FileManager . default. contentsOfDirectory ( at: existingScripts, includingPropertiesForKeys: nil ) {
222- try FileManager . default. copyItem ( at: item, to: tmpScriptsDir. appendingPathComponent ( item. lastPathComponent) )
223- }
224- }
225-
226- // Add preinstall script
227- if let preinstallPath = preinstall {
228- let preinstallURL = URL ( filePath: preinstallPath)
229- guard FileManager . default. fileExists ( atPath: preinstallPath) else {
230- throw QuickPkgError . scriptNotFound ( preinstallPath)
231- }
232- let destURL = tmpScriptsDir. appendingPathComponent ( " preinstall " )
233- if FileManager . default. fileExists ( atPath: destURL. path) {
234- throw QuickPkgError . scriptConflict ( " preinstall script already exists in scripts folder " )
235- }
236- try FileManager . default. copyItem ( at: preinstallURL, to: destURL)
237- try FileManager . default. setAttributes ( [ . posixPermissions: 0o755 ] , ofItemAtPath: destURL. path)
238- logger. log ( " Copied preinstall script to \( destURL. path) " , level: 1 )
239- }
240-
241- // Add postinstall script
242- if let postinstallPath = postinstall {
243- let postinstallURL = URL ( filePath: postinstallPath)
244- guard FileManager . default. fileExists ( atPath: postinstallPath) else {
245- throw QuickPkgError . scriptNotFound ( postinstallPath)
246- }
247- let destURL = tmpScriptsDir. appendingPathComponent ( " postinstall " )
248- if FileManager . default. fileExists ( atPath: destURL. path) {
249- throw QuickPkgError . scriptConflict ( " postinstall script already exists in scripts folder " )
250- }
251- try FileManager . default. copyItem ( at: postinstallURL, to: destURL)
252- try FileManager . default. setAttributes ( [ . posixPermissions: 0o755 ] , ofItemAtPath: destURL. path)
253- logger. log ( " Copied postinstall script to \( destURL. path) " , level: 1 )
254- }
255-
256- scriptsDir = tmpScriptsDir
257- }
149+ let scriptsDir = try prepareScripts ( tempDir: tempDir, logger: logger)
258150
259151 // Build the package
260152 let packageBuilder = PackageBuilder ( executor: executor, logger: logger)
261-
262- // Determine output path
263153 let outputPath = determineOutputPath (
264154 output: output,
265155 name: metadata. name,
@@ -291,6 +181,20 @@ struct QuickPkg: AsyncParsableCommand {
291181
292182 // MARK: - Helpers
293183
184+ /// Normalize a file path (expand tilde, standardize, remove trailing slash)
185+ private func normalizePath( _ path: String ) -> String {
186+ var result = path
187+ if result. hasPrefix ( " ~ " ) {
188+ result = NSString ( string: result) . expandingTildeInPath
189+ }
190+ result = ( result as NSString ) . standardizingPath
191+ if result. hasSuffix ( " / " ) {
192+ result = String ( result. dropLast ( ) )
193+ }
194+ return result
195+ }
196+
197+ /// Find applications in the given directories
294198 private func findApplications( in directories: [ URL ] ) -> [ URL ] {
295199 var apps : [ URL ] = [ ]
296200 let fm = FileManager . default
@@ -309,6 +213,107 @@ struct QuickPkg: AsyncParsableCommand {
309213 return apps
310214 }
311215
216+ /// Validate exactly one application exists and return it
217+ private func validateSingleApplication( in directories: [ URL ] ) throws -> URL {
218+ let apps = findApplications ( in: directories)
219+ guard !apps. isEmpty else {
220+ throw QuickPkgError . noApplicationFound
221+ }
222+ guard apps. count == 1 else {
223+ throw QuickPkgError . multipleApplicationsFound ( apps. map ( \. path) )
224+ }
225+ return apps [ 0 ]
226+ }
227+
228+ /// Find the application from the input source
229+ private func findApplication(
230+ at url: URL ,
231+ inputType: InputType ,
232+ tempDir: TempDirectory ,
233+ dmgManager: DMGManager ,
234+ archiveExtractor: ArchiveExtractor
235+ ) async throws -> URL {
236+ guard FileManager . default. fileExists ( atPath: url. path) else {
237+ throw QuickPkgError . fileNotFound ( url. path)
238+ }
239+
240+ switch inputType {
241+ case . app:
242+ return url
243+
244+ case . dmg:
245+ let mountPoints = try await dmgManager. attach ( url)
246+ return try validateSingleApplication ( in: mountPoints)
247+
248+ case . zip:
249+ let extractDir = tempDir. path. appendingPathComponent ( " unarchive " )
250+ try FileManager . default. createDirectory ( at: extractDir, withIntermediateDirectories: true )
251+ try await archiveExtractor. extractZip ( url, to: extractDir)
252+ return try validateSingleApplication ( in: [ extractDir] )
253+
254+ case . xip:
255+ let extractDir = tempDir. path. appendingPathComponent ( " unarchive " )
256+ try FileManager . default. createDirectory ( at: extractDir, withIntermediateDirectories: true )
257+ try await archiveExtractor. extractXip ( url, to: extractDir)
258+ return try validateSingleApplication ( in: [ extractDir] )
259+ }
260+ }
261+
262+ /// Prepare the scripts directory, merging --scripts with --preinstall/--postinstall if needed
263+ private func prepareScripts( tempDir: TempDirectory , logger: Logger ) throws -> URL ? {
264+ var scriptsDir : URL ?
265+
266+ if let scriptsPath = scripts {
267+ let scriptsURL = URL ( filePath: scriptsPath)
268+ guard FileManager . default. fileExists ( atPath: scriptsPath) else {
269+ throw QuickPkgError . scriptNotFound ( scriptsPath)
270+ }
271+ scriptsDir = scriptsURL
272+ }
273+
274+ guard preinstall != nil || postinstall != nil else {
275+ return scriptsDir
276+ }
277+
278+ let tmpScriptsDir = tempDir. path. appendingPathComponent ( " scripts " )
279+ try FileManager . default. createDirectory ( at: tmpScriptsDir, withIntermediateDirectories: true )
280+
281+ // Copy existing scripts folder if provided
282+ if let existingScripts = scriptsDir {
283+ for item in try FileManager . default. contentsOfDirectory ( at: existingScripts, includingPropertiesForKeys: nil ) {
284+ try FileManager . default. copyItem ( at: item, to: tmpScriptsDir. appendingPathComponent ( item. lastPathComponent) )
285+ }
286+ }
287+
288+ // Add preinstall script
289+ if let preinstallPath = preinstall {
290+ try copyScript ( from: preinstallPath, to: tmpScriptsDir, name: " preinstall " , logger: logger)
291+ }
292+
293+ // Add postinstall script
294+ if let postinstallPath = postinstall {
295+ try copyScript ( from: postinstallPath, to: tmpScriptsDir, name: " postinstall " , logger: logger)
296+ }
297+
298+ return tmpScriptsDir
299+ }
300+
301+ /// Copy a script to the scripts directory with proper permissions
302+ private func copyScript( from sourcePath: String , to scriptsDir: URL , name: String , logger: Logger ) throws {
303+ let sourceURL = URL ( filePath: sourcePath)
304+ guard FileManager . default. fileExists ( atPath: sourcePath) else {
305+ throw QuickPkgError . scriptNotFound ( sourcePath)
306+ }
307+ let destURL = scriptsDir. appendingPathComponent ( name)
308+ if FileManager . default. fileExists ( atPath: destURL. path) {
309+ throw QuickPkgError . scriptConflict ( " \( name) script already exists in scripts folder " )
310+ }
311+ try FileManager . default. copyItem ( at: sourceURL, to: destURL)
312+ try FileManager . default. setAttributes ( [ . posixPermissions: 0o755 ] , ofItemAtPath: destURL. path)
313+ logger. log ( " Copied \( name) script to \( destURL. path) " , level: 1 )
314+ }
315+
316+ /// Determine the output path for the package
312317 private func determineOutputPath( output: String ? , name: String , version: String , identifier: String ) -> String {
313318 let defaultName = " {name}-{version}.pkg "
314319 var path : String
0 commit comments