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