1- import Foundation
2- import ZsignSwift
3- import ZIPFoundation
4-
51struct FileItem {
62 var url : URL ?
73 var name : String { url? . lastPathComponent ?? " " }
8- }
9-
10- class SigningManager {
11- static func sign(
12- appPath: String ,
13- provisionPath: String ,
14- p12Path: String ,
15- p12Password: String ,
16- entitlementsPath: String ,
17- removeProvision: Bool ,
18- completion: @escaping ( Bool , Error ? ) -> Void
19- ) {
20- // Explicitly discard result to silence "result unused" warning
21- _ = Zsign . sign (
22- appPath: appPath,
23- provisionPath: provisionPath,
24- p12Path: p12Path,
25- p12Password: p12Password,
26- entitlementsPath: entitlementsPath,
27- removeProvision: removeProvision,
28- completion: completion
29- )
30- }
31-
32- static func prepareTemporaryWorkspace( ) throws -> ( URL , URL , URL ) {
33- let fm = FileManager . default
34- let tmpRoot = fm. temporaryDirectory. appendingPathComponent ( " zsign_ios_ \( UUID ( ) . uuidString) " )
35- let inputs = tmpRoot. appendingPathComponent ( " inputs " )
36- let work = tmpRoot. appendingPathComponent ( " work " )
37- try fm. createDirectory ( at: inputs, withIntermediateDirectories: true )
38- try fm. createDirectory ( at: work, withIntermediateDirectories: true )
39- return ( tmpRoot, inputs, work)
40- }
41-
42- static func copyInputFiles( ipaURL: URL , p12URL: URL , provURL: URL , to inputsDir: URL ) throws -> ( URL , URL , URL ) {
43- let fm = FileManager . default
44-
45- let localIPA = inputsDir. appendingPathComponent ( ipaURL. lastPathComponent)
46- let localP12 = inputsDir. appendingPathComponent ( p12URL. lastPathComponent)
47- let localProv = inputsDir. appendingPathComponent ( provURL. lastPathComponent)
48-
49- [ localIPA, localP12, localProv] . forEach {
50- if fm. fileExists ( atPath: $0. path) {
51- try ? fm. removeItem ( at: $0)
52- }
53- }
54-
55- try fm. copyItem ( at: ipaURL, to: localIPA)
56- try fm. copyItem ( at: p12URL, to: localP12)
57- try fm. copyItem ( at: provURL, to: localProv)
58-
59- return ( localIPA, localP12, localProv)
60- }
61-
62- static func extractIPA( ipaURL: URL , to workDir: URL ) throws {
63- // Use throwing initializer (deprecated non-throwing variant removed)
64- let archive = try Archive ( url: ipaURL, accessMode: . read)
65-
66- let fm = FileManager . default
67- for entry in archive {
68- let dest = workDir. appendingPathComponent ( entry. path)
69- try fm. createDirectory ( at: dest. deletingLastPathComponent ( ) , withIntermediateDirectories: true )
70- if entry. type == . directory {
71- try fm. createDirectory ( at: dest, withIntermediateDirectories: true )
72- } else {
73- // Explicit discard in case extract returns a value in this version
74- _ = try archive. extract ( entry, to: dest)
75- }
76- }
77- }
78-
79- static func findAppBundle( in payloadDir: URL ) throws -> URL {
80- let fm = FileManager . default
81- guard fm. fileExists ( atPath: payloadDir. path) else {
82- throw NSError ( domain: " Zsign " , code: 1 , userInfo: [ NSLocalizedDescriptionKey: " Payload not found " ] )
83- }
84-
85- let contents = try fm. contentsOfDirectory ( atPath: payloadDir. path)
86- guard let appName = contents. first ( where: { $0. hasSuffix ( " .app " ) } ) else {
87- throw NSError ( domain: " Zsign " , code: 2 , userInfo: [ NSLocalizedDescriptionKey: " No .app bundle in Payload " ] )
88- }
89-
90- return payloadDir. appendingPathComponent ( appName)
91- }
92-
93- static func createSignedIPA( from workDir: URL , originalIPAURL: URL , outputDir: URL ) throws -> URL {
94- let fm = FileManager . default
95-
96- let originalBase = originalIPAURL. deletingPathExtension ( ) . lastPathComponent
97- let finalFileName = " \( originalBase) _signed_ \( UUID ( ) . uuidString) .ipa "
98- let signedIpa = outputDir. appendingPathComponent ( finalFileName)
99-
100- // Throwing initializer
101- let writeArchive = try Archive ( url: signedIpa, accessMode: . create)
102-
103- let enumerator = fm. enumerator ( at: workDir, includingPropertiesForKeys: [ . isDirectoryKey] , options: [ ] , errorHandler: nil ) !
104- var directories : [ URL ] = [ ]
105- var filesList : [ URL ] = [ ]
106-
107- for case let file as URL in enumerator {
108- if file == workDir { continue }
109- let isDir = ( try ? file. resourceValues ( forKeys: [ . isDirectoryKey] ) . isDirectory) ?? file. hasDirectoryPath
110- if isDir { directories. append ( file) } else { filesList. append ( file) }
111- }
112-
113- directories. sort { $0. path. count < $1. path. count }
114- let base = workDir
115-
116- for dir in directories {
117- let relative = dir. path. replacingOccurrences ( of: base. path + " / " , with: " " )
118- let entryPath = relative. hasSuffix ( " / " ) ? relative : relative + " / "
119- try writeArchive. addEntry ( with: entryPath, relativeTo: base, compressionMethod: . none)
120- }
121-
122- for file in filesList {
123- let relative = file. path. replacingOccurrences ( of: base. path + " / " , with: " " )
124- try writeArchive. addEntry ( with: relative, relativeTo: base, compressionMethod: . deflate)
125- }
126-
127- let docs = fm. urls ( for: . documentDirectory, in: . userDomainMask) . first!
128- let outURL = docs. appendingPathComponent ( finalFileName)
129- if fm. fileExists ( atPath: outURL. path) { try fm. removeItem ( at: outURL) }
130- try fm. copyItem ( at: signedIpa, to: outURL)
131-
132- return outURL
133- }
134-
135- static func cleanupTemporaryFiles( at url: URL ) {
136- try ? FileManager . default. removeItem ( at: url)
137- }
138-
139- static func processSigning(
140- ipaURL: URL ,
141- p12URL: URL ,
142- provURL: URL ,
143- p12Password: String ,
144- progressUpdate: @escaping ( String ) -> Void ,
145- completion: @escaping ( Result < URL , Error > ) -> Void
146- ) {
147- DispatchQueue . global ( qos: . userInitiated) . async {
148- do {
149- progressUpdate ( " Preparing files 📂 " )
150-
151- let ( tmpRoot, inputsDir, workDir) = try prepareTemporaryWorkspace ( )
152- defer { cleanupTemporaryFiles ( at: tmpRoot) }
153-
154- let ( localIPA, localP12, localProv) = try copyInputFiles (
155- ipaURL: ipaURL,
156- p12URL: p12URL,
157- provURL: provURL,
158- to: inputsDir
159- )
160-
161- progressUpdate ( " Unzipping IPA 🔓 " )
162- try extractIPA ( ipaURL: localIPA, to: workDir)
163-
164- let payloadDir = workDir. appendingPathComponent ( " Payload " )
165- let appDir = try findAppBundle ( in: payloadDir)
166-
167- progressUpdate ( " Signing \( appDir. lastPathComponent) ✍️ " )
168-
169- // Replace spin-wait with semaphore (optional improvement)
170- let sema = DispatchSemaphore ( value: 0 )
171- var signingError : Error ?
172-
173- sign (
174- appPath: appDir. path,
175- provisionPath: localProv. path,
176- p12Path: localP12. path,
177- p12Password: p12Password,
178- entitlementsPath: " " ,
179- removeProvision: false
180- ) { _, error in
181- signingError = error
182- sema. signal ( )
183- }
184-
185- sema. wait ( )
186- if let error = signingError { throw error }
187-
188- progressUpdate ( " Zipping signed IPA 📦 " )
189- let signedIPAURL = try createSignedIPA (
190- from: workDir,
191- originalIPAURL: ipaURL,
192- outputDir: tmpRoot
193- )
194-
195- completion ( . success( signedIPAURL) )
196-
197- } catch {
198- completion ( . failure( error) )
199- }
200- }
201- }
2024}
0 commit comments