bleam 0.0.7 → 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.
- package/dist/cli.cjs +23 -4
- package/dist/cli.d.cts +2 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +23 -4
- package/dist/{ui-234Plg7Z.d.cts → ui-Bg11tvlc.d.ts} +7 -7
- package/dist/ui.d.ts +1 -1
- package/dist/window.d.ts +1 -1
- package/package.json +4 -1
- package/templates/image-generation/app/index.tsx +1 -1
- package/templates/native/.gitattributes +1 -0
- package/templates/native/App.tsx +26 -0
- package/templates/native/app.json +12 -0
- package/templates/native/index.ts +8 -0
- package/templates/native/ios/.xcode.env +11 -0
- package/templates/native/ios/Bleam/AI/Flux2/Configuration/Flux2Config.swift +288 -0
- package/templates/native/ios/Bleam/AI/Flux2/Configuration/MemoryConfig.swift +233 -0
- package/templates/native/ios/Bleam/AI/Flux2/Configuration/MemoryOptimizationConfig.swift +157 -0
- package/templates/native/ios/Bleam/AI/Flux2/Configuration/ModelRegistry.swift +425 -0
- package/templates/native/ios/Bleam/AI/Flux2/Configuration/QuantizationConfig.swift +138 -0
- package/templates/native/ios/Bleam/AI/Flux2/Configuration/TransformerRepoProfile.swift +59 -0
- package/templates/native/ios/Bleam/AI/Flux2/Configuration/VAEConfig.swift +134 -0
- package/templates/native/ios/Bleam/AI/Flux2/Flux2Core.swift +55 -0
- package/templates/native/ios/Bleam/AI/Flux2/Loading/WeightLoader.swift +1192 -0
- package/templates/native/ios/Bleam/AI/Flux2/Pipeline/FluxImageRunner.swift +1282 -0
- package/templates/native/ios/Bleam/AI/Flux2/Pipeline/LatentUtils.swift +191 -0
- package/templates/native/ios/Bleam/AI/Flux2/Scheduler/FlowMatchEulerScheduler.swift +279 -0
- package/templates/native/ios/Bleam/AI/Flux2/Text/KleinTextEncoder.swift +380 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2Attention.swift +379 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2Embeddings.swift +159 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2FeedForward.swift +154 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2FusedKernels.swift +198 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2KVCache.swift +36 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2Modulation.swift +193 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2ParallelAttention.swift +389 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2RoPE.swift +304 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2SingleBlock.swift +290 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2Transformer.swift +530 -0
- package/templates/native/ios/Bleam/AI/Flux2/Transformer/Flux2TransformerBlock.swift +362 -0
- package/templates/native/ios/Bleam/AI/Flux2/Utils/Flux2Debug.swift +101 -0
- package/templates/native/ios/Bleam/AI/Flux2/Utils/MLXCheckpoint.swift +118 -0
- package/templates/native/ios/Bleam/AI/Flux2/Utils/MemoryManager.swift +201 -0
- package/templates/native/ios/Bleam/AI/Flux2/VAE/AutoencoderKL.swift +476 -0
- package/templates/native/ios/Bleam/AI/Flux2/VAE/ResnetBlock.swift +316 -0
- package/templates/native/ios/Bleam/AI/Flux2/VAE/VAEDecoder.swift +120 -0
- package/templates/native/ios/Bleam/AI/Flux2/VAE/VAEEncoder.swift +136 -0
- package/templates/native/ios/Bleam/AppDelegate.swift +373 -0
- package/templates/native/ios/Bleam/Appearance.swift +62 -0
- package/templates/native/ios/Bleam/Bridging-Header.h +3 -0
- package/templates/native/ios/Bleam/Images.xcassets/AppIcon.appiconset/Contents.json +13 -0
- package/templates/native/ios/Bleam/Images.xcassets/Contents.json +6 -0
- package/templates/native/ios/Bleam/Info.plist +76 -0
- package/templates/native/ios/Bleam/PrivacyInfo.xcprivacy +48 -0
- package/templates/native/ios/Bleam/SceneDelegate.swift +58 -0
- package/templates/native/ios/Bleam/SplashScreen.storyboard +47 -0
- package/templates/native/ios/Bleam/Supporting/Expo.plist +6 -0
- package/templates/native/ios/Bleam/bleam.entitlements +10 -0
- package/templates/native/ios/Bleam.xcodeproj/project.pbxproj +1146 -0
- package/templates/native/ios/Bleam.xcodeproj/xcshareddata/xcschemes/Bleam.xcscheme +88 -0
- package/templates/native/ios/GenerationService/GenerationService.entitlements +10 -0
- package/templates/native/ios/GenerationService/ImageGenerationRunner.swift +591 -0
- package/templates/native/ios/GenerationService/Info.plist +31 -0
- package/templates/native/ios/GenerationService/main.swift +165 -0
- package/templates/native/ios/PlatformHelper/Info.plist +29 -0
- package/templates/native/ios/PlatformHelper/main.swift +335 -0
- package/templates/native/ios/Podfile +198 -0
- package/templates/native/ios/Podfile.lock +2284 -0
- package/templates/native/ios/Podfile.properties.json +5 -0
- package/templates/native/ios/Shared/Generation/GenerationServiceProtocol.swift +12 -0
- package/templates/native/ios/Shared/Generation/GenerationWorkerProtocol.swift +124 -0
- package/templates/native/metro.config.js +6 -0
- package/templates/native/modules/bleam-runtime/BleamRuntime.podspec +14 -0
- package/templates/native/modules/bleam-runtime/expo-module.config.json +8 -0
- package/templates/native/modules/bleam-runtime/ios/AIModule.swift +1258 -0
- package/templates/native/modules/bleam-runtime/ios/GenerationContracts.swift +135 -0
- package/templates/native/modules/bleam-runtime/ios/PlatformModule.swift +313 -0
- package/templates/native/modules/bleam-runtime/package.json +5 -0
- package/templates/native/package.json +39 -0
- package/templates/native/scripts/build/mlx-frameworks.ts +421 -0
- package/templates/native/scripts/start/index.ts +474 -0
- package/templates/native/scripts/start/known-failures.ts +20 -0
- package/templates/native/scripts/start/loader.ts +76 -0
- package/templates/native/scripts/start/native-state.ts +242 -0
- package/templates/native/scripts/start/package.json +4 -0
- package/templates/native/scripts/start/swift-packages.ts +414 -0
- package/templates/native/scripts/start/xcode-formatter.ts +106 -0
- package/templates/native/tsconfig.json +14 -0
- package/templates/native/yarn.lock +3749 -0
- package/dist/ui-BJmXhz9Q.d.ts +0 -92
- package/dist/ui-Dd7SXdbg.d.cts +0 -92
- package/dist/ui-Ds88eETu.d.ts +0 -92
- package/dist/ui-TaqnB5SP.d.ts +0 -92
- package/dist/ui-WQvnXL0W.d.ts +0 -92
|
@@ -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()
|