|
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. |
|
11
|
> Agent reference for future Dart SDK tasks in this repo. |
|
12
|
|
|
13
|
--- |
|
14
|
|
|
15
|
## What This Skill Covers |
|
16
|
|
|
17
|
End-to-end creation of a pub.dev-ready Flutter FFI plugin package that bridges to a |
|
18
|
Rust library using `flutter_rust_bridge` v2. Covers the full lifecycle: |
|
19
|
design → Rust bridge crate → Dart library → platform build files → tests → |
|
20
|
pub.dev dry-run → codegen → `flutter run`. |
|
21
|
|
|
22
|
--- |
|
23
|
|
|
24
|
## Hard-Won Lessons (Read First) |
|
25
|
|
|
26
|
These are non-obvious facts that caused real failures and are not in the FRB docs. |
|
27
|
|
|
28
|
### 1. `flutter_rust_bridge_codegen` is a Rust binary — NOT a Dart package |
|
29
|
|
|
30
|
``` |
|
31
|
# Correct — install the Rust binary once: |
|
32
|
cargo install flutter_rust_bridge_codegen |
|
33
|
|
|
34
|
# WRONG — this package does not exist on pub.dev: |
|
35
|
# dev_dependencies: |
|
36
|
# flutter_rust_bridge_codegen: ^2.12.0 ← will fail `dart pub get` |
|
37
|
``` |
|
38
|
|
|
39
|
Never add it to `pubspec.yaml` `dev_dependencies`. |
|
40
|
|
|
41
|
### 2. `flutter_rust_bridge.yaml` — use module path syntax, not file paths |
|
42
|
|
|
43
|
```yaml |
|
44
|
# FRB 2.x CORRECT syntax: |
|
45
|
rust_input: "crate::api" |
|
46
|
rust_root: "rust" |
|
47
|
|
|
48
|
# WRONG (causes parse error in FRB 2.x): |
|
49
|
# rust_input: |
|
50
|
# - "rust/src/api.rs" |
|
51
|
|
|
52
|
# WRONG (causes migration error in FRB 2.x): |
|
53
|
# rust_input: "rust/src/api.rs" |
|
54
|
``` |
|
55
|
|
|
56
|
### 3. Delete `frb_generated.rs` before running codegen — never pre-create it |
|
57
|
|
|
58
|
FRB codegen uses an **exclusive create** for `rust/src/frb_generated.rs`. |
|
59
|
If the file exists (even as a stub), codegen aborts with `File exists (os error 17)`. |
|
60
|
|
|
61
|
```bash |
|
62
|
# Before first codegen run: |
|
63
|
rm -f rust/src/frb_generated.rs |
|
64
|
|
|
65
|
# frb_generated.dart (the Dart stub) CAN pre-exist — FRB overwrites it safely. |
|
66
|
``` |
|
67
|
|
|
68
|
### 4. `cargo-expand` is required by FRB codegen — auto-installs but takes time |
|
69
|
|
|
70
|
FRB codegen runs `cargo expand` internally to parse macro-expanded Rust. |
|
71
|
If missing, it installs automatically but adds ~2 min to the first run. |
|
72
|
Pre-install it: |
|
73
|
|
|
74
|
```bash |
|
75
|
cargo install cargo-expand |
|
76
|
``` |
|
77
|
|
|
78
|
### 5. Example apps need `flutter create .` — a `pubspec.yaml` alone is not enough |
|
79
|
|
|
80
|
A Flutter project requires platform scaffolding (`ios/`, `android/`, `macos/`, |
|
81
|
`Runner.xcodeproj`, etc.) that only `flutter create` generates. |
|
82
|
|
|
83
|
```bash |
|
84
|
cd sdk/dart/example |
|
85
|
flutter create . \ |
|
86
|
--project-name onde_inference_example \ |
|
87
|
--platforms=ios,macos,android,linux,windows \ |
|
88
|
--org com.ondeinference |
|
89
|
``` |
|
90
|
|
|
91
|
Run this **before** `flutter pub get` or `flutter run`. |
|
92
|
|
|
93
|
### 6. Types with `#[uniffi]` cannot be re-used directly in FRB |
|
94
|
|
|
95
|
If the source Rust crate annotates types with `#[uniffi::Record]`, `#[uniffi::Enum]`, |
|
96
|
etc., those attributes conflict with FRB code generation. The fix is **type mirroring**: |
|
97
|
re-declare every bridged type as a plain struct/enum in the bridge crate (`rust/src/api.rs`) |
|
98
|
and implement bidirectional `From<>` conversions. |
|
99
|
|
|
100
|
```rust |
|
101
|
// In rust/src/api.rs — mirror type (no #[uniffi] here) |
|
102
|
#[derive(Debug, Clone)] |
|
103
|
pub struct ChatMessage { pub role: ChatRole, pub content: String } |
|
104
|
|
|
105
|
// Conversion to/from the onde crate's type |
|
106
|
impl From<OndeChatMessage> for ChatMessage { ... } |
|
107
|
impl From<ChatMessage> for OndeChatMessage { ... } |
|
108
|
``` |
|
109
|
|
|
110
|
### 7. `StreamSink<T>` requires `T: SseEncode` — satisfied by codegen, not by hand |
|
111
|
|
|
112
|
The `StreamSink` type (used for streaming) demands `T: SseEncode`. The real impl |
|
113
|
is generated by codegen. The pre-codegen stub in `frb_generated.rs` must provide |
|
114
|
a no-op impl to let `cargo check` pass: |
|
115
|
|
|
116
|
```rust |
|
117
|
// rust/src/frb_generated.rs (pre-codegen stub) |
|
118
|
use flutter_rust_bridge::for_generated::SseCodec; |
|
119
|
flutter_rust_bridge::frb_generated_boilerplate!( |
|
120
|
default_stream_sink_codec = SseCodec, |
|
121
|
default_rust_opaque = RustOpaqueMoi, |
|
122
|
default_rust_auto_opaque = RustAutoOpaqueMoi, |
|
123
|
); |
|
124
|
impl SseEncode for crate::api::StreamChunk { |
|
125
|
fn sse_encode(self, _s: &mut flutter_rust_bridge::for_generated::SseSerializer) { |
|
126
|
unimplemented!("codegen stub") |
|
127
|
} |
|
128
|
} |
|
129
|
``` |
|
130
|
|
|
131
|
### 8. Dart lint: no leading underscore for library import prefixes |
|
132
|
|
|
133
|
```dart |
|
134
|
// BAD — triggers `no_leading_underscores_for_library_prefixes` lint: |
|
135
|
import 'frb_generated.dart' as _frb; |
|
136
|
|
|
137
|
// GOOD: |
|
138
|
import 'frb_generated.dart' as frb; |
|
139
|
``` |
|
140
|
|
|
141
|
### 9. Dart lint: remove `library <name>;` directive |
|
142
|
|
|
143
|
```dart |
|
144
|
// BAD — triggers `unnecessary_library_name` lint: |
|
145
|
library onde_inference; |
|
146
|
|
|
147
|
// GOOD: |
|
148
|
library; |
|
149
|
``` |
|
150
|
|
|
151
|
### 10. Exclude FRB-generated files from Dart analysis |
|
152
|
|
|
153
|
```yaml |
|
154
|
# analysis_options.yaml |
|
155
|
analyzer: |
|
156
|
exclude: |
|
157
|
- lib/src/frb_generated.dart |
|
158
|
- lib/src/frb_generated.io.dart |
|
159
|
- lib/src/frb_generated.web.dart |
|
160
|
``` |
|
161
|
|
|
162
|
--- |
|
163
|
|
|
164
|
### 11. Flutter version is a single source of truth — never hardcode it in CI |
|
165
|
|
|
166
|
The CI workflow reads the Flutter version from `sdk/dart/.flutter-version` via |
|
167
|
`subosito/flutter-action`'s `flutter-version-file:` parameter. **Never |
|
168
|
hardcode `flutter-version: "x.y.z"` directly in the workflow YAML.** |
|
169
|
|
|
170
|
``` |
|
171
|
sdk/dart/.flutter-version ← only place the Flutter version is written |
|
172
|
``` |
|
173
|
|
|
174
|
```yaml |
|
175
|
# .github/workflows/release-sdk-dart.yml (both jobs) |
|
176
|
- name: Set up Flutter |
|
177
|
uses: subosito/flutter-action@v2 |
|
178
|
with: |
|
179
|
flutter-version-file: sdk/dart/.flutter-version # reads .flutter-version |
|
180
|
channel: stable |
|
181
|
cache: true |
|
182
|
``` |
|
183
|
|
|
184
|
**Why this matters:** Each Flutter release ships a specific Dart SDK version. |
|
185
|
Dev dependencies like `freezed` have minimum Dart SDK requirements that advance |
|
186
|
faster than Flutter stable releases. When you bump a Flutter-sensitive dep, |
|
187
|
you must also bump the Flutter version used in CI — and `.flutter-version` is |
|
188
|
the one place to do that. |
|
189
|
|
|
190
|
**Rule: when bumping a dep that constrains the Dart SDK** (e.g. `freezed`, |
|
191
|
`flutter_rust_bridge`, `build_runner`): |
|
192
|
|
|
193
|
1. Run `flutter pub get` locally — a version-solving failure tells you |
|
194
|
immediately if the Dart SDK shipped with the current Flutter is too old. |
|
195
|
2. Find the Flutter release that ships the required Dart SDK version |
|
196
|
(see https://docs.flutter.dev/release/archive). |
|
197
|
3. Update `sdk/dart/.flutter-version` to that version. |
|
198
|
4. Tighten `pubspec.yaml`'s `environment.sdk` lower bound to match — it must |
|
199
|
honestly reflect what your deps need, not what you originally wrote. |
|
200
|
|
|
201
|
**`pubspec.yaml` environment constraint must be accurate:** |
|
202
|
|
|
203
|
```yaml |
|
204
|
# WRONG — lying lower bound causes CI to pass locally but fail on old runners |
|
205
|
environment: |
|
206
|
sdk: ">=3.3.0 <4.0.0" # freezed ^3.2.x actually requires >=3.8.0 |
|
207
|
|
|
208
|
# CORRECT |
|
209
|
environment: |
|
210
|
sdk: ">=3.8.0 <4.0.0" |
|
211
|
flutter: ">=3.32.0" |
|
212
|
``` |
|
213
|
|
|
214
|
An inaccurate `sdk:` lower bound is invisible locally (you're already on a |
|
215
|
newer SDK) but breaks any runner or user on the stated minimum. |
|
216
|
|
|
217
|
--- |
|
218
|
|
|
219
|
## Package Structure Reference |
|
220
|
|
|
221
|
``` |
|
222
|
sdk/dart/ |
|
223
|
├── pubspec.yaml # Flutter plugin, ffiPlugin:true per platform |
|
224
|
├── analysis_options.yaml # Exclude frb_generated files |
|
225
|
├── flutter_rust_bridge.yaml # Codegen config (rust_input: "crate::api") |
|
226
|
├── CHANGELOG.md |
|
227
|
├── LICENSE |
|
228
|
├── README.md # pub.dev landing page |
|
229
|
│ |
|
230
|
├── rust/ # Rust bridge crate |
|
231
|
│ ├── Cargo.toml # crate-type=[cdylib,staticlib]; publish=false |
|
232
|
│ └── src/ |
|
233
|
│ ├── lib.rs # mod frb_generated; pub mod api; + #[frb(init)] |
|
234
|
│ ├── api.rs # Mirror types + OndeChatEngine + free fns |
|
235
|
│ └── frb_generated.rs # DELETE before codegen; stub only pre-codegen |
|
236
|
│ |
|
237
|
├── lib/ |
|
238
|
│ ├── <package_name>.dart # Public barrel — exports types + engine + RustLib |
|
239
|
│ └── src/ |
|
240
|
│ ├── types.dart # Standalone Dart types — NO native dependency |
|
241
|
│ ├── engine.dart # OndeChatEngine wrapper + static helpers |
|
242
|
│ ├── frb_generated.dart # CAN be stub pre-codegen; overwritten by codegen |
|
243
|
│ └── frb_generated_stub.dart # Hand-written stub: RustLib + OndeChatEngine |
|
244
|
│ |
|
245
|
├── android/ |
|
246
|
│ ├── build.gradle # ffiPlugin CMake integration |
|
247
|
│ └── CMakeLists.txt # cargo build per Android ABI |
|
248
|
├── ios/ |
|
249
|
│ └── <package>.podspec # script_phase: cargo build aarch64-apple-ios[-sim] |
|
250
|
├── macos/ |
|
251
|
│ └── <package>.podspec # script_phase: cargo build aarch64-apple-darwin |
|
252
|
├── linux/ |
|
253
|
│ ├── CMakeLists.txt |
|
254
|
│ └── <package>_plugin.cc |
|
255
|
├── windows/ |
|
256
|
│ ├── CMakeLists.txt |
|
257
|
│ └── <package>_plugin.cpp |
|
258
|
│ |
|
259
|
├── test/ |
|
260
|
│ └── dart_test.dart # 22+ unit tests — all pass WITHOUT native binary |
|
261
|
└── example/ |
|
262
|
├── pubspec.yaml |
|
263
|
└── lib/main.dart # Full Material 3 demo app |
|
264
|
``` |
|
265
|
|
|
266
|
--- |
|
267
|
|
|
268
|
## `pubspec.yaml` Template |
|
269
|
|
|
270
|
```yaml |
|
271
|
name: <package_name> # e.g. onde_inference (lowercase_snake) |
|
272
|
description: >- |
|
273
|
One-liner for pub.dev search (80 chars max for score). |
|
274
|
version: 0.1.0 |
|
275
|
repository: https://github.com/ondeinference/onde |
|
276
|
homepage: https://ondeinference.com |
|
277
|
issue_tracker: https://github.com/ondeinference/onde/issues |
|
278
|
|
|
279
|
environment: |
|
280
|
sdk: '>=3.8.0 <4.0.0' # must reflect the actual minimum required by deps |
|
281
|
flutter: '>=3.32.0' # Flutter 3.32 ships Dart 3.8; update .flutter-version too |
|
282
|
|
|
283
|
dependencies: |
|
284
|
flutter: |
|
285
|
sdk: flutter |
|
286
|
flutter_rust_bridge: ^2.12.0 # Must match Rust crate version exactly |
|
287
|
|
|
288
|
dev_dependencies: |
|
289
|
flutter_test: |
|
290
|
sdk: flutter |
|
291
|
test: ^1.25.0 # For pure-Dart unit tests |
|
292
|
flutter_lints: ^5.0.0 |
|
293
|
# NOTE: flutter_rust_bridge_codegen is a Rust binary, NOT a Dart package. |
|
294
|
# Install: cargo install flutter_rust_bridge_codegen |
|
295
|
|
|
296
|
flutter: |
|
297
|
plugin: |
|
298
|
platforms: |
|
299
|
android: |
|
300
|
ffiPlugin: true |
|
301
|
ios: |
|
302
|
ffiPlugin: true |
|
303
|
macos: |
|
304
|
ffiPlugin: true |
|
305
|
linux: |
|
306
|
ffiPlugin: true |
|
307
|
windows: |
|
308
|
ffiPlugin: true |
|
309
|
``` |
|
310
|
|
|
311
|
--- |
|
312
|
|
|
313
|
## `flutter_rust_bridge.yaml` Template |
|
314
|
|
|
315
|
```yaml |
|
316
|
rust_root: "rust" |
|
317
|
rust_input: "crate::api" # Module path, not file path |
|
318
|
dart_output: "lib/src/frb_generated.dart" |
|
319
|
dart_entrypoint_class_name: RustLib |
|
320
|
dart_preamble: | |
|
321
|
// ignore_for_file: type=lint, unused_import, unnecessary_import |
|
322
|
web: false # On-device inference requires native access |
|
323
|
``` |
|
324
|
|
|
325
|
--- |
|
326
|
|
|
327
|
## Rust Bridge Crate (`rust/Cargo.toml`) Template |
|
328
|
|
|
329
|
```toml |
|
330
|
[package] |
|
331
|
name = "<package>_dart" # e.g. onde_inference_dart |
|
332
|
version = "0.1.0" |
|
333
|
edition = "2021" |
|
334
|
publish = false # Bridge crate is not published to crates.io |
|
335
|
|
|
336
|
[lib] |
|
337
|
crate-type = ["cdylib", "staticlib"] |
|
338
|
# cdylib → .so / .dylib / .dll for Android / macOS / Linux / Windows |
|
339
|
# staticlib → .a for iOS (dynamic linking not allowed) |
|
340
|
|
|
341
|
[dependencies] |
|
342
|
flutter_rust_bridge = "=2.12.0" # Must match Dart package version exactly |
|
343
|
<source_crate> = { path = "../../.." } # Relative path to the wrapped crate |
|
344
|
tokio = { version = "1", features = ["rt-multi-thread", "macros"] } |
|
345
|
thiserror = "2" |
|
346
|
log = "0.4" |
|
347
|
``` |
|
348
|
|
|
349
|
--- |
|
350
|
|
|
351
|
## Rust `src/lib.rs` Template |
|
352
|
|
|
353
|
```rust |
|
354
|
mod frb_generated; // Generated by flutter_rust_bridge_codegen |
|
355
|
pub mod api; |
|
356
|
|
|
357
|
/// Called once when the Dart RustLib.init() is invoked. |
|
358
|
#[flutter_rust_bridge::frb(init)] |
|
359
|
pub fn init_app() { |
|
360
|
flutter_rust_bridge::setup_default_user_utils(); |
|
361
|
} |
|
362
|
``` |
|
363
|
|
|
364
|
--- |
|
365
|
|
|
366
|
## Type Mirroring Pattern in `src/api.rs` |
|
367
|
|
|
368
|
Use this pattern for every Rust type that crosses the bridge. |
|
369
|
|
|
370
|
```rust |
|
371
|
use flutter_rust_bridge::frb; |
|
372
|
use <source_crate>::inference::{ |
|
373
|
ChatMessage as SrcChatMessage, |
|
374
|
ChatRole as SrcChatRole, |
|
375
|
// ... |
|
376
|
}; |
|
377
|
|
|
378
|
// 1. Declare a mirror type with no framework annotations |
|
379
|
#[derive(Debug, Clone, PartialEq)] |
|
380
|
pub enum ChatRole { System, User, Assistant } |
|
381
|
|
|
382
|
// 2. Bidirectional conversions |
|
383
|
impl From<SrcChatRole> for ChatRole { ... } |
|
384
|
impl From<ChatRole> for SrcChatRole { ... } |
|
385
|
|
|
386
|
// 3. Opaque engine wrapper — FRB auto-wraps in Arc<RwLock<T>> |
|
387
|
pub struct OndeChatEngine { inner: <source_crate>::inference::ChatEngine } |
|
388
|
|
|
389
|
impl OndeChatEngine { |
|
390
|
#[frb(sync)] // sync constructor → no Future on Dart side |
|
391
|
pub fn new() -> OndeChatEngine { OndeChatEngine { inner: ChatEngine::new() } } |
|
392
|
|
|
393
|
pub async fn send_message(&self, message: String) -> Result<InferenceResult, OndeError> { |
|
394
|
self.inner.send_message(message).await |
|
395
|
.map(InferenceResult::from) |
|
396
|
.map_err(OndeError::from) |
|
397
|
} |
|
398
|
|
|
399
|
// Streaming: use StreamSink (maps to Dart Stream<T>) |
|
400
|
pub async fn stream_message( |
|
401
|
&self, |
|
402
|
message: String, |
|
403
|
sink: StreamSink<StreamChunk>, |
|
404
|
) -> Result<(), OndeError> { |
|
405
|
let mut rx = self.inner.stream_message(message).await.map_err(OndeError::from)?; |
|
406
|
while let Some(chunk) = rx.recv().await { |
|
407
|
let done = chunk.done; |
|
408
|
sink.add(StreamChunk::from(chunk)); |
|
409
|
if done { break; } |
|
410
|
} |
|
411
|
Ok(()) |
|
412
|
} |
|
413
|
} |
|
414
|
|
|
415
|
// Free functions with #[frb(sync)] → synchronous on Dart side |
|
416
|
#[frb(sync)] |
|
417
|
pub fn default_model_config() -> GgufModelConfig { |
|
418
|
<source_crate>::inference::GgufModelConfig::platform_default().into() |
|
419
|
} |
|
420
|
``` |
|
421
|
|
|
422
|
--- |
|
423
|
|
|
424
|
## Dart Stub Pattern (`lib/src/frb_generated_stub.dart`) |
|
425
|
|
|
426
|
The stub lets the package compile and all pure-Dart unit tests pass **before** |
|
427
|
the native binary exists. Key rules: |
|
428
|
|
|
429
|
1. Import only `types.dart` — never `dart:ffi` or anything platform-native. |
|
430
|
2. `RustLib.init()` sets a bool flag; `new_()` and every inference method throw |
|
431
|
`UnimplementedError`. |
|
432
|
3. Config/sampling free functions return **real hardcoded values** (matching the |
|
433
|
Rust constants) so model-selection UIs can be built and tested immediately. |
|
434
|
4. Make every method signature match exactly what FRB will generate (same names, |
|
435
|
same parameter types, same return types) — this makes the post-codegen |
|
436
|
transition a drop-in replacement. |
|
437
|
|
|
438
|
```dart |
|
439
|
// frb_generated.dart (stub redirect) |
|
440
|
export 'frb_generated_stub.dart'; |
|
441
|
|
|
442
|
// frb_generated_stub.dart |
|
443
|
import 'types.dart'; |
|
444
|
|
|
445
|
class RustLib { |
|
446
|
static bool _initialized = false; |
|
447
|
static bool get isInitialized => _initialized; |
|
448
|
static Future<void> init() async { _initialized = true; } |
|
449
|
} |
|
450
|
|
|
451
|
class OndeChatEngine { |
|
452
|
static Future<OndeChatEngine> new_() async => |
|
453
|
throw UnimplementedError('Run flutter_rust_bridge_codegen generate'); |
|
454
|
|
|
455
|
Future<double> loadDefaultModel({ String? systemPrompt, SamplingConfig? sampling }) => |
|
456
|
throw UnimplementedError(_msg('loadDefaultModel')); |
|
457
|
|
|
458
|
Stream<StreamChunk> streamMessage({ required String message }) => |
|
459
|
Stream.error(UnimplementedError(_msg('streamMessage')), StackTrace.current); |
|
460
|
|
|
461
|
static String _msg(String m) => 'OndeChatEngine.$m: run codegen first'; |
|
462
|
} |
|
463
|
|
|
464
|
// Free functions — return real data so UI can be built/tested without native |
|
465
|
GgufModelConfig qwen251_5bConfig() => const GgufModelConfig( |
|
466
|
modelId: 'bartowski/Qwen2.5-1.5B-Instruct-GGUF', |
|
467
|
files: ['Qwen2.5-1.5B-Instruct-Q4_K_M.gguf'], |
|
468
|
displayName: 'Qwen 2.5 1.5B', |
|
469
|
approxMemory: '~941 MB', |
|
470
|
); |
|
471
|
``` |
|
472
|
|
|
473
|
--- |
|
474
|
|
|
475
|
## Dart Public API Pattern (`lib/src/engine.dart`) |
|
476
|
|
|
477
|
```dart |
|
478
|
import 'dart:async'; |
|
479
|
import 'dart:io' show Platform; |
|
480
|
|
|
481
|
import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart' |
|
482
|
show ExternalLibrary; |
|
483
|
|
|
484
|
import 'frb_generated.dart/frb_generated.dart' as frb; |
|
485
|
import 'frb_generated.dart/api.dart' as api; |
|
486
|
import 'types.dart'; |
|
487
|
|
|
488
|
export 'frb_generated.dart/frb_generated.dart' show RustLib; |
|
489
|
export 'frb_generated.dart/api.dart' |
|
490
|
show OndeChatEngine, OndeError, OndeError_NoModelLoaded, /* ... */; |
|
491
|
|
|
492
|
/// Extension on the FRB-generated OndeChatEngine opaque type. |
|
493
|
extension OndeChatEngineX on api.OndeChatEngine { |
|
494
|
Future<double> loadDefaultModel({ |
|
495
|
String? systemPrompt, |
|
496
|
SamplingConfig? sampling, |
|
497
|
}) => |
|
498
|
loadGgufModel( |
|
499
|
config: api.defaultModelConfig(), |
|
500
|
systemPrompt: systemPrompt, |
|
501
|
sampling: sampling, |
|
502
|
); |
|
503
|
|
|
504
|
Future<int> clearHistoryCount() async => (await clearHistory()).toInt(); |
|
505
|
} |
|
506
|
|
|
507
|
abstract final class OndeInference { |
|
508
|
/// Initialises the Rust shared library. |
|
509
|
/// |
|
510
|
/// On macOS and iOS the Rust static library is force-loaded into the |
|
511
|
/// onde_inference CocoaPods framework via OTHER_LDFLAGS in the podspec. |
|
512
|
/// We must open that framework explicitly — DynamicLibrary.process() |
|
513
|
/// (RTLD_DEFAULT) cannot resolve symbols inside a separately-loaded |
|
514
|
/// framework. |
|
515
|
static Future<void> init() async { |
|
516
|
ExternalLibrary? lib; |
|
517
|
if (Platform.isMacOS || Platform.isIOS) { |
|
518
|
lib = ExternalLibrary.open( |
|
519
|
'onde_inference.framework/onde_inference', |
|
520
|
); |
|
521
|
} |
|
522
|
await frb.RustLib.init(externalLibrary: lib); |
|
523
|
} |
|
524
|
|
|
525
|
static GgufModelConfig defaultModelConfig() => api.defaultModelConfig(); |
|
526
|
static SamplingConfig deterministicSamplingConfig() => api.deterministicSamplingConfig(); |
|
527
|
} |
|
528
|
``` |
|
529
|
|
|
530
|
### Why `ExternalLibrary.open(...)` and not `.process()` |
|
531
|
|
|
532
|
The `-force_load` linker flag embeds Rust symbols into the **pod framework** |
|
533
|
(`onde_inference.framework/onde_inference`), NOT into the main executable. |
|
534
|
`DynamicLibrary.process()` maps to `RTLD_DEFAULT`, which only searches the |
|
535
|
main executable and already-opened images in certain configurations. On |
|
536
|
macOS/iOS with CocoaPods `use_frameworks!`, the pod framework is a separate |
|
537
|
Mach-O image — you must `dlopen` it by name. |
|
538
|
|
|
539
|
FRB 2.12's `ExternalLibrary.open(path)` calls `DynamicLibrary.open(path)` |
|
540
|
under the hood. On non-Apple platforms (Android, Linux, Windows) the default |
|
541
|
`ExternalLibraryLoaderConfig` stem-based lookup works, so `lib` stays `null` |
|
542
|
and `RustLib.init()` handles it automatically. |
|
543
|
|
|
544
|
**Do NOT use `ExternalLibrary.process(iKnowHowToUseIt: true)`** — it will |
|
545
|
produce `Failed to lookup symbol 'frb_get_rust_content_hash': dlsym(RTLD_DEFAULT, ...)`. |
|
546
|
|
|
547
|
--- |
|
548
|
|
|
549
|
## Platform Build Files |
|
550
|
|
|
551
|
### Dummy Swift Plugin Classes (required) |
|
552
|
|
|
553
|
Both `macos/Classes/OndeInferencePlugin.swift` and `ios/Classes/OndeInferencePlugin.swift` |
|
554
|
**must exist** — they are no-op `FlutterPlugin` stubs. Without them, CocoaPods |
|
555
|
has no source files to compile and won't produce a real `.framework` target. |
|
556
|
The Rust static library is force-loaded INTO this framework. |
|
557
|
|
|
558
|
```swift |
|
559
|
// macos/Classes/OndeInferencePlugin.swift |
|
560
|
import FlutterMacOS |
|
561
|
|
|
562
|
public class OndeInferencePlugin: NSObject, FlutterPlugin { |
|
563
|
public static func register(with registrar: FlutterPluginRegistrar) { |
|
564
|
// No-op — all FFI symbols come from the Rust static library. |
|
565
|
} |
|
566
|
} |
|
567
|
``` |
|
568
|
|
|
569
|
```swift |
|
570
|
// ios/Classes/OndeInferencePlugin.swift |
|
571
|
import Flutter // NOT FlutterMacOS |
|
572
|
|
|
573
|
public class OndeInferencePlugin: NSObject, FlutterPlugin { |
|
574
|
public static func register(with registrar: FlutterPluginRegistrar) {} |
|
575
|
} |
|
576
|
``` |
|
577
|
|
|
578
|
### macOS podspec — full working version |
|
579
|
|
|
580
|
```ruby |
|
581
|
Pod::Spec.new do |s| |
|
582
|
s.name = 'onde_inference' |
|
583
|
s.version = '0.1.0' |
|
584
|
s.summary = 'On-device LLM inference SDK for Flutter (macOS).' |
|
585
|
s.description = 'Runs Qwen 2.5 models locally on macOS with Metal acceleration.' |
|
586
|
s.homepage = 'https://ondeinference.com' |
|
587
|
s.license = { :type => 'MIT', :file => '../LICENSE' } |
|
588
|
s.author = { 'Onde Inference' => 'hello@ondeinference.com' } |
|
589
|
s.source = { :path => '.' } |
|
590
|
s.source_files = 'Classes/**/*' |
|
591
|
s.dependency 'FlutterMacOS' |
|
592
|
s.platform = :osx, '10.15' |
|
593
|
s.swift_version = '5.0' |
|
594
|
|
|
595
|
s.script_phases = [ |
|
596
|
{ |
|
597
|
:name => 'Build Rust bridge (onde_inference_dart)', |
|
598
|
:script => <<~SHELL, |
|
599
|
set -e |
|
600
|
RUST_DIR="${PODS_TARGET_SRCROOT}/../rust" |
|
601
|
cd "$RUST_DIR" |
|
602
|
ARCH=$(uname -m) |
|
603
|
if [[ "$ARCH" == "arm64" ]]; then |
|
604
|
TARGET="aarch64-apple-darwin" |
|
605
|
else |
|
606
|
TARGET="x86_64-apple-darwin" |
|
607
|
fi |
|
608
|
cargo build --release --target "$TARGET" |
|
609
|
cp "target/$TARGET/release/libonde_inference_dart.a" \ |
|
610
|
"${PODS_TARGET_SRCROOT}/libonde_inference_dart.a" |
|
611
|
SHELL |
|
612
|
:execution_position => :before_compile, |
|
613
|
:output_files => ["${PODS_TARGET_SRCROOT}/libonde_inference_dart.a"], |
|
614
|
} |
|
615
|
] |
|
616
|
|
|
617
|
# SystemConfiguration is required by Rust hyper_util (proxy detection) |
|
618
|
s.frameworks = 'SystemConfiguration' |
|
619
|
|
|
620
|
s.pod_target_xcconfig = { |
|
621
|
'DEFINES_MODULE' => 'YES', |
|
622
|
'OTHER_LDFLAGS' => '-force_load ${PODS_TARGET_SRCROOT}/libonde_inference_dart.a', |
|
623
|
} |
|
624
|
s.preserve_paths = '../rust/**/*' |
|
625
|
end |
|
626
|
``` |
|
627
|
|
|
628
|
**Key points:** |
|
629
|
- `s.frameworks = 'SystemConfiguration'` — required because the Rust `hyper_util` |
|
630
|
crate (transitive via `reqwest`) references `SCDynamicStoreCopyProxies`, |
|
631
|
`SCNetworkReachabilityCreateWithName`, etc. Without it you get |
|
632
|
`ld: symbol(s) not found for architecture arm64`. |
|
633
|
- `-force_load` links ALL symbols from the `.a` into the pod framework. |
|
634
|
- `s.source_files = 'Classes/**/*'` picks up the dummy Swift plugin. |
|
635
|
|
|
636
|
### iOS podspec — differs from macOS |
|
637
|
|
|
638
|
```ruby |
|
639
|
s.dependency 'Flutter' # NOT 'FlutterMacOS' |
|
640
|
s.platform = :ios, '13.0' # NOT :osx |
|
641
|
|
|
642
|
# Target selection differs: |
|
643
|
# iphonesimulator → aarch64-apple-ios-sim |
|
644
|
# iphoneos → aarch64-apple-ios |
|
645
|
|
|
646
|
s.pod_target_xcconfig = { |
|
647
|
'DEFINES_MODULE' => 'YES', |
|
648
|
'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', |
|
649
|
'OTHER_LDFLAGS' => |
|
650
|
'-force_load ${PODS_TARGET_SRCROOT}/libonde_inference_dart.a', |
|
651
|
} |
|
652
|
``` |
|
653
|
|
|
654
|
### Android `CMakeLists.txt` — key section |
|
655
|
|
|
656
|
```cmake |
|
657
|
if(ANDROID_ABI STREQUAL "arm64-v8a") |
|
658
|
set(CARGO_TARGET "aarch64-linux-android") |
|
659
|
elseif(ANDROID_ABI STREQUAL "armeabi-v7a") |
|
660
|
set(CARGO_TARGET "armv7-linux-androideabi") |
|
661
|
elseif(ANDROID_ABI STREQUAL "x86_64") |
|
662
|
set(CARGO_TARGET "x86_64-linux-android") |
|
663
|
elseif(ANDROID_ABI STREQUAL "x86") |
|
664
|
set(CARGO_TARGET "i686-linux-android") |
|
665
|
endif() |
|
666
|
add_custom_command(OUTPUT "${LIB_OUTPUT}" |
|
667
|
COMMAND cargo build --release --target ${CARGO_TARGET} |
|
668
|
--manifest-path "${RUST_DIR}/Cargo.toml" |
|
669
|
WORKING_DIRECTORY ${RUST_DIR}) |
|
670
|
add_library(onde_inference_dart SHARED IMPORTED GLOBAL) |
|
671
|
set_target_properties(onde_inference_dart PROPERTIES IMPORTED_LOCATION ${LIB_OUTPUT}) |
|
672
|
``` |
|
673
|
|
|
674
|
Android uses CMake to invoke `cargo build` for the right NDK triple, producing |
|
675
|
a `.so` shared library (unlike Apple which uses `.a` staticlibs). |
|
676
|
|
|
677
|
### Android NDK Linker Config (`rust/.cargo/config.toml`) |
|
678
|
|
|
679
|
```toml |
|
680
|
[target.aarch64-linux-android] |
|
681
|
linker = "aarch64-linux-android24-clang" |
|
682
|
|
|
683
|
[target.armv7-linux-androideabi] |
|
684
|
linker = "armv7a-linux-androideabi24-clang" |
|
685
|
|
|
686
|
[target.x86_64-linux-android] |
|
687
|
linker = "x86_64-linux-android24-clang" |
|
688
|
|
|
689
|
[target.i686-linux-android] |
|
690
|
linker = "i686-linux-android24-clang" |
|
691
|
``` |
|
692
|
|
|
693
|
These short names require the NDK toolchain `bin/` to be on PATH. |
|
694
|
Flutter's Gradle + CMake integration typically handles this automatically |
|
695
|
during `flutter build apk` / `flutter run -d <android>`. |
|
696
|
|
|
697
|
--- |
|
698
|
|
|
699
|
## Testing Strategy |
|
700
|
|
|
701
|
All pure-Dart unit tests run **without** building the native binary. |
|
702
|
|
|
703
|
``` |
|
704
|
Tests that PASS pre-codegen (using the stub): |
|
705
|
✓ All type constructors, equality, hashCode, copyWith |
|
706
|
✓ Enum toString and isReady helpers |
|
707
|
✓ SamplingConfig presets (default, deterministic, mobile) |
|
708
|
✓ GgufModelConfig factory functions (real hardcoded values in stub) |
|
709
|
✓ RustLib.init() is idempotent |
|
710
|
✓ OndeInference.* static helpers |
|
711
|
✓ OndeChatEngine.create() throws UnimplementedError pre-codegen |
|
712
|
|
|
713
|
Tests that require codegen + native binary: |
|
714
|
✗ Any actual inference call |
|
715
|
✗ loadDefaultModel / sendMessage / streamMessage |
|
716
|
``` |
|
717
|
|
|
718
|
Run tests: |
|
719
|
```bash |
|
720
|
cd sdk/dart && dart test test/dart_test.dart |
|
721
|
``` |
|
722
|
|
|
723
|
--- |
|
724
|
|
|
725
|
## Pub.dev Readiness Checklist |
|
726
|
|
|
727
|
Run this before declaring the SDK ready: |
|
728
|
|
|
729
|
```bash |
|
730
|
# 1. Dart analysis — must be zero issues |
|
731
|
dart analyze lib/ |
|
732
|
|
|
733
|
# 2. All tests pass |
|
734
|
dart test test/dart_test.dart |
|
735
|
|
|
736
|
# 3. Dry-run publish — must show compressed size < 100 MB and minimal warnings |
|
737
|
dart pub publish --dry-run |
|
738
|
``` |
|
739
|
|
|
740
|
Common pub.dev score deductions to avoid: |
|
741
|
- Missing `repository:` in pubspec → loses points |
|
742
|
- No `example/` directory → loses points |
|
743
|
- `description:` under 60 chars or over 180 chars → loses points |
|
744
|
- Library exports not documented → loses points |
|
745
|
- `library <name>;` directive → triggers lint |
|
746
|
|
|
747
|
### Pub.dev Upload Size (100 MB limit) |
|
748
|
|
|
749
|
`dart pub publish` bundles everything not excluded by `.pubignore` (or |
|
750
|
`.gitignore` if `.pubignore` doesn't exist). The Rust `target/` dir alone |
|
751
|
can be **7+ GB**. A `.pubignore` file is mandatory. |
|
752
|
|
|
753
|
**Files that MUST be included** (consumers build from source): |
|
754
|
- `rust/src/`, `rust/Cargo.toml`, `rust/Cargo.lock` |
|
755
|
- `ios/`, `macos/`, `android/`, `linux/`, `windows/` (build scripts, podspecs, CMakeLists) |
|
756
|
- `ios/Classes/`, `macos/Classes/` (dummy plugin Swift files) |
|
757
|
- `lib/` (all Dart code) |
|
758
|
|
|
759
|
**Files that MUST be excluded** (`.pubignore`): |
|
760
|
- `rust/target/` — multi-GB build artifacts |
|
761
|
- `**/*.a`, `**/*.so`, `**/*.dylib`, `**/*.dll` — precompiled binaries |
|
762
|
- `.dart_tool/`, `pubspec.lock` |
|
763
|
- `example/build/`, `example/.dart_tool/` |
|
764
|
- `logs/`, `.idea/`, `.vscode/`, `.DS_Store` |
|
765
|
|
|
766
|
After adding `.pubignore`, verify: `dart pub publish --dry-run` should report |
|
767
|
~280 KB compressed size, not hundreds of MB. |
|
768
|
|
|
769
|
--- |
|
770
|
|
|
771
|
## Codegen Workflow (Full, Correct Order) |
|
772
|
|
|
773
|
```bash |
|
774
|
# ── Prerequisites (one-time) ────────────────────────────────────────────── |
|
775
|
|
|
776
|
rustup target add aarch64-apple-ios aarch64-apple-ios-sim # iOS |
|
777
|
rustup target add aarch64-apple-darwin x86_64-apple-darwin # macOS |
|
778
|
|
|
779
|
cargo install cargo-expand # Required by FRB codegen parser |
|
780
|
cargo install flutter_rust_bridge_codegen # The codegen tool itself |
|
781
|
|
|
782
|
# ── Per-package setup ───────────────────────────────────────────────────── |
|
783
|
|
|
784
|
cd sdk/dart |
|
785
|
|
|
786
|
# IMPORTANT: delete the pre-codegen Rust stub first |
|
787
|
rm -f rust/src/frb_generated.rs |
|
788
|
|
|
789
|
# Run codegen — generates: |
|
790
|
# rust/src/frb_generated.rs (Rust glue) |
|
791
|
# lib/src/frb_generated.dart (Dart FFI bindings, replaces stub) |
|
792
|
flutter_rust_bridge_codegen generate |
|
793
|
|
|
794
|
# Verify Rust still compiles cleanly |
|
795
|
cd rust && cargo check |
|
796
|
|
|
797
|
# Verify Dart analysis is still clean |
|
798
|
cd .. && dart analyze lib/ |
|
799
|
``` |
|
800
|
|
|
801
|
--- |
|
802
|
|
|
803
|
## `flutter run` on iOS (Full Steps) |
|
804
|
|
|
805
|
```bash |
|
806
|
# 1. Scaffold the example Flutter project (one-time) |
|
807
|
cd sdk/dart/example |
|
808
|
flutter create . \ |
|
809
|
--project-name <example_name> \ |
|
810
|
--platforms=ios,macos \ |
|
811
|
--org com.<your_org> |
|
812
|
|
|
813
|
# 2. Get dependencies |
|
814
|
flutter pub get |
|
815
|
|
|
816
|
# 3. Run codegen (from the SDK package root, not example root) |
|
817
|
cd .. |
|
818
|
flutter_rust_bridge_codegen generate |
|
819
|
|
|
820
|
# 4. Run the app (from example dir) |
|
821
|
cd example |
|
822
|
flutter run # picks up connected device/simulator automatically |
|
823
|
flutter run -d iPhone # target a specific device |
|
824
|
``` |
|
825
|
|
|
826
|
**What happens during the first `flutter run` on iOS:** |
|
827
|
1. Flutter invokes `pod install` → reads `ios/onde_inference.podspec` |
|
828
|
2. The podspec script phase runs `cargo build --release --target aarch64-apple-ios-sim` |
|
829
|
(or `-device`) inside `sdk/dart/rust/` — this takes 5–15 min first time |
|
830
|
3. The resulting `libonde_inference_dart.a` is linked into the Runner app |
|
831
|
4. Flutter's Dart VM starts, calls `RustLib.init()`, loads the native library |
|
832
|
5. The model downloads from HuggingFace Hub on first inference (~941 MB or ~1.93 GB) |
|
833
|
|
|
834
|
**HuggingFace token for private/gated models:** |
|
835
|
```bash |
|
836
|
# Bake token at build time (required for iOS — no filesystem access): |
|
837
|
export HF_TOKEN=hf_xxxxxxxxxxxxxxxxxxxxxxxxxxxx |
|
838
|
flutter run |
|
839
|
``` |
|
840
|
|
|
841
|
--- |
|
842
|
|
|
843
|
## Version Pinning Rule |
|
844
|
|
|
845
|
The `flutter_rust_bridge` Dart package and `flutter_rust_bridge` Rust crate |
|
846
|
**must be the same version**. Use exact pins: |
|
847
|
|
|
848
|
```toml |
|
849
|
# rust/Cargo.toml |
|
850
|
flutter_rust_bridge = "=2.12.0" |
|
851
|
``` |
|
852
|
```yaml |
|
853
|
# pubspec.yaml |
|
854
|
dependencies: |
|
855
|
flutter_rust_bridge: ^2.12.0 # ^ is OK on Dart side; codegen auto-upgrades |
|
856
|
``` |
|
857
|
|
|
858
|
If they drift, generated code won't compile. Always check both when upgrading. |
|
859
|
|
|
860
|
--- |
|
861
|
|
|
862
|
## Common Errors and Fixes |
|
863
|
|
|
864
|
| Error | Cause | Fix | |
|
865
|
|-------|-------|-----| |
|
866
|
| `rust_input: invalid type: sequence` | YAML list syntax in FRB 2.x | Change to `rust_input: "crate::api"` | |
|
867
|
| `Please migrate configuration rust_input` | File path in FRB 2.x | Change to `rust_input: "crate::api"` | |
|
868
|
| `File exists (os error 17)` | `frb_generated.rs` pre-exists | `rm rust/src/frb_generated.rs` | |
|
869
|
| `could not find package flutter_rust_bridge_codegen` | Added codegen as Dart dep | Remove it; install via `cargo install` | |
|
870
|
| `no such command: expand` | `cargo-expand` not installed | `cargo install cargo-expand` | |
|
871
|
| `unexpected cfg condition name: frb_expand` | Rust check-cfg lint | Harmless FRB macro warning; suppress with `#[allow(unexpected_cfgs)]` or ignore | |
|
872
|
| `no_leading_underscores_for_library_prefixes` | Import alias `as _frb` | Rename to `as frb` | |
|
873
|
| `unnecessary_library_name` | `library <name>;` in barrel | Change to bare `library;` | |
|
874
|
| `OndeChatEngine.create() throws UnimplementedError` | Codegen not run yet | Run `flutter_rust_bridge_codegen generate` | |
|
875
|
| `flutter run` fails, missing Runner.xcodeproj | `flutter create` not run | Run `flutter create . --platforms=ios ...` in example dir | |
|
876
|
| `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 | |
|
877
|
| `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')` | |
|
878
|
| `ld: symbol(s) not found ... _kSCNetworkReachability...` | Missing `SystemConfiguration` framework in podspec | Add `s.frameworks = 'SystemConfiguration'` to macOS podspec | |
|
879
|
| `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) | |
|
880
|
| `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 | |
|
881
|
| `EntityTooLarge` on `dart pub publish` | Package > 100 MB (rust/target/ included) | Create `.pubignore` excluding `rust/target/`, `**/*.a`, `**/*.so`, etc. | |
|
882
|
| `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. | |
|
883
|
| `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. | |
|
884
|
|
|
885
|
## Xcode 26 Compatibility |
|
886
|
|
|
887
|
### The NDK-on-PATH Trap |
|
888
|
|
|
889
|
If your `~/.zshrc` (or similar) has **global** Android NDK toolchain exports: |
|
890
|
|
|
891
|
```bash |
|
892
|
# DANGEROUS — breaks ALL non-Android builds (Xcode, Rust for Apple, etc.) |
|
893
|
export TOOLCHAIN=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64 |
|
894
|
export LD=$TOOLCHAIN/bin/ld # ← THIS HIJACKS XCODE'S LINKER |
|
895
|
export AR=$TOOLCHAIN/bin/llvm-ar |
|
896
|
export CXX=$TOOLCHAIN/bin/clang++ |
|
897
|
export RANLIB=$TOOLCHAIN/bin/llvm-ranlib |
|
898
|
export STRIP=$TOOLCHAIN/bin/llvm-strip |
|
899
|
``` |
|
900
|
|
|
901
|
Xcode reads `$LD` and invokes the Android NDK's LLD instead of Apple's linker. |
|
902
|
The errors look like Xcode 26 ld-prime incompatibility (`unknown argument '-Xlinker'`) |
|
903
|
but the root cause is the **wrong linker binary**. |
|
904
|
|
|
905
|
**Fix:** Comment out all NDK tool exports. Keep only the env vars: |
|
906
|
|
|
907
|
```bash |
|
908
|
export NDK_HOME="$ANDROID_HOME/ndk/29.0.14206865" |
|
909
|
export ANDROID_NDK="$NDK_HOME" |
|
910
|
export ANDROID_NDK_HOME="$NDK_HOME" |
|
911
|
# DO NOT export LD, AR, CXX, RANLIB, STRIP globally. |
|
912
|
# Cargo uses rust/.cargo/config.toml for Android linker selection. |
|
913
|
``` |
|
914
|
|
|
915
|
### Does Xcode 26's ld-prime work with Flutter? |
|
916
|
|
|
917
|
**Yes** — once the NDK tool exports are removed, Apple's ld-prime handles all |
|
918
|
CocoaPods/Flutter linker flags correctly. No `ALTERNATE_LINKER = classic` or |
|
919
|
`-ld_classic` workaround is needed for macOS builds. The `-ld_classic` |
|
920
|
deprecation warning from iOS Podfiles is harmless. |
|
921
|
|
|
922
|
### iOS 26 Debug Mode Crash |
|
923
|
|
|
924
|
Flutter debug builds crash on iOS 26 beta with |
|
925
|
`NOTIFY_DEBUGGER_ABOUT_RX_PAGES was not intercepted as expected` |
|
926
|
([flutter#170416](https://github.com/flutter/flutter/issues/170416)). |
|
927
|
This is a Dart VM JIT issue — **not fixable in the SDK**. |
|
928
|
|
|
929
|
| Mode | iOS 26 Physical | iOS 26 Simulator | iOS 18.x | |
|
930
|
|------|----------------|-----------------|----------| |
|
931
|
| `--debug` | ❌ Dart VM crash | ❌ Dart VM crash | ✅ Works | |
|
932
|
| `--profile` | ✅ Works | ❌ Not supported on sim | ✅ Works | |
|
933
|
| `--release` | ⚠️ Use `flutter build ipa` | ❌ Not supported on sim | ✅ Works | |
|
934
|
|
|
935
|
**Workaround:** Use an iOS 18.x simulator runtime (download via Xcode Settings → Platforms). |
|
936
|
|
|
937
|
--- |
|
938
|
|
|
939
|
## File Interdependency Map |
|
940
|
|
|
941
|
``` |
|
942
|
pubspec.yaml |
|
943
|
└── declares: flutter_rust_bridge ^2.12.0 |
|
944
|
|
|
945
|
flutter_rust_bridge.yaml |
|
946
|
├── rust_root: "rust/" → rust/Cargo.toml |
|
947
|
├── rust_input: "crate::api" → rust/src/api.rs (source of truth) |
|
948
|
└── dart_output: "lib/src/frb_generated.dart" (codegen writes here) |
|
949
|
|
|
950
|
rust/Cargo.toml |
|
951
|
├── crate-type = ["cdylib", "staticlib"] |
|
952
|
├── flutter_rust_bridge = "=2.12.0" (exact pin!) |
|
953
|
└── onde = { path = "../../../" } (the main onde crate) |
|
954
|
|
|
955
|
rust/.cargo/config.toml |
|
956
|
└── Android NDK linker overrides (aarch64, armv7, x86_64, i686) |
|
957
|
|
|
958
|
rust/src/api.rs |
|
959
|
├── imports onde::{ChatEngine, ...} (path dep → ../../..) |
|
960
|
├── declares mirror types (ChatRole, ChatMessage, ...) |
|
961
|
├── declares OndeChatEngine (wraps ChatEngine) |
|
962
|
└── declares free functions (default_model_config, ...) |
|
963
|
|
|
964
|
lib/src/frb_generated.dart/ ← DIRECTORY (not a file!) |
|
965
|
├── api.dart ← generated API bindings |
|
966
|
├── api.freezed.dart ← freezed union types (OndeError) |
|
967
|
├── frb_generated.dart ← RustLib class, ExternalLibraryLoaderConfig |
|
968
|
└── frb_generated.io.dart ← RustLibWire, platform FFI glue |
|
969
|
|
|
970
|
lib/src/types.dart ← hand-written, FRB-independent |
|
971
|
└── ChatRole, ChatMessage, SamplingConfig, GgufModelConfig, |
|
972
|
InferenceResult, StreamChunk, EngineStatus, EngineInfo, OndeException |
|
973
|
|
|
974
|
lib/src/engine.dart ← hand-written, imports frb_generated.dart/ + types.dart |
|
975
|
├── extension OndeChatEngineX (loadDefaultModel, clearHistoryCount) |
|
976
|
└── abstract class OndeInference |
|
977
|
├── init() → ExternalLibrary.open('onde_inference.framework/...') on Apple |
|
978
|
├── defaultModelConfig(), qwen2515bConfig(), qwen253bConfig(), ... |
|
979
|
└── defaultSamplingConfig(), deterministicSamplingConfig(), mobileSamplingConfig() |
|
980
|
|
|
981
|
lib/onde_inference.dart (barrel) |
|
982
|
├── export 'src/types.dart' |
|
983
|
└── export 'src/engine.dart' |
|
984
|
|
|
985
|
macos/ |
|
986
|
├── Classes/OndeInferencePlugin.swift ← dummy (required for CocoaPods framework) |
|
987
|
└── onde_inference.podspec ← script_phase + -force_load + SystemConfiguration |
|
988
|
|
|
989
|
ios/ |
|
990
|
├── Classes/OndeInferencePlugin.swift ← dummy (required for CocoaPods framework) |
|
991
|
└── onde_inference.podspec ← script_phase + -force_load |
|
992
|
|
|
993
|
android/ |
|
994
|
├── CMakeLists.txt ← invokes cargo build per ABI |
|
995
|
└── build.gradle ← externalNativeBuild → cmake |
|
996
|
|
|
997
|
.pubignore ← excludes rust/target/, *.a, *.so, etc. |
|
998
|
.gitignore ← excludes same + build artifacts |
|
999
|
``` |
|
1000
|
|
|
1001
|
--- |
|
1002
|
|
|
1003
|
*Last updated: July 2025 — Xcode 26 compatibility, ExternalLibrary.open pattern, |
|
1004
|
SystemConfiguration framework, .pubignore for pub.dev, NDK PATH pitfall.* |