| name | sdk-swift-package |
| 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. |
| allowed-tools | Read, Write, Edit, Glob, Grep, Bash |
| user-invocable | true |
Skill: Swift Package (XCFramework Distribution)
What this skill covers
Building the Onde Swift package from Rust source using UniFFI, assembling an
XCFramework, and distributing it via a remote-binary Package.swift for
Swift Package Index.
Repository layout
onde/
├── src/ # Rust source (lib.rs, inference/, hf_cache.rs)
├── sdk/Onde/ # Output: generated Swift package (git-ignored locally)
│ ├── Package.swift
│ ├── Sources/Onde/ # UniFFI-generated onde.swift glue + header
│ └── OndeFramework.xcframework
├── uniffi-bindgen/ # onde's own uniffi-bindgen CLI binary
│ ├── Cargo.toml
│ └── uniffi-bindgen.rs # fn main() { uniffi::uniffi_bindgen_main() }
├── uniffi.toml # UniFFI binding config (package_name, cdylib_name)
├── build.rs # Provides tvOS ___chkstk_darwin stub via cc
└── Cargo.toml # uniffi = "=0.31.0" — version MUST stay pinned
Key constraints
| Constraint | Detail |
|---|
| 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. |
| tvOS targets | Tier-3 — require cargo +nightly -Z build-std. Stable toolchain is used for iOS/macOS. |
| 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. |
cargo-swift | Not required. The project owns its own uniffi-bindgen binary. Use it directly (same as build-kotlin.sh does for Android). |
.xcframework in Package.swift | Published Package.swift must use remote url: + checksum: — never a local path:. |
| Swift package name | Onde (PascalCase). Git repo slug: onde-swift under the ondeinference org. |
| App Group ID | group.com.ondeinference.apps — shared across all Onde-powered apps (Flutter, Tauri, native Swift). |
| HF cache subdirectory | <container>/models/hub/ — NOT huggingface/hub/. Must match the Tauri and Dart SDK convention. |
Apple target triples
| Slice | Triple | Toolchain |
|---|
| iOS device | aarch64-apple-ios | stable |
| iOS simulator (Intel) | x86_64-apple-ios | stable |
| iOS simulator (Apple Silicon) | aarch64-apple-ios-sim | stable |
| tvOS device | aarch64-apple-tvos | nightly + -Z build-std |
| tvOS simulator (Intel) | x86_64-apple-tvos | nightly + -Z build-std |
| tvOS simulator (Apple Silicon) | aarch64-apple-tvos-sim | nightly + -Z build-std |
| macOS (Apple Silicon) | aarch64-apple-darwin | stable |
| macOS (Intel) | x86_64-apple-darwin | stable |
Build sequence (manual / CI)
1. Build the uniffi-bindgen CLI (host)
cargo build --manifest-path uniffi-bindgen/Cargo.toml --release
BINDGEN=uniffi-bindgen/target/release/uniffi-bindgen
2. Compile Rust staticlibs per Apple target
cargo build --target aarch64-apple-ios --release
cargo build --target x86_64-apple-ios --release
cargo build --target aarch64-apple-ios-sim --release
cargo build --target aarch64-apple-darwin --release
cargo build --target x86_64-apple-darwin --release
cargo +nightly build -Z build-std \
--target aarch64-apple-tvos --release
cargo +nightly build -Z build-std \
--target x86_64-apple-tvos --release
cargo +nightly build -Z build-std \
--target aarch64-apple-tvos-sim --release
3. lipo simulator slices
lipo -create \
target/x86_64-apple-ios/release/libonde.a \
target/aarch64-apple-ios-sim/release/libonde.a \
-output /tmp/libonde-ios-sim.a
lipo -create \
target/x86_64-apple-tvos/release/libonde.a \
target/aarch64-apple-tvos-sim/release/libonde.a \
-output /tmp/libonde-tvos-sim.a
lipo -create \
target/aarch64-apple-darwin/release/libonde.a \
target/x86_64-apple-darwin/release/libonde.a \
-output /tmp/libonde-macos.a
4. Generate Swift bindings
mkdir -p sdk/Onde/Sources/Onde
$BINDGEN generate \
--library target/aarch64-apple-ios/release/libonde.a \
--language swift \
--out-dir sdk/Onde/Sources/Onde \
--config uniffi.toml
This produces onde.swift and ondeFFI.h (+ ondeFFI.modulemap).
5. Create the .xcframework
Each slice needs a headers directory alongside its .a:
HEADERS=sdk/Onde/Sources/Onde
xcodebuild -create-xcframework \
-library target/aarch64-apple-ios/release/libonde.a -headers $HEADERS \
-library /tmp/libonde-ios-sim.a -headers $HEADERS \
-library target/aarch64-apple-tvos/release/libonde.a -headers $HEADERS \
-library /tmp/libonde-tvos-sim.a -headers $HEADERS \
-library /tmp/libonde-macos.a -headers $HEADERS \
-output sdk/Onde/OndeFramework.xcframework
6. Zip and compute checksum
cd sdk/Onde
zip -r OndeFramework.xcframework.zip OndeFramework.xcframework
CHECKSUM=$(shasum -a 256 OndeFramework.xcframework.zip | cut -d ' ' -f1)
echo $CHECKSUM
7. Publish (two-repo strategy)
- Upload
OndeFramework.xcframework.zip as a GitHub Release asset on the
Rust source repo.
- In the
ondeinference/onde-kit Swift-only repo, update Package.swift:
url: → the GitHub Release asset download URL
checksum: → the SHA-256 from step 6
- Commit, tag the version (
0.1.0, etc.), and push to onde-kit.
- Swift Package Index indexes
onde-kit on every new tag.
Package.swift (published form)
import PackageDescription
let package = Package(
name: "Onde",
platforms: [
.iOS(.v14),
.macOS(.v12),
.tvOS(.v13),
],
products: [
.library(name: "Onde", targets: ["Onde"])
],
targets: [
.binaryTarget(
name: "OndeFramework",
url: "https://github.com/ondeinference/onde-kit/releases/download/0.1.0/OndeFramework.xcframework.zip",
checksum: "<sha256>"
),
.target(
name: "Onde",
dependencies: [.target(name: "OndeFramework")]
),
]
)
UniFFI type map (Rust → Swift)
| Rust type | UniFFI derive | Swift type |
|---|
ChatRole | uniffi::Enum | enum ChatRole |
ChatMessage | uniffi::Record | struct ChatMessage |
SamplingConfig | uniffi::Record | struct SamplingConfig |
GgufModelConfig | uniffi::Record | struct GgufModelConfig |
InferenceResult | uniffi::Record | struct InferenceResult |
StreamChunk | uniffi::Record | struct StreamChunk |
EngineStatus | uniffi::Enum | enum EngineStatus |
EngineInfo | uniffi::Record | struct EngineInfo |
InferenceError | uniffi::Error | enum OndeError : Error |
OndeChatEngine | uniffi::Object | class OndeChatEngine |
StreamChunkListener | callback_interface | protocol StreamChunkListener |
UniFFI compatibility rules:
- Use
u64/u32 instead of usize (UniFFI does not support usize).
- Streaming is exposed as a free function (
streamChatMessage) not an
Object method — UniFFI 0.31 does not support callback_interface in Object
method parameters.
- Use concrete
String parameters in OndeChatEngine methods — impl Into<String>
does not cross the FFI boundary.
Swift API quick reference
import Onde
let engine = OndeChatEngine()
try await engine.loadDefaultModel(systemPrompt: "You are helpful.", sampling: nil)
try await engine.loadGgufModel(config: qwen251_5bConfig(), systemPrompt: nil, sampling: nil)
await engine.unloadModel()
let loaded: Bool = await engine.isLoaded()
let info: EngineInfo = await engine.info()
let result: InferenceResult = try await engine.sendMessage(message: "Hello!")
let oneShot: InferenceResult = try await engine.generate(messages: [...], sampling: nil)
let history: [ChatMessage] = await engine.history()
let removed: UInt64 = await engine.clearHistory()
await engine.pushHistory(message: userMessage(content: "..."))
try await streamChatMessage(engine: engine, message: "Tell me a story.", listener: myHandler)
defaultModelConfig()
qwen251_5bConfig()
qwen253bConfig()
defaultSamplingConfig()
deterministicSamplingConfig()
mobileSamplingConfig()
systemMessage(content:)
userMessage(content:)
assistantMessage(content:)
App Group Shared Container & Cache Convention
All Onde-powered apps use the App Group group.com.ondeinference.apps to share
downloaded models. The cache layout inside the shared container is:
<group container>/
├── models/ ← HF_HOME
│ └── hub/ ← HF_HUB_CACHE (GGUF files live here)
└── tmp/ ← TMPDIR (iOS sandbox restricts system TMPDIR)
Important: The subdirectory is models/, NOT huggingface/. This was
chosen to be user-friendly when browsing the container in Finder/Files. All
SDKs (Rust, Swift, Dart) and all app types (Tauri, Flutter, native) must use
the same path or models won't be shared.
Swift setup (called once at app launch)
import Foundation
func setupInferenceEnvironment() {
guard let container = FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: "group.com.ondeinference.apps"
) else {
let appSupport = FileManager.default
.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
setupHfCache(at: appSupport)
return
}
setupHfCache(at: container)
}
private func setupHfCache(at base: URL) {
let hfHome = base.appendingPathComponent("models")
let hfHub = hfHome.appendingPathComponent("hub")
try? FileManager.default.createDirectory(at: hfHub, withIntermediateDirectories: true)
setenv("HF_HOME", hfHome.path, 1)
setenv("HF_HUB_CACHE", hfHub.path, 1)
let tmp = base.appendingPathComponent("tmp")
try? FileManager.default.createDirectory(at: tmp, withIntermediateDirectories: true)
setenv("TMPDIR", tmp.path, 1)
}
Entitlements required
Both iOS and macOS targets need the App Group entitlement:
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.ondeinference.apps</string>
</array>
The App Group must also be registered in the Apple Developer Portal under
Identifiers → App Groups before Xcode can provision it.
Local development (Xcode)
Use the local path form during development — the remote binary form is only
needed for distribution:
.binaryTarget(name: "OndeFramework", path: "./OndeFramework.xcframework")
In Xcode: File → Add Package Dependencies → Add Local → select sdk/Onde/.
Common pitfalls
| Pitfall | Fix |
|---|
Local path: in published Package.swift | Always url: + checksum: in onde-swift |
| Stale checksum after rebuild | Always recompute shasum -a 256 — never hardcode |
| Missing simulator slice | lipo x86_64 + aarch64-sim before passing to xcodebuild |
| UniFFI version drift | Keep uniffi = "=0.31.0" in both Cargo.toml and uniffi-bindgen/Cargo.toml |
| tvOS build with stable toolchain | tvOS targets are tier-3; always use cargo +nightly -Z build-std |
Empty Sources/ in onde-swift | Copy the generated onde.swift (and header) into the repo on every release |
___chkstk_darwin linker error | Ensure tvos_chkstk.s exists at crate root; build.rs compiles it automatically |
| Missing Metal toolchain (Xcode 26+) | Run xcodebuild -downloadComponent MetalToolchain, then cargo clean -p mistralrs-quant |
Cache path mismatch (huggingface/ vs models/) | All SDKs MUST use <container>/models/hub/ — using huggingface/hub/ means models are not shared across apps |
| 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 |
CODE_SIGN_IDENTITY = "-" blocks App Groups | Remove ad-hoc signing from project-level build settings; App Groups require a real development certificate |
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 |