|
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 | |