| name | sdk-dart |
| 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. |
| allowed-tools | Read, Write, Edit, Glob, Grep |
| user-invocable | false |
SKILL: Dart / Flutter SDK with flutter_rust_bridge v2
Captured from: building onde_inference pub.dev package wrapping the onde Rust crate.
Agent reference for future Dart SDK tasks in this repo.
What This Skill Covers
End-to-end creation of a pub.dev-ready Flutter FFI plugin package that bridges to a
Rust library using flutter_rust_bridge v2. Covers the full lifecycle:
design → Rust bridge crate → Dart library → platform build files → tests →
pub.dev dry-run → codegen → flutter run.
Hard-Won Lessons (Read First)
These are non-obvious facts that caused real failures and are not in the FRB docs.
1. flutter_rust_bridge_codegen is a Rust binary — NOT a Dart package
# Correct — install the Rust binary once:
cargo install flutter_rust_bridge_codegen
# WRONG — this package does not exist on pub.dev:
# dev_dependencies:
# flutter_rust_bridge_codegen: ^2.12.0 ← will fail `dart pub get`
Never add it to pubspec.yaml dev_dependencies.
2. flutter_rust_bridge.yaml — use module path syntax, not file paths
rust_input: "crate::api"
rust_root: "rust"
3. Delete frb_generated.rs before running codegen — never pre-create it
FRB codegen uses an exclusive create for rust/src/frb_generated.rs.
If the file exists (even as a stub), codegen aborts with File exists (os error 17).
rm -f rust/src/frb_generated.rs
4. cargo-expand is required by FRB codegen — auto-installs but takes time
FRB codegen runs cargo expand internally to parse macro-expanded Rust.
If missing, it installs automatically but adds ~2 min to the first run.
Pre-install it:
cargo install cargo-expand
5. Example apps need flutter create . — a pubspec.yaml alone is not enough
A Flutter project requires platform scaffolding (ios/, android/, macos/,
Runner.xcodeproj, etc.) that only flutter create generates.
cd sdk/dart/example
flutter create . \
--project-name onde_inference_example \
--platforms=ios,macos,android,linux,windows \
--org com.ondeinference
Run this before flutter pub get or flutter run.
6. Types with #[uniffi] cannot be re-used directly in FRB
If the source Rust crate annotates types with #[uniffi::Record], #[uniffi::Enum],
etc., those attributes conflict with FRB code generation. The fix is type mirroring:
re-declare every bridged type as a plain struct/enum in the bridge crate (rust/src/api.rs)
and implement bidirectional From<> conversions.
#[derive(Debug, Clone)]
pub struct ChatMessage { pub role: ChatRole, pub content: String }
impl From<OndeChatMessage> for ChatMessage { ... }
impl From<ChatMessage> for OndeChatMessage { ... }
7. StreamSink<T> requires T: SseEncode — satisfied by codegen, not by hand
The StreamSink type (used for streaming) demands T: SseEncode. The real impl
is generated by codegen. The pre-codegen stub in frb_generated.rs must provide
a no-op impl to let cargo check pass:
use flutter_rust_bridge::for_generated::SseCodec;
flutter_rust_bridge::frb_generated_boilerplate!(
default_stream_sink_codec = SseCodec,
default_rust_opaque = RustOpaqueMoi,
default_rust_auto_opaque = RustAutoOpaqueMoi,
);
impl SseEncode for crate::api::StreamChunk {
fn sse_encode(self, _s: &mut flutter_rust_bridge::for_generated::SseSerializer) {
unimplemented!("codegen stub")
}
}
8. Dart lint: no leading underscore for library import prefixes
// BAD — triggers `no_leading_underscores_for_library_prefixes` lint:
import 'frb_generated.dart' as _frb;
// GOOD:
import 'frb_generated.dart' as frb;
9. Dart lint: remove library <name>; directive
// BAD — triggers `unnecessary_library_name` lint:
library onde_inference;
// GOOD:
library;
10. Exclude FRB-generated files from Dart analysis
analyzer:
exclude:
- lib/src/frb_generated.dart
- lib/src/frb_generated.io.dart
- lib/src/frb_generated.web.dart
11. Flutter version is a single source of truth — never hardcode it in CI
The CI workflow reads the Flutter version from sdk/dart/.flutter-version via
subosito/flutter-action's flutter-version-file: parameter. Never
hardcode flutter-version: "x.y.z" directly in the workflow YAML.
sdk/dart/.flutter-version ← only place the Flutter version is written
- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
flutter-version-file: sdk/dart/.flutter-version
channel: stable
cache: true
Why this matters: Each Flutter release ships a specific Dart SDK version.
Dev dependencies like freezed have minimum Dart SDK requirements that advance
faster than Flutter stable releases. When you bump a Flutter-sensitive dep,
you must also bump the Flutter version used in CI — and .flutter-version is
the one place to do that.
Rule: when bumping a dep that constrains the Dart SDK (e.g. freezed,
flutter_rust_bridge, build_runner):
- Run
flutter pub get locally — a version-solving failure tells you
immediately if the Dart SDK shipped with the current Flutter is too old.
- Find the Flutter release that ships the required Dart SDK version
(see https://docs.flutter.dev/release/archive).
- Update
sdk/dart/.flutter-version to that version.
- Tighten
pubspec.yaml's environment.sdk lower bound to match — it must
honestly reflect what your deps need, not what you originally wrote.
pubspec.yaml environment constraint must be accurate:
environment:
sdk: ">=3.3.0 <4.0.0"
environment:
sdk: ">=3.8.0 <4.0.0"
flutter: ">=3.32.0"
An inaccurate sdk: lower bound is invisible locally (you're already on a
newer SDK) but breaks any runner or user on the stated minimum.
Package Structure Reference
sdk/dart/
├── pubspec.yaml # Flutter plugin, ffiPlugin:true per platform
├── analysis_options.yaml # Exclude frb_generated files
├── flutter_rust_bridge.yaml # Codegen config (rust_input: "crate::api")
├── CHANGELOG.md
├── LICENSE
├── README.md # pub.dev landing page
│
├── rust/ # Rust bridge crate
│ ├── Cargo.toml # crate-type=[cdylib,staticlib]; publish=false
│ └── src/
│ ├── lib.rs # mod frb_generated; pub mod api; + #[frb(init)]
│ ├── api.rs # Mirror types + OndeChatEngine + free fns
│ └── frb_generated.rs # DELETE before codegen; stub only pre-codegen
│
├── lib/
│ ├── <package_name>.dart # Public barrel — exports types + engine + RustLib
│ └── src/
│ ├── types.dart # Standalone Dart types — NO native dependency
│ ├── engine.dart # OndeChatEngine wrapper + static helpers
│ ├── frb_generated.dart # CAN be stub pre-codegen; overwritten by codegen
│ └── frb_generated_stub.dart # Hand-written stub: RustLib + OndeChatEngine
│
├── android/
│ ├── build.gradle # ffiPlugin CMake integration
│ └── CMakeLists.txt # cargo build per Android ABI
├── ios/
│ └── <package>.podspec # script_phase: cargo build aarch64-apple-ios[-sim]
├── macos/
│ └── <package>.podspec # script_phase: cargo build aarch64-apple-darwin
├── linux/
│ ├── CMakeLists.txt
│ └── <package>_plugin.cc
├── windows/
│ ├── CMakeLists.txt
│ └── <package>_plugin.cpp
│
├── test/
│ └── dart_test.dart # 22+ unit tests — all pass WITHOUT native binary
└── example/
├── pubspec.yaml
└── lib/main.dart # Full Material 3 demo app
pubspec.yaml Template
name: <package_name>
description: >-
One-liner for pub.dev search (80 chars max for score).
version: 0.1.0
repository: https://github.com/ondeinference/onde
homepage: https://ondeinference.com
issue_tracker: https://github.com/ondeinference/onde/issues
environment:
sdk: '>=3.8.0 <4.0.0'
flutter: '>=3.32.0'
dependencies:
flutter:
sdk: flutter
flutter_rust_bridge: ^2.12.0
dev_dependencies:
flutter_test:
sdk: flutter
test: ^1.25.0
flutter_lints: ^5.0.0
flutter:
plugin:
platforms:
android:
ffiPlugin: true
ios:
ffiPlugin: true
macos:
ffiPlugin: true
linux:
ffiPlugin: true
windows:
ffiPlugin: true
flutter_rust_bridge.yaml Template
rust_root: "rust"
rust_input: "crate::api"
dart_output: "lib/src/frb_generated.dart"
dart_entrypoint_class_name: RustLib
dart_preamble: |
// ignore_for_file: type=lint, unused_import, unnecessary_import
web: false
Rust Bridge Crate (rust/Cargo.toml) Template
[package]
name = "<package>_dart"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
crate-type = ["cdylib", "staticlib"]
[dependencies]
flutter_rust_bridge = "=2.12.0"
<source_crate> = { path = "../../.." }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
thiserror = "2"
log = "0.4"
Rust src/lib.rs Template
mod frb_generated;
pub mod api;
#[flutter_rust_bridge::frb(init)]
pub fn init_app() {
flutter_rust_bridge::setup_default_user_utils();
}
Type Mirroring Pattern in src/api.rs
Use this pattern for every Rust type that crosses the bridge.
use flutter_rust_bridge::frb;
use <source_crate>::inference::{
ChatMessage as SrcChatMessage,
ChatRole as SrcChatRole,
};
#[derive(Debug, Clone, PartialEq)]
pub enum ChatRole { System, User, Assistant }
impl From<SrcChatRole> for ChatRole { ... }
impl From<ChatRole> for SrcChatRole { ... }
pub struct OndeChatEngine { inner: <source_crate>::inference::ChatEngine }
impl OndeChatEngine {
#[frb(sync)]
pub fn new() -> OndeChatEngine { OndeChatEngine { inner: ChatEngine::new() } }
pub async fn send_message(&self, message: String) -> Result<InferenceResult, OndeError> {
self.inner.send_message(message).await
.map(InferenceResult::from)
.map_err(OndeError::from)
}
pub async fn stream_message(
&self,
message: String,
sink: StreamSink<StreamChunk>,
) -> Result<(), OndeError> {
let mut rx = self.inner.stream_message(message).await.map_err(OndeError::from)?;
while let Some(chunk) = rx.recv().await {
let done = chunk.done;
sink.add(StreamChunk::from(chunk));
if done { break; }
}
Ok(())
}
}
#[frb(sync)]
pub fn default_model_config() -> GgufModelConfig {
<source_crate>::inference::GgufModelConfig::platform_default().into()
}
Dart Stub Pattern (lib/src/frb_generated_stub.dart)
The stub lets the package compile and all pure-Dart unit tests pass before
the native binary exists. Key rules:
- Import only
types.dart — never dart:ffi or anything platform-native.
RustLib.init() sets a bool flag; new_() and every inference method throw
UnimplementedError.
- Config/sampling free functions return real hardcoded values (matching the
Rust constants) so model-selection UIs can be built and tested immediately.
- Make every method signature match exactly what FRB will generate (same names,
same parameter types, same return types) — this makes the post-codegen
transition a drop-in replacement.
// frb_generated.dart (stub redirect)
export 'frb_generated_stub.dart';
// frb_generated_stub.dart
import 'types.dart';
class RustLib {
static bool _initialized = false;
static bool get isInitialized => _initialized;
static Future<void> init() async { _initialized = true; }
}
class OndeChatEngine {
static Future<OndeChatEngine> new_() async =>
throw UnimplementedError('Run flutter_rust_bridge_codegen generate');
Future<double> loadDefaultModel({ String? systemPrompt, SamplingConfig? sampling }) =>
throw UnimplementedError(_msg('loadDefaultModel'));
Stream<StreamChunk> streamMessage({ required String message }) =>
Stream.error(UnimplementedError(_msg('streamMessage')), StackTrace.current);
static String _msg(String m) => 'OndeChatEngine.$m: run codegen first';
}
// Free functions — return real data so UI can be built/tested without native
GgufModelConfig qwen251_5bConfig() => const GgufModelConfig(
modelId: 'bartowski/Qwen2.5-1.5B-Instruct-GGUF',
files: ['Qwen2.5-1.5B-Instruct-Q4_K_M.gguf'],
displayName: 'Qwen 2.5 1.5B',
approxMemory: '~941 MB',
);
Dart Public API Pattern (lib/src/engine.dart)
import 'dart:async';
import 'dart:io' show Platform;
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'
show ExternalLibrary;
import 'frb_generated.dart/frb_generated.dart' as frb;
import 'frb_generated.dart/api.dart' as api;
import 'types.dart';
export 'frb_generated.dart/frb_generated.dart' show RustLib;
export 'frb_generated.dart/api.dart'
show OndeChatEngine, OndeError, OndeError_NoModelLoaded, /* ... */;
/// Extension on the FRB-generated OndeChatEngine opaque type.
extension OndeChatEngineX on api.OndeChatEngine {
Future<double> loadDefaultModel({
String? systemPrompt,
SamplingConfig? sampling,
}) =>
loadGgufModel(
config: api.defaultModelConfig(),
systemPrompt: systemPrompt,
sampling: sampling,
);
Future<int> clearHistoryCount() async => (await clearHistory()).toInt();
}
abstract final class OndeInference {
/// Initialises the Rust shared library.
///
/// On macOS and iOS the Rust static library is force-loaded into the
/// onde_inference CocoaPods framework via OTHER_LDFLAGS in the podspec.
/// We must open that framework explicitly — DynamicLibrary.process()
/// (RTLD_DEFAULT) cannot resolve symbols inside a separately-loaded
/// framework.
static Future<void> init() async {
ExternalLibrary? lib;
if (Platform.isMacOS || Platform.isIOS) {
lib = ExternalLibrary.open(
'onde_inference.framework/onde_inference',
);
}
await frb.RustLib.init(externalLibrary: lib);
}
static GgufModelConfig defaultModelConfig() => api.defaultModelConfig();
static SamplingConfig deterministicSamplingConfig() => api.deterministicSamplingConfig();
}
Why ExternalLibrary.open(...) and not .process()
The -force_load linker flag embeds Rust symbols into the pod framework
(onde_inference.framework/onde_inference), NOT into the main executable.
DynamicLibrary.process() maps to RTLD_DEFAULT, which only searches the
main executable and already-opened images in certain configurations. On
macOS/iOS with CocoaPods use_frameworks!, the pod framework is a separate
Mach-O image — you must dlopen it by name.
FRB 2.12's ExternalLibrary.open(path) calls DynamicLibrary.open(path)
under the hood. On non-Apple platforms (Android, Linux, Windows) the default
ExternalLibraryLoaderConfig stem-based lookup works, so lib stays null
and RustLib.init() handles it automatically.
Do NOT use ExternalLibrary.process(iKnowHowToUseIt: true) — it will
produce Failed to lookup symbol 'frb_get_rust_content_hash': dlsym(RTLD_DEFAULT, ...).
Platform Build Files
Dummy Swift Plugin Classes (required)
Both macos/Classes/OndeInferencePlugin.swift and ios/Classes/OndeInferencePlugin.swift
must exist — they are no-op FlutterPlugin stubs. Without them, CocoaPods
has no source files to compile and won't produce a real .framework target.
The Rust static library is force-loaded INTO this framework.
import FlutterMacOS
public class OndeInferencePlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
}
}
import Flutter
public class OndeInferencePlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {}
}
macOS podspec — full working version
Pod::Spec.new do |s|
s.name = 'onde_inference'
s.version = '0.1.0'
s.summary = 'On-device LLM inference SDK for Flutter (macOS).'
s.description = 'Runs Qwen 2.5 models locally on macOS with Metal acceleration.'
s.homepage = 'https://ondeinference.com'
s.license = { :type => 'MIT', :file => '../LICENSE' }
s.author = { 'Onde Inference' => 'hello@ondeinference.com' }
s.source = { :path => '.' }
s.source_files = 'Classes/**/*'
s.dependency 'FlutterMacOS'
s.platform = :osx, '10.15'
s.swift_version = '5.0'
s.script_phases = [
{
:name => 'Build Rust bridge (onde_inference_dart)',
:script => <<~SHELL,
set -e
RUST_DIR="${PODS_TARGET_SRCROOT}/../rust"
cd "$RUST_DIR"
ARCH=$(uname -m)
if [[ "$ARCH" == "arm64" ]]; then
TARGET="aarch64-apple-darwin"
else
TARGET="x86_64-apple-darwin"
fi
cargo build --release --target "$TARGET"
cp "target/$TARGET/release/libonde_inference_dart.a" \
"${PODS_TARGET_SRCROOT}/libonde_inference_dart.a"
SHELL
:execution_position => :before_compile,
:output_files => ["${PODS_TARGET_SRCROOT}/libonde_inference_dart.a"],
}
]
s.frameworks = 'SystemConfiguration'
s.pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES',
'OTHER_LDFLAGS' => '-force_load ${PODS_TARGET_SRCROOT}/libonde_inference_dart.a',
}
s.preserve_paths = '../rust/**/*'
end
Key points:
s.frameworks = 'SystemConfiguration' — required because the Rust hyper_util
crate (transitive via reqwest) references SCDynamicStoreCopyProxies,
SCNetworkReachabilityCreateWithName, etc. Without it you get
ld: symbol(s) not found for architecture arm64.
-force_load links ALL symbols from the .a into the pod framework.
s.source_files = 'Classes/**/*' picks up the dummy Swift plugin.
iOS podspec — differs from macOS
s.dependency 'Flutter'
s.platform = :ios, '13.0'
s.pod_target_xcconfig = {
'DEFINES_MODULE' => 'YES',
'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386',
'OTHER_LDFLAGS' =>
'-force_load ${PODS_TARGET_SRCROOT}/libonde_inference_dart.a',
}
Android CMakeLists.txt — key section
if(ANDROID_ABI STREQUAL "arm64-v8a")
set(CARGO_TARGET "aarch64-linux-android")
elseif(ANDROID_ABI STREQUAL "armeabi-v7a")
set(CARGO_TARGET "armv7-linux-androideabi")
elseif(ANDROID_ABI STREQUAL "x86_64")
set(CARGO_TARGET "x86_64-linux-android")
elseif(ANDROID_ABI STREQUAL "x86")
set(CARGO_TARGET "i686-linux-android")
endif()
add_custom_command(OUTPUT "${LIB_OUTPUT}"
COMMAND cargo build --release --target ${CARGO_TARGET}
--manifest-path "${RUST_DIR}/Cargo.toml"
WORKING_DIRECTORY ${RUST_DIR})
add_library(onde_inference_dart SHARED IMPORTED GLOBAL)
set_target_properties(onde_inference_dart PROPERTIES IMPORTED_LOCATION ${LIB_OUTPUT})
Android uses CMake to invoke cargo build for the right NDK triple, producing
a .so shared library (unlike Apple which uses .a staticlibs).
Android NDK Linker Config (rust/.cargo/config.toml)
[target.aarch64-linux-android]
linker = "aarch64-linux-android24-clang"
[target.armv7-linux-androideabi]
linker = "armv7a-linux-androideabi24-clang"
[target.x86_64-linux-android]
linker = "x86_64-linux-android24-clang"
[target.i686-linux-android]
linker = "i686-linux-android24-clang"
These short names require the NDK toolchain bin/ to be on PATH.
Flutter's Gradle + CMake integration typically handles this automatically
during flutter build apk / flutter run -d <android>.
Testing Strategy
All pure-Dart unit tests run without building the native binary.
Tests that PASS pre-codegen (using the stub):
✓ All type constructors, equality, hashCode, copyWith
✓ Enum toString and isReady helpers
✓ SamplingConfig presets (default, deterministic, mobile)
✓ GgufModelConfig factory functions (real hardcoded values in stub)
✓ RustLib.init() is idempotent
✓ OndeInference.* static helpers
✓ OndeChatEngine.create() throws UnimplementedError pre-codegen
Tests that require codegen + native binary:
✗ Any actual inference call
✗ loadDefaultModel / sendMessage / streamMessage
Run tests:
cd sdk/dart && dart test test/dart_test.dart
Pub.dev Readiness Checklist
Run this before declaring the SDK ready:
dart analyze lib/
dart test test/dart_test.dart
dart pub publish --dry-run
Common pub.dev score deductions to avoid:
- Missing
repository: in pubspec → loses points
- No
example/ directory → loses points
description: under 60 chars or over 180 chars → loses points
- Library exports not documented → loses points
library <name>; directive → triggers lint
Pub.dev Upload Size (100 MB limit)
dart pub publish bundles everything not excluded by .pubignore (or
.gitignore if .pubignore doesn't exist). The Rust target/ dir alone
can be 7+ GB. A .pubignore file is mandatory.
Files that MUST be included (consumers build from source):
rust/src/, rust/Cargo.toml, rust/Cargo.lock
ios/, macos/, android/, linux/, windows/ (build scripts, podspecs, CMakeLists)
ios/Classes/, macos/Classes/ (dummy plugin Swift files)
lib/ (all Dart code)
Files that MUST be excluded (.pubignore):
rust/target/ — multi-GB build artifacts
**/*.a, **/*.so, **/*.dylib, **/*.dll — precompiled binaries
.dart_tool/, pubspec.lock
example/build/, example/.dart_tool/
logs/, .idea/, .vscode/, .DS_Store
After adding .pubignore, verify: dart pub publish --dry-run should report
~280 KB compressed size, not hundreds of MB.
Codegen Workflow (Full, Correct Order)
rustup target add aarch64-apple-ios aarch64-apple-ios-sim
rustup target add aarch64-apple-darwin x86_64-apple-darwin
cargo install cargo-expand
cargo install flutter_rust_bridge_codegen
cd sdk/dart
rm -f rust/src/frb_generated.rs
flutter_rust_bridge_codegen generate
cd rust && cargo check
cd .. && dart analyze lib/
flutter run on iOS (Full Steps)
cd sdk/dart/example
flutter create . \
--project-name <example_name> \
--platforms=ios,macos \
--org com.<your_org>
flutter pub get
cd ..
flutter_rust_bridge_codegen generate
cd example
flutter run
flutter run -d iPhone
What happens during the first flutter run on iOS:
- Flutter invokes
pod install → reads ios/onde_inference.podspec
- The podspec script phase runs
cargo build --release --target aarch64-apple-ios-sim
(or -device) inside sdk/dart/rust/ — this takes 5–15 min first time
- The resulting
libonde_inference_dart.a is linked into the Runner app
- Flutter's Dart VM starts, calls
RustLib.init(), loads the native library
- The model downloads from HuggingFace Hub on first inference (~941 MB or ~1.93 GB)
HuggingFace token for private/gated models:
export HF_TOKEN=hf_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
flutter run
Version Pinning Rule
The flutter_rust_bridge Dart package and flutter_rust_bridge Rust crate
must be the same version. Use exact pins:
flutter_rust_bridge = "=2.12.0"
dependencies:
flutter_rust_bridge: ^2.12.0
If they drift, generated code won't compile. Always check both when upgrading.
Common Errors and Fixes
| Error | Cause | Fix |
|---|
rust_input: invalid type: sequence | YAML list syntax in FRB 2.x | Change to rust_input: "crate::api" |
Please migrate configuration rust_input | File path in FRB 2.x | Change to rust_input: "crate::api" |
File exists (os error 17) | frb_generated.rs pre-exists | rm rust/src/frb_generated.rs |
could not find package flutter_rust_bridge_codegen | Added codegen as Dart dep | Remove it; install via cargo install |
no such command: expand | cargo-expand not installed | cargo install cargo-expand |
unexpected cfg condition name: frb_expand | Rust check-cfg lint | Harmless FRB macro warning; suppress with #[allow(unexpected_cfgs)] or ignore |
no_leading_underscores_for_library_prefixes | Import alias as _frb | Rename to as frb |
unnecessary_library_name | library <name>; in barrel | Change to bare library; |
OndeChatEngine.create() throws UnimplementedError | Codegen not run yet | Run flutter_rust_bridge_codegen generate |
flutter run fails, missing Runner.xcodeproj | flutter create not run | Run flutter create . --platforms=ios ... in example dir |
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 |
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') |
ld: symbol(s) not found ... _kSCNetworkReachability... | Missing SystemConfiguration framework in podspec | Add s.frameworks = 'SystemConfiguration' to macOS podspec |
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) |
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 |
EntityTooLarge on dart pub publish | Package > 100 MB (rust/target/ included) | Create .pubignore excluding rust/target/, **/*.a, **/*.so, etc. |
Because <pkg> requires SDK version >=X.Y.0 ... version solving failed | pubspec.yaml environment.sdk lower bound is too old; CI Flutter version too old | 1. Update sdk/dart/.flutter-version to a Flutter that ships the required Dart SDK. 2. Tighten environment.sdk in pubspec.yaml to the real minimum. See Hard-Won Lesson 11. |
You must have a LICENSE file in the root directory on dart pub publish | pub.dev requires a file named exactly LICENSE; LICENSE-MIT / LICENSE-APACHE alone do not satisfy it | Create sdk/dart/LICENSE with an "either of … at your option" preamble, then append both full license texts. A bare concatenation without a preamble is legally ambiguous — it implies users must comply with both, defeating the purpose of dual licensing. Run from sdk/dart/: cat > LICENSE << 'EOF' then the preamble (see legal-and-trademarks SKILL.md for exact wording), then { cat LICENSE-MIT; printf '\n\n---\n\n'; cat LICENSE-APACHE; } >> LICENSE. Keep LICENSE-MIT and LICENSE-APACHE alongside it. |
Xcode 26 Compatibility
The NDK-on-PATH Trap
If your ~/.zshrc (or similar) has global Android NDK toolchain exports:
export TOOLCHAIN=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64
export LD=$TOOLCHAIN/bin/ld
export AR=$TOOLCHAIN/bin/llvm-ar
export CXX=$TOOLCHAIN/bin/clang++
export RANLIB=$TOOLCHAIN/bin/llvm-ranlib
export STRIP=$TOOLCHAIN/bin/llvm-strip
Xcode reads $LD and invokes the Android NDK's LLD instead of Apple's linker.
The errors look like Xcode 26 ld-prime incompatibility (unknown argument '-Xlinker')
but the root cause is the wrong linker binary.
Fix: Comment out all NDK tool exports. Keep only the env vars:
export NDK_HOME="$ANDROID_HOME/ndk/29.0.14206865"
export ANDROID_NDK="$NDK_HOME"
export ANDROID_NDK_HOME="$NDK_HOME"
Does Xcode 26's ld-prime work with Flutter?
Yes — once the NDK tool exports are removed, Apple's ld-prime handles all
CocoaPods/Flutter linker flags correctly. No ALTERNATE_LINKER = classic or
-ld_classic workaround is needed for macOS builds. The -ld_classic
deprecation warning from iOS Podfiles is harmless.
iOS 26 Debug Mode Crash
Flutter debug builds crash on iOS 26 beta with
NOTIFY_DEBUGGER_ABOUT_RX_PAGES was not intercepted as expected
(flutter#170416).
This is a Dart VM JIT issue — not fixable in the SDK.
| Mode | iOS 26 Physical | iOS 26 Simulator | iOS 18.x |
|---|
--debug | ❌ Dart VM crash | ❌ Dart VM crash | ✅ Works |
--profile | ✅ Works | ❌ Not supported on sim | ✅ Works |
--release | ⚠️ Use flutter build ipa | ❌ Not supported on sim | ✅ Works |
Workaround: Use an iOS 18.x simulator runtime (download via Xcode Settings → Platforms).
File Interdependency Map
pubspec.yaml
└── declares: flutter_rust_bridge ^2.12.0
flutter_rust_bridge.yaml
├── rust_root: "rust/" → rust/Cargo.toml
├── rust_input: "crate::api" → rust/src/api.rs (source of truth)
└── dart_output: "lib/src/frb_generated.dart" (codegen writes here)
rust/Cargo.toml
├── crate-type = ["cdylib", "staticlib"]
├── flutter_rust_bridge = "=2.12.0" (exact pin!)
└── onde = { path = "../../../" } (the main onde crate)
rust/.cargo/config.toml
└── Android NDK linker overrides (aarch64, armv7, x86_64, i686)
rust/src/api.rs
├── imports onde::{ChatEngine, ...} (path dep → ../../..)
├── declares mirror types (ChatRole, ChatMessage, ...)
├── declares OndeChatEngine (wraps ChatEngine)
└── declares free functions (default_model_config, ...)
lib/src/frb_generated.dart/ ← DIRECTORY (not a file!)
├── api.dart ← generated API bindings
├── api.freezed.dart ← freezed union types (OndeError)
├── frb_generated.dart ← RustLib class, ExternalLibraryLoaderConfig
└── frb_generated.io.dart ← RustLibWire, platform FFI glue
lib/src/types.dart ← hand-written, FRB-independent
└── ChatRole, ChatMessage, SamplingConfig, GgufModelConfig,
InferenceResult, StreamChunk, EngineStatus, EngineInfo, OndeException
lib/src/engine.dart ← hand-written, imports frb_generated.dart/ + types.dart
├── extension OndeChatEngineX (loadDefaultModel, clearHistoryCount)
└── abstract class OndeInference
├── init() → ExternalLibrary.open('onde_inference.framework/...') on Apple
├── defaultModelConfig(), qwen2515bConfig(), qwen253bConfig(), ...
└── defaultSamplingConfig(), deterministicSamplingConfig(), mobileSamplingConfig()
lib/onde_inference.dart (barrel)
├── export 'src/types.dart'
└── export 'src/engine.dart'
macos/
├── Classes/OndeInferencePlugin.swift ← dummy (required for CocoaPods framework)
└── onde_inference.podspec ← script_phase + -force_load + SystemConfiguration
ios/
├── Classes/OndeInferencePlugin.swift ← dummy (required for CocoaPods framework)
└── onde_inference.podspec ← script_phase + -force_load
android/
├── CMakeLists.txt ← invokes cargo build per ABI
└── build.gradle ← externalNativeBuild → cmake
.pubignore ← excludes rust/target/, *.a, *.so, etc.
.gitignore ← excludes same + build artifacts
Last updated: July 2025 — Xcode 26 compatibility, ExternalLibrary.open pattern,
SystemConfiguration framework, .pubignore for pub.dev, NDK PATH pitfall.