@ondeinference / onde / commits / 28611e3

Fix running macOS example project

keypair34 committed Apr 5, 2026 at 14:31 UTC 28611e34b627ac9e04aba7af20578e5d1c9a345d
11 files changed +681 -61
.agents/skills/sdk-dart/SKILL.md
+313 -56
@@ -1,3 +1,10 @@
1 +---
2 +name: sdk-dart
3 +description: Dart/Flutter SDK skill for the onde_inference pub.dev package. Covers flutter_rust_bridge v2 codegen, CocoaPods podspec patterns, ExternalLibrary.open init, .pubignore, Xcode 26 compatibility, Android NDK config, and pub.dev publishing. Apply to all Dart SDK edits.
4 +allowed-tools: Read, Write, Edit, Glob, Grep
5 +user-invocable: false
6 +---
7 +
8 # SKILL: Dart / Flutter SDK with flutter_rust_bridge v2
9
10 > Captured from: building `onde_inference` pub.dev package wrapping the `onde` Rust crate.
@@ -413,60 +420,180 @@ GgufModelConfig qwen251_5bConfig() => const GgufModelConfig(
420 ## Dart Public API Pattern (`lib/src/engine.dart`)
421
422 ```dart
416 -import 'frb_generated.dart' as frb; // NO leading underscore
417 -import 'types.dart';
418 -export 'frb_generated.dart' show RustLib;
423 +import 'dart:async';
424 +import 'dart:io' show Platform;
425
420 -class OndeChatEngine {
421 - final frb.OndeChatEngine _native;
422 - OndeChatEngine._(this._native);
426 +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'
427 + show ExternalLibrary;
428
424 - static Future<OndeChatEngine> create() async {
425 - if (!frb.RustLib.isInitialized) await frb.RustLib.init();
426 - return OndeChatEngine._(await frb.OndeChatEngine.new_());
427 - }
429 +import 'frb_generated.dart/frb_generated.dart' as frb;
430 +import 'frb_generated.dart/api.dart' as api;
431 +import 'types.dart';
432
429 - Stream<StreamChunk> streamMessage(String message) =>
430 - _native.streamMessage(message: message);
433 +export 'frb_generated.dart/frb_generated.dart' show RustLib;
434 +export 'frb_generated.dart/api.dart'
435 + show OndeChatEngine, OndeError, OndeError_NoModelLoaded, /* ... */;
436 +
437 +/// Extension on the FRB-generated OndeChatEngine opaque type.
438 +extension OndeChatEngineX on api.OndeChatEngine {
439 + Future<double> loadDefaultModel({
440 + String? systemPrompt,
441 + SamplingConfig? sampling,
442 + }) =>
443 + loadGgufModel(
444 + config: api.defaultModelConfig(),
445 + systemPrompt: systemPrompt,
446 + sampling: sampling,
447 + );
448 +
449 + Future<int> clearHistoryCount() async => (await clearHistory()).toInt();
450 }
451
452 abstract final class OndeInference {
434 - static Future<void> init() => frb.RustLib.init();
435 - static GgufModelConfig defaultModelConfig() => frb.defaultModelConfig();
436 - static SamplingConfig deterministicSamplingConfig() => frb.deterministicSamplingConfig();
453 + /// Initialises the Rust shared library.
454 + ///
455 + /// On macOS and iOS the Rust static library is force-loaded into the
456 + /// onde_inference CocoaPods framework via OTHER_LDFLAGS in the podspec.
457 + /// We must open that framework explicitly — DynamicLibrary.process()
458 + /// (RTLD_DEFAULT) cannot resolve symbols inside a separately-loaded
459 + /// framework.
460 + static Future<void> init() async {
461 + ExternalLibrary? lib;
462 + if (Platform.isMacOS || Platform.isIOS) {
463 + lib = ExternalLibrary.open(
464 + 'onde_inference.framework/onde_inference',
465 + );
466 + }
467 + await frb.RustLib.init(externalLibrary: lib);
468 + }
469 +
470 + static GgufModelConfig defaultModelConfig() => api.defaultModelConfig();
471 + static SamplingConfig deterministicSamplingConfig() => api.deterministicSamplingConfig();
472 }
473 ```
474
475 +### Why `ExternalLibrary.open(...)` and not `.process()`
476 +
477 +The `-force_load` linker flag embeds Rust symbols into the **pod framework**
478 +(`onde_inference.framework/onde_inference`), NOT into the main executable.
479 +`DynamicLibrary.process()` maps to `RTLD_DEFAULT`, which only searches the
480 +main executable and already-opened images in certain configurations. On
481 +macOS/iOS with CocoaPods `use_frameworks!`, the pod framework is a separate
482 +Mach-O image — you must `dlopen` it by name.
483 +
484 +FRB 2.12's `ExternalLibrary.open(path)` calls `DynamicLibrary.open(path)`
485 +under the hood. On non-Apple platforms (Android, Linux, Windows) the default
486 +`ExternalLibraryLoaderConfig` stem-based lookup works, so `lib` stays `null`
487 +and `RustLib.init()` handles it automatically.
488 +
489 +**Do NOT use `ExternalLibrary.process(iKnowHowToUseIt: true)`** — it will
490 +produce `Failed to lookup symbol 'frb_get_rust_content_hash': dlsym(RTLD_DEFAULT, ...)`.
491 +
492 ---
493
494 ## Platform Build Files
495
444 -### iOS podspec — key section
496 +### Dummy Swift Plugin Classes (required)
497 +
498 +Both `macos/Classes/OndeInferencePlugin.swift` and `ios/Classes/OndeInferencePlugin.swift`
499 +**must exist** — they are no-op `FlutterPlugin` stubs. Without them, CocoaPods
500 +has no source files to compile and won't produce a real `.framework` target.
501 +The Rust static library is force-loaded INTO this framework.
502 +
503 +```swift
504 +// macos/Classes/OndeInferencePlugin.swift
505 +import FlutterMacOS
506 +
507 +public class OndeInferencePlugin: NSObject, FlutterPlugin {
508 + public static func register(with registrar: FlutterPluginRegistrar) {
509 + // No-op — all FFI symbols come from the Rust static library.
510 + }
511 +}
512 +```
513 +
514 +```swift
515 +// ios/Classes/OndeInferencePlugin.swift
516 +import Flutter // NOT FlutterMacOS
517 +
518 +public class OndeInferencePlugin: NSObject, FlutterPlugin {
519 + public static func register(with registrar: FlutterPluginRegistrar) {}
520 +}
521 +```
522 +
523 +### macOS podspec — full working version
524
525 ```ruby
447 -s.script_phases = [{
448 - :name => 'Build Rust bridge',
449 - :script => <<~SHELL,
450 - set -e
451 - cd "${PODS_TARGET_SRCROOT}/../rust"
452 - TARGET=$([[ "$PLATFORM_NAME" == "iphonesimulator" ]] && echo "aarch64-apple-ios-sim" || echo "aarch64-apple-ios")
453 - cargo build --release --target "$TARGET" --lib --crate-type staticlib
454 - cp "target/$TARGET/release/lib<crate>.a" "${BUILT_PRODUCTS_DIR}/lib<crate>.a"
455 - SHELL
456 - :execution_position => :before_compile,
457 - :output_files => ["${BUILT_PRODUCTS_DIR}/lib<crate>.a"],
458 -}]
459 -s.xcconfig = { 'OTHER_LDFLAGS' => '-l<crate>' }
460 -s.libraries = '<crate>'
461 -```
462 -
463 -### macOS podspec — differs from iOS
526 +Pod::Spec.new do |s|
527 + s.name = 'onde_inference'
528 + s.version = '0.1.0'
529 + s.summary = 'On-device LLM inference SDK for Flutter (macOS).'
530 + s.description = 'Runs Qwen 2.5 models locally on macOS with Metal acceleration.'
531 + s.homepage = 'https://ondeinference.com'
532 + s.license = { :type => 'MIT', :file => '../LICENSE' }
533 + s.author = { 'Onde Inference' => 'hello@ondeinference.com' }
534 + s.source = { :path => '.' }
535 + s.source_files = 'Classes/**/*'
536 + s.dependency 'FlutterMacOS'
537 + s.platform = :osx, '10.15'
538 + s.swift_version = '5.0'
539 +
540 + s.script_phases = [
541 + {
542 + :name => 'Build Rust bridge (onde_inference_dart)',
543 + :script => <<~SHELL,
544 + set -e
545 + RUST_DIR="${PODS_TARGET_SRCROOT}/../rust"
546 + cd "$RUST_DIR"
547 + ARCH=$(uname -m)
548 + if [[ "$ARCH" == "arm64" ]]; then
549 + TARGET="aarch64-apple-darwin"
550 + else
551 + TARGET="x86_64-apple-darwin"
552 + fi
553 + cargo build --release --target "$TARGET"
554 + cp "target/$TARGET/release/libonde_inference_dart.a" \
555 + "${PODS_TARGET_SRCROOT}/libonde_inference_dart.a"
556 + SHELL
557 + :execution_position => :before_compile,
558 + :output_files => ["${PODS_TARGET_SRCROOT}/libonde_inference_dart.a"],
559 + }
560 + ]
561 +
562 + # SystemConfiguration is required by Rust hyper_util (proxy detection)
563 + s.frameworks = 'SystemConfiguration'
564 +
565 + s.pod_target_xcconfig = {
566 + 'DEFINES_MODULE' => 'YES',
567 + 'OTHER_LDFLAGS' => '-force_load ${PODS_TARGET_SRCROOT}/libonde_inference_dart.a',
568 + }
569 + s.preserve_paths = '../rust/**/*'
570 +end
571 +```
572 +
573 +**Key points:**
574 +- `s.frameworks = 'SystemConfiguration'` — required because the Rust `hyper_util`
575 + crate (transitive via `reqwest`) references `SCDynamicStoreCopyProxies`,
576 + `SCNetworkReachabilityCreateWithName`, etc. Without it you get
577 + `ld: symbol(s) not found for architecture arm64`.
578 +- `-force_load` links ALL symbols from the `.a` into the pod framework.
579 +- `s.source_files = 'Classes/**/*'` picks up the dummy Swift plugin.
580 +
581 +### iOS podspec — differs from macOS
582
583 ```ruby
466 -s.dependency 'FlutterMacOS' # NOT 'Flutter'
467 -s.platform = :osx, '10.15' # NOT :ios
468 -# Script: TARGET = "aarch64-apple-darwin"
469 -# No iphonesimulator check needed
584 + s.dependency 'Flutter' # NOT 'FlutterMacOS'
585 + s.platform = :ios, '13.0' # NOT :osx
586 +
587 + # Target selection differs:
588 + # iphonesimulator → aarch64-apple-ios-sim
589 + # iphoneos → aarch64-apple-ios
590 +
591 + s.pod_target_xcconfig = {
592 + 'DEFINES_MODULE' => 'YES',
593 + 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386',
594 + 'OTHER_LDFLAGS' =>
595 + '-force_load ${PODS_TARGET_SRCROOT}/libonde_inference_dart.a',
596 + }
597 ```
598
599 ### Android `CMakeLists.txt` — key section
@@ -478,13 +605,40 @@ elseif(ANDROID_ABI STREQUAL "armeabi-v7a")
605 set(CARGO_TARGET "armv7-linux-androideabi")
606 elseif(ANDROID_ABI STREQUAL "x86_64")
607 set(CARGO_TARGET "x86_64-linux-android")
608 +elseif(ANDROID_ABI STREQUAL "x86")
609 + set(CARGO_TARGET "i686-linux-android")
610 endif()
611 add_custom_command(OUTPUT "${LIB_OUTPUT}"
483 - COMMAND cargo build --release --target ${CARGO_TARGET} --manifest-path ${RUST_DIR}/Cargo.toml)
484 -add_library(<crate> SHARED IMPORTED GLOBAL)
485 -set_target_properties(<crate> PROPERTIES IMPORTED_LOCATION ${LIB_OUTPUT})
612 + COMMAND cargo build --release --target ${CARGO_TARGET}
613 + --manifest-path "${RUST_DIR}/Cargo.toml"
614 + WORKING_DIRECTORY ${RUST_DIR})
615 +add_library(onde_inference_dart SHARED IMPORTED GLOBAL)
616 +set_target_properties(onde_inference_dart PROPERTIES IMPORTED_LOCATION ${LIB_OUTPUT})
617 ```
618
619 +Android uses CMake to invoke `cargo build` for the right NDK triple, producing
620 +a `.so` shared library (unlike Apple which uses `.a` staticlibs).
621 +
622 +### Android NDK Linker Config (`rust/.cargo/config.toml`)
623 +
624 +```toml
625 +[target.aarch64-linux-android]
626 +linker = "aarch64-linux-android24-clang"
627 +
628 +[target.armv7-linux-androideabi]
629 +linker = "armv7a-linux-androideabi24-clang"
630 +
631 +[target.x86_64-linux-android]
632 +linker = "x86_64-linux-android24-clang"
633 +
634 +[target.i686-linux-android]
635 +linker = "i686-linux-android24-clang"
636 +```
637 +
638 +These short names require the NDK toolchain `bin/` to be on PATH.
639 +Flutter's Gradle + CMake integration typically handles this automatically
640 +during `flutter build apk` / `flutter run -d <android>`.
641 +
642 ---
643
644 ## Testing Strategy
@@ -524,7 +678,7 @@ dart analyze lib/
678 # 2. All tests pass
679 dart test test/dart_test.dart
680
527 -# 3. Dry-run publish — must show "Package has 0 warnings"
681 +# 3. Dry-run publish — must show compressed size < 100 MB and minimal warnings
682 dart pub publish --dry-run
683 ```
684
@@ -535,6 +689,28 @@ Common pub.dev score deductions to avoid:
689 - Library exports not documented → loses points
690 - `library <name>;` directive → triggers lint
691
692 +### Pub.dev Upload Size (100 MB limit)
693 +
694 +`dart pub publish` bundles everything not excluded by `.pubignore` (or
695 +`.gitignore` if `.pubignore` doesn't exist). The Rust `target/` dir alone
696 +can be **7+ GB**. A `.pubignore` file is mandatory.
697 +
698 +**Files that MUST be included** (consumers build from source):
699 +- `rust/src/`, `rust/Cargo.toml`, `rust/Cargo.lock`
700 +- `ios/`, `macos/`, `android/`, `linux/`, `windows/` (build scripts, podspecs, CMakeLists)
701 +- `ios/Classes/`, `macos/Classes/` (dummy plugin Swift files)
702 +- `lib/` (all Dart code)
703 +
704 +**Files that MUST be excluded** (`.pubignore`):
705 +- `rust/target/` — multi-GB build artifacts
706 +- `**/*.a`, `**/*.so`, `**/*.dylib`, `**/*.dll` — precompiled binaries
707 +- `.dart_tool/`, `pubspec.lock`
708 +- `example/build/`, `example/.dart_tool/`
709 +- `logs/`, `.idea/`, `.vscode/`, `.DS_Store`
710 +
711 +After adding `.pubignore`, verify: `dart pub publish --dry-run` should report
712 +~280 KB compressed size, not hundreds of MB.
713 +
714 ---
715
716 ## Codegen Workflow (Full, Correct Order)
@@ -642,6 +818,64 @@ If they drift, generated code won't compile. Always check both when upgrading.
818 | `unnecessary_library_name` | `library <name>;` in barrel | Change to bare `library;` |
819 | `OndeChatEngine.create() throws UnimplementedError` | Codegen not run yet | Run `flutter_rust_bridge_codegen generate` |
820 | `flutter run` fails, missing Runner.xcodeproj | `flutter create` not run | Run `flutter create . --platforms=ios ...` in example dir |
821 +| `dlopen(onde_inference_dart.framework/...): no such file` | FRB default loader looks for wrong framework | Use `ExternalLibrary.open('onde_inference.framework/onde_inference')` in `init()` — see Dart Public API Pattern above |
822 +| `Failed to lookup symbol 'frb_get_rust_content_hash': dlsym(RTLD_DEFAULT, ...)` | Used `DynamicLibrary.process()` but symbols are in pod framework, not main executable | Switch from `ExternalLibrary.process()` to `ExternalLibrary.open('onde_inference.framework/onde_inference')` |
823 +| `ld: symbol(s) not found ... _kSCNetworkReachability...` | Missing `SystemConfiguration` framework in podspec | Add `s.frameworks = 'SystemConfiguration'` to macOS podspec |
824 +| `ld: error: unknown argument '-Xlinker'` / `-dynamiclib` / `-filelist` | Android NDK's `ld` (LLD) is on PATH — Xcode picks it up instead of Apple's linker | Remove `export LD=$TOOLCHAIN/bin/ld` and other NDK tool exports from `~/.zshrc` (see Xcode 26 section) |
825 +| `clang: error: invalid linker name in argument '-fuse-ld=classic'` | `ALTERNATE_LINKER = classic` set but clang doesn't support it | Remove `ALTERNATE_LINKER`; was only needed to work around the NDK-on-PATH issue — once that's fixed, Apple's ld-prime works fine |
826 +| `EntityTooLarge` on `dart pub publish` | Package > 100 MB (rust/target/ included) | Create `.pubignore` excluding `rust/target/`, `**/*.a`, `**/*.so`, etc. |
827 +
828 +## Xcode 26 Compatibility
829 +
830 +### The NDK-on-PATH Trap
831 +
832 +If your `~/.zshrc` (or similar) has **global** Android NDK toolchain exports:
833 +
834 +```bash
835 +# DANGEROUS — breaks ALL non-Android builds (Xcode, Rust for Apple, etc.)
836 +export TOOLCHAIN=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64
837 +export LD=$TOOLCHAIN/bin/ld # ← THIS HIJACKS XCODE'S LINKER
838 +export AR=$TOOLCHAIN/bin/llvm-ar
839 +export CXX=$TOOLCHAIN/bin/clang++
840 +export RANLIB=$TOOLCHAIN/bin/llvm-ranlib
841 +export STRIP=$TOOLCHAIN/bin/llvm-strip
842 +```
843 +
844 +Xcode reads `$LD` and invokes the Android NDK's LLD instead of Apple's linker.
845 +The errors look like Xcode 26 ld-prime incompatibility (`unknown argument '-Xlinker'`)
846 +but the root cause is the **wrong linker binary**.
847 +
848 +**Fix:** Comment out all NDK tool exports. Keep only the env vars:
849 +
850 +```bash
851 +export NDK_HOME="$ANDROID_HOME/ndk/29.0.14206865"
852 +export ANDROID_NDK="$NDK_HOME"
853 +export ANDROID_NDK_HOME="$NDK_HOME"
854 +# DO NOT export LD, AR, CXX, RANLIB, STRIP globally.
855 +# Cargo uses rust/.cargo/config.toml for Android linker selection.
856 +```
857 +
858 +### Does Xcode 26's ld-prime work with Flutter?
859 +
860 +**Yes** — once the NDK tool exports are removed, Apple's ld-prime handles all
861 +CocoaPods/Flutter linker flags correctly. No `ALTERNATE_LINKER = classic` or
862 +`-ld_classic` workaround is needed for macOS builds. The `-ld_classic`
863 +deprecation warning from iOS Podfiles is harmless.
864 +
865 +### iOS 26 Debug Mode Crash
866 +
867 +Flutter debug builds crash on iOS 26 beta with
868 +`NOTIFY_DEBUGGER_ABOUT_RX_PAGES was not intercepted as expected`
869 +([flutter#170416](https://github.com/flutter/flutter/issues/170416)).
870 +This is a Dart VM JIT issue — **not fixable in the SDK**.
871 +
872 +| Mode | iOS 26 Physical | iOS 26 Simulator | iOS 18.x |
873 +|------|----------------|-----------------|----------|
874 +| `--debug` | ❌ Dart VM crash | ❌ Dart VM crash | ✅ Works |
875 +| `--profile` | ✅ Works | ❌ Not supported on sim | ✅ Works |
876 +| `--release` | ⚠️ Use `flutter build ipa` | ❌ Not supported on sim | ✅ Works |
877 +
878 +**Workaround:** Use an iOS 18.x simulator runtime (download via Xcode Settings → Platforms).
879
880 ---
881
@@ -656,35 +890,58 @@ flutter_rust_bridge.yaml
890 ├── rust_input: "crate::api" → rust/src/api.rs (source of truth)
891 └── dart_output: "lib/src/frb_generated.dart" (codegen writes here)
892
893 +rust/Cargo.toml
894 + ├── crate-type = ["cdylib", "staticlib"]
895 + ├── flutter_rust_bridge = "=2.12.0" (exact pin!)
896 + └── onde = { path = "../../../" } (the main onde crate)
897 +
898 +rust/.cargo/config.toml
899 + └── Android NDK linker overrides (aarch64, armv7, x86_64, i686)
900 +
901 rust/src/api.rs
902 ├── imports onde::{ChatEngine, ...} (path dep → ../../..)
903 ├── declares mirror types (ChatRole, ChatMessage, ...)
904 ├── declares OndeChatEngine (wraps ChatEngine)
905 └── declares free functions (default_model_config, ...)
906
665 -lib/src/frb_generated.dart ← GENERATED (overwritten by codegen)
666 - ├── pre-codegen: re-exports frb_generated_stub.dart
667 - └── post-codegen: full dart:ffi implementation
668 -
669 -lib/src/frb_generated_stub.dart
670 - ├── class RustLib (init no-op)
671 - ├── class OndeChatEngine (all methods throw UnimplementedError)
672 - └── free functions (return real hardcoded config/sampling values)
907 +lib/src/frb_generated.dart/ ← DIRECTORY (not a file!)
908 + ├── api.dart ← generated API bindings
909 + ├── api.freezed.dart ← freezed union types (OndeError)
910 + ├── frb_generated.dart ← RustLib class, ExternalLibraryLoaderConfig
911 + └── frb_generated.io.dart ← RustLibWire, platform FFI glue
912
913 lib/src/types.dart ← hand-written, FRB-independent
914 └── ChatRole, ChatMessage, SamplingConfig, GgufModelConfig,
915 InferenceResult, StreamChunk, EngineStatus, EngineInfo, OndeException
916
678 -lib/src/engine.dart ← hand-written, imports frb_generated.dart + types.dart
679 - ├── class OndeChatEngine (wraps frb.OndeChatEngine)
680 - └── abstract class OndeInference (static helpers)
917 +lib/src/engine.dart ← hand-written, imports frb_generated.dart/ + types.dart
918 + ├── extension OndeChatEngineX (loadDefaultModel, clearHistoryCount)
919 + └── abstract class OndeInference
920 + ├── init() → ExternalLibrary.open('onde_inference.framework/...') on Apple
921 + ├── defaultModelConfig(), qwen2515bConfig(), qwen253bConfig(), ...
922 + └── defaultSamplingConfig(), deterministicSamplingConfig(), mobileSamplingConfig()
923
682 -lib/<package>.dart (barrel)
924 +lib/onde_inference.dart (barrel)
925 ├── export 'src/types.dart'
684 - ├── export 'src/engine.dart'
685 - └── export 'src/frb_generated.dart' show RustLib
926 + └── export 'src/engine.dart'
927 +
928 +macos/
929 + ├── Classes/OndeInferencePlugin.swift ← dummy (required for CocoaPods framework)
930 + └── onde_inference.podspec ← script_phase + -force_load + SystemConfiguration
931 +
932 +ios/
933 + ├── Classes/OndeInferencePlugin.swift ← dummy (required for CocoaPods framework)
934 + └── onde_inference.podspec ← script_phase + -force_load
935 +
936 +android/
937 + ├── CMakeLists.txt ← invokes cargo build per ABI
938 + └── build.gradle ← externalNativeBuild → cmake
939 +
940 +.pubignore ← excludes rust/target/, *.a, *.so, etc.
941 +.gitignore ← excludes same + build artifacts
942 ```
943
944 ---
945
690 -*Last updated: April 2025 — based on implementing `onde_inference 0.1.0`.*
\ No newline at end of file
946 +*Last updated: July 2025 — Xcode 26 compatibility, ExternalLibrary.open pattern,
947 +SystemConfiguration framework, .pubignore for pub.dev, NDK PATH pitfall.*
\ No newline at end of file
.agents/skills/sdk-regressions/SKILL.md new
+270
@@ -0,0 +1,270 @@
1 +---
2 +name: sdk-regressions
3 +description: Cross-compilation regression checks for the onde crate. Covers macOS, iOS, tvOS (nightly), Android, Windows, and Linux target verification. Run before merging any change that touches platform-gated code, Cargo.toml, .cargo/config.toml, or build.rs.
4 +allowed-tools: Read, Edit, Glob, Grep, Terminal
5 +user-invocable: true
6 +---
7 +
8 +# SKILL: SDK Cross-Compilation Regression Checks
9 +
10 +> Always run these checks before merging changes that touch `onde/src/`,
11 +> `onde/Cargo.toml`, `onde/.cargo/config.toml`, `onde/build.rs`, or any
12 +> platform-gated dependency.
13 +
14 +---
15 +
16 +## Why This Exists
17 +
18 +The `onde` crate compiles for **6 target OSes** with heavy `#[cfg(target_os)]`
19 +branching (106+ occurrences in `engine.rs` alone). A change that compiles on
20 +macOS can silently break iOS, tvOS, or Android because:
21 +
22 +- Different `mistralrs` feature sets per platform (`["metal"]` vs `[]`)
23 +- Platform-conditional dependencies (`hf-hub` only on Android)
24 +- Tier-3 targets (tvOS) require nightly + `-Z build-std`
25 +- Assembly stubs (`tvos_chkstk.s`) and `build.rs` gating
26 +- `.cargo/config.toml` sets `+fp16` rustflags on 5 Apple targets
27 +
28 +---
29 +
30 +## Tier 1 — Must Pass on Every PR (stable toolchain)
31 +
32 +These use the default stable toolchain and must always succeed:
33 +
34 +```bash
35 +# macOS — host platform, Metal backend
36 +cargo check
37 +
38 +# iOS device — Metal backend, +fp16
39 +cargo check --target aarch64-apple-ios
40 +
41 +# iOS simulator — Metal backend, +fp16
42 +cargo check --target aarch64-apple-ios-sim
43 +```
44 +
45 +### Quick one-liner
46 +
47 +```bash
48 +cargo check && \
49 +cargo check --target aarch64-apple-ios && \
50 +cargo check --target aarch64-apple-ios-sim
51 +```
52 +
53 +---
54 +
55 +## Tier 2 — tvOS (nightly toolchain, `-Z build-std`)
56 +
57 +tvOS targets are tier-3 in Rust — they are **not** in `rustup target list` and
58 +require nightly with `-Z build-std` and the `rust-src` component.
59 +
60 +```bash
61 +# tvOS device
62 +cargo +nightly check -Z build-std --target aarch64-apple-tvos
63 +
64 +# tvOS simulator
65 +cargo +nightly check -Z build-std --target aarch64-apple-tvos-sim
66 +```
67 +
68 +### tvOS-specific regression risks
69 +
70 +| Risk | What breaks | How to detect |
71 +|------|-------------|---------------|
72 +| `scripts/tvos_chkstk.s` deleted | `Undefined symbol: ___chkstk_darwin` at link time | `test -f scripts/tvos_chkstk.s` |
73 +| `cc` build-dep removed from `Cargo.toml` | `build.rs` can't compile the assembly stub | `grep 'cc' Cargo.toml` |
74 +| `build.rs` tvOS block removed | Same linker error | `grep 'tvos' build.rs` |
75 +| nightly `rust-src` not installed | `-Z build-std` fails immediately | `rustup +nightly component list --installed \| grep rust-src` |
76 +| `.cargo/config.toml` `+fp16` removed | Silent precision loss or candle kernel panics | `grep 'fp16' .cargo/config.toml` |
77 +
78 +---
79 +
80 +## Tier 3 — Android, Windows, Linux (stable, may need cross-linker)
81 +
82 +These check that CPU-only code paths compile. They may fail on linker resolution
83 +if cross-linkers aren't installed, but `cargo check` (no linking) should pass:
84 +
85 +```bash
86 +# Android arm64 — CPU, requires hf-hub sandbox workaround
87 +cargo check --target aarch64-linux-android
88 +
89 +# Windows — CPU
90 +cargo check --target x86_64-pc-windows-msvc
91 +
92 +# Linux — CPU
93 +cargo check --target x86_64-unknown-linux-gnu
94 +```
95 +
96 +---
97 +
98 +## Full Regression Suite (copy-paste)
99 +
100 +```bash
101 +#!/usr/bin/env bash
102 +set -euo pipefail
103 +
104 +echo "═══ Tier 1: macOS + iOS (stable) ═══"
105 +cargo check
106 +cargo check --target aarch64-apple-ios
107 +cargo check --target aarch64-apple-ios-sim
108 +
109 +echo "═══ Tier 2: tvOS (nightly + build-std) ═══"
110 +cargo +nightly check -Z build-std --target aarch64-apple-tvos
111 +cargo +nightly check -Z build-std --target aarch64-apple-tvos-sim
112 +
113 +echo "═══ Tier 3: Android / Windows / Linux ═══"
114 +cargo check --target aarch64-linux-android
115 +cargo check --target x86_64-pc-windows-msvc
116 +cargo check --target x86_64-unknown-linux-gnu
117 +
118 +echo "✅ All cross-compilation checks passed"
119 +```
120 +
121 +---
122 +
123 +## Required Rustup Targets
124 +
125 +Install these once on a development machine:
126 +
127 +```bash
128 +# Tier 1 (stable)
129 +rustup target add aarch64-apple-darwin x86_64-apple-darwin
130 +rustup target add aarch64-apple-ios aarch64-apple-ios-sim
131 +
132 +# Tier 3 (stable)
133 +rustup target add aarch64-linux-android armv7-linux-androideabi
134 +rustup target add x86_64-linux-android i686-linux-android
135 +rustup target add x86_64-pc-windows-msvc
136 +rustup target add x86_64-unknown-linux-gnu
137 +
138 +# Tier 2 — tvOS (nightly, no `rustup target add` — uses -Z build-std)
139 +rustup toolchain install nightly
140 +rustup +nightly component add rust-src
141 +```
142 +
143 +---
144 +
145 +## `.cargo/config.toml` — Critical Settings
146 +
147 +These rustflags are set at the workspace level and **must not be removed**:
148 +
149 +```toml
150 +[target.aarch64-apple-ios]
151 +rustflags = ["-C", "target-feature=+fp16"]
152 +
153 +[target.aarch64-apple-ios-sim]
154 +rustflags = ["-C", "target-feature=+fp16"]
155 +
156 +[target.aarch64-apple-tvos]
157 +rustflags = ["-C", "target-feature=+fp16"]
158 +
159 +[target.aarch64-apple-tvos-sim]
160 +rustflags = ["-C", "target-feature=+fp16"]
161 +
162 +[target.x86_64-apple-tvos]
163 +rustflags = ["-C", "target-feature=+fp16"]
164 +```
165 +
166 +Removing `+fp16` causes silent precision degradation or runtime panics in
167 +candle's Metal/NEON kernels.
168 +
169 +---
170 +
171 +## Platform Dependency Matrix
172 +
173 +| `target_os` | `mistralrs` features | Extra deps | GPU | Default model |
174 +|-------------|---------------------|------------|-----|---------------|
175 +| `macos` | `["metal"]` | — | Metal | Qwen 2.5 3B |
176 +| `ios` | `["metal"]` | — | Metal | Qwen 2.5 1.5B |
177 +| `tvos` | `["metal"]` | — | Metal | Qwen 2.5 1.5B |
178 +| `android` | `[]` | `hf-hub`, `mistralrs-core` | CPU | Qwen 2.5 1.5B |
179 +| `windows` | `[]` | — | CPU | Qwen 2.5 3B |
180 +| `linux` | `[]` | — | CPU | Qwen 2.5 3B |
181 +
182 +---
183 +
184 +## Common Regression Patterns
185 +
186 +### 1. New model added but not to all 6 platform `cfg` blocks
187 +
188 +`engine.rs` has separate `#[cfg(target_os = "...")]` blocks for model builder
189 +logic. A new model constructor added only under `macos` will fail on `ios`.
190 +
191 +**Detection:** Tier 1 checks catch iOS; Tier 2 catches tvOS; Tier 3 catches
192 +Android/Windows/Linux.
193 +
194 +### 2. Dependency added without platform gating
195 +
196 +Adding a dep that uses `SystemConfiguration.framework` (e.g., `hyper_util`
197 +proxy detection) works on macOS but causes linker errors on iOS/tvOS unless the
198 +podspec declares `s.frameworks = 'SystemConfiguration'`.
199 +
200 +**Detection:** Tier 1 `cargo check` passes (no linking), but `flutter build ios`
201 +fails. Must also test a full `flutter build` periodically.
202 +
203 +### 3. Fork branch divergence
204 +
205 +All `mistralrs` deps point to a fork:
206 +`git = "https://github.com/setoelkahfi/mistral.rs"`, branch
207 +`fix/all-platform-fixes`. If that branch is rebased, deleted, or diverges from
208 +upstream, **all** platforms break.
209 +
210 +**Detection:** Any `cargo check` fails with "could not find branch".
211 +
212 +### 4. UniFFI version mismatch
213 +
214 +`uniffi = "=0.31.0"` must be pinned identically in three places:
215 +- `onde/Cargo.toml` dependencies
216 +- `onde/Cargo.toml` build-dependencies
217 +- `onde/uniffi-bindgen/Cargo.toml`
218 +
219 +Mixing versions causes bindgen panics at codegen time.
220 +
221 +**Detection:** `cargo build` in `uniffi-bindgen/` or `build.rs` panics.
222 +
223 +### 5. Android `home_dir()` sandbox panic
224 +
225 +`dirs::home_dir()` panics in the Android sandbox. The `hf-hub` dep is added
226 +explicitly on Android so `HF_HOME` can be set programmatically. Never call
227 +`dirs::home_dir()` or `home::home_dir()` in code paths reachable on Android.
228 +
229 +**Detection:** Tier 3 `cargo check --target aarch64-linux-android` catches
230 +compile-time issues; runtime panics need an Android emulator test.
231 +
232 +---
233 +
234 +## Dart SDK Cross-Build Checks
235 +
236 +When changes touch `sdk/dart/`, also verify the Flutter plugin builds:
237 +
238 +```bash
239 +cd sdk/dart/example
240 +
241 +# macOS (requires Xcode)
242 +flutter build macos --debug
243 +
244 +# iOS (requires Xcode + iOS SDK)
245 +flutter build ios --debug --no-codesign
246 +```
247 +
248 +### Known environment pitfall
249 +
250 +**Never export Android NDK toolchain binaries globally in `.zshrc`:**
251 +
252 +```bash
253 +# ❌ BREAKS Xcode builds — NDK's ld/ar/strip shadow Apple's tools
254 +export LD=$TOOLCHAIN/bin/ld
255 +export AR=$TOOLCHAIN/bin/llvm-ar
256 +
257 +# ✅ Safe — env vars only, no PATH/tool overrides
258 +export ANDROID_NDK_HOME="$HOME/Library/Android/sdk/ndk/29.0.14206865"
259 +```
260 +
261 +If Xcode invokes the NDK's `ld` instead of Apple's, you get:
262 +`ld: error: unknown argument '-Xlinker'`
263 +
264 +The fix: comment out the tool exports, use Cargo's
265 +`sdk/dart/rust/.cargo/config.toml` for Android linker configuration instead.
266 +
267 +---
268 +
269 +*Last updated: July 2025 — based on onde 0.2.x with Xcode 26, Rust 1.92–1.94,
270 +nightly for tvOS.*
\ No newline at end of file
.gitignore
+3
@@ -26,3 +26,6 @@ sdk/python/python/onde_inference/*.pyd
26 sdk/python/**/__pycache__/
27 sdk/python/**/*.egg-info/
28 sdk/python/.pytest_cache/
29 +
30 +# Zed settings
31 +.zed
sdk/dart/example/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java new
+19
@@ -0,0 +1,19 @@
1 +package io.flutter.plugins;
2 +
3 +import androidx.annotation.Keep;
4 +import androidx.annotation.NonNull;
5 +import io.flutter.Log;
6 +
7 +import io.flutter.embedding.engine.FlutterEngine;
8 +
9 +/**
10 + * Generated file. Do not edit.
11 + * This file is generated by the Flutter tool based on the
12 + * plugins that support the Android platform.
13 + */
14 +@Keep
15 +public final class GeneratedPluginRegistrant {
16 + private static final String TAG = "GeneratedPluginRegistrant";
17 + public static void registerWith(@NonNull FlutterEngine flutterEngine) {
18 + }
19 +}
sdk/dart/example/android/local.properties new
+2
@@ -0,0 +1,2 @@
1 +sdk.dir=/Users/setoelkahfi/Library/Android/sdk
2 +flutter.sdk=/Users/setoelkahfi/Develop/flutter
\ No newline at end of file
sdk/dart/example/macos/Podfile.lock
+1 -1
@@ -15,7 +15,7 @@ EXTERNAL SOURCES:
15
16 SPEC CHECKSUMS:
17 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
18 - onde_inference: 5973955f39e2df79843e047d9564a048950a6533
18 + onde_inference: c791e4b4f9e94363844a1bb3162334d9c5e34fcf
19
20 PODFILE CHECKSUM: 54d867c82ac51cbd61b565781b9fada492027009
21
sdk/dart/example/macos/Runner.xcodeproj/project.pbxproj
+21 -3
@@ -240,6 +240,7 @@
240 33CC10EB2044A3C60003C045 /* Resources */,
241 33CC110E2044A8840003C045 /* Bundle Framework */,
242 3399D490228B24CF009A79C7 /* ShellScript */,
243 + 6A28882B1B88ED036724CECC /* [CP] Embed Pods Frameworks */,
244 );
245 buildRules = (
246 );
@@ -382,6 +383,23 @@
383 shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
384 showEnvVarsInLog = 0;
385 };
386 + 6A28882B1B88ED036724CECC /* [CP] Embed Pods Frameworks */ = {
387 + isa = PBXShellScriptBuildPhase;
388 + buildActionMask = 2147483647;
389 + files = (
390 + );
391 + inputFileListPaths = (
392 + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
393 + );
394 + name = "[CP] Embed Pods Frameworks";
395 + outputFileListPaths = (
396 + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
397 + );
398 + runOnlyForDeploymentPostprocessing = 0;
399 + shellPath = /bin/sh;
400 + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
401 + showEnvVarsInLog = 0;
402 + };
403 A0C24FC16955FA8CB4B2F299 /* [CP] Check Pods Manifest.lock */ = {
404 isa = PBXShellScriptBuildPhase;
405 buildActionMask = 2147483647;
@@ -540,8 +558,8 @@
558 GCC_WARN_UNUSED_FUNCTION = YES;
559 GCC_WARN_UNUSED_VARIABLE = YES;
560 MACOSX_DEPLOYMENT_TARGET = 10.15;
543 - OTHER_LDFLAGS = "-ld_classic";
561 MTL_ENABLE_DEBUG_INFO = NO;
562 + OTHER_LDFLAGS = "-ld_classic";
563 SDKROOT = macosx;
564 SWIFT_COMPILATION_MODE = wholemodule;
565 SWIFT_OPTIMIZATION_LEVEL = "-O";
@@ -623,9 +641,9 @@
641 GCC_WARN_UNUSED_FUNCTION = YES;
642 GCC_WARN_UNUSED_VARIABLE = YES;
643 MACOSX_DEPLOYMENT_TARGET = 10.15;
626 - OTHER_LDFLAGS = "-ld_classic";
644 MTL_ENABLE_DEBUG_INFO = YES;
645 ONLY_ACTIVE_ARCH = YES;
646 + OTHER_LDFLAGS = "-ld_classic";
647 SDKROOT = macosx;
648 SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
649 SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -674,8 +692,8 @@
692 GCC_WARN_UNUSED_FUNCTION = YES;
693 GCC_WARN_UNUSED_VARIABLE = YES;
694 MACOSX_DEPLOYMENT_TARGET = 10.15;
677 - OTHER_LDFLAGS = "-ld_classic";
695 MTL_ENABLE_DEBUG_INFO = NO;
696 + OTHER_LDFLAGS = "-ld_classic";
697 SDKROOT = macosx;
698 SWIFT_COMPILATION_MODE = wholemodule;
699 SWIFT_OPTIMIZATION_LEVEL = "-O";
sdk/dart/ios/Classes/OndeInferencePlugin.swift new
+14
@@ -0,0 +1,14 @@
1 +// This file exists so CocoaPods creates a real framework target for the
2 +// onde_inference pod. The actual native code is the Rust static library
3 +// (libonde_inference_dart.a) which is force-loaded into this framework
4 +// via OTHER_LDFLAGS in the podspec.
5 +//
6 +// Do not add any logic here.
7 +
8 +import Flutter
9 +
10 +public class OndeInferencePlugin: NSObject, FlutterPlugin {
11 + public static func register(with registrar: FlutterPluginRegistrar) {
12 + // No-op — all FFI symbols come from the Rust static library.
13 + }
14 +}
sdk/dart/lib/src/engine.dart
+22 -1
@@ -1,6 +1,10 @@
1 // ignore_for_file: prefer_const_constructors
2
3 import 'dart:async';
4 +import 'dart:io' show Platform;
5 +
6 +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'
7 + show ExternalLibrary;
8
9 import 'frb_generated.dart/frb_generated.dart' as frb;
10 import 'frb_generated.dart/api.dart' as api;
@@ -130,7 +134,24 @@ abstract final class OndeInference {
134 ///
135 /// Call this in `main()` or in a Flutter `initState` override before any
136 /// user interaction that could trigger model loading.
133 - static Future<void> init() => frb.RustLib.init();
137 + ///
138 + /// On macOS and iOS the Rust static library is force-loaded into the
139 + /// application process by the CocoaPods podspec (`-force_load`), so the
140 + /// symbols are available via [ffi.DynamicLibrary.process] rather than a
141 + /// separate `.framework` bundle.
142 + static Future<void> init() async {
143 + ExternalLibrary? lib;
144 + if (Platform.isMacOS || Platform.isIOS) {
145 + // The Rust static library is force-loaded into the onde_inference
146 + // CocoaPods framework via OTHER_LDFLAGS in the podspec. We must open
147 + // that framework explicitly — DynamicLibrary.process() (RTLD_DEFAULT)
148 + // cannot resolve symbols inside a separately-loaded framework.
149 + lib = ExternalLibrary.open(
150 + 'onde_inference.framework/onde_inference',
151 + );
152 + }
153 + await frb.RustLib.init(externalLibrary: lib);
154 + }
155
156 // -------------------------------------------------------------------------
157 // Model config factories
sdk/dart/macos/Classes/OndeInferencePlugin.swift new
+14
@@ -0,0 +1,14 @@
1 +// This file exists so CocoaPods creates a real framework target for the
2 +// onde_inference pod. The actual native code is the Rust static library
3 +// (libonde_inference_dart.a) which is force-loaded into this framework
4 +// via OTHER_LDFLAGS in the podspec.
5 +//
6 +// Do not add any logic here.
7 +
8 +import FlutterMacOS
9 +
10 +public class OndeInferencePlugin: NSObject, FlutterPlugin {
11 + public static func register(with registrar: FlutterPluginRegistrar) {
12 + // No-op — all FFI symbols come from the Rust static library.
13 + }
14 +}
sdk/dart/macos/onde_inference.podspec
+2
@@ -35,6 +35,8 @@ Pod::Spec.new do |s|
35 }
36 ]
37
38 + s.frameworks = 'SystemConfiguration'
39 +
40 s.pod_target_xcconfig = {
41 'DEFINES_MODULE' => 'YES',
42 'OTHER_LDFLAGS' => '-force_load ${PODS_TARGET_SRCROOT}/libonde_inference_dart.a',