bleam 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/dist/cli.cjs +23 -4
  2. package/dist/cli.d.cts +2 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +23 -4
  5. package/dist/{ui-Ce-pnUUA.d.ts → ui-Bg11tvlc.d.ts} +7 -7
  6. package/dist/ui.d.ts +1 -1
  7. package/dist/window.d.ts +1 -1
  8. package/package.json +4 -1
  9. package/templates/image-generation/app/index.tsx +1 -1
  10. package/templates/native/.gitattributes +1 -0
  11. package/templates/native/App.tsx +26 -0
  12. package/templates/native/app.json +12 -0
  13. package/templates/native/index.ts +8 -0
  14. package/templates/native/ios/.xcode.env +11 -0
  15. package/templates/native/ios/Bleam/AI/Flux2/Configuration/Flux2Config.swift +288 -0
  16. package/templates/native/ios/Bleam/AI/Flux2/Configuration/MemoryConfig.swift +233 -0
  17. package/templates/native/ios/Bleam/AI/Flux2/Configuration/MemoryOptimizationConfig.swift +157 -0
  18. package/templates/native/ios/Bleam/AI/Flux2/Configuration/ModelRegistry.swift +425 -0
  19. package/templates/native/ios/Bleam/AI/Flux2/Configuration/QuantizationConfig.swift +138 -0
  20. package/templates/native/ios/Bleam/AI/Flux2/Configuration/TransformerRepoProfile.swift +59 -0
  21. package/templates/native/ios/Bleam/AI/Flux2/Configuration/VAEConfig.swift +134 -0
  22. package/templates/native/ios/Bleam/AI/Flux2/Flux2Core.swift +55 -0
  23. package/templates/native/ios/Bleam/AI/Flux2/Loading/WeightLoader.swift +1192 -0
  24. package/templates/native/ios/Bleam/AI/Flux2/Pipeline/FluxImageRunner.swift +1282 -0
  25. package/templates/native/ios/Bleam/AI/Flux2/Pipeline/LatentUtils.swift +191 -0
  26. package/templates/native/ios/Bleam/AI/Flux2/Scheduler/FlowMatchEulerScheduler.swift +279 -0
  27. package/templates/native/ios/Bleam/AI/Flux2/Text/KleinTextEncoder.swift +380 -0
  28. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2Attention.swift +379 -0
  29. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2Embeddings.swift +159 -0
  30. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2FeedForward.swift +154 -0
  31. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2FusedKernels.swift +198 -0
  32. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2KVCache.swift +36 -0
  33. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2Modulation.swift +193 -0
  34. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2ParallelAttention.swift +389 -0
  35. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2RoPE.swift +304 -0
  36. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2SingleBlock.swift +290 -0
  37. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2Transformer.swift +530 -0
  38. package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2TransformerBlock.swift +362 -0
  39. package/templates/native/ios/Bleam/AI/Flux2/Utils/Flux2Debug.swift +101 -0
  40. package/templates/native/ios/Bleam/AI/Flux2/Utils/MLXCheckpoint.swift +118 -0
  41. package/templates/native/ios/Bleam/AI/Flux2/Utils/MemoryManager.swift +201 -0
  42. package/templates/native/ios/Bleam/AI/Flux2/VAE/AutoencoderKL.swift +476 -0
  43. package/templates/native/ios/Bleam/AI/Flux2/VAE/ResnetBlock.swift +316 -0
  44. package/templates/native/ios/Bleam/AI/Flux2/VAE/VAEDecoder.swift +120 -0
  45. package/templates/native/ios/Bleam/AI/Flux2/VAE/VAEEncoder.swift +136 -0
  46. package/templates/native/ios/Bleam/AppDelegate.swift +373 -0
  47. package/templates/native/ios/Bleam/Appearance.swift +62 -0
  48. package/templates/native/ios/Bleam/Bridging-Header.h +3 -0
  49. package/templates/native/ios/Bleam/Images.xcassets/AppIcon.appiconset/Contents.json +13 -0
  50. package/templates/native/ios/Bleam/Images.xcassets/Contents.json +6 -0
  51. package/templates/native/ios/Bleam/Info.plist +76 -0
  52. package/templates/native/ios/Bleam/PrivacyInfo.xcprivacy +48 -0
  53. package/templates/native/ios/Bleam/SceneDelegate.swift +58 -0
  54. package/templates/native/ios/Bleam/SplashScreen.storyboard +47 -0
  55. package/templates/native/ios/Bleam/Supporting/Expo.plist +6 -0
  56. package/templates/native/ios/Bleam/bleam.entitlements +10 -0
  57. package/templates/native/ios/Bleam.xcodeproj/project.pbxproj +1146 -0
  58. package/templates/native/ios/Bleam.xcodeproj/xcshareddata/xcschemes/Bleam.xcscheme +88 -0
  59. package/templates/native/ios/GenerationService/GenerationService.entitlements +10 -0
  60. package/templates/native/ios/GenerationService/ImageGenerationRunner.swift +591 -0
  61. package/templates/native/ios/GenerationService/Info.plist +31 -0
  62. package/templates/native/ios/GenerationService/main.swift +165 -0
  63. package/templates/native/ios/PlatformHelper/Info.plist +29 -0
  64. package/templates/native/ios/PlatformHelper/main.swift +335 -0
  65. package/templates/native/ios/Podfile +198 -0
  66. package/templates/native/ios/Podfile.lock +2284 -0
  67. package/templates/native/ios/Podfile.properties.json +5 -0
  68. package/templates/native/ios/Shared/Generation/GenerationServiceProtocol.swift +12 -0
  69. package/templates/native/ios/Shared/Generation/GenerationWorkerProtocol.swift +124 -0
  70. package/templates/native/metro.config.js +6 -0
  71. package/templates/native/modules/bleam-runtime/BleamRuntime.podspec +14 -0
  72. package/templates/native/modules/bleam-runtime/expo-module.config.json +8 -0
  73. package/templates/native/modules/bleam-runtime/ios/AIModule.swift +1258 -0
  74. package/templates/native/modules/bleam-runtime/ios/GenerationContracts.swift +135 -0
  75. package/templates/native/modules/bleam-runtime/ios/PlatformModule.swift +313 -0
  76. package/templates/native/modules/bleam-runtime/package.json +5 -0
  77. package/templates/native/package.json +39 -0
  78. package/templates/native/scripts/build/mlx-frameworks.ts +421 -0
  79. package/templates/native/scripts/start/index.ts +474 -0
  80. package/templates/native/scripts/start/known-failures.ts +20 -0
  81. package/templates/native/scripts/start/loader.ts +76 -0
  82. package/templates/native/scripts/start/native-state.ts +242 -0
  83. package/templates/native/scripts/start/package.json +4 -0
  84. package/templates/native/scripts/start/swift-packages.ts +414 -0
  85. package/templates/native/scripts/start/xcode-formatter.ts +106 -0
  86. package/templates/native/tsconfig.json +14 -0
  87. package/templates/native/yarn.lock +3749 -0
@@ -0,0 +1,31 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>CFBundleDevelopmentRegion</key>
6
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
7
+ <key>CFBundleExecutable</key>
8
+ <string>$(EXECUTABLE_NAME)</string>
9
+ <key>CFBundleIdentifier</key>
10
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
11
+ <key>CFBundleInfoDictionaryVersion</key>
12
+ <string>6.0</string>
13
+ <key>CFBundleName</key>
14
+ <string>$(PRODUCT_NAME)</string>
15
+ <key>CFBundlePackageType</key>
16
+ <string>XPC!</string>
17
+ <key>CFBundleShortVersionString</key>
18
+ <string>$(MARKETING_VERSION)</string>
19
+ <key>CFBundleVersion</key>
20
+ <string>$(CURRENT_PROJECT_VERSION)</string>
21
+ <key>AppGroupIdentifier</key>
22
+ <string>group.dev.bleam.app</string>
23
+ <key>RCTNewArchEnabled</key>
24
+ <true/>
25
+ <key>XPCService</key>
26
+ <dict>
27
+ <key>ServiceType</key>
28
+ <string>Application</string>
29
+ </dict>
30
+ </dict>
31
+ </plist>
@@ -0,0 +1,165 @@
1
+ import Foundation
2
+ import MLX
3
+
4
+ private final class GenerationRuntime: @unchecked Sendable {
5
+ private let modelWarmTimeout: Duration = .seconds(60)
6
+ private var activeTask: Task<Void, Never>?
7
+ private var activeRunner: ImageGenerationRunner?
8
+ private var trimTask: Task<Void, Never>?
9
+ private var isCancelled = false
10
+
11
+ var isRunning: Bool {
12
+ activeTask != nil
13
+ }
14
+
15
+ func start(
16
+ request: GenerationWorkerRequest,
17
+ emit: @escaping @Sendable (GenerationWorkerEvent) -> Void
18
+ ) -> Bool {
19
+ guard activeTask == nil else { return false }
20
+
21
+ trimTask?.cancel()
22
+ trimTask = nil
23
+ isCancelled = false
24
+ activeTask = Task { [weak self] in
25
+ await self?.run(request, emit: emit)
26
+ }
27
+ return true
28
+ }
29
+
30
+ func cancel() {
31
+ isCancelled = true
32
+ trimTask?.cancel()
33
+ trimTask = nil
34
+ activeTask?.cancel()
35
+ activeRunner?.cancel()
36
+ }
37
+
38
+ func trimModelsNow() async {
39
+ trimTask?.cancel()
40
+ trimTask = nil
41
+ // We currently reload every stage per generation, but keep the explicit
42
+ // trim hook from ImageStudio's service architecture. Warm model caching
43
+ // should be added here later, never back in the app process.
44
+ MLX.Memory.clearCache()
45
+ await Task.yield()
46
+ MLX.Memory.clearCache()
47
+ print("[GenerationService] trim active=\(MLX.Memory.activeMemory / 1_048_576)MB cache=\(MLX.Memory.cacheMemory / 1_048_576)MB")
48
+ }
49
+
50
+ private func run(
51
+ _ request: GenerationWorkerRequest,
52
+ emit: @escaping @Sendable (GenerationWorkerEvent) -> Void
53
+ ) async {
54
+ let runner = ImageGenerationRunner(request: request, emit: emit)
55
+ activeRunner = runner
56
+ defer {
57
+ if activeRunner === runner {
58
+ activeRunner = nil
59
+ }
60
+ activeTask = nil
61
+ scheduleTrimAfterIdle()
62
+ }
63
+
64
+ do {
65
+ try await runner.run(isCancelled: { [weak self] in
66
+ Task.isCancelled || (self?.isCancelled ?? false)
67
+ })
68
+ } catch is CancellationError {
69
+ emit(.init(type: .canceled))
70
+ } catch let failure as GenerationWorkerFailure {
71
+ print("[GenerationService] failed - \(failure.localizedDescription)")
72
+ emit(.init(type: .failed, message: failure.localizedDescription, stage: failure.stage))
73
+ } catch {
74
+ print("[GenerationService] failed - \(error.localizedDescription)")
75
+ emit(.init(type: .failed, message: error.localizedDescription))
76
+ }
77
+ }
78
+
79
+ private func scheduleTrimAfterIdle() {
80
+ trimTask?.cancel()
81
+ trimTask = Task { [weak self] in
82
+ guard let self else { return }
83
+ do {
84
+ try await Task.sleep(for: modelWarmTimeout)
85
+ } catch {
86
+ return
87
+ }
88
+ guard !Task.isCancelled else { return }
89
+ await self.trimModelsNow()
90
+ }
91
+ print("[GenerationService] model trim scheduled 60s")
92
+ }
93
+ }
94
+
95
+ private final class GenerationService: NSObject, GenerationServiceProtocol {
96
+ private weak var connection: NSXPCConnection?
97
+ private let runtime = GenerationRuntime()
98
+
99
+ init(connection: NSXPCConnection) {
100
+ self.connection = connection
101
+ }
102
+
103
+ func startGeneration(requestData: Data, reply: @escaping (Bool) -> Void) {
104
+ guard let request = try? JSONDecoder().decode(GenerationWorkerRequest.self, from: requestData) else {
105
+ emit(.init(type: .failed, message: "Invalid generation request."))
106
+ reply(false)
107
+ return
108
+ }
109
+
110
+ reply(runtime.start(request: request) { [weak self] event in
111
+ self?.emit(event)
112
+ })
113
+ }
114
+
115
+ func cancelGeneration(reply: @escaping (Bool) -> Void) {
116
+ runtime.cancel()
117
+ reply(true)
118
+ }
119
+
120
+ func memoryTelemetry(reply: @escaping (Data?) -> Void) {
121
+ let value: [String: Any] = [
122
+ "activeMB": Double(MLX.Memory.activeMemory) / 1_048_576,
123
+ "cacheMB": Double(MLX.Memory.cacheMemory) / 1_048_576,
124
+ ]
125
+ reply(try? JSONSerialization.data(withJSONObject: value))
126
+ }
127
+
128
+ func shutdown(reply: @escaping (Bool) -> Void) {
129
+ guard !runtime.isRunning else {
130
+ reply(false)
131
+ return
132
+ }
133
+ Task { [runtime] in
134
+ await runtime.trimModelsNow()
135
+ reply(true)
136
+ exit(0)
137
+ }
138
+ }
139
+
140
+ private func emit(_ event: GenerationWorkerEvent) {
141
+ guard let data = try? JSONEncoder().encode(event) else { return }
142
+ let client = connection?.remoteObjectProxy as? GenerationServiceClientProtocol
143
+ client?.generationWorkerDidEmit(eventData: data)
144
+ }
145
+ }
146
+
147
+ private final class GenerationServiceDelegate: NSObject, NSXPCListenerDelegate {
148
+ func listener(_ listener: NSXPCListener, shouldAcceptNewConnection connection: NSXPCConnection) -> Bool {
149
+ connection.exportedInterface = NSXPCInterface(with: GenerationServiceProtocol.self)
150
+ connection.remoteObjectInterface = NSXPCInterface(with: GenerationServiceClientProtocol.self)
151
+ connection.exportedObject = GenerationService(connection: connection)
152
+ connection.resume()
153
+ return true
154
+ }
155
+ }
156
+
157
+ // Heavy MLX generation is intentionally isolated in this XPC service. If model
158
+ // loading or Metal memory pressure takes the service down, the app shell can
159
+ // reconnect or report failure instead of dying with the generation process.
160
+ private let delegate = GenerationServiceDelegate()
161
+ private let listener = NSXPCListener.service()
162
+ listener.delegate = delegate
163
+ listener.resume()
164
+
165
+ RunLoop.main.run()
@@ -0,0 +1,29 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>CFBundleDevelopmentRegion</key>
6
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
7
+ <key>CFBundleExecutable</key>
8
+ <string>$(EXECUTABLE_NAME)</string>
9
+ <key>CFBundleIdentifier</key>
10
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
11
+ <key>CFBundleInfoDictionaryVersion</key>
12
+ <string>6.0</string>
13
+ <key>CFBundleName</key>
14
+ <string>$(PRODUCT_NAME)</string>
15
+ <key>CFBundlePackageType</key>
16
+ <string>XPC!</string>
17
+ <key>CFBundleShortVersionString</key>
18
+ <string>$(MARKETING_VERSION)</string>
19
+ <key>CFBundleVersion</key>
20
+ <string>$(CURRENT_PROJECT_VERSION)</string>
21
+ <key>BleamPlatformVersion</key>
22
+ <string>0.0.9</string>
23
+ <key>XPCService</key>
24
+ <dict>
25
+ <key>ServiceType</key>
26
+ <string>Application</string>
27
+ </dict>
28
+ </dict>
29
+ </plist>
@@ -0,0 +1,335 @@
1
+ import Foundation
2
+ import CryptoKit
3
+ import Security
4
+ import Darwin
5
+
6
+ @objc private protocol PlatformHelperProtocol {
7
+ func health(reply: @escaping (Data?) -> Void)
8
+ func downloadPlatformUpdate(requestData: Data, reply: @escaping (Data?) -> Void)
9
+ func applyPlatformUpdate(requestData: Data, reply: @escaping (Bool) -> Void)
10
+ }
11
+
12
+ private struct DownloadRequest: Decodable {
13
+ let serviceURL: URL
14
+ let bundleIdentifier: String
15
+ let teamIdentifier: String
16
+ }
17
+
18
+ private struct ApplyRequest: Decodable {
19
+ let releaseId: String
20
+ let bundleIdentifier: String
21
+ let teamIdentifier: String
22
+ let installedAppPath: String
23
+ let appPID: Int32
24
+ }
25
+
26
+ private struct PlatformRelease: Decodable {
27
+ struct Artifact: Decodable {
28
+ let format: String
29
+ let url: URL
30
+ let sizeBytes: Int
31
+ let sha256: String
32
+ }
33
+
34
+ let kind: String
35
+ let releaseId: String
36
+ let bundleIdentifier: String
37
+ let teamIdentifier: String
38
+ let appVersion: String
39
+ let buildNumber: String
40
+ let bleamPlatformVersion: String
41
+ let target: Target
42
+ let artifact: Artifact
43
+
44
+ struct Target: Decodable {
45
+ let minimumMacOSVersion: String
46
+ }
47
+ }
48
+
49
+ private final class PlatformHelper: NSObject, PlatformHelperProtocol {
50
+ func health(reply: @escaping (Data?) -> Void) {
51
+ let response: [String: Any] = [
52
+ "healthy": true,
53
+ "protocolVersion": 1,
54
+ "helperVersion": Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "unknown",
55
+ "platformVersion": Bundle.main.object(forInfoDictionaryKey: "BleamPlatformVersion") as? String ?? "unknown",
56
+ "capabilities": ["platform-updates"],
57
+ ]
58
+ reply(try? JSONSerialization.data(withJSONObject: response))
59
+ }
60
+
61
+ func downloadPlatformUpdate(requestData: Data, reply: @escaping (Data?) -> Void) {
62
+ guard
63
+ let request = try? JSONDecoder().decode(DownloadRequest.self, from: requestData),
64
+ let resolverURL = URL(
65
+ string: "/v1/apps/\(request.bundleIdentifier)/platform/macos-arm64",
66
+ relativeTo: request.serviceURL
67
+ )?.absoluteURL
68
+ else {
69
+ reply(nil)
70
+ return
71
+ }
72
+
73
+ URLSession.shared.dataTask(with: resolverURL) { data, response, _ in
74
+ guard
75
+ let data,
76
+ (response as? HTTPURLResponse)?.statusCode == 200,
77
+ let release = try? JSONDecoder().decode(PlatformRelease.self, from: data),
78
+ release.kind == "platform",
79
+ release.bundleIdentifier == request.bundleIdentifier,
80
+ release.teamIdentifier == request.teamIdentifier,
81
+ release.artifact.format == "bleam-macos-app-zip-v1",
82
+ release.releaseId.range(of: "^[a-z][a-z0-9_-]{2,127}$", options: .regularExpression) != nil
83
+ else {
84
+ reply(nil)
85
+ return
86
+ }
87
+
88
+ URLSession.shared.downloadTask(with: release.artifact.url) { temporaryURL, _, _ in
89
+ guard let temporaryURL else {
90
+ reply(nil)
91
+ return
92
+ }
93
+
94
+ do {
95
+ let cache = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
96
+ .appendingPathComponent("Bleam/Platform", isDirectory: true)
97
+ .appendingPathComponent(request.bundleIdentifier, isDirectory: true)
98
+ .appendingPathComponent(release.releaseId, isDirectory: true)
99
+ let completionReceipt = URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)
100
+ .appendingPathComponent("Library/Application Support/Bleam/Updates", isDirectory: true)
101
+ .appendingPathComponent(request.bundleIdentifier, isDirectory: true)
102
+ .appendingPathComponent("\(release.releaseId).complete")
103
+ guard !FileManager.default.fileExists(atPath: completionReceipt.path) else {
104
+ reply(nil)
105
+ return
106
+ }
107
+ try FileManager.default.createDirectory(at: cache, withIntermediateDirectories: true)
108
+ let artifactURL = cache.appendingPathComponent("artifact.zip")
109
+ try? FileManager.default.removeItem(at: artifactURL)
110
+ try FileManager.default.moveItem(at: temporaryURL, to: artifactURL)
111
+ let appURL = try self.verify(
112
+ artifactURL: artifactURL,
113
+ release: release,
114
+ request: request,
115
+ cache: cache
116
+ )
117
+ let result: [String: Any] = [
118
+ "releaseId": release.releaseId,
119
+ "appVersion": release.appVersion,
120
+ "buildNumber": release.buildNumber,
121
+ "platformVersion": release.bleamPlatformVersion,
122
+ "artifactPath": artifactURL.path,
123
+ "appPath": appURL.path,
124
+ "sizeBytes": release.artifact.sizeBytes,
125
+ "sha256": release.artifact.sha256,
126
+ "verified": true,
127
+ ]
128
+ let receipt: [String: Any] = [
129
+ "releaseId": release.releaseId,
130
+ "bundleIdentifier": request.bundleIdentifier,
131
+ "teamIdentifier": request.teamIdentifier,
132
+ "appPath": appURL.path,
133
+ ]
134
+ let receiptData = try PropertyListSerialization.data(
135
+ fromPropertyList: receipt,
136
+ format: .binary,
137
+ options: 0
138
+ )
139
+ try receiptData.write(to: cache.appendingPathComponent("verified.plist"), options: .atomic)
140
+ reply(try? JSONSerialization.data(withJSONObject: result))
141
+ } catch {
142
+ reply(try? JSONSerialization.data(withJSONObject: [
143
+ "error": String(describing: error),
144
+ ]))
145
+ }
146
+ }.resume()
147
+ }.resume()
148
+ }
149
+
150
+ func applyPlatformUpdate(requestData: Data, reply: @escaping (Bool) -> Void) {
151
+ guard
152
+ let request = try? JSONDecoder().decode(ApplyRequest.self, from: requestData),
153
+ let installedURL = supportedInstalledURL(path: request.installedAppPath),
154
+ let candidateURL = verifiedCandidate(request: request)
155
+ else {
156
+ reply(false)
157
+ return
158
+ }
159
+
160
+ reply(true)
161
+ let activity = ProcessInfo.processInfo.beginActivity(
162
+ options: [.userInitiated, .idleSystemSleepDisabled, .suddenTerminationDisabled],
163
+ reason: "Applying a verified Bleam platform update"
164
+ )
165
+ DispatchQueue.global(qos: .userInitiated).async {
166
+ defer { ProcessInfo.processInfo.endActivity(activity) }
167
+ for _ in 0..<50 where kill(request.appPID, 0) == 0 {
168
+ usleep(100_000)
169
+ }
170
+ if kill(request.appPID, 0) == 0 {
171
+ _ = kill(request.appPID, SIGTERM)
172
+ for _ in 0..<50 where kill(request.appPID, 0) == 0 {
173
+ usleep(100_000)
174
+ }
175
+ }
176
+ guard kill(request.appPID, 0) != 0 else {
177
+ NSLog("Bleam platform replacement failed: app did not terminate")
178
+ return
179
+ }
180
+
181
+ let releaseRoot = candidateURL
182
+ .deletingLastPathComponent().deletingLastPathComponent()
183
+ .deletingLastPathComponent()
184
+ let backupURL = releaseRoot.appendingPathComponent("previous.app")
185
+ do {
186
+ try? FileManager.default.removeItem(at: backupURL)
187
+ try FileManager.default.moveItem(at: installedURL, to: backupURL)
188
+ do {
189
+ try FileManager.default.moveItem(at: candidateURL, to: installedURL)
190
+ } catch {
191
+ try? FileManager.default.moveItem(at: backupURL, to: installedURL)
192
+ throw error
193
+ }
194
+ let receipts = URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)
195
+ .appendingPathComponent("Library/Application Support/Bleam/Updates", isDirectory: true)
196
+ .appendingPathComponent(request.bundleIdentifier, isDirectory: true)
197
+ try FileManager.default.createDirectory(at: receipts, withIntermediateDirectories: true)
198
+ try Data().write(to: receipts.appendingPathComponent("\(request.releaseId).complete"))
199
+ _ = try self.run("/usr/bin/open", [installedURL.path])
200
+ } catch {
201
+ NSLog("Bleam platform replacement failed: %@", error.localizedDescription)
202
+ }
203
+ }
204
+ }
205
+
206
+ private func supportedInstalledURL(path: String) -> URL? {
207
+ let applications = URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true)
208
+ .appendingPathComponent("Applications", isDirectory: true).standardizedFileURL
209
+ let candidate = URL(fileURLWithPath: path, isDirectory: true).standardizedFileURL
210
+ guard
211
+ candidate.deletingLastPathComponent() == applications,
212
+ candidate.pathExtension == "app"
213
+ else { return nil }
214
+ return candidate
215
+ }
216
+
217
+ private func verifiedCandidate(request: ApplyRequest) -> URL? {
218
+ let root = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
219
+ .appendingPathComponent("Bleam/Platform", isDirectory: true)
220
+ .appendingPathComponent(request.bundleIdentifier, isDirectory: true)
221
+ .appendingPathComponent(request.releaseId, isDirectory: true)
222
+ let receiptURL = root.appendingPathComponent("verified.plist")
223
+ guard
224
+ let data = try? Data(contentsOf: receiptURL),
225
+ let receipt = try? PropertyListSerialization.propertyList(from: data, format: nil) as? [String: String],
226
+ receipt["releaseId"] == request.releaseId,
227
+ receipt["bundleIdentifier"] == request.bundleIdentifier,
228
+ receipt["teamIdentifier"] == request.teamIdentifier,
229
+ let path = receipt["appPath"]
230
+ else { return nil }
231
+ return URL(fileURLWithPath: path, isDirectory: true)
232
+ }
233
+
234
+ private func verify(
235
+ artifactURL: URL,
236
+ release: PlatformRelease,
237
+ request: DownloadRequest,
238
+ cache: URL
239
+ ) throws -> URL {
240
+ let data = try Data(contentsOf: artifactURL, options: .mappedIfSafe)
241
+ guard
242
+ data.count == release.artifact.sizeBytes,
243
+ SHA256.hash(data: data).map({ String(format: "%02x", $0) }).joined() == release.artifact.sha256
244
+ else { throw VerificationError.invalidArtifact }
245
+
246
+ let entries = try run("/usr/bin/zipinfo", ["-1", artifactURL.path])
247
+ .split(separator: "\n").map(String.init)
248
+ guard !entries.isEmpty, entries.allSatisfy({ entry in
249
+ !entry.hasPrefix("/") && entry.split(separator: "/").allSatisfy { $0 != ".." }
250
+ }) else { throw VerificationError.unsafeArchivePath }
251
+ let appNames = Set(entries.compactMap { entry -> String? in
252
+ let parts = entry.split(separator: "/")
253
+ guard parts.count >= 2, parts[0] == "app", parts[1].hasSuffix(".app") else { return nil }
254
+ return String(parts[1])
255
+ })
256
+ guard
257
+ appNames.count == 1,
258
+ let appName = appNames.first,
259
+ entries.allSatisfy({ $0 == "app/" || $0.hasPrefix("app/\(appName)/") })
260
+ else { throw VerificationError.invalidArchiveLayout }
261
+
262
+ let extracted = cache.appendingPathComponent("extracted", isDirectory: true)
263
+ try? FileManager.default.removeItem(at: extracted)
264
+ try FileManager.default.createDirectory(at: extracted, withIntermediateDirectories: true)
265
+ _ = try run("/usr/bin/ditto", ["-x", "-k", artifactURL.path, extracted.path])
266
+ let appURL = extracted.appendingPathComponent("app/\(appName)")
267
+ guard
268
+ let info = NSDictionary(contentsOf: appURL.appendingPathComponent("Contents/Info.plist")),
269
+ info["CFBundleIdentifier"] as? String == request.bundleIdentifier,
270
+ info["CFBundleShortVersionString"] as? String == release.appVersion,
271
+ info["CFBundleVersion"] as? String == release.buildNumber,
272
+ let executable = info["CFBundleExecutable"] as? String
273
+ else { throw VerificationError.invalidIdentity }
274
+
275
+ var staticCode: SecStaticCode?
276
+ guard
277
+ SecStaticCodeCreateWithPath(appURL as CFURL, [], &staticCode) == errSecSuccess,
278
+ let staticCode,
279
+ SecStaticCodeCheckValidity(staticCode, SecCSFlags(rawValue: kSecCSStrictValidate), nil) == errSecSuccess
280
+ else { throw VerificationError.invalidSignature }
281
+ var signingInfo: CFDictionary?
282
+ guard
283
+ SecCodeCopySigningInformation(staticCode, SecCSFlags(rawValue: kSecCSSigningInformation), &signingInfo) == errSecSuccess,
284
+ (signingInfo as? [String: Any])?[kSecCodeInfoTeamIdentifier as String] as? String == request.teamIdentifier
285
+ else { throw VerificationError.invalidSignature }
286
+
287
+ let architectures = try run(
288
+ "/usr/bin/lipo",
289
+ ["-archs", appURL.appendingPathComponent("Contents/MacOS/\(executable)").path]
290
+ ).trimmingCharacters(in: .whitespacesAndNewlines)
291
+ guard architectures == "arm64" else { throw VerificationError.invalidArchitecture }
292
+
293
+ let required = release.target.minimumMacOSVersion.split(separator: ".").compactMap { Int($0) }
294
+ let current = ProcessInfo.processInfo.operatingSystemVersion
295
+ let installed = [current.majorVersion, current.minorVersion, current.patchVersion]
296
+ guard installed.lexicographicallyPrecedes(required) == false else {
297
+ throw VerificationError.unsupportedOS
298
+ }
299
+ return appURL
300
+ }
301
+
302
+ private func run(_ executable: String, _ arguments: [String]) throws -> String {
303
+ let process = Process()
304
+ let output = Pipe()
305
+ let errors = Pipe()
306
+ process.executableURL = URL(fileURLWithPath: executable)
307
+ process.arguments = arguments
308
+ process.standardOutput = output
309
+ process.standardError = errors
310
+ try process.run()
311
+ process.waitUntilExit()
312
+ guard process.terminationStatus == 0 else { throw VerificationError.commandFailed }
313
+ return String(decoding: output.fileHandleForReading.readDataToEndOfFile(), as: UTF8.self)
314
+ }
315
+ }
316
+
317
+ private enum VerificationError: Error {
318
+ case invalidArtifact, unsafeArchivePath, invalidArchiveLayout, invalidIdentity, invalidSignature
319
+ case invalidArchitecture, unsupportedOS, commandFailed
320
+ }
321
+
322
+ private final class PlatformHelperDelegate: NSObject, NSXPCListenerDelegate {
323
+ func listener(_ listener: NSXPCListener, shouldAcceptNewConnection connection: NSXPCConnection) -> Bool {
324
+ connection.exportedInterface = NSXPCInterface(with: PlatformHelperProtocol.self)
325
+ connection.exportedObject = PlatformHelper()
326
+ connection.resume()
327
+ return true
328
+ }
329
+ }
330
+
331
+ private let delegate = PlatformHelperDelegate()
332
+ private let listener = NSXPCListener.service()
333
+ listener.delegate = delegate
334
+ listener.resume()
335
+ RunLoop.main.run()