main
md 363 lines 13.3 KB
Rendered Raw
1 ---
2 name: sdk-swift-package
3 description: Build the Onde Swift package (XCFramework) from Rust source using UniFFI and distribute it via a remote-binary Package.swift for Swift Package Index. Covers xcframework assembly, App Group shared container, models/hub cache convention, and the onde-swift release workflow.
4 allowed-tools: Read, Write, Edit, Glob, Grep, Bash
5 user-invocable: true
6 ---
7
8 # Skill: Swift Package (XCFramework Distribution)
9
10 ## What this skill covers
11
12 Building the `Onde` Swift package from Rust source using UniFFI, assembling an
13 `XCFramework`, and distributing it via a remote-binary `Package.swift` for
14 Swift Package Index.
15
16 ---
17
18 ## Repository layout
19
20 ```
21 onde/
22 ├── src/ # Rust source (lib.rs, inference/, hf_cache.rs)
23 ├── sdk/Onde/ # Output: generated Swift package (git-ignored locally)
24 │ ├── Package.swift
25 │ ├── Sources/Onde/ # UniFFI-generated onde.swift glue + header
26 │ └── OndeFramework.xcframework
27 ├── uniffi-bindgen/ # onde's own uniffi-bindgen CLI binary
28 │ ├── Cargo.toml
29 │ └── uniffi-bindgen.rs # fn main() { uniffi::uniffi_bindgen_main() }
30 ├── uniffi.toml # UniFFI binding config (package_name, cdylib_name)
31 ├── build.rs # Provides tvOS ___chkstk_darwin stub via cc
32 └── Cargo.toml # uniffi = "=0.31.0" — version MUST stay pinned
33 ```
34
35 ---
36
37 ## Key constraints
38
39 | Constraint | Detail |
40 |---|---|
41 | UniFFI version | Pinned to **`=0.31.0`** in `Cargo.toml` and `uniffi-bindgen/Cargo.toml`. Never bump without coordinating with the Kotlin and Python SDKs. |
42 | tvOS targets | Tier-3 — require `cargo +nightly -Z build-std`. Stable toolchain is used for iOS/macOS. |
43 | tvOS linker | `___chkstk_darwin` is missing from tvOS libSystem. `build.rs` compiles `tvos_chkstk.s` as a no-op stub. This is automatic — do not remove it. |
44 | `cargo-swift` | **Not required.** The project owns its own `uniffi-bindgen` binary. Use it directly (same as `build-kotlin.sh` does for Android). |
45 | `.xcframework` in `Package.swift` | Published `Package.swift` must use **remote** `url:` + `checksum:` — never a local `path:`. |
46 | Swift package name | `Onde` (PascalCase). Git repo slug: `onde-swift` under the `ondeinference` org. |
47 | App Group ID | `group.com.ondeinference.apps` — shared across all Onde-powered apps (Flutter, Tauri, native Swift). |
48 | HF cache subdirectory | `<container>/models/hub/` — NOT `huggingface/hub/`. Must match the Tauri and Dart SDK convention. |
49
50 ---
51
52 ## Apple target triples
53
54 | Slice | Triple | Toolchain |
55 |---|---|---|
56 | iOS device | `aarch64-apple-ios` | stable |
57 | iOS simulator (Intel) | `x86_64-apple-ios` | stable |
58 | iOS simulator (Apple Silicon) | `aarch64-apple-ios-sim` | stable |
59 | tvOS device | `aarch64-apple-tvos` | nightly + `-Z build-std` |
60 | tvOS simulator (Intel) | `x86_64-apple-tvos` | nightly + `-Z build-std` |
61 | tvOS simulator (Apple Silicon) | `aarch64-apple-tvos-sim` | nightly + `-Z build-std` |
62 | macOS (Apple Silicon) | `aarch64-apple-darwin` | stable |
63 | macOS (Intel) | `x86_64-apple-darwin` | stable |
64
65 ---
66
67 ## Build sequence (manual / CI)
68
69 ### 1. Build the `uniffi-bindgen` CLI (host)
70
71 ```bash
72 cargo build --manifest-path uniffi-bindgen/Cargo.toml --release
73 BINDGEN=uniffi-bindgen/target/release/uniffi-bindgen # or workspace target/
74 ```
75
76 ### 2. Compile Rust staticlibs per Apple target
77
78 ```bash
79 # Stable targets (iOS, macOS)
80 cargo build --target aarch64-apple-ios --release
81 cargo build --target x86_64-apple-ios --release
82 cargo build --target aarch64-apple-ios-sim --release
83 cargo build --target aarch64-apple-darwin --release
84 cargo build --target x86_64-apple-darwin --release
85
86 # Nightly targets (tvOS — tier 3)
87 cargo +nightly build -Z build-std \
88 --target aarch64-apple-tvos --release
89 cargo +nightly build -Z build-std \
90 --target x86_64-apple-tvos --release
91 cargo +nightly build -Z build-std \
92 --target aarch64-apple-tvos-sim --release
93 ```
94
95 ### 3. lipo simulator slices
96
97 ```bash
98 # iOS simulator fat lib
99 lipo -create \
100 target/x86_64-apple-ios/release/libonde.a \
101 target/aarch64-apple-ios-sim/release/libonde.a \
102 -output /tmp/libonde-ios-sim.a
103
104 # tvOS simulator fat lib
105 lipo -create \
106 target/x86_64-apple-tvos/release/libonde.a \
107 target/aarch64-apple-tvos-sim/release/libonde.a \
108 -output /tmp/libonde-tvos-sim.a
109
110 # macOS universal fat lib
111 lipo -create \
112 target/aarch64-apple-darwin/release/libonde.a \
113 target/x86_64-apple-darwin/release/libonde.a \
114 -output /tmp/libonde-macos.a
115 ```
116
117 ### 4. Generate Swift bindings
118
119 ```bash
120 mkdir -p sdk/Onde/Sources/Onde
121
122 $BINDGEN generate \
123 --library target/aarch64-apple-ios/release/libonde.a \
124 --language swift \
125 --out-dir sdk/Onde/Sources/Onde \
126 --config uniffi.toml
127 ```
128
129 This produces `onde.swift` and `ondeFFI.h` (+ `ondeFFI.modulemap`).
130
131 ### 5. Create the `.xcframework`
132
133 Each slice needs a headers directory alongside its `.a`:
134
135 ```bash
136 HEADERS=sdk/Onde/Sources/Onde # contains ondeFFI.h + ondeFFI.modulemap
137
138 xcodebuild -create-xcframework \
139 -library target/aarch64-apple-ios/release/libonde.a -headers $HEADERS \
140 -library /tmp/libonde-ios-sim.a -headers $HEADERS \
141 -library target/aarch64-apple-tvos/release/libonde.a -headers $HEADERS \
142 -library /tmp/libonde-tvos-sim.a -headers $HEADERS \
143 -library /tmp/libonde-macos.a -headers $HEADERS \
144 -output sdk/Onde/OndeFramework.xcframework
145 ```
146
147 ### 6. Zip and compute checksum
148
149 ```bash
150 cd sdk/Onde
151 zip -r OndeFramework.xcframework.zip OndeFramework.xcframework
152 CHECKSUM=$(shasum -a 256 OndeFramework.xcframework.zip | cut -d ' ' -f1)
153 echo $CHECKSUM
154 ```
155
156 ### 7. Publish (two-repo strategy)
157
158 1. Upload `OndeFramework.xcframework.zip` as a GitHub Release asset on the
159 Rust source repo.
160 2. In the `ondeinference/onde-kit` Swift-only repo, update `Package.swift`:
161 - `url:` → the GitHub Release asset download URL
162 - `checksum:` → the SHA-256 from step 6
163 3. Commit, tag the version (`0.1.0`, etc.), and push to `onde-kit`.
164 4. Swift Package Index indexes `onde-kit` on every new tag.
165
166 ---
167
168 ## `Package.swift` (published form)
169
170 ```swift
171 // swift-tools-version:5.5
172 import PackageDescription
173
174 let package = Package(
175 name: "Onde",
176 platforms: [
177 .iOS(.v14),
178 .macOS(.v12),
179 .tvOS(.v13),
180 ],
181 products: [
182 .library(name: "Onde", targets: ["Onde"])
183 ],
184 targets: [
185 .binaryTarget(
186 name: "OndeFramework",
187 url: "https://github.com/ondeinference/onde-kit/releases/download/0.1.0/OndeFramework.xcframework.zip",
188 checksum: "<sha256>"
189 ),
190 .target(
191 name: "Onde",
192 dependencies: [.target(name: "OndeFramework")]
193 ),
194 ]
195 )
196 ```
197
198 ---
199
200 ## UniFFI type map (Rust → Swift)
201
202 | Rust type | UniFFI derive | Swift type |
203 |---|---|---|
204 | `ChatRole` | `uniffi::Enum` | `enum ChatRole` |
205 | `ChatMessage` | `uniffi::Record` | `struct ChatMessage` |
206 | `SamplingConfig` | `uniffi::Record` | `struct SamplingConfig` |
207 | `GgufModelConfig` | `uniffi::Record` | `struct GgufModelConfig` |
208 | `InferenceResult` | `uniffi::Record` | `struct InferenceResult` |
209 | `StreamChunk` | `uniffi::Record` | `struct StreamChunk` |
210 | `EngineStatus` | `uniffi::Enum` | `enum EngineStatus` |
211 | `EngineInfo` | `uniffi::Record` | `struct EngineInfo` |
212 | `InferenceError` | `uniffi::Error` | `enum OndeError : Error` |
213 | `OndeChatEngine` | `uniffi::Object` | `class OndeChatEngine` |
214 | `StreamChunkListener` | `callback_interface` | `protocol StreamChunkListener` |
215
216 **UniFFI compatibility rules:**
217 - Use `u64`/`u32` instead of `usize` (UniFFI does not support `usize`).
218 - Streaming is exposed as a **free function** (`streamChatMessage`) not an
219 Object method — UniFFI 0.31 does not support `callback_interface` in Object
220 method parameters.
221 - Use concrete `String` parameters in `OndeChatEngine` methods — `impl Into<String>`
222 does not cross the FFI boundary.
223
224 ---
225
226 ## Swift API quick reference
227
228 ```swift
229 import Onde
230
231 // Engine lifecycle
232 let engine = OndeChatEngine()
233 try await engine.loadDefaultModel(systemPrompt: "You are helpful.", sampling: nil)
234 try await engine.loadGgufModel(config: qwen251_5bConfig(), systemPrompt: nil, sampling: nil)
235 await engine.unloadModel()
236 let loaded: Bool = await engine.isLoaded()
237 let info: EngineInfo = await engine.info()
238
239 // Chat
240 let result: InferenceResult = try await engine.sendMessage(message: "Hello!")
241 let oneShot: InferenceResult = try await engine.generate(messages: [...], sampling: nil)
242
243 // History
244 let history: [ChatMessage] = await engine.history()
245 let removed: UInt64 = await engine.clearHistory()
246 await engine.pushHistory(message: userMessage(content: "..."))
247
248 // Streaming (free function, callback-based)
249 try await streamChatMessage(engine: engine, message: "Tell me a story.", listener: myHandler)
250
251 // Config free functions
252 defaultModelConfig() // platform-appropriate default
253 qwen251_5bConfig() // ~941 MB, iOS/tvOS/Android
254 qwen253bConfig() // ~1.93 GB, macOS/desktop
255
256 // Sampling presets
257 defaultSamplingConfig() // temp=0.7, top_p=0.95, max=512
258 deterministicSamplingConfig() // temp=0.0
259 mobileSamplingConfig() // temp=0.7, max=128
260
261 // Message constructors
262 systemMessage(content:)
263 userMessage(content:)
264 assistantMessage(content:)
265 ```
266
267 ---
268
269 ## App Group Shared Container & Cache Convention
270
271 All Onde-powered apps use the App Group `group.com.ondeinference.apps` to share
272 downloaded models. The cache layout inside the shared container is:
273
274 ```
275 <group container>/
276 ├── models/ ← HF_HOME
277 │ └── hub/ ← HF_HUB_CACHE (GGUF files live here)
278 └── tmp/ ← TMPDIR (iOS sandbox restricts system TMPDIR)
279 ```
280
281 **Important:** The subdirectory is `models/`, NOT `huggingface/`. This was
282 chosen to be user-friendly when browsing the container in Finder/Files. All
283 SDKs (Rust, Swift, Dart) and all app types (Tauri, Flutter, native) must use
284 the same path or models won't be shared.
285
286 ### Swift setup (called once at app launch)
287
288 ```swift
289 import Foundation
290
291 func setupInferenceEnvironment() {
292 guard let container = FileManager.default.containerURL(
293 forSecurityApplicationGroupIdentifier: "group.com.ondeinference.apps"
294 ) else {
295 // Fall back to app-private Application Support if App Group is unavailable.
296 let appSupport = FileManager.default
297 .urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
298 setupHfCache(at: appSupport)
299 return
300 }
301 setupHfCache(at: container)
302 }
303
304 private func setupHfCache(at base: URL) {
305 let hfHome = base.appendingPathComponent("models")
306 let hfHub = hfHome.appendingPathComponent("hub")
307 try? FileManager.default.createDirectory(at: hfHub, withIntermediateDirectories: true)
308
309 setenv("HF_HOME", hfHome.path, 1)
310 setenv("HF_HUB_CACHE", hfHub.path, 1)
311
312 let tmp = base.appendingPathComponent("tmp")
313 try? FileManager.default.createDirectory(at: tmp, withIntermediateDirectories: true)
314 setenv("TMPDIR", tmp.path, 1)
315 }
316 ```
317
318 ### Entitlements required
319
320 Both iOS and macOS targets need the App Group entitlement:
321
322 ```xml
323 <key>com.apple.security.application-groups</key>
324 <array>
325 <string>group.com.ondeinference.apps</string>
326 </array>
327 ```
328
329 The App Group must also be registered in the Apple Developer Portal under
330 **Identifiers → App Groups** before Xcode can provision it.
331
332 ---
333
334 ## Local development (Xcode)
335
336 Use the **local path** form during development — the remote binary form is only
337 needed for distribution:
338
339 ```swift
340 // Package.swift (local dev only — never commit to onde-kit)
341 .binaryTarget(name: "OndeFramework", path: "./OndeFramework.xcframework")
342 ```
343
344 In Xcode: **File → Add Package Dependencies → Add Local** → select `sdk/Onde/`.
345
346 ---
347
348 ## Common pitfalls
349
350 | Pitfall | Fix |
351 |---|---|
352 | Local `path:` in published `Package.swift` | Always `url:` + `checksum:` in `onde-swift` |
353 | Stale checksum after rebuild | Always recompute `shasum -a 256` — never hardcode |
354 | Missing simulator slice | lipo `x86_64` + `aarch64-sim` before passing to `xcodebuild` |
355 | UniFFI version drift | Keep `uniffi = "=0.31.0"` in both `Cargo.toml` and `uniffi-bindgen/Cargo.toml` |
356 | tvOS build with stable toolchain | tvOS targets are tier-3; always use `cargo +nightly -Z build-std` |
357 | Empty `Sources/` in `onde-swift` | Copy the generated `onde.swift` (and header) into the repo on every release |
358 | `___chkstk_darwin` linker error | Ensure `tvos_chkstk.s` exists at crate root; `build.rs` compiles it automatically |
359 | Missing Metal toolchain (Xcode 26+) | Run `xcodebuild -downloadComponent MetalToolchain`, then `cargo clean -p mistralrs-quant` |
360 | Cache path mismatch (`huggingface/` vs `models/`) | All SDKs MUST use `<container>/models/hub/` — using `huggingface/hub/` means models are not shared across apps |
361 | App Group not working | Register `group.com.ondeinference.apps` in Apple Developer Portal → Identifiers → App Groups; enable App Groups capability on the App ID; add entitlement to the target |
362 | `CODE_SIGN_IDENTITY = "-"` blocks App Groups | Remove ad-hoc signing from project-level build settings; App Groups require a real development certificate |
363 | Android NDK `LD` on PATH breaks Xcode | Never `export LD=$TOOLCHAIN/bin/ld` globally — it hijacks Xcode's linker. Use Cargo's `.cargo/config.toml` for Android linker config |