From 0f24f23a3c99b2518f9dae178b3ed3bc43d64a4f Mon Sep 17 00:00:00 2001 From: Tovli Date: Mon, 13 Jul 2026 18:02:44 +0300 Subject: [PATCH] Add Dart-only pub.dev SDK packaging and release flow --- ({ | 0 .cargo/config.toml | 2 +- .github/workflows/bindings.yml | 83 ++- .github/workflows/release.yml | 334 +++++++-- .gitignore | 1 - Cargo.lock | 1 + Makefile | 62 +- README.md | 16 +- crates/adapters/el-engine-candle/src/lib.rs | 161 ++++- crates/adapters/el-ffi/Cargo.toml | 3 +- crates/adapters/el-ffi/README.md | 47 +- crates/adapters/el-ffi/src/frb_generated.rs | 580 ++++++++++++++++ crates/adapters/el-ffi/src/lib.rs | 161 ++++- crates/el-runtime/src/session.rs | 53 ++ ...bassembly-as-cross-platform-sdk-runtime.md | 11 +- ...9-flutter-rust-bridge-for-dart-bindings.md | 3 +- ...vider-trait-with-opt-in-frontier-egress.md | 14 +- ...gistry-release-ci-crates-io-npm-pub-dev.md | 36 +- ...ncremental-decoding-and-token-streaming.md | 2 +- ...dart-only-platform-agnostic-pub-dev-sdk.md | 110 +++ docs/adr/README.md | 11 +- packaging/dart/.gitignore | 3 + packaging/dart/README.md | 94 +++ packaging/dart/android/build.gradle | 35 + .../dart/android/src/main/AndroidManifest.xml | 1 + .../dart/android/src/main/jniLibs/README.md | 2 + packaging/dart/example/.gitignore | 45 ++ packaging/dart/example/.metadata | 33 + packaging/dart/example/README.md | 9 + packaging/dart/example/analysis_options.yaml | 28 + packaging/dart/example/android/.gitignore | 14 + .../dart/example/android/app/build.gradle.kts | 45 ++ .../android/app/src/debug/AndroidManifest.xml | 7 + .../android/app/src/main/AndroidManifest.xml | 45 ++ .../edge_intelligence_example/MainActivity.kt | 5 + .../res/drawable-v21/launch_background.xml | 12 + .../main/res/drawable/launch_background.xml | 12 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 544 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 442 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 721 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 1031 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 1443 bytes .../app/src/main/res/values-night/styles.xml | 18 + .../app/src/main/res/values/styles.xml | 18 + .../app/src/profile/AndroidManifest.xml | 7 + .../dart/example/android/build.gradle.kts | 24 + .../dart/example/android/gradle.properties | 7 + .../gradle/wrapper/gradle-wrapper.properties | 5 + .../dart/example/android/settings.gradle.kts | 26 + packaging/dart/example/dart_cli.dart | 27 + packaging/dart/example/ios/.gitignore | 34 + .../ios/Flutter/AppFrameworkInfo.plist | 24 + .../dart/example/ios/Flutter/Debug.xcconfig | 1 + .../dart/example/ios/Flutter/Release.xcconfig | 1 + .../ios/Runner.xcodeproj/project.pbxproj | 644 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 119 ++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../dart/example/ios/Runner/AppDelegate.swift | 16 + .../AppIcon.appiconset/Contents.json | 122 ++++ .../Icon-App-1024x1024@1x.png | Bin 0 -> 10932 bytes .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 0 -> 295 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 0 -> 450 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 0 -> 282 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 0 -> 462 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 0 -> 704 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 0 -> 406 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 0 -> 586 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 0 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 0 -> 1674 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 0 -> 762 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 0 -> 1226 bytes .../Icon-App-83.5x83.5@2x.png | Bin 0 -> 1418 bytes .../LaunchImage.imageset/Contents.json | 23 + .../LaunchImage.imageset/LaunchImage.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@2x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/LaunchImage@3x.png | Bin 0 -> 68 bytes .../LaunchImage.imageset/README.md | 5 + .../Runner/Base.lproj/LaunchScreen.storyboard | 37 + .../ios/Runner/Base.lproj/Main.storyboard | 26 + packaging/dart/example/ios/Runner/Info.plist | 70 ++ .../ios/Runner/Runner-Bridging-Header.h | 1 + .../example/ios/Runner/SceneDelegate.swift | 6 + .../example/ios/RunnerTests/RunnerTests.swift | 12 + packaging/dart/example/lib/main.dart | 192 ++++++ packaging/dart/example/pubspec.yaml | 23 + packaging/dart/example/test/widget_test.dart | 15 + packaging/dart/ios/Frameworks/README.md | 2 + packaging/dart/ios/edge_intelligence.podspec | 19 + .../dart/ios/edge_intelligence/Package.swift | 29 + .../edge_intelligence/EdgeIntelligence.swift | 1 + packaging/dart/lib/edge_intelligence.dart | 83 +++ packaging/dart/lib/src/runtime_loader.dart | 3 + packaging/dart/lib/src/runtime_loader_io.dart | 75 ++ .../dart/lib/src/runtime_loader_stub.dart | 10 + .../dart/lib/src/runtime_loader_web.dart | 9 + packaging/dart/pubspec.yaml | 24 + packaging/dart/test/missing_runtime_test.dart | 8 + .../test/packaged_runtime_smoke_test.dart | 9 + packaging/flutter/README.md | 24 - scripts/build-ios-xcframework.sh | 84 +++ scripts/verify-android-page-alignment.py | 54 ++ tests/check_ci_workflows.py | 171 ++++- 109 files changed, 3993 insertions(+), 242 deletions(-) create mode 100644 ({ create mode 100644 crates/adapters/el-ffi/src/frb_generated.rs create mode 100644 docs/adr/ADR-024-dart-only-platform-agnostic-pub-dev-sdk.md create mode 100644 packaging/dart/.gitignore create mode 100644 packaging/dart/README.md create mode 100644 packaging/dart/android/build.gradle create mode 100644 packaging/dart/android/src/main/AndroidManifest.xml create mode 100644 packaging/dart/android/src/main/jniLibs/README.md create mode 100644 packaging/dart/example/.gitignore create mode 100644 packaging/dart/example/.metadata create mode 100644 packaging/dart/example/README.md create mode 100644 packaging/dart/example/analysis_options.yaml create mode 100644 packaging/dart/example/android/.gitignore create mode 100644 packaging/dart/example/android/app/build.gradle.kts create mode 100644 packaging/dart/example/android/app/src/debug/AndroidManifest.xml create mode 100644 packaging/dart/example/android/app/src/main/AndroidManifest.xml create mode 100644 packaging/dart/example/android/app/src/main/kotlin/com/tovli/edge_intelligence_example/MainActivity.kt create mode 100644 packaging/dart/example/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 packaging/dart/example/android/app/src/main/res/drawable/launch_background.xml create mode 100644 packaging/dart/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 packaging/dart/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 packaging/dart/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 packaging/dart/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 packaging/dart/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 packaging/dart/example/android/app/src/main/res/values-night/styles.xml create mode 100644 packaging/dart/example/android/app/src/main/res/values/styles.xml create mode 100644 packaging/dart/example/android/app/src/profile/AndroidManifest.xml create mode 100644 packaging/dart/example/android/build.gradle.kts create mode 100644 packaging/dart/example/android/gradle.properties create mode 100644 packaging/dart/example/android/gradle/wrapper/gradle-wrapper.properties create mode 100644 packaging/dart/example/android/settings.gradle.kts create mode 100644 packaging/dart/example/dart_cli.dart create mode 100644 packaging/dart/example/ios/.gitignore create mode 100644 packaging/dart/example/ios/Flutter/AppFrameworkInfo.plist create mode 100644 packaging/dart/example/ios/Flutter/Debug.xcconfig create mode 100644 packaging/dart/example/ios/Flutter/Release.xcconfig create mode 100644 packaging/dart/example/ios/Runner.xcodeproj/project.pbxproj create mode 100644 packaging/dart/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 packaging/dart/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packaging/dart/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 packaging/dart/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 packaging/dart/example/ios/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 packaging/dart/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 packaging/dart/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 packaging/dart/example/ios/Runner/AppDelegate.swift create mode 100644 packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png create mode 100644 packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png create mode 100644 packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png create mode 100644 packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png create mode 100644 packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png create mode 100644 packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png create mode 100644 packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png create mode 100644 packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png create mode 100644 packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png create mode 100644 packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png create mode 100644 packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png create mode 100644 packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png create mode 100644 packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png create mode 100644 packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png create mode 100644 packaging/dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json create mode 100644 packaging/dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png create mode 100644 packaging/dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png create mode 100644 packaging/dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png create mode 100644 packaging/dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md create mode 100644 packaging/dart/example/ios/Runner/Base.lproj/LaunchScreen.storyboard create mode 100644 packaging/dart/example/ios/Runner/Base.lproj/Main.storyboard create mode 100644 packaging/dart/example/ios/Runner/Info.plist create mode 100644 packaging/dart/example/ios/Runner/Runner-Bridging-Header.h create mode 100644 packaging/dart/example/ios/Runner/SceneDelegate.swift create mode 100644 packaging/dart/example/ios/RunnerTests/RunnerTests.swift create mode 100644 packaging/dart/example/lib/main.dart create mode 100644 packaging/dart/example/pubspec.yaml create mode 100644 packaging/dart/example/test/widget_test.dart create mode 100644 packaging/dart/ios/Frameworks/README.md create mode 100644 packaging/dart/ios/edge_intelligence.podspec create mode 100644 packaging/dart/ios/edge_intelligence/Package.swift create mode 100644 packaging/dart/ios/edge_intelligence/Sources/edge_intelligence/EdgeIntelligence.swift create mode 100644 packaging/dart/lib/edge_intelligence.dart create mode 100644 packaging/dart/lib/src/runtime_loader.dart create mode 100644 packaging/dart/lib/src/runtime_loader_io.dart create mode 100644 packaging/dart/lib/src/runtime_loader_stub.dart create mode 100644 packaging/dart/lib/src/runtime_loader_web.dart create mode 100644 packaging/dart/pubspec.yaml create mode 100644 packaging/dart/test/missing_runtime_test.dart create mode 100644 packaging/dart/test/packaged_runtime_smoke_test.dart delete mode 100644 packaging/flutter/README.md create mode 100644 scripts/build-ios-xcframework.sh create mode 100644 scripts/verify-android-page-alignment.py diff --git a/({ b/({ new file mode 100644 index 0000000..e69de29 diff --git a/.cargo/config.toml b/.cargo/config.toml index 7b25cd7..e55558e 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -6,7 +6,7 @@ rustflags = ["-C", "target-feature=+neon,+dotprod,+i8mm"] # Linker: set CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER to the NDK clang path. # In CI this is set by nttld/setup-ndk; locally: # export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=\ -# $ANDROID_NDK_HOME/toolchains/llvm/prebuilt/-x86_64/bin/aarch64-linux-android35-clang +# $ANDROID_NDK_HOME/toolchains/llvm/prebuilt/-x86_64/bin/aarch64-linux-android24-clang [target.aarch64-apple-ios] rustflags = ["-C", "target-feature=+neon,+dotprod,+i8mm"] diff --git a/.github/workflows/bindings.yml b/.github/workflows/bindings.yml index fee636c..5157d96 100644 --- a/.github/workflows/bindings.yml +++ b/.github/workflows/bindings.yml @@ -8,8 +8,11 @@ on: - 'crates/el-core/**' - 'crates/el-runtime/**' - 'crates/adapters/el-engine-candle/**' + - 'packaging/dart/**' - '.cargo/config.toml' - 'Makefile' + - 'scripts/build-ios-xcframework.sh' + - 'scripts/verify-android-page-alignment.py' - 'scripts/install-cargo-tool.sh' - 'scripts/retry-command.sh' - 'tests/check_ci_workflows.py' @@ -22,10 +25,13 @@ on: - 'crates/el-core/**' - 'crates/el-runtime/**' - 'crates/adapters/el-engine-candle/**' + - 'packaging/dart/**' - '.cargo/config.toml' - 'Cargo.lock' - 'Cargo.toml' - 'Makefile' + - 'scripts/build-ios-xcframework.sh' + - 'scripts/verify-android-page-alignment.py' - 'scripts/install-cargo-tool.sh' - 'scripts/retry-command.sh' - 'tests/check_ci_workflows.py' @@ -53,42 +59,77 @@ jobs: # ── Android: aarch64-linux-android cross-compile ────────────────────────── android: - name: Android aarch64 cross-compile + name: Android armv7 + arm64 + x64 cross-compile runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: - targets: aarch64-linux-android + targets: armv7-linux-androideabi,aarch64-linux-android,x86_64-linux-android - uses: Swatinem/rust-cache@v2 - uses: nttld/setup-ndk@v1 id: setup-ndk with: - ndk-version: r27c + ndk-version: r28c + - name: Cross-compile el-ffi → armv7-linux-androideabi + env: + CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER: >- + ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi24-clang + CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_AR: >- + ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar + CC_armv7_linux_androideabi: >- + ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi24-clang + AR_armv7_linux_androideabi: >- + ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar + RANLIB_armv7_linux_androideabi: >- + ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ranlib + run: cargo build --manifest-path crates/adapters/el-ffi/Cargo.toml --target armv7-linux-androideabi --release - name: Cross-compile el-ffi → aarch64-linux-android env: CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER: >- - ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android35-clang + ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang CARGO_TARGET_AARCH64_LINUX_ANDROID_AR: >- ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar CC_aarch64_linux_android: >- - ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android35-clang + ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang AR_aarch64_linux_android: >- ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar RANLIB_aarch64_linux_android: >- ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ranlib run: cargo build --manifest-path crates/adapters/el-ffi/Cargo.toml --target aarch64-linux-android --release + - name: Cross-compile el-ffi → x86_64-linux-android + env: + CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER: >- + ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android24-clang + CARGO_TARGET_X86_64_LINUX_ANDROID_AR: >- + ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar + CC_x86_64_linux_android: >- + ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android24-clang + AR_x86_64_linux_android: >- + ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar + RANLIB_x86_64_linux_android: >- + ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ranlib + run: cargo build --manifest-path crates/adapters/el-ffi/Cargo.toml --target x86_64-linux-android --release + - name: Verify Android 16 KB page alignment + env: + NDK_PATH: ${{ steps.setup-ndk.outputs.ndk-path }} + run: | + python3 scripts/verify-android-page-alignment.py \ + "$NDK_PATH/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-readelf" \ + target/aarch64-linux-android/release/libel_ffi.so \ + target/x86_64-linux-android/release/libel_ffi.so + - name: Install uniffi-bindgen-react-native run: npm install --global uniffi-bindgen-react-native@0.31.0-3 - name: Codegen — React Native (TypeScript + JSI) env: CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER: >- - ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android35-clang + ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang CARGO_TARGET_AARCH64_LINUX_ANDROID_AR: >- ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar CC_aarch64_linux_android: >- - ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android35-clang + ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang AR_aarch64_linux_android: >- ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar RANLIB_aarch64_linux_android: >- @@ -102,16 +143,16 @@ jobs: # ── iOS: aarch64-apple-ios cross-compile ────────────────────────────────── ios: - name: iOS aarch64 cross-compile + name: iOS device + simulator XCFramework runs-on: macos-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: - targets: aarch64-apple-ios + targets: aarch64-apple-ios,aarch64-apple-ios-sim,x86_64-apple-ios - uses: Swatinem/rust-cache@v2 - - name: Cross-compile el-ffi → aarch64-apple-ios - run: cargo build --manifest-path crates/adapters/el-ffi/Cargo.toml --target aarch64-apple-ios --release + - name: Build el-ffi iOS XCFramework + run: bash scripts/build-ios-xcframework.sh # ── Web: wasm32-unknown-unknown + wasm-pack ─────────────────────────────── wasm: @@ -146,9 +187,9 @@ jobs: name: web-bindings path: out/web/ - # ── Flutter: Dart binding codegen ──────────────────────────────────────── - flutter: - name: Flutter/Dart codegen + # ── Dart binding codegen ───────────────────────────────────────────────── + dart: + name: Dart codegen runs-on: ubuntu-latest env: FRB_CODEGEN_VERSION: 2.12.0 @@ -158,14 +199,18 @@ jobs: - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - uses: dart-lang/setup-dart@v1 - - name: Install Flutter codegen cargo tools + - name: Install Dart codegen cargo tools run: | bash scripts/install-cargo-tool.sh flutter_rust_bridge_codegen flutter_rust_bridge_codegen "$FRB_CODEGEN_VERSION" bash scripts/install-cargo-tool.sh cargo-expand cargo-expand "$CARGO_EXPAND_VERSION" - - name: Codegen — Flutter/Dart - run: make codegen-flutter + - name: Codegen — Dart + run: make codegen-dart + - name: Assert Dart FRB Rust glue is committed + run: | + git ls-files --error-unmatch crates/adapters/el-ffi/src/frb_generated.rs + git diff --exit-code -- crates/adapters/el-ffi/src/frb_generated.rs - uses: actions/upload-artifact@v4 if: github.event_name != 'pull_request' with: - name: flutter-bindings - path: out/flutter/ + name: dart-bindings + path: out/dart/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 31d5dbe..59b62a1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -94,42 +94,76 @@ jobs: # ── Stage 2: Build artefacts ─────────────────────────────────────────────── build-android: - name: Build Android (aarch64) + RN codegen + name: Build Android (armv7 + arm64 + x64) + RN codegen needs: verify runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: - targets: aarch64-linux-android + targets: armv7-linux-androideabi,aarch64-linux-android,x86_64-linux-android - uses: Swatinem/rust-cache@v2 - uses: nttld/setup-ndk@v1 id: setup-ndk with: - ndk-version: r27c + ndk-version: r28c + - name: Cross-compile el-ffi → armv7-linux-androideabi + env: + CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER: >- + ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi24-clang + CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_AR: >- + ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar + CC_armv7_linux_androideabi: >- + ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi24-clang + AR_armv7_linux_androideabi: >- + ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar + RANLIB_armv7_linux_androideabi: >- + ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ranlib + run: cargo build --manifest-path crates/adapters/el-ffi/Cargo.toml --target armv7-linux-androideabi --release - name: Cross-compile el-ffi → aarch64-linux-android env: CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER: >- - ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android35-clang + ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang CARGO_TARGET_AARCH64_LINUX_ANDROID_AR: >- ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar CC_aarch64_linux_android: >- - ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android35-clang + ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang AR_aarch64_linux_android: >- ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar RANLIB_aarch64_linux_android: >- ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ranlib run: cargo build --manifest-path crates/adapters/el-ffi/Cargo.toml --target aarch64-linux-android --release + - name: Cross-compile el-ffi → x86_64-linux-android + env: + CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER: >- + ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android24-clang + CARGO_TARGET_X86_64_LINUX_ANDROID_AR: >- + ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar + CC_x86_64_linux_android: >- + ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/x86_64-linux-android24-clang + AR_x86_64_linux_android: >- + ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar + RANLIB_x86_64_linux_android: >- + ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ranlib + run: cargo build --manifest-path crates/adapters/el-ffi/Cargo.toml --target x86_64-linux-android --release + - name: Verify Android 16 KB page alignment + env: + NDK_PATH: ${{ steps.setup-ndk.outputs.ndk-path }} + run: | + python3 scripts/verify-android-page-alignment.py \ + "$NDK_PATH/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-readelf" \ + target/aarch64-linux-android/release/libel_ffi.so \ + target/x86_64-linux-android/release/libel_ffi.so - name: Install uniffi-bindgen-react-native run: npm install --global uniffi-bindgen-react-native@0.31.0-3 - name: Codegen — React Native (TypeScript + JSI) env: CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER: >- - ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android35-clang + ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang CARGO_TARGET_AARCH64_LINUX_ANDROID_AR: >- ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar CC_aarch64_linux_android: >- - ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android35-clang + ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/aarch64-linux-android24-clang AR_aarch64_linux_android: >- ${{ steps.setup-ndk.outputs.ndk-path }}/toolchains/llvm/prebuilt/linux-x86_64/bin/llvm-ar RANLIB_aarch64_linux_android: >- @@ -140,28 +174,103 @@ jobs: with: name: rn-bindings path: out/rn/ - - name: Upload Android native library + - name: Stage Android native libraries + run: | + mkdir -p out/android-libs/armeabi-v7a out/android-libs/arm64-v8a out/android-libs/x86_64 + cp target/armv7-linux-androideabi/release/libel_ffi.so out/android-libs/armeabi-v7a/libel_ffi.so + cp target/aarch64-linux-android/release/libel_ffi.so out/android-libs/arm64-v8a/libel_ffi.so + cp target/x86_64-linux-android/release/libel_ffi.so out/android-libs/x86_64/libel_ffi.so + - name: Upload Android native libraries uses: actions/upload-artifact@v4 with: - name: android-lib - path: target/aarch64-linux-android/release/libel_ffi.so + name: android-libs + path: out/android-libs/ + if-no-files-found: error build-ios: - name: Build iOS (aarch64) + name: Build iOS (device + simulator) needs: verify runs-on: macos-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: - targets: aarch64-apple-ios + targets: aarch64-apple-ios,aarch64-apple-ios-sim,x86_64-apple-ios - uses: Swatinem/rust-cache@v2 - - name: Cross-compile el-ffi → aarch64-apple-ios - run: cargo build --manifest-path crates/adapters/el-ffi/Cargo.toml --target aarch64-apple-ios --release + - name: Build el-ffi iOS XCFramework + run: bash scripts/build-ios-xcframework.sh + - name: Stage iOS XCFramework + run: | + mkdir -p target/ios-artifact + cp -R target/ios-xcframework/el_ffi.xcframework target/ios-artifact/el_ffi.xcframework - uses: actions/upload-artifact@v4 with: name: ios-lib path: target/aarch64-apple-ios/release/libel_ffi.a + if-no-files-found: error + - uses: actions/upload-artifact@v4 + with: + name: ios-xcframework + path: target/ios-artifact/ + if-no-files-found: error + + build-linux-x64: + name: Build Linux desktop (x64) + needs: verify + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Build el-ffi → x86_64-unknown-linux-gnu + run: cargo build --manifest-path crates/adapters/el-ffi/Cargo.toml --release + - uses: actions/upload-artifact@v4 + with: + name: linux-x64-lib + path: target/release/libel_ffi.so + if-no-files-found: error + + build-macos-universal: + name: Build macOS desktop (universal) + needs: verify + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-apple-darwin,aarch64-apple-darwin + - uses: Swatinem/rust-cache@v2 + - name: Build el-ffi → macOS universal dylib + run: | + cargo build --manifest-path crates/adapters/el-ffi/Cargo.toml --target x86_64-apple-darwin --release + cargo build --manifest-path crates/adapters/el-ffi/Cargo.toml --target aarch64-apple-darwin --release + mkdir -p target/universal-apple-darwin/release + lipo -create \ + target/x86_64-apple-darwin/release/libel_ffi.dylib \ + target/aarch64-apple-darwin/release/libel_ffi.dylib \ + -output target/universal-apple-darwin/release/libel_ffi.dylib + lipo -info target/universal-apple-darwin/release/libel_ffi.dylib + - uses: actions/upload-artifact@v4 + with: + name: macos-universal-lib + path: target/universal-apple-darwin/release/libel_ffi.dylib + if-no-files-found: error + + build-windows-x64: + name: Build Windows desktop (x64) + needs: verify + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Build el-ffi → x86_64-pc-windows-msvc + run: cargo build --manifest-path crates/adapters/el-ffi/Cargo.toml --release + - uses: actions/upload-artifact@v4 + with: + name: windows-x64-lib + path: target/release/el_ffi.dll + if-no-files-found: error build-wasm: name: Build WASM (web surface) @@ -196,8 +305,8 @@ jobs: path: out/web/ if-no-files-found: error - build-flutter: - name: Flutter/Dart codegen + build-dart: + name: Dart codegen needs: verify runs-on: ubuntu-latest env: @@ -208,22 +317,57 @@ jobs: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable - uses: Swatinem/rust-cache@v2 - - uses: dart-lang/setup-dart@v1 - - name: Install Flutter codegen cargo tools + - uses: subosito/flutter-action@v2 + with: + channel: stable + cache: true + - name: Install Dart codegen cargo tools run: | bash scripts/install-cargo-tool.sh flutter_rust_bridge_codegen flutter_rust_bridge_codegen "$FRB_CODEGEN_VERSION" bash scripts/install-cargo-tool.sh cargo-expand cargo-expand "$CARGO_EXPAND_VERSION" - - name: Codegen — Flutter/Dart - run: make codegen-flutter + - name: Codegen — Dart + run: make codegen-dart + - name: Assert Dart FRB Rust glue is committed + run: | + git ls-files --error-unmatch crates/adapters/el-ffi/src/frb_generated.rs + git diff --exit-code -- crates/adapters/el-ffi/src/frb_generated.rs + - name: Resolve Flutter plugin dependencies + run: flutter pub get --no-example + working-directory: out/dart - name: Stamp pub.dev version from tag run: | sed -i "s/^version: .*/version: $VERSION/" pubspec.yaml printf '# Changelog\n\n## %s\n\n- Release %s.\n' "$VERSION" "$VERSION" > CHANGELOG.md - working-directory: out/flutter + working-directory: out/dart - uses: actions/upload-artifact@v4 with: - name: flutter-bindings - path: out/flutter/ + name: dart-bindings + path: out/dart/ + if-no-files-found: error + + verify-flutter-ios: + name: Verify Flutter iOS package + needs: [build-dart, build-ios] + runs-on: macos-latest + steps: + - uses: actions/download-artifact@v4 + with: + name: dart-bindings + path: assembly/ + - uses: actions/download-artifact@v4 + with: + name: ios-xcframework + path: assembly/ios/Frameworks/ + - uses: subosito/flutter-action@v2 + with: + channel: stable + cache: true + - name: Resolve Flutter example dependencies + run: flutter pub get + working-directory: assembly/example/ + - name: Run Flutter iOS package smoke build + run: flutter build ios --simulator --debug + working-directory: assembly/example/ # ── Stage 2b: Assemble distribution packages ────────────────────────────── # Both assembly jobs wait for the native build jobs so each package contains @@ -250,8 +394,8 @@ jobs: path: assembly/src/rn/ - uses: actions/download-artifact@v4 with: - name: android-lib - path: assembly/android/jniLibs/arm64-v8a/ + name: android-libs + path: assembly/android/jniLibs/ - uses: actions/download-artifact@v4 with: name: ios-lib @@ -285,9 +429,19 @@ jobs: name: npm-package path: assembly/ - assemble-flutter: + assemble-dart: name: Assemble edge_intelligence pub.dev package - needs: [verify, build-android, build-ios, build-wasm, build-flutter] + needs: + [ + verify, + build-linux-x64, + build-macos-universal, + build-windows-x64, + build-dart, + build-android, + build-ios, + verify-flutter-ios, + ] runs-on: ubuntu-latest env: VERSION: ${{ needs.verify.outputs.version }} @@ -295,25 +449,118 @@ jobs: - uses: actions/checkout@v4 - uses: actions/download-artifact@v4 with: - name: flutter-bindings + name: dart-bindings path: assembly/ - uses: actions/download-artifact@v4 with: - name: android-lib - path: assembly/android/src/main/jniLibs/arm64-v8a/ + name: linux-x64-lib + path: native-artifacts/linux/x64/ - uses: actions/download-artifact@v4 with: - name: ios-lib - path: assembly/ios/ + name: macos-universal-lib + path: native-artifacts/macos/ - uses: actions/download-artifact@v4 with: - name: wasm-output - path: assembly/pkg/ - - uses: dart-lang/setup-dart@v1 + name: windows-x64-lib + path: native-artifacts/windows/x64/ + - uses: actions/download-artifact@v4 + with: + name: android-libs + path: native-artifacts/android/ + - uses: actions/download-artifact@v4 + with: + name: ios-xcframework + path: native-artifacts/ios/ + - name: Copy Dart package native runtime artifacts + run: | + mkdir -p assembly/lib/native/linux/x64 + mkdir -p assembly/lib/native/macos + mkdir -p assembly/lib/native/windows/x64 + mkdir -p assembly/android/src/main/jniLibs/armeabi-v7a + mkdir -p assembly/android/src/main/jniLibs/arm64-v8a + mkdir -p assembly/android/src/main/jniLibs/x86_64 + mkdir -p assembly/ios/Frameworks + cp native-artifacts/linux/x64/libel_ffi.so assembly/lib/native/linux/x64/libel_ffi.so + cp native-artifacts/macos/libel_ffi.dylib assembly/lib/native/macos/libel_ffi.dylib + cp native-artifacts/windows/x64/el_ffi.dll assembly/lib/native/windows/x64/el_ffi.dll + cp native-artifacts/android/armeabi-v7a/libel_ffi.so assembly/android/src/main/jniLibs/armeabi-v7a/libel_ffi.so + cp native-artifacts/android/arm64-v8a/libel_ffi.so assembly/android/src/main/jniLibs/arm64-v8a/libel_ffi.so + cp native-artifacts/android/x86_64/libel_ffi.so assembly/android/src/main/jniLibs/x86_64/libel_ffi.so + cp -R native-artifacts/ios/el_ffi.xcframework assembly/ios/Frameworks/el_ffi.xcframework + - name: Assert native library artifacts are non-empty + run: | + python3 - <<'PY' + import sys + from pathlib import Path + MIN_BYTES = 10 * 1024 # A real shared library is always larger than 10 KiB + artifacts = [ + "assembly/lib/native/linux/x64/libel_ffi.so", + "assembly/lib/native/macos/libel_ffi.dylib", + "assembly/lib/native/windows/x64/el_ffi.dll", + "assembly/android/src/main/jniLibs/armeabi-v7a/libel_ffi.so", + "assembly/android/src/main/jniLibs/arm64-v8a/libel_ffi.so", + "assembly/android/src/main/jniLibs/x86_64/libel_ffi.so", + ] + errors = [] + for path_str in artifacts: + p = Path(path_str) + size = p.stat().st_size + print(f"{p.name}: {size} bytes") + if size < MIN_BYTES: + errors.append(f"{p}: {size} bytes (expected >= {MIN_BYTES}; empty or corrupt artifact?)") + if errors: + raise SystemExit("\n".join(["Native library size check failed:"] + errors)) + PY + - uses: subosito/flutter-action@v2 + with: + channel: stable + cache: true - name: Assert pub.dev package includes README.md run: bash scripts/assert-release-readmes.sh pub assembly - - name: dart pub publish --dry-run (validation gate) - run: dart pub publish --dry-run + - name: Check pub.dev package size + run: | + python3 - <<'PY' + from pathlib import Path + + # pub.dev enforces a ~100 MB compressed archive limit. + # Native binaries compress poorly, so guard at 100 MiB uncompressed + # to catch obviously oversized packages before the dry-run publish. + max_uncompressed_bytes = 100 * 1024 * 1024 + total = sum(path.stat().st_size for path in Path(".").rglob("*") if path.is_file()) + print(f"pub.dev package uncompressed size: {total} bytes") + if total > max_uncompressed_bytes: + raise SystemExit( + "pub.dev package exceeds 100 MiB uncompressed (pub.dev cap is " + "~100 MB compressed; native binaries compress poorly); split " + "runtime artifacts or exclude unnecessary files" + ) + PY + working-directory: assembly/ + - name: Resolve Flutter plugin dependencies + run: flutter pub get --no-example + working-directory: assembly/ + - name: Run Dart package runtime smoke tests + run: | + dart test test/packaged_runtime_smoke_test.dart + mv lib/native/linux/x64/libel_ffi.so lib/native/linux/x64/libel_ffi.so.missing + trap 'mv lib/native/linux/x64/libel_ffi.so.missing lib/native/linux/x64/libel_ffi.so' EXIT + dart test test/missing_runtime_test.dart + working-directory: assembly/ + - name: Run Flutter example tests + run: | + flutter pub get + flutter analyze + flutter test + working-directory: assembly/example/ + - name: Run Flutter Android package smoke build + run: | + flutter build apk --debug --target-platform android-arm,android-arm64,android-x64 + unzip -l build/app/outputs/flutter-apk/app-debug.apk | grep 'lib/armeabi-v7a/libel_ffi.so' + unzip -l build/app/outputs/flutter-apk/app-debug.apk | grep 'lib/arm64-v8a/libel_ffi.so' + unzip -l build/app/outputs/flutter-apk/app-debug.apk | grep 'lib/x86_64/libel_ffi.so' + working-directory: assembly/example/ + - name: flutter pub publish --dry-run (validation gate) + run: flutter pub publish --dry-run working-directory: assembly/ - uses: actions/upload-artifact@v4 with: @@ -325,11 +572,11 @@ jobs: # first publish — gate it on BOTH assembly jobs (which include the pub.dev # dry-run and npm package assembly). Each registry then gates on the previous # one: if crates fails, npm and pub are not touched; if npm fails, pub is not. - # publish-crates → assemble-{npm,flutter} transitively covers all build jobs. + # publish-crates → assemble-{npm,dart} transitively covers all build jobs. publish-crates: name: Publish → crates.io - needs: [verify, assemble-npm, assemble-flutter] + needs: [verify, assemble-npm, assemble-dart] runs-on: ubuntu-latest env: VERSION: ${{ needs.verify.outputs.version }} @@ -366,14 +613,17 @@ jobs: publish-pub: name: Publish → pub.dev - needs: [publish-npm, assemble-flutter] + needs: [publish-npm, assemble-dart] runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v4 with: name: pub-package path: pub-package/ - - uses: dart-lang/setup-dart@v1 + - uses: subosito/flutter-action@v2 + with: + channel: stable + cache: true - name: Assert pub.dev package artifact includes README.md run: test -s pub-package/README.md - name: Configure pub.dev credentials @@ -381,5 +631,5 @@ jobs: mkdir -p ~/.config/dart echo '${{ secrets.PUB_CREDENTIALS }}' > ~/.config/dart/pub-credentials.json - name: Publish edge_intelligence - run: dart pub publish --force + run: flutter pub publish --force working-directory: pub-package/ diff --git a/.gitignore b/.gitignore index a6f4dcc..10f1e72 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ target/ out/ # Local model assets for the el-chat test client (downloaded, not committed). models/ -crates/adapters/el-ffi/src/frb_generated.rs # Claude Code local state (worktrees, settings.local.json) .claude/ benchmarks/data/ diff --git a/Cargo.lock b/Cargo.lock index 8bce09d..02245f5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -840,6 +840,7 @@ dependencies = [ name = "el-ffi" version = "0.3.11" dependencies = [ + "anyhow", "el-cloud", "el-core", "el-engine-candle", diff --git a/Makefile b/Makefile index 086c46b..d3d5ed9 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,13 @@ IOS_TARGET := aarch64-apple-ios FFI := --manifest-path crates/adapters/el-ffi/Cargo.toml OUT := out FRB_VERSION := 2.12.0 +FRB_RUST_OUTPUT := crates/adapters/el-ffi/src/frb_generated.rs + +# FRB 2.12 canonicalizes the crate root with the Windows extended-path prefix +# but does not do the same for a relative rust-output path. +ifeq ($(OS),Windows_NT) +FRB_RUST_OUTPUT := $(shell python -c "from pathlib import Path; print('\\\\?\\' + str(Path('crates/adapters/el-ffi/src/frb_generated.rs').resolve()))") +endif ifneq ($(strip $(CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER)),) ANDROID_TOOLCHAIN_BIN := $(dir $(CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER)) @@ -17,7 +24,7 @@ export AR_aarch64_linux_android export RANLIB_aarch64_linux_android endif -.PHONY: check build-android build-ios build-wasm codegen-rn codegen-flutter codegen-web bindings +.PHONY: check build-android build-ios build-ios-xcframework build-wasm codegen-rn codegen-dart codegen-flutter codegen-web bindings # ─── Workspace ─────────────────────────────────────────────────────────────── @@ -36,7 +43,7 @@ check: # iOS: rustup target add aarch64-apple-ios (macOS + Xcode required) # wasm: cargo install wasm-pack -## Cross-compile el-ffi as a shared library for Android (aarch64, API 35). +## Cross-compile el-ffi as a shared library for Android (aarch64; linker selects the API). build-android: cargo build $(FFI) --target $(ANDROID_TARGET) --release @@ -44,6 +51,10 @@ build-android: build-ios: cargo build $(FFI) --target $(IOS_TARGET) --release +## Build a dynamic iOS XCFramework for Flutter device and simulator hosts. +build-ios-xcframework: + bash scripts/build-ios-xcframework.sh + ## Build el-ffi as a WASM + wasm-bindgen ESM package. build-wasm: wasm-pack build crates/adapters/el-ffi \ @@ -54,7 +65,7 @@ build-wasm: # # Prerequisites (install once) # RN: npm install --global uniffi-bindgen-react-native@0.31.0-3 -# Flutter: cargo install flutter_rust_bridge_codegen --version $(FRB_VERSION) --locked +# Dart: cargo install flutter_rust_bridge_codegen --version $(FRB_VERSION) --locked # Web: (wasm-pack, covered by build-wasm) ## Generate React Native JSI bindings (TypeScript + C++). @@ -69,40 +80,45 @@ codegen-rn: build-android --no-format \ target/$(ANDROID_TARGET)/release/libel_ffi.so -## Generate Flutter/Dart bindings via flutter_rust_bridge v2 codegen. -codegen-flutter: - @mkdir -p $(OUT)/flutter/lib - @printf '%s\n' \ - 'name: edge_intelligence' \ - 'version: 0.1.0' \ - 'description: Generated Flutter bindings for Edge Intelligence.' \ - 'repository: https://github.com/Tovli/EdgeIntelligence' \ - 'environment:' \ - ' sdk: ">=3.3.0 <4.0.0"' \ - 'dependencies:' \ - ' flutter_rust_bridge: ^$(FRB_VERSION)' \ - > $(OUT)/flutter/pubspec.yaml - @cp LICENSE $(OUT)/flutter/LICENSE - @cp packaging/flutter/README.md $(OUT)/flutter/README.md +## Generate Dart bindings via flutter_rust_bridge v2 codegen. +codegen-dart: + @mkdir -p $(OUT)/dart/lib/src + @rm -rf $(OUT)/dart/android $(OUT)/dart/ios $(OUT)/dart/example + @cp packaging/dart/pubspec.yaml $(OUT)/dart/pubspec.yaml + @cp LICENSE $(OUT)/dart/LICENSE + @cp packaging/dart/README.md $(OUT)/dart/README.md + @cp packaging/dart/lib/edge_intelligence.dart $(OUT)/dart/lib/edge_intelligence.dart + @cp -R packaging/dart/android $(OUT)/dart/android + @cp -R packaging/dart/ios $(OUT)/dart/ios + @cp -R packaging/dart/example $(OUT)/dart/example + @cp -R packaging/dart/test $(OUT)/dart/test @printf '%s\n' \ '# Changelog' \ '' \ '## 0.1.0' \ '' \ 'Release notes are tracked in the Edge Intelligence repository tags and GitHub releases.' \ - > $(OUT)/flutter/CHANGELOG.md + > $(OUT)/dart/CHANGELOG.md flutter_rust_bridge_codegen generate \ --rust-root crates/adapters/el-ffi \ - --rust-input crate:: \ - --dart-root $(OUT)/flutter \ - --dart-output $(OUT)/flutter/lib \ + --rust-input crate::dart_api \ + --rust-output "$(FRB_RUST_OUTPUT)" \ + --dart-root $(OUT)/dart \ + --dart-output $(OUT)/dart/lib/src \ --no-add-mod-to-lib \ --no-auto-upgrade-dependency \ --no-deps-check \ --no-dart-format + @cp packaging/dart/lib/src/runtime_loader*.dart $(OUT)/dart/lib/src/ + @dart format $(OUT)/dart/lib $(OUT)/dart/example/lib $(OUT)/dart/example/dart_cli.dart >/dev/null + +## Compatibility alias for older automation; prefer codegen-dart. +codegen-flutter: codegen-dart + @rm -rf $(OUT)/flutter + @cp -R $(OUT)/dart $(OUT)/flutter ## Build WASM output (identical to build-wasm; alias for consistency). codegen-web: build-wasm ## Run all three codegen surfaces. -bindings: codegen-rn codegen-flutter codegen-web +bindings: codegen-rn codegen-dart codegen-web diff --git a/README.md b/README.md index 60da119..401db09 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ comes from three layers, each with a clear entry point: | Layer | Crate · symbol | Where it fits | |-------|----------------|---------------| -| **Device SDK facade** | [`el-ffi`](crates/adapters/el-ffi) · `EdgeLlm` | The composition root shipped to devices. Wires the local engine and opt-in cloud behind one flat API and projects it to React Native (UniFFI/JSI), Flutter (FRB), and Web (wasm-bindgen). **Start here to build an app.** | +| **Device SDK facade** | [`el-ffi`](crates/adapters/el-ffi) · `EdgeLlm` | The composition root shipped to devices. Wires the local engine and opt-in cloud behind one flat API and projects it to React Native (UniFFI/JSI), Dart/pub.dev desktop and Flutter mobile runtimes (FRB-generated Dart), and Web/npm (wasm-bindgen). **Start here to build an app.** | | **Rust API seam** | [`el-core`](crates/el-core) · `LlmProvider` | The single trait every backend implements and every Rust consumer calls (ADR-010). **Start here to embed the SDK in Rust.** | | **Orchestrator** | [`el-runtime`](crates/el-runtime) · `InferenceSession` | Composes provenance, memory, safety, and grammar into the decode loop — the engine the providers drive. | @@ -95,11 +95,12 @@ comes from three layers, each with a clear entry point:
Which entry point should I use? -- **Building a mobile or web app** → use [`el-ffi`](crates/adapters/el-ffi)'s +- **Building a device, desktop, mobile, or web app** → use [`el-ffi`](crates/adapters/el-ffi)'s `EdgeLlm`. Construct `EdgeLlm::local(model_uri)` (air-gapped) or - `EdgeLlm::cloud(model, api_key)` (opt-in), then call `ask(...)` / - `ask_stream(...)`. The crate compiles to a native library and a wasm package - and ships generated TypeScript/Dart bindings. + `EdgeLlm::cloud(model, api_key)` (opt-in), then call `ask(...)`; use + `ask_stream_cb(...)` from React Native, `askStream(...)` from Dart, or the + wasm-bindgen web stream surface from npm. The crate compiles to native and + wasm artifacts and ships generated TypeScript/Dart bindings. - **Embedding the SDK in Rust** → construct a concrete provider and talk to it through [`el_core::LlmProvider`](crates/el-core): `el_engine_candle::QwenChatProvider` for on-device chat, or `el_cloud::CloudProvider` for a frontier backend. @@ -255,7 +256,7 @@ identify the runnable clients. | [`crates/adapters/el-provenance-ed25519`](crates/adapters/el-provenance-ed25519) | Real ED25519 signature verification | Implemented and tested | | [`crates/adapters/el-engine-candle`](crates/adapters/el-engine-candle) | Candle inference adapter: engine-seam proof plus a real Qwen2 transformer engine and chat provider | Implemented; real on-device chat | | [`crates/adapters/el-cloud`](crates/adapters/el-cloud) | Opt-in OpenAI-compatible provider backend | Implemented; egress opt-in at construction | -| [`crates/adapters/el-ffi`](crates/adapters/el-ffi) | **Device SDK facade (`EdgeLlm`):** Flutter / UniFFI / wasm-bindgen binding surfaces | Implemented and tested; host build is a workspace member, cross-target builds via `make` | +| [`crates/adapters/el-ffi`](crates/adapters/el-ffi) | **Device SDK facade (`EdgeLlm`):** Dart/pub.dev desktop, Flutter Android/iOS, UniFFI, and wasm-bindgen binding surfaces | Implemented and tested; host build is a workspace member, cross-target builds via `make` | | [`crates/adapters/el-grammar-llguidance`](crates/adapters/el-grammar-llguidance) | llguidance JSON-schema token masking | Implemented and tested; workspace-excluded (crates.io deps) | | [`apps/el-chat`](apps/el-chat) | Interactive chat test client; SDK-only deps, drives the runtime end-to-end | Implemented; runs real on-device chat | | [`apps/el-bench`](apps/el-bench) | Benchmark harness; SDK-only deps, replays quality/safety task sets through the runtime | Implemented; model-agnostic, reproducible | @@ -343,8 +344,9 @@ ADRs. | [ADR-006](docs/adr/ADR-006-mandatory-ed25519-model-signature-verification-load-gate.md) | Mandatory ED25519 model-signature verification | | [ADR-007](docs/adr/ADR-007-content-free-domain-events-privacy-by-construction-telemetry.md) | Content-free domain events for privacy-preserving telemetry | | [ADR-008](docs/adr/ADR-008-implement-the-sdk-in-rust-instead-of-c-cpp.md) | Rust instead of C/C++ | -| [ADR-009](docs/adr/ADR-009-flutter-rust-bridge-for-dart-bindings.md) | Flutter Rust Bridge for Dart bindings | +| [ADR-009](docs/adr/ADR-009-flutter-rust-bridge-for-dart-bindings.md) | Superseded Flutter Rust Bridge framing | | [ADR-010](docs/adr/ADR-010-unified-llm-provider-trait-with-opt-in-frontier-egress.md) | Unified local/cloud `LlmProvider` trait | +| [ADR-024](docs/adr/ADR-024-dart-only-platform-agnostic-pub-dev-sdk.md) | Dart-first pub.dev SDK with Flutter mobile runtimes | See the full index in [`docs/adr/README.md`](docs/adr/README.md). diff --git a/crates/adapters/el-engine-candle/src/lib.rs b/crates/adapters/el-engine-candle/src/lib.rs index 8c7b586..ac53c33 100644 --- a/crates/adapters/el-engine-candle/src/lib.rs +++ b/crates/adapters/el-engine-candle/src/lib.rs @@ -404,6 +404,49 @@ pub struct QwenEngine { cache_dirty: bool, } +/// Whether a failed forward has already appended the token to Candle's KV. +/// +/// `next_logits` cannot return an error to the runtime. It must therefore know +/// whether to consume the committed token before returning neutral logits. +enum ForwardOneError { + BeforeForward(EdgeError), + AfterForward(EdgeError), +} + +impl ForwardOneError { + fn into_edge(self) -> EdgeError { + match self { + Self::BeforeForward(error) | Self::AfterForward(error) => error, + } + } +} + +fn apply_committed_forward_result( + result: std::result::Result, ForwardOneError>, + token: Token, + cached: &mut Vec, + fed: &mut usize, + last_logits: &mut Vec, + vocab: usize, +) -> Option> { + match result { + Ok(logits) => { + *last_logits = logits; + cached.push(token); + *fed += 1; + None + } + Err(ForwardOneError::AfterForward(_)) => { + // Candle has already appended this token. Consume it in the Rust + // bookkeeping too so the next decode step cannot feed it twice. + cached.push(token); + *fed += 1; + Some(vec![0; vocab.max(1)]) + } + Err(ForwardOneError::BeforeForward(_)) => Some(vec![0; vocab.max(1)]), + } +} + impl QwenEngine { /// Load Qwen2 weights from a consumer-supplied GGUF file. pub fn from_path(path: impl AsRef, eos: Token) -> Result { @@ -431,32 +474,33 @@ impl QwenEngine { /// One forward over a single token at the current position; advances the KV /// cache and returns milli-logits for the next token. - fn forward_one(&mut self, token: Token) -> Result> { + fn forward_one(&mut self, token: Token) -> std::result::Result, ForwardOneError> { // Any forward may write conversation K/V into candle's cache; mark dirty // before the fallible call so a forward that fails part-way still leaves // the cache flagged for clearing (ADR-018). self.cache_dirty = true; let t_total = bench::enabled().then(std::time::Instant::now); - let input = Tensor::from_vec(vec![token], (1, 1), &self.device) - .map_err(|_| EdgeError::Engine("candle: input tensor build failed"))?; + let input = Tensor::from_vec(vec![token], (1, 1), &self.device).map_err(|_| { + ForwardOneError::BeforeForward(EdgeError::Engine("candle: input tensor build failed")) + })?; let t_model = bench::enabled().then(std::time::Instant::now); - let logits = self - .model - .forward(&input, self.index_pos) - .map_err(|_| EdgeError::Engine("candle: Qwen2 forward failed"))?; + let logits = self.model.forward(&input, self.index_pos).map_err(|_| { + ForwardOneError::BeforeForward(EdgeError::Engine("candle: Qwen2 forward failed")) + })?; + // Candle appends to its KV cache inside `forward`, before logits are + // extracted below. Keep the logical position aligned if extraction fails. + self.index_pos += 1; let model_dur = t_model.map(|t| t.elapsed()).unwrap_or_default(); - self.index_pos += 1; - let row = logits - .squeeze(0) - .map_err(|_| EdgeError::Engine("candle: squeeze logits failed"))?; - let floats = row - .to_vec1::() - .map_err(|_| EdgeError::Engine("candle: logits to vec failed"))?; + let row = logits.squeeze(0).map_err(|_| { + ForwardOneError::AfterForward(EdgeError::Engine("candle: squeeze logits failed")) + })?; + let floats = row.to_vec1::().map_err(|_| { + ForwardOneError::AfterForward(EdgeError::Engine("candle: logits to vec failed")) + })?; let out: Vec = floats.iter().map(|x| (x * 1000.0).round() as i32).collect(); - if let Some(t) = t_total { bench::record(t.elapsed(), model_dur); } @@ -471,7 +515,7 @@ impl InferenceEngine for QwenEngine { self.prompt = tokens.to_vec(); // retained for rollback replay self.cached = Vec::with_capacity(tokens.len()); for &t in tokens { - self.last_logits = self.forward_one(t)?; + self.last_logits = self.forward_one(t).map_err(ForwardOneError::into_edge)?; self.cached.push(t); } self.vocab = self.last_logits.len(); @@ -484,12 +528,16 @@ impl InferenceEngine for QwenEngine { // token the runtime just sampled and returns the next distribution. while self.fed < committed.len() { let t = committed[self.fed]; - match self.forward_one(t) { - Ok(l) => self.last_logits = l, - Err(_) => return vec![0; self.vocab.max(1)], + if let Some(fallback) = apply_committed_forward_result( + self.forward_one(t), + t, + &mut self.cached, + &mut self.fed, + &mut self.last_logits, + self.vocab, + ) { + return fallback; } - self.cached.push(t); // keep `cached` == the KV's token sequence - self.fed += 1; } self.last_logits.clone() } @@ -509,14 +557,14 @@ impl InferenceEngine for QwenEngine { // by `max_rollbacks` (ADR-012). self.index_pos = 0; self.fed = 0; + self.cached = Vec::with_capacity(self.prompt.len()); for i in 0..self.prompt.len() { let t = self.prompt[i]; - self.last_logits = self.forward_one(t)?; + self.last_logits = self.forward_one(t).map_err(ForwardOneError::into_edge)?; + // Keep cached in lock-step with index_pos so a mid-replay error + // leaves the invariant intact rather than holding the old value. + self.cached.push(t); } - // The cache now represents exactly the replayed prompt; the session - // re-feeds the retained committed prefix via `next_logits`, extending - // `cached` back in lock-step. - self.cached = self.prompt.clone(); Ok(()) } @@ -544,7 +592,7 @@ impl InferenceEngine for QwenEngine { // fails after replacing some layers, it stays set so the next call // re-clears (a partially-cleared cache is never reported as clean). self.index_pos = 0; - self.forward_one(0)?; + self.forward_one(0).map_err(ForwardOneError::into_edge)?; self.cache_dirty = false; } self.index_pos = 0; @@ -583,7 +631,7 @@ impl InferenceEngine for QwenEngine { // Fast path: the cache is an exact prefix of `full_context`. Feed only // the new suffix at the live position; the existing KV is reused as-is. for &t in &full_context[reuse..] { - self.last_logits = self.forward_one(t)?; + self.last_logits = self.forward_one(t).map_err(ForwardOneError::into_edge)?; self.cached.push(t); } } else { @@ -597,7 +645,7 @@ impl InferenceEngine for QwenEngine { self.cached = Vec::with_capacity(full_context.len()); self.last_logits = Vec::new(); for &t in full_context { - self.last_logits = self.forward_one(t)?; + self.last_logits = self.forward_one(t).map_err(ForwardOneError::into_edge)?; self.cached.push(t); } } @@ -784,6 +832,10 @@ struct ExpertState { /// GGUF gives real steering. pub struct QwenExpert { state: std::sync::Mutex, + /// Last-known vocab size, updated after each successful `logits()` call. + /// Enables returning zeros of the correct length on mutex poison, where + /// `ExpertState` is inaccessible. Initialized from the primed engine. + vocab: std::sync::atomic::AtomicUsize, /// Evidence the expert weights passed the ADR-006 load gate (R5). Held for /// the engine's lifetime; never used after construction. _permit: LoadPermit, @@ -801,8 +853,10 @@ impl QwenExpert { ) -> Result { let mut engine = QwenEngine::from_path(path, eos)?; engine.prefill(prompt)?; + let init_vocab = engine.vocab; Ok(Self { state: std::sync::Mutex::new(ExpertState { engine, fed: 0 }), + vocab: std::sync::atomic::AtomicUsize::new(init_vocab), _permit: permit, }) } @@ -818,8 +872,10 @@ impl QwenExpert { .lock() .map_err(|_| EdgeError::Engine("expert mutex poisoned"))?; st.engine.reset_cache()?; - st.engine.prefill(prompt)?; + // reset_cache succeeded: engine is blank. Set fed to 0 before prefill so + // a prefill failure leaves fed consistent with the blank engine state. st.fed = 0; + st.engine.prefill(prompt)?; Ok(()) } @@ -840,7 +896,14 @@ impl ExpertLogits for QwenExpert { fn logits(&self, committed: &[Token]) -> Vec { let mut st = match self.state.lock() { Ok(st) => st, - Err(_) => return Vec::new(), + Err(_) => { + // Mutex is poisoned: a prior call panicked while holding the lock. + // Return zeros of the last-known vocab size — a neutral expert + // signal — rather than an empty vec that would mismatch the base + // logit length in the steerer. + let v = self.vocab.load(std::sync::atomic::Ordering::Relaxed); + return vec![0; v.max(1)]; + } }; // Base rolled back? `committed` shrank below what we've fed. Re-prime the // expert to the prompt (QwenEngine::rollback replays the prompt and @@ -848,12 +911,17 @@ impl ExpertLogits for QwenExpert { // state — keeping the contrastive context aligned with the base. Cost is // bounded by `max_rollbacks` (ADR-012), same as the base engine. if committed.len() < st.fed { - if st.engine.rollback(0).is_err() { - return Vec::new(); + if st.engine.rollback(committed.len() as u32).is_err() { + return vec![0; st.engine.vocab.max(1)]; } st.fed = 0; } let out = st.engine.next_logits(committed); + let vocab = out.len(); + if vocab > 0 { + self.vocab + .store(vocab, std::sync::atomic::Ordering::Relaxed); + } st.fed = committed.len(); out } @@ -1181,8 +1249,12 @@ impl LlmProvider for QwenChatProvider { // conversation (clearing the engine's possibly half-fed cache) and // start fresh instead. _ => { + let dirty_phase = session.phase().as_str(); session.reset()?; session.load_prompt(&ports, &prompt_tokens)?; + eprintln!( + "[session] partial state ({dirty_phase}) detected — context reset, this turn starts fresh" + ); } } let d_prefill = t_prefill.map(|t| t.elapsed()).unwrap_or_default(); @@ -1701,6 +1773,29 @@ mod tests { assert_eq!(longest_common_prefix(&[5, 6], &[1, 2]), 0); // immediate divergence } + #[test] + fn post_forward_decode_error_consumes_the_committed_token_once() { + let mut cached = vec![10]; + let mut fed = 0; + let mut last_logits = vec![7, 8, 9, 10]; + + let fallback = apply_committed_forward_result( + Err(ForwardOneError::AfterForward(EdgeError::Engine( + "logit extraction failed", + ))), + 11, + &mut cached, + &mut fed, + &mut last_logits, + 4, + ); + + assert_eq!(fallback, Some(vec![0, 0, 0, 0])); + assert_eq!(cached, vec![10, 11]); + assert_eq!(fed, 1); + assert_eq!(last_logits, vec![7, 8, 9, 10]); + } + #[test] fn local_load_permit_passes_the_provenance_gate() { // The runtime requires a LoadPermit; the local-trust path must yield one diff --git a/crates/adapters/el-ffi/Cargo.toml b/crates/adapters/el-ffi/Cargo.toml index 67ed2d9..0289685 100644 --- a/crates/adapters/el-ffi/Cargo.toml +++ b/crates/adapters/el-ffi/Cargo.toml @@ -1,7 +1,7 @@ # ADR-001 / ADR-009 / ADR-010 host bindings. [package] name = "el-ffi" -description = "Host bindings: uniffi-bindgen-react-native (RN), flutter_rust_bridge v2 (Flutter), wasm-bindgen (web) — ADR-001/009." +description = "Host bindings: uniffi-bindgen-react-native (RN), flutter_rust_bridge-generated Dart, wasm-bindgen (web) — ADR-001/024." version.workspace = true edition.workspace = true license.workspace = true @@ -17,6 +17,7 @@ el-runtime = { workspace = true } # flutter_rust_bridge v2 works with plain Rust — codegen runs externally. # el-engine-candle and el-cloud wire the real LlmProvider backends (ADR-010). [target.'cfg(not(target_arch = "wasm32"))'.dependencies] +anyhow = "1" uniffi = { version = "0.28", features = ["tokio"] } flutter_rust_bridge = "=2.12.0" el-engine-candle = { workspace = true } diff --git a/crates/adapters/el-ffi/README.md b/crates/adapters/el-ffi/README.md index 0e9f17f..105da13 100644 --- a/crates/adapters/el-ffi/README.md +++ b/crates/adapters/el-ffi/README.md @@ -1,4 +1,4 @@ -# el-ffi — host bindings (React Native, Flutter, Web) +# el-ffi — host bindings (React Native, Dart, Web) One Rust API surface exported three ways, so mobile and web apps can call the SDK in their native idiom (ADR-001, ADR-009, ADR-010): @@ -6,11 +6,12 @@ SDK in their native idiom (ADR-001, ADR-009, ADR-010): | Surface | Tool | Output | |---------|------|--------| | **React Native** | `uniffi-bindgen-react-native` | TypeScript + JSI C++ + Turbo Module | -| **Flutter** | `flutter_rust_bridge` v2 | Dart opaque handle, `Future`/`Stream` | +| **Dart / Flutter / pub.dev** | `flutter_rust_bridge` v2 codegen | Dart opaque handle, `Future`/`Stream` | | **Web / npm** | `wasm-bindgen` | ESM TypeScript package via `wasm-pack` | -No `unsafe` (`#![forbid(unsafe_code)]`). The crate is `cdylib` + `staticlib` + -`lib` so each toolchain can link the form it needs. +Hand-written code denies `unsafe`; generated flutter_rust_bridge glue is the +only module allowed to contain FFI `unsafe`. The crate is `cdylib` + +`staticlib` + `lib` so each toolchain can link the form it needs. ## What it provides @@ -23,10 +24,11 @@ No `unsafe` (`#![forbid(unsafe_code)]`). The crate is `cdylib` + `staticlib` + **Native only** — see the web limitation below. `api_key` must come from the platform keystore, never embedded. - `ask(prompt) -> Result` — blocking chat. - - `ask_stream(prompt, |token| …)` — closure streaming (Flutter / FRB v2). - `ask_stream_cb(prompt, handler)` — `StreamHandler` callback streaming (React Native; UniFFI cannot export `impl FnMut`). - `reset()`. +- **Dart / Flutter / pub.dev wrappers** — `edge_llm_*` FRB functions wrapped by the Dart + facade as `EdgeLlm.local`, `EdgeLlm.cloud`, `ask`, `askStream`, and `reset`. - **`SdkError`** — a thin, FFI-safe projection of `el_core::EdgeError` (`el-core`'s `Box`/Rust-specific variants are not FFI-safe). Projects to the host language's exception type, or a JS exception on wasm. @@ -47,9 +49,6 @@ const QWEN_0_5B_GGUF: &str = "models/qwen2.5-0.5b-instruct-q4_k_m.gguf"; let sdk = EdgeLlm::local(QWEN_0_5B_GGUF.into())?; let reply = sdk.ask("Summarize edge inference in one sentence.".into())?; assert!(!reply.is_empty()); - -// Streaming (Flutter / closure form): -sdk.ask_stream("Give me two deployment tips.".into(), |fragment| print!("{fragment}"))?; # Ok::<(), el_ffi::SdkError>(()) ``` @@ -88,17 +87,28 @@ sdk.ask_stream_cb("Give me two deployment tips.", { }); ``` -## Usage (pub.dev / Flutter) +## Usage (pub.dev / Dart and Flutter) ```dart -import "package:edge_intelligence/edge_intelligence.dart"; +import "dart:io"; -final qwen05b = "/path/to/app/models/qwen2.5-0.5b-instruct-q4_k_m.gguf"; -final sdk = await EdgeLlm.local(qwen05b); +import "package:edge_intelligence/edge_intelligence.dart"; -final reply = await sdk.ask("Summarize edge inference in one sentence."); -await for (final token in sdk.askStream("Give me two deployment tips.")) { - stdout.write(token); +Future main() async { + await initEdgeIntelligence(); + + try { + const qwen05b = "/path/to/app/models/qwen2.5-0.5b-instruct-q4_k_m.gguf"; + final sdk = await EdgeLlm.local(qwen05b); + + final reply = await sdk.ask("Summarize edge inference in one sentence."); + print(reply); + await for (final token in sdk.askStream("Give me two deployment tips.")) { + stdout.write(token); + } + } finally { + disposeEdgeIntelligence(); + } } ``` @@ -110,10 +120,11 @@ codegen run via the [`Makefile`](../../../Makefile): ```sh make build-android # cargo build --target aarch64-linux-android (shared lib) make build-ios # cargo build --target aarch64-apple-ios (static lib) +make build-ios-xcframework # device + simulator framework for Flutter iOS make build-wasm # wasm-pack build → out/web ESM package make codegen-rn # React Native JSI bindings (needs build-android) -make codegen-flutter # flutter_rust_bridge v2 Dart bindings +make codegen-dart # flutter_rust_bridge v2 Dart bindings make bindings # all three surfaces ``` @@ -127,7 +138,7 @@ On `wasm32` the local path currently uses a dev-stage echo placeholder until Candle-on-wasm is wired, and the **cloud backend is not available on web** (ADR-010 amendment): `el-cloud`'s blocking HTTP transport has no wasm implementation, so `EdgeLlm.cloud` throws an explicit error there instead of -silently degrading. Use a native binding (React Native / Flutter) for cloud +silently degrading. Use a native binding (React Native / Dart native) for cloud access. ## Status @@ -142,5 +153,5 @@ out-of-band. Part of the [Edge Intelligence](../../../README.md) workspace. Realizes [ADR-001](../../../docs/adr/ADR-001-adopt-webassembly-as-cross-platform-sdk-runtime.md), -[ADR-009](../../../docs/adr/ADR-009-flutter-rust-bridge-for-dart-bindings.md), +[ADR-024](../../../docs/adr/ADR-024-dart-only-platform-agnostic-pub-dev-sdk.md), and [ADR-010](../../../docs/adr/ADR-010-unified-llm-provider-trait-with-opt-in-frontier-egress.md). diff --git a/crates/adapters/el-ffi/src/frb_generated.rs b/crates/adapters/el-ffi/src/frb_generated.rs new file mode 100644 index 0000000..a2aee52 --- /dev/null +++ b/crates/adapters/el-ffi/src/frb_generated.rs @@ -0,0 +1,580 @@ +// This file is automatically generated, so please do not edit it. +// @generated by `flutter_rust_bridge`@ 2.12.0. + +#![allow( + non_camel_case_types, + unused, + non_snake_case, + clippy::needless_return, + clippy::redundant_closure_call, + clippy::redundant_closure, + clippy::useless_conversion, + clippy::unit_arg, + clippy::unused_unit, + clippy::double_parens, + clippy::let_and_return, + clippy::too_many_arguments, + clippy::match_single_binding, + clippy::clone_on_copy, + clippy::let_unit_value, + clippy::deref_addrof, + clippy::explicit_auto_deref, + clippy::borrow_deref_ref, + clippy::uninlined_format_args, + clippy::needless_borrow +)] + +// Section: imports + +use crate::*; +use flutter_rust_bridge::for_generated::byteorder::{NativeEndian, ReadBytesExt, WriteBytesExt}; +use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; +use flutter_rust_bridge::{Handler, IntoIntoDart}; + +// Section: boilerplate + +flutter_rust_bridge::frb_generated_boilerplate!( + default_stream_sink_codec = SseCodec, + default_rust_opaque = RustOpaqueMoi, + default_rust_auto_opaque = RustAutoOpaqueMoi, +); +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0"; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -509925229; + +// Section: executor + +flutter_rust_bridge::frb_generated_default_handler!(); + +// Section: wire_funcs + +fn wire__crate__dart_api__edge_llm_ask_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "edge_llm_ask", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_sdk = , + >>::sse_decode(&mut deserializer); + let api_prompt = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || { + let mut api_sdk_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order( + vec![flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_sdk, 0, false, + )], + ); + for i in decode_indices_ { + match i { + 0 => api_sdk_guard = Some(api_sdk.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_sdk_guard = api_sdk_guard.unwrap(); + let output_ok = crate::dart_api::edge_llm_ask(&*api_sdk_guard, api_prompt)?; + Ok(output_ok) + })(), + ) + } + }, + ) +} +fn wire__crate__dart_api__edge_llm_ask_stream_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "edge_llm_ask_stream", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_sdk = , + >>::sse_decode(&mut deserializer); + let api_prompt = ::sse_decode(&mut deserializer); + let api_sink = + >::sse_decode( + &mut deserializer, + ); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let mut api_sdk_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_sdk, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_sdk_guard = Some(api_sdk.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_sdk_guard = api_sdk_guard.unwrap(); + let output_ok = Result::<_, ()>::Ok({ + crate::dart_api::edge_llm_ask_stream(&*api_sdk_guard, api_prompt, api_sink); + })?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__dart_api__edge_llm_cloud_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "edge_llm_cloud", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_model = ::sse_decode(&mut deserializer); + let api_api_key = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let output_ok = Result::<_, ()>::Ok(crate::dart_api::edge_llm_cloud( + api_model, + api_api_key, + ))?; + Ok(output_ok) + })()) + } + }, + ) +} +fn wire__crate__dart_api__edge_llm_local_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "edge_llm_local", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_model_uri = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, flutter_rust_bridge::for_generated::anyhow::Error>( + (move || { + let output_ok = crate::dart_api::edge_llm_local(api_model_uri)?; + Ok(output_ok) + })(), + ) + } + }, + ) +} +fn wire__crate__dart_api__edge_llm_reset_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_normal::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "edge_llm_reset", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_sdk = , + >>::sse_decode(&mut deserializer); + deserializer.end(); + move |context| { + transform_result_sse::<_, ()>((move || { + let mut api_sdk_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order(vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_sdk, 0, false, + ), + ]); + for i in decode_indices_ { + match i { + 0 => api_sdk_guard = Some(api_sdk.lockable_decode_sync_ref()), + _ => unreachable!(), + } + } + let api_sdk_guard = api_sdk_guard.unwrap(); + let output_ok = Result::<_, ()>::Ok({ + crate::dart_api::edge_llm_reset(&*api_sdk_guard); + })?; + Ok(output_ok) + })()) + } + }, + ) +} + +// Section: related_funcs + +flutter_rust_bridge::frb_generated_moi_arc_impl_value!( + flutter_rust_bridge::for_generated::RustAutoOpaqueInner +); + +// Section: dart2rust + +impl SseDecode for flutter_rust_bridge::for_generated::anyhow::Error { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::anyhow::anyhow!("{}", inner); + } +} + +impl SseDecode for EdgeLlm { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = , + >>::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::rust_auto_opaque_decode_owned(inner); + } +} + +impl SseDecode for RustOpaqueMoi> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return decode_rust_opaque_moi(inner); + } +} + +impl SseDecode for StreamSink { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return StreamSink::deserialize(inner); + } +} + +impl SseDecode for String { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = >::sse_decode(deserializer); + return String::from_utf8(inner).unwrap(); + } +} + +impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = Vec::with_capacity(len_ as usize); + for idx_ in 0..len_ { + ans_.push(::sse_decode(deserializer)); + } + return ans_; + } +} + +impl SseDecode for u8 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u8().unwrap() + } +} + +impl SseDecode for () { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {} +} + +impl SseDecode for usize { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u64::().unwrap() as _ + } +} + +impl SseDecode for i32 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_i32::().unwrap() + } +} + +impl SseDecode for bool { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + deserializer.cursor.read_u8().unwrap() != 0 + } +} + +fn pde_ffi_dispatcher_primary_impl( + func_id: i32, + port: flutter_rust_bridge::for_generated::MessagePort, + ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len: i32, + data_len: i32, +) { + // Codec=Pde (Serialization + dispatch), see doc to use other codecs + match func_id { + 1 => wire__crate__dart_api__edge_llm_ask_impl(port, ptr, rust_vec_len, data_len), + 2 => wire__crate__dart_api__edge_llm_ask_stream_impl(port, ptr, rust_vec_len, data_len), + 3 => wire__crate__dart_api__edge_llm_cloud_impl(port, ptr, rust_vec_len, data_len), + 4 => wire__crate__dart_api__edge_llm_local_impl(port, ptr, rust_vec_len, data_len), + 5 => wire__crate__dart_api__edge_llm_reset_impl(port, ptr, rust_vec_len, data_len), + _ => unreachable!(), + } +} + +fn pde_ffi_dispatcher_sync_impl( + func_id: i32, + ptr: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len: i32, + data_len: i32, +) -> flutter_rust_bridge::for_generated::WireSyncRust2DartSse { + // Codec=Pde (Serialization + dispatch), see doc to use other codecs + match func_id { + _ => unreachable!(), + } +} + +// Section: rust2dart + +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for FrbWrapper { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self.0) + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for FrbWrapper {} + +impl flutter_rust_bridge::IntoIntoDart> for EdgeLlm { + fn into_into_dart(self) -> FrbWrapper { + self.into() + } +} + +impl SseEncode for flutter_rust_bridge::for_generated::anyhow::Error { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(format!("{:?}", self), serializer); + } +} + +impl SseEncode for EdgeLlm { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >>::sse_encode(flutter_rust_bridge::for_generated::rust_auto_opaque_encode::<_, MoiArc<_>>(self), serializer); + } +} + +impl SseEncode for RustOpaqueMoi> { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + let (ptr, size) = self.sse_encode_raw(); + ::sse_encode(ptr, serializer); + ::sse_encode(size, serializer); + } +} + +impl SseEncode for StreamSink { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + unimplemented!("") + } +} + +impl SseEncode for String { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >::sse_encode(self.into_bytes(), serializer); + } +} + +impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + ::sse_encode(item, serializer); + } + } +} + +impl SseEncode for u8 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_u8(self).unwrap(); + } +} + +impl SseEncode for () { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {} +} + +impl SseEncode for usize { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer + .cursor + .write_u64::(self as _) + .unwrap(); + } +} + +impl SseEncode for i32 { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_i32::(self).unwrap(); + } +} + +impl SseEncode for bool { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + serializer.cursor.write_u8(self as _).unwrap(); + } +} + +#[cfg(not(target_family = "wasm"))] +mod io { + // This file is automatically generated, so please do not edit it. + // @generated by `flutter_rust_bridge`@ 2.12.0. + + // Section: imports + + use super::*; + use crate::*; + use flutter_rust_bridge::for_generated::byteorder::{ + NativeEndian, ReadBytesExt, WriteBytesExt, + }; + use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; + use flutter_rust_bridge::{Handler, IntoIntoDart}; + + // Section: boilerplate + + flutter_rust_bridge::frb_generated_boilerplate_io!(); + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_edge_intelligence_rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerEdgeLlm( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[unsafe(no_mangle)] + pub extern "C" fn frbgen_edge_intelligence_rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerEdgeLlm( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } +} +#[cfg(not(target_family = "wasm"))] +pub use io::*; + +/// cbindgen:ignore +#[cfg(target_family = "wasm")] +mod web { + // This file is automatically generated, so please do not edit it. + // @generated by `flutter_rust_bridge`@ 2.12.0. + + // Section: imports + + use super::*; + use crate::*; + use flutter_rust_bridge::for_generated::byteorder::{ + NativeEndian, ReadBytesExt, WriteBytesExt, + }; + use flutter_rust_bridge::for_generated::wasm_bindgen; + use flutter_rust_bridge::for_generated::wasm_bindgen::prelude::*; + use flutter_rust_bridge::for_generated::{transform_result_dco, Lifetimeable, Lockable}; + use flutter_rust_bridge::{Handler, IntoIntoDart}; + + // Section: boilerplate + + flutter_rust_bridge::frb_generated_boilerplate_web!(); + + #[wasm_bindgen] + pub fn rust_arc_increment_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerEdgeLlm( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::increment_strong_count(ptr as _); + } + + #[wasm_bindgen] + pub fn rust_arc_decrement_strong_count_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerEdgeLlm( + ptr: *const std::ffi::c_void, + ) { + MoiArc::>::decrement_strong_count(ptr as _); + } +} +#[cfg(target_family = "wasm")] +pub use web::*; diff --git a/crates/adapters/el-ffi/src/lib.rs b/crates/adapters/el-ffi/src/lib.rs index d5577d5..955a64c 100644 --- a/crates/adapters/el-ffi/src/lib.rs +++ b/crates/adapters/el-ffi/src/lib.rs @@ -7,10 +7,10 @@ //! Turbo Module. Streaming via `StreamHandler` callback interface (UniFFI //! cannot export `impl FnMut` parameters). //! -//! ## Flutter — `flutter_rust_bridge` v2 (ADR-009) +//! ## Dart / pub.dev — `flutter_rust_bridge` v2 codegen (ADR-024) //! `#[frb(opaque)]` on `EdgeLlm` → Dart opaque handle. `ask()` → -//! `Future`, `ask_stream()` (closure variant) → `Stream`. -//! FRB v2 handles `impl FnMut` natively. +//! `Future`, `edge_llm_ask_stream()` + `StreamSink` → +//! `Stream`. //! //! ## Web / npm — `wasm-bindgen` (ADR-001) //! `#[wasm_bindgen]` on both the struct **and** the impl block → ESM TypeScript @@ -23,7 +23,11 @@ //! implementation, so `EdgeLlm.cloud` throws an explicit error there instead //! of silently degrading. -#![forbid(unsafe_code)] +// `#![forbid(unsafe_code)]` cannot be used: `forbid` is unoverridable even by +// inner `#[allow]`, so `frb_generated` (generated FFI glue) would not compile. +// `deny` permits the scoped override below. Invariant: the only permitted use +// of `#[allow(unsafe_code)]` in this crate is on `mod frb_generated`. +#![deny(unsafe_code)] #[cfg(not(target_arch = "wasm32"))] use el_core::CredentialRef; @@ -33,9 +37,17 @@ use el_core::{ChatMessage, ChatRequest, ChatToken, LlmProvider}; #[cfg(not(target_arch = "wasm32"))] uniffi::setup_scaffolding!("el_ffi"); +#[cfg(not(target_arch = "wasm32"))] +use flutter_rust_bridge::for_generated::DcoCodec; #[cfg(not(target_arch = "wasm32"))] use flutter_rust_bridge::frb; +#[cfg(not(target_arch = "wasm32"))] +#[allow(unsafe_code)] +mod frb_generated; +#[cfg(not(target_arch = "wasm32"))] +use frb_generated::StreamSink; + #[cfg(target_arch = "wasm32")] use wasm_bindgen::prelude::*; @@ -72,6 +84,18 @@ impl From for SdkError { } } +fn emit_stream_error( + result: std::result::Result<(), SdkError>, + sink_closed: bool, + mut emit: impl FnMut(String), +) { + if let Err(error) = result { + if !sink_closed { + emit(error.to_string()); + } + } +} + // ── Streaming callback interface (UniFFI / React Native) ───────────────────── /// Token-by-token callback for streaming on React Native. @@ -80,7 +104,8 @@ impl From for SdkError { /// [`EdgeLlm::ask_stream_cb`]. Each call delivers one text fragment; the /// method returns (and calls nothing more) when generation is complete. /// -/// Flutter uses the closure-based [`EdgeLlm::ask_stream`] instead. +/// Dart bindings use the `edge_llm_ask_stream` FRB wrapper and +/// `StreamSink` instead. #[cfg(not(target_arch = "wasm32"))] #[uniffi::export(callback_interface)] pub trait StreamHandler: Send + Sync { @@ -93,7 +118,7 @@ pub trait StreamHandler: Send + Sync { /// /// Annotated for all three binding surfaces: /// - `uniffi::Object` (native) → opaque UniFFI / React Native handle -/// - `frb(opaque)` (native) → opaque Flutter/Dart handle via FRB v2 +/// - `frb(opaque)` (native) → opaque Dart handle via FRB v2 codegen /// - `wasm_bindgen` (wasm32) → satisfies `IntoWasmAbi`/`WasmDescribe` so /// that `#[wasm_bindgen] impl EdgeLlm { ... }` compiles #[cfg_attr(not(target_arch = "wasm32"), derive(uniffi::Object))] @@ -107,9 +132,8 @@ pub struct EdgeLlm { /// UniFFI-exported methods: constructors, blocking chat, and reset. /// -/// `ask_stream` (closure variant) lives in a separate plain impl block — -/// UniFFI cannot export `impl FnMut` parameters. The RN streaming surface is -/// `ask_stream_cb` in the block below. +/// Dart uses the `edge_llm_*` free-function wrappers below so FRB does not +/// need to parse the UniFFI-decorated impl block after macro expansion. #[cfg_attr(not(target_arch = "wasm32"), uniffi::export)] impl EdgeLlm { /// Construct with the local Candle engine (air-gapped, ADR-002/004). @@ -180,7 +204,7 @@ impl EdgeLlm { /// `"gemini/gemini-2.0-flash"`, or any OpenAI-compat base URL. /// `api_key` must come from the platform keystore — never embedded. /// - /// **Native only** (React Native / Flutter). On wasm32 this constructor + /// **Native only** (React Native / Dart native). On wasm32 this constructor /// does not exist — the web surface exposes a throwing `cloud` instead /// (see the wasm32 impl block below and the ADR-010 amendment). #[cfg(not(target_arch = "wasm32"))] @@ -217,52 +241,100 @@ impl EdgeLlm { } } -/// Streaming via callback interface — exported for React Native (UniFFI). -/// -/// Separated from the main block because UniFFI cannot export `impl FnMut`. -#[cfg(not(target_arch = "wasm32"))] -#[uniffi::export] impl EdgeLlm { - /// Stream tokens to a [`StreamHandler`] callback (React Native path). - /// - /// Returns an error on network/auth/engine failure so callers are not - /// left waiting for a stream that will never arrive. - pub fn ask_stream_cb( + fn ask_stream_with( &self, prompt: String, - handler: Box, + mut on_token: impl FnMut(String), ) -> Result<(), SdkError> { let req = ChatRequest::new(self.default_model.clone(), vec![ChatMessage::user(prompt)]); self.provider .chat_stream(&req, &mut |t: ChatToken| { if !t.is_final { - handler.on_token(t.text); + on_token(t.text); } }) .map_err(SdkError::from) } } -/// Closure-based streaming — used by Flutter (FRB v2) and tests. +/// Streaming via callback interface — exported for React Native (UniFFI). /// -/// FRB v2 wraps `impl FnMut` into a Dart `Stream` automatically. +/// Separated from the main block because UniFFI cannot export `impl FnMut`. +#[cfg(not(target_arch = "wasm32"))] +#[uniffi::export] impl EdgeLlm { - /// Stream tokens via closure (Flutter / FRB path). + /// Stream tokens to a [`StreamHandler`] callback (React Native path). /// - /// Returns `Err` on provider failure so the stream is not silently truncated. - pub fn ask_stream( + /// Returns an error on network/auth/engine failure so callers are not + /// left waiting for a stream that will never arrive. + pub fn ask_stream_cb( &self, prompt: String, - mut on_token: impl FnMut(String), + handler: Box, ) -> Result<(), SdkError> { - let req = ChatRequest::new(self.default_model.clone(), vec![ChatMessage::user(prompt)]); - self.provider - .chat_stream(&req, &mut |t: ChatToken| { - if !t.is_final { - on_token(t.text.clone()); + self.ask_stream_with(prompt, |token| handler.on_token(token)) + } +} + +/// Dart / FRB wrappers. +/// +/// These are intentionally separate from the UniFFI impl blocks. FRB parses +/// these plain Rust functions and the Dart facade wraps them into the public +/// `EdgeLlm` class API. +#[cfg(not(target_arch = "wasm32"))] +pub mod dart_api { + use super::*; + + #[frb] + pub fn edge_llm_local(model_uri: String) -> anyhow::Result { + EdgeLlm::local(model_uri).map_err(to_anyhow) + } + + #[frb] + pub fn edge_llm_cloud(model: String, api_key: String) -> EdgeLlm { + EdgeLlm::cloud(model, api_key) + } + + #[frb] + pub fn edge_llm_ask(sdk: &EdgeLlm, prompt: String) -> anyhow::Result { + sdk.ask(prompt).map_err(to_anyhow) + } + + #[frb] + pub fn edge_llm_reset(sdk: &EdgeLlm) { + sdk.reset(); + } + + #[frb] + pub fn edge_llm_ask_stream(sdk: &EdgeLlm, prompt: String, sink: StreamSink) { + let mut sink_closed = false; + let result = sdk.ask_stream_with(prompt, |token| { + if !sink_closed { + if sink.add(token).is_err() { + // Dart cancelled the stream (e.g. take(n), listen().cancel()). + // LlmProvider has no cancellation hook so generation runs to + // completion; remaining tokens are silently dropped. + sink_closed = true; } - }) - .map_err(SdkError::from) + } + }); + + // The returned Dart Stream is the consumer-facing error channel. + // Completing the generated task successfully prevents its unawaited + // future from reporting a duplicate global error. + emit_stream_error(result, sink_closed, |message| { + let _ = sink.add_error(message); + }); + } + + // Converts SdkError to an anyhow string error for FRB's Dart propagation. + // FRB surfaces this as a Dart AnyhowException(message) — variant type is + // erased. If SdkError grows structured variants (e.g. AuthError { code }), + // replace this with a #[frb]-annotated error enum in dart_api and return + // Result<_, DartError> directly instead of going through anyhow. + fn to_anyhow(error: SdkError) -> anyhow::Error { + anyhow::anyhow!(error.to_string()) } } @@ -289,7 +361,7 @@ impl EdgeLlm { /// `el-cloud`'s blocking HTTP transport has no wasm implementation, and /// the synchronous `LlmProvider` trait cannot await the browser's async /// `fetch`. Always throws so callers fail loudly instead of silently - /// receiving an echo stub. Use a native binding (React Native / Flutter) + /// receiving an echo stub. Use a native binding (React Native / Dart native) /// for cloud access. #[wasm_bindgen] pub fn cloud(_model: String, _api_key: String) -> Result { @@ -398,7 +470,7 @@ mod tests { fn stream_ends_with_final_and_has_content() { let sdk = EdgeLlm::local("".into()).expect("toy model never fails"); let mut parts: Vec = Vec::new(); - sdk.ask_stream("hi".into(), |t| parts.push(t)) + sdk.ask_stream_with("hi".into(), |t| parts.push(t)) .expect("local toy model stream should not error"); assert!(!parts.is_empty()); } @@ -417,6 +489,21 @@ mod tests { ); } + #[test] + fn active_dart_stream_receives_one_provider_error() { + let mut errors = Vec::new(); + + emit_stream_error( + Err(SdkError::ProviderError { + message: "stream interrupted".into(), + }), + false, + |error| errors.push(error), + ); + + assert_eq!(errors, vec!["stream interrupted"]); + } + #[test] fn local_missing_gguf_path_returns_sdk_error() { let r = EdgeLlm::local("/nonexistent/model.gguf".into()); diff --git a/crates/el-runtime/src/session.rs b/crates/el-runtime/src/session.rs index eaac3fd..84aa17b 100644 --- a/crates/el-runtime/src/session.rs +++ b/crates/el-runtime/src/session.rs @@ -193,6 +193,10 @@ impl InferenceSession { found: self.phase.as_str(), }); } + // Reset step so this turn's events start from 0 (same as a fresh turn + // via `new` + `load_prompt`). Events are drained per-turn, so per-turn + // step numbering is the right level of granularity. + self.step = 0; // The whole re-rendered conversation is this turn's prompt for ADR-013 // ingress triage (re-scored in `generate_with_policy`). self.prompt = full_context.to_vec(); @@ -211,6 +215,8 @@ impl InferenceSession { self.emit(DomainEvent::PrefillCompleted { prompt_tokens: full_context.len() as u32, kv_len, + // `prefill_reuse` reports final KV length, not the number of tokens + // forwarded. Reporting a rate here would count reused tokens too. prefill_tps: 0, }); self.phase = Phase::Decoding; @@ -1681,6 +1687,7 @@ mod tests { /// When set, the favoured token is always EOS so `generate` terminates on /// the first decode step (keeps the reuse-savings test deterministic). eos_immediately: bool, + prefill_delay: std::time::Duration, } impl ReuseEngine { fn new(eos: Token, vocab: usize, eos_immediately: bool) -> Self { @@ -1694,8 +1701,19 @@ mod tests { last_logits: Vec::new(), forwards: std::rc::Rc::new(std::cell::Cell::new(0)), eos_immediately, + prefill_delay: std::time::Duration::ZERO, } } + fn with_prefill_delay( + eos: Token, + vocab: usize, + eos_immediately: bool, + prefill_delay: std::time::Duration, + ) -> Self { + let mut engine = Self::new(eos, vocab, eos_immediately); + engine.prefill_delay = prefill_delay; + engine + } fn forward(&mut self, t: Token) { self.forwards.set(self.forwards.get() + 1); self.cached.push(t); @@ -1764,6 +1782,7 @@ mod tests { Ok(()) } fn prefill_reuse(&mut self, full_context: &[Token]) -> Result { + std::thread::sleep(self.prefill_delay); let reuse = self .cached .iter() @@ -1849,6 +1868,40 @@ mod tests { assert_eq!(s.phase(), Phase::Completed); } + #[test] + fn continue_prompt_does_not_report_throughput_for_reused_tokens() { + let engine = + ReuseEngine::with_prefill_delay(1, 4, true, std::time::Duration::from_millis(2)); + let mut s = + InferenceSession::new(SessionId(55), SessionConfig::default(), engine, permit()); + let ports = Ports::permissive(); + + s.load_prompt(&ports, &[10, 11]).unwrap(); + s.generate(&ports, 8).unwrap(); + s.drain_events(); + + s.continue_prompt(&ports, &[10, 11, 20, 21, 22]).unwrap(); + let prefill = s + .drain_events() + .into_iter() + .find_map(|envelope| match envelope.event { + DomainEvent::PrefillCompleted { + prompt_tokens, + kv_len, + prefill_tps, + } => Some((prompt_tokens, kv_len, prefill_tps)), + _ => None, + }) + .expect("continue_prompt emits PrefillCompleted"); + + assert_eq!(prefill.0, 5); + assert_eq!(prefill.1, 5); + assert_eq!( + prefill.2, 0, + "the engine API does not report forwarded-token count, so reused tokens must not be included in prefill throughput" + ); + } + #[test] fn continue_prompt_requires_a_completed_prior_turn() { // AC-3d: continue is valid only after a finished turn; an unprefilled diff --git a/docs/adr/ADR-001-adopt-webassembly-as-cross-platform-sdk-runtime.md b/docs/adr/ADR-001-adopt-webassembly-as-cross-platform-sdk-runtime.md index 8c30a77..a8427a0 100644 --- a/docs/adr/ADR-001-adopt-webassembly-as-cross-platform-sdk-runtime.md +++ b/docs/adr/ADR-001-adopt-webassembly-as-cross-platform-sdk-runtime.md @@ -1,6 +1,6 @@ # ADR-001: WebAssembly (Wasmtime) as the portable target, alongside native ARM -- **Status**: accepted +- **Status**: accepted - partially amended by [ADR-024](./ADR-024-dart-only-platform-agnostic-pub-dev-sdk.md) - **Date**: 2026-06-10 - **Deciders**: - **Tags**: runtime, portability, wasm, rust, core @@ -40,7 +40,7 @@ identical across targets. Host bindings are generated from **one Rust API in | Consumer | Tool | Output | |----------|------|--------| | React Native (Android + iOS) | **`uniffi-bindgen-react-native`** | TypeScript + JSI C++ + Turbo Module | -| Flutter (Android + iOS + desktop) | **`flutter_rust_bridge` v2** | Dart package + Rust glue (see [ADR-009](./ADR-009-flutter-rust-bridge-for-dart-bindings.md)) | +| Dart / pub.dev | Dart FFI/WASM bindings | Framework-neutral Dart package (see [ADR-024](./ADR-024-dart-only-platform-agnostic-pub-dev-sdk.md)) | | Web / npm | **`wasm-bindgen`** | TypeScript ESM package | `uniffi-bindgen-react-native` (Mozilla/Filament, 2024) generates TypeScript and @@ -56,8 +56,8 @@ boundary via shared linear memory (WASM) or shared `Arc<[u8]>` buffers (native). - Wasmtime keeps the entire stack in Rust (no C++ runtime dependency). - `uniffi-bindgen-react-native` generates TypeScript + JSI C++ directly — no hand-written JNI, Obj-C, or intermediate Kotlin/Swift bridge layer. -- `flutter_rust_bridge` v2 gives Flutter a `Stream` token callback - and opaque handles without any hand-written `dart:ffi`. +- The Dart pub.dev surface gives framework-neutral `Future`/`Stream` APIs and + opaque handles without making Flutter the package identity. ### Negative - Two active targets (native + wasm32) widen the test matrix and require @@ -74,5 +74,6 @@ boundary via shared linear memory (WASM) or shared `Arc<[u8]>` buffers (native). - PRD: `docs/prd.md` §"Proposed Edge-Native Pipeline" → "Runtime & Target Selection" - DDD: [Inference Runtime context](../ddd/bounded-contexts/01-inference-runtime.md) - Driven by: [ADR-008](./ADR-008-implement-the-sdk-in-rust-instead-of-c-cpp.md) -- Extended by: [ADR-009](./ADR-009-flutter-rust-bridge-for-dart-bindings.md) (Flutter bindings) +- Extended by: [ADR-024](./ADR-024-dart-only-platform-agnostic-pub-dev-sdk.md) (Dart pub.dev SDK surface) +- Superseded binding note: [ADR-009](./ADR-009-flutter-rust-bridge-for-dart-bindings.md) is superseded by ADR-024 - Related: [ADR-002](./ADR-002-candle-as-rust-native-inference-engine.md), [ADR-003](./ADR-003-static-memory-planning-with-zero-allocation-arena.md) diff --git a/docs/adr/ADR-009-flutter-rust-bridge-for-dart-bindings.md b/docs/adr/ADR-009-flutter-rust-bridge-for-dart-bindings.md index 6134747..b459e4c 100644 --- a/docs/adr/ADR-009-flutter-rust-bridge-for-dart-bindings.md +++ b/docs/adr/ADR-009-flutter-rust-bridge-for-dart-bindings.md @@ -1,6 +1,6 @@ # ADR-009: flutter_rust_bridge for Dart/Flutter bindings -- **Status**: accepted +- **Status**: superseded by [ADR-024](./ADR-024-dart-only-platform-agnostic-pub-dev-sdk.md) - **Date**: 2026-06-10 - **Deciders**: - **Tags**: ffi, flutter, dart, bindings, mobile @@ -73,6 +73,7 @@ build), keeping the offline-build guarantee of the 7 core crates intact. `.so`/`.dylib`; the Rust workspace itself is unchanged. ## Links +- Superseded by: [ADR-024](./ADR-024-dart-only-platform-agnostic-pub-dev-sdk.md) - Supersedes: nothing; extends ADR-001's binding strategy - Related: [ADR-001](./ADR-001-adopt-webassembly-as-cross-platform-sdk-runtime.md), [ADR-008](./ADR-008-implement-the-sdk-in-rust-instead-of-c-cpp.md) diff --git a/docs/adr/ADR-010-unified-llm-provider-trait-with-opt-in-frontier-egress.md b/docs/adr/ADR-010-unified-llm-provider-trait-with-opt-in-frontier-egress.md index 5b9ae97..9cd68f2 100644 --- a/docs/adr/ADR-010-unified-llm-provider-trait-with-opt-in-frontier-egress.md +++ b/docs/adr/ADR-010-unified-llm-provider-trait-with-opt-in-frontier-egress.md @@ -10,10 +10,10 @@ The SDK was originally designed as air-gapped by default ([ADR-004](./ADR-004-air-gapped-by-default-with-opt-in-hybrid-mode.md)), with all inference running on-device via Candle ([ADR-002](./ADR-002-candle-as-rust-native-inference-engine.md)). -Flutter and React Native are now confirmed deployment targets -([ADR-009](./ADR-009-flutter-rust-bridge-for-dart-bindings.md), -[ADR-001](./ADR-001-adopt-webassembly-as-cross-platform-sdk-runtime.md)), and -the requirement has been extended: **the SDK must support frontier LLMs (OpenAI, +React Native, Dart/pub.dev, and web are confirmed deployment surfaces +([ADR-001](./ADR-001-adopt-webassembly-as-cross-platform-sdk-runtime.md), +[ADR-024](./ADR-024-dart-only-platform-agnostic-pub-dev-sdk.md)), and the +requirement has been extended: **the SDK must support frontier LLMs (OpenAI, Anthropic, Gemini, Ollama, and compatible providers) as an opt-in backend** alongside the primary local Candle path. @@ -102,8 +102,8 @@ air-gap guarantee is preserved by default. Apps using `el-cloud` emit a ### Positive - Single `LlmProvider` trait: mobile apps swap local ↔ frontier backend with one constructor change, no API surface difference. -- Frontier LLMs work from both native binding surfaces (React Native, - Flutter); the npm/web surface fails explicitly rather than silently +- Frontier LLMs work from both native binding surfaces (React Native and + Dart native); the npm/web surface fails explicitly rather than silently (see Negative). - Ollama as a provider means local models served by Ollama (on a dev machine or LAN server) are also reachable — useful for development and privacy-first @@ -134,5 +134,5 @@ air-gap guarantee is preserved by default. Apps using `el-cloud` emit a - Depends on: [ADR-002](./ADR-002-candle-as-rust-native-inference-engine.md) (local path), [ADR-008](./ADR-008-implement-the-sdk-in-rust-instead-of-c-cpp.md) - Related: [ADR-001](./ADR-001-adopt-webassembly-as-cross-platform-sdk-runtime.md), - [ADR-009](./ADR-009-flutter-rust-bridge-for-dart-bindings.md) + [ADR-024](./ADR-024-dart-only-platform-agnostic-pub-dev-sdk.md) - Crates: `el-core` (trait), `crates/adapters/el-cloud` (new) diff --git a/docs/adr/ADR-011-multi-registry-release-ci-crates-io-npm-pub-dev.md b/docs/adr/ADR-011-multi-registry-release-ci-crates-io-npm-pub-dev.md index d1ad819..a21276c 100644 --- a/docs/adr/ADR-011-multi-registry-release-ci-crates-io-npm-pub-dev.md +++ b/docs/adr/ADR-011-multi-registry-release-ci-crates-io-npm-pub-dev.md @@ -1,6 +1,6 @@ # ADR-011: Multi-Registry Release CI (crates.io, npm, pub.dev) -- **Status**: accepted +- **Status**: accepted - partially amended by [ADR-024](./ADR-024-dart-only-platform-agnostic-pub-dev-sdk.md) - **Date**: 2026-06-12 - **Deciders**: - **Tags**: ci, release, crates.io, npm, pub.dev, packaging @@ -13,7 +13,7 @@ The SDK produces three independently consumable artefacts: |----------|----------|----------| | `el-*` Rust crates | crates.io | Rust projects embedding the SDK directly | | `edge-intelligence-sdk` npm package | npmjs.com | React Native / web TypeScript consumers (wasm-bindgen output, ADR-001) | -| `edge_intelligence` Dart package | pub.dev | Flutter consumers (FRB v2 output, ADR-009) | +| `edge_intelligence` Dart package | pub.dev | Framework-neutral Dart consumers (ADR-024) | Each registry has its own publish toolchain, credential model, and version contract. Without automation, publishing is a manual, error-prone multi-step @@ -21,14 +21,20 @@ process that must be repeated in exact order (core crates before adapter crates due to path-dependency resolution on crates.io). The `Makefile` added alongside ADR-011 already drives the binding codegen -(`make codegen-rn`, `make codegen-flutter`, `make build-wasm`). A release CI -workflow sits one step above: it gates on semver tags, runs the codegen, and -then publishes to all three registries from a single push. +(`make codegen-rn`, `make codegen-dart`, `make build-wasm`; legacy +`codegen-flutter` aliases may exist during migration). A release CI workflow +sits one step above: it gates on semver tags, runs the codegen, and then +publishes to all three registries from a single push. Key constraints: - **Publish order on crates.io**: el-core → el-memory / el-telemetry / el-provenance / el-safety → el-runtime → el-grammar → el-provenance-ed25519 → el-engine-candle → el-cloud → el-ffi. Each crate must be published before its dependants. - **npm publish** requires the wasm-pack output in `out/web/` to be present and a valid `package.json` with the correct `name` / `version`. -- **pub.dev publish** requires the FRB-generated Dart package in `out/flutter/` to have a `pubspec.yaml` with matching version and a valid `dart pub publish --dry-run` pass. +- **pub.dev publish** requires the generated Dart package to have a + `pubspec.yaml` with matching version and a valid `dart pub publish --dry-run` + pass, plus packaged native desktop artifacts for each declared Dart platform + (Linux, macOS, Windows). New work should use Dart naming (`out/dart/`, + `codegen-dart`) per ADR-024; transitional `out/flutter/` paths may remain + only as compatibility aliases during migration. - **Credentials**: crates.io API token, npm access token, pub.dev refresh token — all injected as GitHub Actions secrets, never committed. - **Versioning**: the single source of truth for the version number is the git tag (`v0.2.0`). Cargo workspace version, npm `package.json` version, and Dart `pubspec.yaml` version must all be stamped from the tag before publish. @@ -47,12 +53,15 @@ Extract the semver from the git tag and patch: - Each `[package] version` in the workspace `Cargo.toml` members (via `cargo set-version` from `cargo-edit`, or `sed` on the TOML). - `version` in `out/web/package.json` (created by `wasm-pack`). -- `version` in `out/flutter/pubspec.yaml` (created by `flutter_rust_bridge_codegen`). +- `version` in the generated Dart package `pubspec.yaml` (`out/dart/` for new + work; transitional `out/flutter/` aliases may exist during migration). ### Stage 3 — Build artefacts Run `make codegen-rn` (Android + RN), `make build-ios`, `make build-wasm`, -`make codegen-flutter` on their respective runners (reuse the -`bindings.yml` matrix). Each job uploads its output as a workflow artefact. +`make codegen-dart`, and desktop native builds for Linux, macOS, and Windows +on their respective runners. Each job uploads its output as a workflow +artefact. The pub.dev assembly consumes only the generated Dart package and the +desktop native artifacts declared by ADR-024. ### Stage 4 — Publish (serial, on separate runners that download Stage 3 artefacts) @@ -60,7 +69,7 @@ Run `make codegen-rn` (Android + RN), `make build-ios`, `make build-wasm`, |-----|------|--------| | `publish-crates` | `cargo publish -p el-core`, then each dependant in order, with `--no-verify` only when the crate was already verified in Stage 1 | `CARGO_REGISTRY_TOKEN` | | `publish-npm` | `npm publish out/web/ --access public` | `NPM_TOKEN` | -| `publish-pub` | `dart pub publish --force` in `out/flutter/` | `PUB_CREDENTIALS` (JSON refresh token) | +| `publish-pub` | `dart pub publish --force` in the generated Dart package directory | `PUB_CREDENTIALS` (JSON refresh token) | `publish-crates` inserts a 10-second sleep between crates to allow crates.io's index propagation before the next dependent is submitted. @@ -95,8 +104,8 @@ artifacts because later assembly jobs consume them. must be passed (preferred: commit the version bump before tagging). ### Neutral -- The Flutter and npm packages are thin wrappers around the compiled native - artefact; API surface is defined by `el-ffi` (ADR-001, ADR-009), not +- The Dart pub.dev and npm packages are thin wrappers around compiled runtime + artifacts; API surface is defined by `el-ffi` (ADR-001, ADR-024), not duplicated here. - Yanking a broken release requires separate registry-specific commands (`cargo yank`, `npm deprecate`, pub.dev retract) — no single-command @@ -104,7 +113,8 @@ artifacts because later assembly jobs consume them. ## Links - Extends: [ADR-001](./ADR-001-adopt-webassembly-as-cross-platform-sdk-runtime.md) (wasm-bindgen / npm surface) -- Extends: [ADR-009](./ADR-009-flutter-rust-bridge-for-dart-bindings.md) (FRB / pub.dev surface) +- Partially amended by: [ADR-024](./ADR-024-dart-only-platform-agnostic-pub-dev-sdk.md) (Dart-first pub.dev surface with Flutter mobile runtime packaging) +- Historical extension: [ADR-009](./ADR-009-flutter-rust-bridge-for-dart-bindings.md) (FRB / Flutter framing, now superseded) - Extends: [ADR-008](./ADR-008-implement-the-sdk-in-rust-instead-of-c-cpp.md) (Rust workspace / crates.io surface) - Related: `Makefile`, `.github/workflows/bindings.yml` - Implements: `crates/adapters/el-ffi`, all `crates/el-*` members diff --git a/docs/adr/ADR-019-in-loop-incremental-decoding-and-token-streaming.md b/docs/adr/ADR-019-in-loop-incremental-decoding-and-token-streaming.md index 32dd9bf..6b8b8af 100644 --- a/docs/adr/ADR-019-in-loop-incremental-decoding-and-token-streaming.md +++ b/docs/adr/ADR-019-in-loop-incremental-decoding-and-token-streaming.md @@ -94,7 +94,7 @@ from it, with emission gated by the safety control loop. ## Links - Source: [docs/research/improvements-plan.md](../research/improvements-plan.md) §P0.2, EPIC-2. -- Builds on: [ADR-010](./ADR-010-unified-llm-provider-trait-with-opt-in-frontier-egress.md) (`chat_stream`/`ChatToken` seam), [ADR-018](./ADR-018-persistent-model-instances-and-stateful-sessions.md) (persistent session to stream over), [ADR-009](./ADR-009-flutter-rust-bridge-for-dart-bindings.md) (Dart stream binding). +- Builds on: [ADR-010](./ADR-010-unified-llm-provider-trait-with-opt-in-frontier-egress.md) (`chat_stream`/`ChatToken` seam), [ADR-018](./ADR-018-persistent-model-instances-and-stateful-sessions.md) (persistent session to stream over), [ADR-024](./ADR-024-dart-only-platform-agnostic-pub-dev-sdk.md) (Dart stream binding). - Constrained by: [ADR-012](./ADR-012-layered-decode-time-safety-control-loop-with-checkpointed-rollback.md) (emit only guard-verified tokens — no streaming a rolled-back span), [ADR-005](./ADR-005-on-device-only-tiered-decoder-time-safety.md) (decode order invariant). - Implementation seams: `crates/el-runtime` (`generate_with_policy` emit hook), `crates/adapters/el-engine-candle` (`chat_stream` incremental detokenize), `crates/adapters/el-ffi` (per-binding stream wrappers). - Measured by: [ADR-023](./ADR-023-baseline-performance-instrumentation.md) (real `ttft_ms`). diff --git a/docs/adr/ADR-024-dart-only-platform-agnostic-pub-dev-sdk.md b/docs/adr/ADR-024-dart-only-platform-agnostic-pub-dev-sdk.md new file mode 100644 index 0000000..e947769 --- /dev/null +++ b/docs/adr/ADR-024-dart-only-platform-agnostic-pub-dev-sdk.md @@ -0,0 +1,110 @@ +# ADR-024: Dart-first, platform-agnostic pub.dev SDK with Flutter mobile runtimes + +- **Status**: accepted +- **Date**: 2026-06-25 +- **Deciders**: +- **Tags**: dart, pub.dev, bindings, packaging, platform-agnostic + +## Context + +[ADR-009](./ADR-009-flutter-rust-bridge-for-dart-bindings.md) framed the +pub.dev package as a Flutter binding layer. That framing made sense when +Flutter was treated as the primary Dart consumer, but it is too narrow for the +SDK architecture. + +Edge Intelligence is a platform-agnostic SDK with a Rust core and generated host +bindings. Flutter is the installation host for the pub.dev plugin, while the SDK +surface remains plain Dart for mobile, desktop, test, and embedded application +code. The package must expose one framework-neutral Dart API while packaging the +native runtime in the form each supported host requires. + +The current package review also exposed a packaging smell: generated +platform-specific files were public top-level Dart libraries, so pub.dev/pana +evaluated the IO and web implementation files as independent libraries and +reported false platform incompatibilities. The deeper issue is the same: the +public package boundary should be one stable, framework-neutral Dart facade, not +generated implementation files or Flutter-specific layout. + +## Decision + +Supersede ADR-009's Flutter-specific API decision. The `edge_intelligence` +pub.dev artifact is a Dart-first hybrid FFI package: its public API is plain +Dart, and its Android/iOS runtime installation uses Flutter FFI plugin metadata. + +The Dart package must follow these rules: + +1. The public entrypoint is `lib/edge_intelligence.dart`. +2. Generated binding files live under `lib/src/` and are implementation detail. +3. Public examples live under `example/` and demonstrate both plain Dart and a + runnable Flutter Android/iOS application. +4. `pubspec.yaml` declares both Dart and Flutter SDK environments because pub.dev + requires a Flutter lower bound for plugin packages. The package must not add + Flutter API dependencies; Flutter FFI plugin metadata exists solely to bundle + native artifacts. +5. Android shared libraries and the iOS XCFramework are implementation details + under the standard Flutter plugin directories. They do not introduce a + Flutter-specific Dart API or platform-channel contract. +6. The release pipeline should use Dart naming (`codegen-dart`, `out/dart`, + `packaging/dart`) for new work. Transitional Flutter-named aliases may remain + only to avoid breaking existing automation while the implementation migrates. + +`flutter_rust_bridge` remains an internal code generation and FFI mechanism. The +published facade must remain usable without importing Flutter. Mobile hosts use +the same `initEdgeIntelligence`, `EdgeLlm.local`, `ask`, and `askStream` API as +desktop Dart hosts. + +The package is platform-agnostic at the SDK boundary: the same Dart API selects +the available runtime for the host. Releases declare and ship native runtimes +for Flutter Android and iOS plus Dart Linux, macOS, and Windows. Android bundles +ABI-specific shared libraries; iOS bundles a dynamic XCFramework for device and +simulator builds. Browser/WASM remains a separate npm surface until the Dart +package has a working FRB web loader. A release must ship every runtime artifact +for a declared platform or fail clearly during initialization. + +## Consequences + +### Positive + +- Dart and Flutter consumers use one stable, framework-neutral API. +- Flutter Android/iOS applications receive the native runtime automatically. +- Flutter mobile and desktop applications use the same plain-Dart API surface; + non-UI Dart entrypoints can use it when dependency resolution is performed by + a Flutter SDK. +- Generated IO/web implementation files stop defining the public platform + compatibility signal. +- The package can regain pub.dev platform and documentation points without + overstating Flutter support. +- Release and documentation language align with the SDK's platform-agnostic + architecture. + +### Negative + +- Mobile packaging adds Android/iOS project metadata and increases the pub.dev + archive size. +- Every supported Android ABI and iOS device/simulator slice must be built and + tested in release CI. +- Existing workflow names and generated paths that contain `flutter` must be + migrated or kept as compatibility aliases for a transition period. +- Flutter applications that need framework-specific installation helpers may + require a separate adapter package later. + +### Neutral + +- The Rust core, native targets, WASM target, and `el-ffi` ownership remain + unchanged. +- React Native and npm/web packaging are unaffected and remain separate + distribution surfaces for mobile and browser hosts. +- Flutter remains a host and packaging mechanism, not a separate SDK API. +- The Flutter SDK constraint means a standalone Dart SDK by itself cannot + resolve this plugin package. + +## Links + +- Supersedes: [ADR-009](./ADR-009-flutter-rust-bridge-for-dart-bindings.md) +- Amends: [ADR-011](./ADR-011-multi-registry-release-ci-crates-io-npm-pub-dev.md) + for the pub.dev package identity and naming +- Partially amends: [ADR-001](./ADR-001-adopt-webassembly-as-cross-platform-sdk-runtime.md) + by replacing the Flutter binding surface with a Dart SDK surface +- Related: [ADR-019](./ADR-019-in-loop-incremental-decoding-and-token-streaming.md) + for Dart stream semantics +- Package: `edge_intelligence` on pub.dev diff --git a/docs/adr/README.md b/docs/adr/README.md index 05b75ad..b15d99b 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -8,7 +8,7 @@ the `adr-patterns` namespace. | ADR | Title | Status | Tags | |-----|-------|--------|------| -| [001](./ADR-001-adopt-webassembly-as-cross-platform-sdk-runtime.md) | WebAssembly (Wasmtime) as the portable target, alongside native ARM | accepted | runtime, wasm, rust, core | +| [001](./ADR-001-adopt-webassembly-as-cross-platform-sdk-runtime.md) | WebAssembly (Wasmtime) as the portable target, alongside native ARM | accepted - partially amended by ADR-024 | runtime, wasm, rust, core | | [002](./ADR-002-candle-as-rust-native-inference-engine.md) | Candle as the Rust-native inference engine | accepted | runtime, candle, rust, core | | [003](./ADR-003-static-memory-planning-with-zero-allocation-arena.md) | Static memory planning with a zero-allocation arena (Runtime↔Memory Shared Kernel) | accepted | memory, shared-kernel, core | | [004](./ADR-004-air-gapped-by-default-with-opt-in-hybrid-mode.md) | Air-gapped by default with opt-in local-network HybridMode | accepted | privacy, networking, invariant | @@ -16,9 +16,9 @@ the `adr-patterns` namespace. | [006](./ADR-006-mandatory-ed25519-model-signature-verification-load-gate.md) | Mandatory ED25519 model-signature verification as a hard load gate | accepted | security, provenance, generic | | [007](./ADR-007-content-free-domain-events-privacy-by-construction-telemetry.md) | Content-free domain events for privacy-by-construction telemetry | accepted | privacy, telemetry, generic | | [008](./ADR-008-implement-the-sdk-in-rust-instead-of-c-cpp.md) | Implement the SDK in Rust instead of C/C++ | accepted | language, rust, foundational | -| [009](./ADR-009-flutter-rust-bridge-for-dart-bindings.md) | flutter_rust_bridge v2 for Dart/Flutter bindings | accepted | ffi, flutter, dart, mobile | +| [009](./ADR-009-flutter-rust-bridge-for-dart-bindings.md) | flutter_rust_bridge v2 for Dart/Flutter bindings | superseded by ADR-024 | ffi, flutter, dart, mobile | | [010](./ADR-010-unified-llm-provider-trait-with-opt-in-frontier-egress.md) | Unified LlmProvider trait with opt-in frontier LLM cloud egress | accepted | llm, cloud, networking, trait | -| [011](./ADR-011-multi-registry-release-ci-crates-io-npm-pub-dev.md) | Multi-Registry Release CI (crates.io, npm, pub.dev) | accepted | ci, release, crates.io, npm, pub.dev, packaging | +| [011](./ADR-011-multi-registry-release-ci-crates-io-npm-pub-dev.md) | Multi-Registry Release CI (crates.io, npm, pub.dev) | accepted - partially amended by ADR-024 | ci, release, crates.io, npm, pub.dev, packaging | | [012](./ADR-012-layered-decode-time-safety-control-loop-with-checkpointed-rollback.md) | Layered decode-time safety control loop with checkpointed rollback | accepted | safety, security, on-device, runtime, supporting | | [013](./ADR-013-model-backed-steering-layers-for-the-hybrid-safety-control-loop.md) | Model-backed steering layers for the hybrid safety control loop | proposed | safety, security, on-device, runtime, follow-up | | [018](./ADR-018-persistent-model-instances-and-stateful-sessions.md) | Persistent model instances and stateful inference sessions | proposed | runtime, performance, on-device, follow-up, P0 | @@ -27,6 +27,7 @@ the `adr-patterns` namespace. | [021](./ADR-021-memory-mapped-verified-gguf-loading.md) | Memory-mapped verified GGUF loading | proposed | runtime, performance, provenance, on-device, follow-up, P0 | | [022](./ADR-022-two-tier-quantized-kv-cache-with-attention-aware-eviction.md) | Two-tier quantized KV cache with attention-aware eviction | proposed | memory, runtime, performance, on-device, follow-up, P0 | | [023](./ADR-023-baseline-performance-instrumentation.md) | Baseline performance instrumentation | proposed | telemetry, performance, testing, follow-up, P0 | +| [024](./ADR-024-dart-only-platform-agnostic-pub-dev-sdk.md) | Dart-first pub.dev SDK with Flutter mobile runtimes | accepted | dart, flutter, pub.dev, bindings, packaging, platform-agnostic | ## Decision relationships @@ -36,7 +37,8 @@ flowchart LR A008 --> A002[002 Candle engine] A008 --> A006[006 ed25519-dalek load gate] A001 --> A002 - A001 --> A009[009 flutter_rust_bridge] + A001 --> A024[024 Dart-first pub.dev SDK] + A009[009 flutter_rust_bridge] -. superseded by .-> A024 A002 --> A003[003 Static memory / Shared Kernel] A002 --> A010[010 LlmProvider + el-cloud] A004[004 Air-gap + HybridMode] --> A005[005 On-device tiered safety] @@ -64,6 +66,7 @@ flowchart LR A023 -. measures .-> A020 A023 -. measures .-> A021 A023 -. measures .-> A022 + A011[011 Multi-registry release CI] -. pub.dev identity amended by .-> A024 ``` > **ADR-008 is foundational** (the language decision) and drives the revisions to diff --git a/packaging/dart/.gitignore b/packaging/dart/.gitignore new file mode 100644 index 0000000..61608ef --- /dev/null +++ b/packaging/dart/.gitignore @@ -0,0 +1,3 @@ +.dart_tool/ +build/ +pubspec.lock diff --git a/packaging/dart/README.md b/packaging/dart/README.md new file mode 100644 index 0000000..284d721 --- /dev/null +++ b/packaging/dart/README.md @@ -0,0 +1,94 @@ +# edge_intelligence + +Dart bindings for the Edge Intelligence SDK, including Flutter Android/iOS and +native Dart desktop runtimes. + +The package exposes one framework-neutral Dart API. Flutter plugin metadata is +used only to bundle the Android and iOS native runtimes; the public library does +not import Flutter or expose Flutter types. + +## Usage + +```dart +import "package:edge_intelligence/edge_intelligence.dart"; + +Future main() async { + await initEdgeIntelligence(); + + try { + final sdk = await EdgeLlm.local("/path/to/model.gguf"); + final reply = await sdk.ask("Summarize edge inference in one sentence."); + print(reply); + } finally { + disposeEdgeIntelligence(); + } +} +``` + +On a desktop host, `initEdgeIntelligence` resolves the packaged artifact for the +current operating system: + +- Linux x64: `lib/native/linux/x64/libel_ffi.so` +- macOS universal: `lib/native/macos/libel_ffi.dylib` +- Windows x64: `lib/native/windows/x64/el_ffi.dll` + +If the package assets cannot be loaded from the host filesystem, +initialization fails with an actionable `UnsupportedError`; hosts with a custom +runtime location can pass an `externalLibrary` directly. + +## Flutter Android and iOS + +Add `edge_intelligence` as a normal Flutter dependency. The plugin bundles +`libel_ffi.so` on Android and `el_ffi.xcframework` on iOS, so no manual native +library path is required. + +```dart +import "package:edge_intelligence/edge_intelligence.dart"; +import "package:flutter/widgets.dart"; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await initEdgeIntelligence(); + runApp(const MyApp()); +} +``` + +Call `disposeEdgeIntelligence()` from the root application's teardown hook, not +immediately after `runApp`, because `runApp` returns while the application is +still active. + +The mobile release includes: + +- Android API 24+ for `armeabi-v7a`, `arm64-v8a`, and `x86_64`. +- iOS 13+ device and simulator slices in a dynamic XCFramework. + +Keep framework-specific model storage, permissions, and UI state in the Flutter +application. Pass the resulting local GGUF file path to `EdgeLlm.local`. + +## Supported hosts + +| Host | Runtime packaging | +|---|---| +| Flutter Android | APK/AAB native libraries | +| Flutter iOS | SwiftPM or CocoaPods XCFramework | +| Dart Linux x64 | Packaged shared library | +| Dart macOS | Packaged universal dynamic library | +| Dart Windows x64 | Packaged DLL | + +Web is not currently supported by this pub.dev package. Use the npm/WASM package +for browser applications. + +## Example + +`example/` contains a runnable Flutter Android/iOS application that selects a +local GGUF model and streams a response. Run it with: + +```shell +cd example +flutter pub get +flutter run +``` + +`example/dart_cli.dart` demonstrates the same API from a Dart entrypoint. Because +this package includes Flutter plugin metadata, dependency resolution requires a +Flutter SDK even when the consuming code imports no Flutter APIs. diff --git a/packaging/dart/android/build.gradle b/packaging/dart/android/build.gradle new file mode 100644 index 0000000..c4da493 --- /dev/null +++ b/packaging/dart/android/build.gradle @@ -0,0 +1,35 @@ +group = "com.tovli.edge_intelligence" +version = "1.0" + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath "com.android.tools.build:gradle:8.7.3" + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: "com.android.library" + +android { + namespace = "com.tovli.edge_intelligence" + compileSdk = 35 + + defaultConfig { + minSdk = 24 + } + + sourceSets { + main.jniLibs.srcDirs = ["src/main/jniLibs"] + } +} diff --git a/packaging/dart/android/src/main/AndroidManifest.xml b/packaging/dart/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..94cbbcf --- /dev/null +++ b/packaging/dart/android/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/packaging/dart/android/src/main/jniLibs/README.md b/packaging/dart/android/src/main/jniLibs/README.md new file mode 100644 index 0000000..26e45b8 --- /dev/null +++ b/packaging/dart/android/src/main/jniLibs/README.md @@ -0,0 +1,2 @@ +Release assembly places the Android `el_ffi` shared libraries in ABI-specific +subdirectories here. They are generated artifacts and are not committed. diff --git a/packaging/dart/example/.gitignore b/packaging/dart/example/.gitignore new file mode 100644 index 0000000..3820a95 --- /dev/null +++ b/packaging/dart/example/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packaging/dart/example/.metadata b/packaging/dart/example/.metadata new file mode 100644 index 0000000..5d8703b --- /dev/null +++ b/packaging/dart/example/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "559ffa3f75e7402d65a8def9c28389a9b2e6fe42" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + - platform: android + create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + - platform: ios + create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/packaging/dart/example/README.md b/packaging/dart/example/README.md new file mode 100644 index 0000000..a615865 --- /dev/null +++ b/packaging/dart/example/README.md @@ -0,0 +1,9 @@ +# Edge Intelligence Flutter example + +This Android/iOS example initializes the packaged Rust runtime, lets the user +select a local GGUF model, and streams a response through the public Dart API. + +```shell +flutter pub get +flutter run +``` diff --git a/packaging/dart/example/analysis_options.yaml b/packaging/dart/example/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/packaging/dart/example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packaging/dart/example/android/.gitignore b/packaging/dart/example/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/packaging/dart/example/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/packaging/dart/example/android/app/build.gradle.kts b/packaging/dart/example/android/app/build.gradle.kts new file mode 100644 index 0000000..483d35a --- /dev/null +++ b/packaging/dart/example/android/app/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + id("com.android.application") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.tovli.edge_intelligence_example" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.tovli.edge_intelligence_example" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + +flutter { + source = "../.." +} diff --git a/packaging/dart/example/android/app/src/debug/AndroidManifest.xml b/packaging/dart/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/packaging/dart/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packaging/dart/example/android/app/src/main/AndroidManifest.xml b/packaging/dart/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..14d4720 --- /dev/null +++ b/packaging/dart/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/packaging/dart/example/android/app/src/main/kotlin/com/tovli/edge_intelligence_example/MainActivity.kt b/packaging/dart/example/android/app/src/main/kotlin/com/tovli/edge_intelligence_example/MainActivity.kt new file mode 100644 index 0000000..e1cc2f5 --- /dev/null +++ b/packaging/dart/example/android/app/src/main/kotlin/com/tovli/edge_intelligence_example/MainActivity.kt @@ -0,0 +1,5 @@ +package com.tovli.edge_intelligence_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/packaging/dart/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packaging/dart/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/packaging/dart/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packaging/dart/example/android/app/src/main/res/drawable/launch_background.xml b/packaging/dart/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/packaging/dart/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packaging/dart/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packaging/dart/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..db77bb4b7b0906d62b1847e87f15cdcacf6a4f29 GIT binary patch literal 544 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY3?!3`olAj~WQl7;NpOBzNqJ&XDuZK6ep0G} zXKrG8YEWuoN@d~6R2!h8bpbvhu0Wd6uZuB!w&u2PAxD2eNXD>P5D~Wn-+_Wa#27Xc zC?Zj|6r#X(-D3u$NCt}(Ms06KgJ4FxJVv{GM)!I~&n8Bnc94O7-Hd)cjDZswgC;Qs zO=b+9!WcT8F?0rF7!Uys2bs@gozCP?z~o%U|N3vA*22NaGQG zlg@K`O_XuxvZ&Ks^m&R!`&1=spLvfx7oGDKDwpwW`#iqdw@AL`7MR}m`rwr|mZgU`8P7SBkL78fFf!WnuYWm$5Z0 zNXhDbCv&49sM544K|?c)WrFfiZvCi9h0O)B3Pgg&ebxsLQ05GG~ AQ2+n{ literal 0 HcmV?d00001 diff --git a/packaging/dart/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packaging/dart/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..17987b79bb8a35cc66c3c1fd44f5a5526c1b78be GIT binary patch literal 442 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA3?vioaBc-sk|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*D5Xx&nMcT!A!W`0S9QKQy;}1Cl^CgaH=;G9cpY;r$Q>i*pfB zP2drbID<_#qf;rPZx^FqH)F_D#*k@@q03KywUtLX8Ua?`H+NMzkczFPK3lFz@i_kW%1NOn0|D2I9n9wzH8m|-tHjsw|9>@K=iMBhxvkv6m8Y-l zytQ?X=U+MF$@3 zt`~i=@j|6y)RWMK--}M|=T`o&^Ni>IoWKHEbBXz7?A@mgWoL>!*SXo`SZH-*HSdS+ yn*9;$7;m`l>wYBC5bq;=U}IMqLzqbYCidGC!)_gkIk_C@Uy!y&wkt5C($~2D>~)O*cj@FGjOCM)M>_ixfudOh)?xMu#Fs z#}Y=@YDTwOM)x{K_j*Q;dPdJ?Mz0n|pLRx{4n|)f>SXlmV)XB04CrSJn#dS5nK2lM zrZ9#~WelCp7&e13Y$jvaEXHskn$2V!!DN-nWS__6T*l;H&Fopn?A6HZ-6WRLFP=R` zqG+CE#d4|IbyAI+rJJ`&x9*T`+a=p|0O(+s{UBcyZdkhj=yS1>AirP+0R;mf2uMgM zC}@~JfByORAh4SyRgi&!(cja>F(l*O+nd+@4m$|6K6KDn_&uvCpV23&>G9HJp{xgg zoq1^2_p9@|WEo z*X_Uko@K)qYYv~>43eQGMdbiGbo>E~Q& zrYBH{QP^@Sti!`2)uG{irBBq@y*$B zi#&(U-*=fp74j)RyIw49+0MRPMRU)+a2r*PJ$L5roHt2$UjExCTZSbq%V!HeS7J$N zdG@vOZB4v_lF7Plrx+hxo7(fCV&}fHq)$ literal 0 HcmV?d00001 diff --git a/packaging/dart/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packaging/dart/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..d5f1c8d34e7a88e3f88bea192c3a370d44689c3c GIT binary patch literal 1031 zcmeAS@N?(olHy`uVBq!ia0vp^6F``Q8Ax83A=Cw=BuiW)N`mv#O3D+9QW+dm@{>{( zJaZG%Q-e|yQz{EjrrIztFa`(sgt!6~Yi|1%a`XoT0ojZ}lNrNjb9xjc(B0U1_% zz5^97Xt*%oq$rQy4?0GKNfJ44uvxI)gC`h-NZ|&0-7(qS@?b!5r36oQ}zyZrNO3 zMO=Or+<~>+A&uN&E!^Sl+>xE!QC-|oJv`ApDhqC^EWD|@=#J`=d#Xzxs4ah}w&Jnc z$|q_opQ^2TrnVZ0o~wh<3t%W&flvYGe#$xqda2bR_R zvPYgMcHgjZ5nSA^lJr%;<&0do;O^tDDh~=pIxA#coaCY>&N%M2^tq^U%3DB@ynvKo}b?yu-bFc-u0JHzced$sg7S3zqI(2 z#Km{dPr7I=pQ5>FuK#)QwK?Y`E`B?nP+}U)I#c1+FM*1kNvWG|a(TpksZQ3B@sD~b zpQ2)*V*TdwjFOtHvV|;OsiDqHi=6%)o4b!)x$)%9pGTsE z-JL={-Ffv+T87W(Xpooq<`r*VzWQcgBN$$`u}f>-ZQI1BB8ykN*=e4rIsJx9>z}*o zo~|9I;xof literal 0 HcmV?d00001 diff --git a/packaging/dart/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packaging/dart/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..4d6372eebdb28e45604e46eeda8dd24651419bc0 GIT binary patch literal 1443 zcmb`G{WsKk6vsdJTdFg%tJav9_E4vzrOaqkWF|A724Nly!y+?N9`YV6wZ}5(X(D_N(?!*n3`|_r0Hc?=PQw&*vnU?QTFY zB_MsH|!j$PP;I}?dppoE_gA(4uc!jV&0!l7_;&p2^pxNo>PEcNJv za5_RT$o2Mf!<+r?&EbHH6nMoTsDOa;mN(wv8RNsHpG)`^ymG-S5By8=l9iVXzN_eG%Xg2@Xeq76tTZ*dGh~Lo9vl;Zfs+W#BydUw zCkZ$o1LqWQO$FC9aKlLl*7x9^0q%0}$OMlp@Kk_jHXOjofdePND+j!A{q!8~Jn+s3 z?~~w@4?egS02}8NuulUA=L~QQfm;MzCGd)XhiftT;+zFO&JVyp2mBww?;QByS_1w! zrQlx%{^cMj0|Bo1FjwY@Q8?Hx0cIPF*@-ZRFpPc#bBw{5@tD(5%sClzIfl8WU~V#u zm5Q;_F!wa$BSpqhN>W@2De?TKWR*!ujY;Yylk_X5#~V!L*Gw~;$%4Q8~Mad z@`-kG?yb$a9cHIApZDVZ^U6Xkp<*4rU82O7%}0jjHlK{id@?-wpN*fCHXyXh(bLt* zPc}H-x0e4E&nQ>y%B-(EL=9}RyC%MyX=upHuFhAk&MLbsF0LP-q`XnH78@fT+pKPW zu72MW`|?8ht^tz$iC}ZwLp4tB;Q49K!QCF3@!iB1qOI=?w z7In!}F~ij(18UYUjnbmC!qKhPo%24?8U1x{7o(+?^Zu0Hx81|FuS?bJ0jgBhEMzf< zCgUq7r2OCB(`XkKcN-TL>u5y#dD6D!)5W?`O5)V^>jb)P)GBdy%t$uUMpf$SNV31$ zb||OojAbvMP?T@$h_ZiFLFVHDmbyMhJF|-_)HX3%m=CDI+ID$0^C>kzxprBW)hw(v zr!Gmda);ICoQyhV_oP5+C%?jcG8v+D@9f?Dk*!BxY}dazmrT@64UrP3hlslANK)bq z$67n83eh}OeW&SV@HG95P|bjfqJ7gw$e+`Hxo!4cx`jdK1bJ>YDSpGKLPZ^1cv$ek zIB?0S<#tX?SJCLWdMd{-ME?$hc7A$zBOdIJ)4!KcAwb=VMov)nK;9z>x~rfT1>dS+ zZ6#`2v@`jgbqq)P22H)Tx2CpmM^o1$B+xT6`(v%5xJ(?j#>Q$+rx_R|7TzDZe{J6q zG1*EcU%tE?!kO%^M;3aM6JN*LAKUVb^xz8-Pxo#jR5(-KBeLJvA@-gxNHx0M-ZJLl z;#JwQoh~9V?`UVo#}{6ka@II>++D@%KqGpMdlQ}?9E*wFcf5(#XQnP$Dk5~%iX^>f z%$y;?M0BLp{O3a(-4A?ewryHrrD%cx#Q^%KY1H zNre$ve+vceSLZcNY4U(RBX&)oZn*Py()h)XkE?PL$!bNb{N5FVI2Y%LKEm%yvpyTP z(1P?z~7YxD~Rf<(a@_y` literal 0 HcmV?d00001 diff --git a/packaging/dart/example/android/app/src/main/res/values-night/styles.xml b/packaging/dart/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/packaging/dart/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packaging/dart/example/android/app/src/main/res/values/styles.xml b/packaging/dart/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/packaging/dart/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packaging/dart/example/android/app/src/profile/AndroidManifest.xml b/packaging/dart/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/packaging/dart/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packaging/dart/example/android/build.gradle.kts b/packaging/dart/example/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/packaging/dart/example/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/packaging/dart/example/android/gradle.properties b/packaging/dart/example/android/gradle.properties new file mode 100644 index 0000000..2967219 --- /dev/null +++ b/packaging/dart/example/android/gradle.properties @@ -0,0 +1,7 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +kotlin.incremental=false +# This newDsl flag was added by the Flutter template +android.newDsl=false +# This builtInKotlin flag was added by the Flutter template +android.builtInKotlin=false diff --git a/packaging/dart/example/android/gradle/wrapper/gradle-wrapper.properties b/packaging/dart/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..2d428bf --- /dev/null +++ b/packaging/dart/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip diff --git a/packaging/dart/example/android/settings.gradle.kts b/packaging/dart/example/android/settings.gradle.kts new file mode 100644 index 0000000..c21f0c5 --- /dev/null +++ b/packaging/dart/example/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "9.0.1" apply false + id("org.jetbrains.kotlin.android") version "2.3.20" apply false +} + +include(":app") diff --git a/packaging/dart/example/dart_cli.dart b/packaging/dart/example/dart_cli.dart new file mode 100644 index 0000000..54ebff7 --- /dev/null +++ b/packaging/dart/example/dart_cli.dart @@ -0,0 +1,27 @@ +import 'dart:io'; + +import 'package:edge_intelligence/edge_intelligence.dart'; + +Future main(List arguments) async { + if (arguments.isEmpty) { + stderr + .writeln('Usage: dart run example/dart_cli.dart [prompt]'); + exitCode = 64; + return; + } + + final prompt = arguments.length > 1 + ? arguments.skip(1).join(' ') + : 'Summarize edge inference in one sentence.'; + + await initEdgeIntelligence(); + try { + final sdk = await EdgeLlm.local(arguments.first); + await for (final token in sdk.askStream(prompt)) { + stdout.write(token); + } + stdout.writeln(); + } finally { + disposeEdgeIntelligence(); + } +} diff --git a/packaging/dart/example/ios/.gitignore b/packaging/dart/example/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/packaging/dart/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/packaging/dart/example/ios/Flutter/AppFrameworkInfo.plist b/packaging/dart/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..391a902 --- /dev/null +++ b/packaging/dart/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + + diff --git a/packaging/dart/example/ios/Flutter/Debug.xcconfig b/packaging/dart/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/packaging/dart/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/packaging/dart/example/ios/Flutter/Release.xcconfig b/packaging/dart/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..592ceee --- /dev/null +++ b/packaging/dart/example/ios/Flutter/Release.xcconfig @@ -0,0 +1 @@ +#include "Generated.xcconfig" diff --git a/packaging/dart/example/ios/Runner.xcodeproj/project.pbxproj b/packaging/dart/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..9f35743 --- /dev/null +++ b/packaging/dart/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,644 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; }; + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */, + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + packageProductDependencies = ( + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */, + ); + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + packageReferences = ( + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */, + ); + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.tovli.edgeIntelligenceExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.tovli.edgeIntelligenceExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.tovli.edgeIntelligenceExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.tovli.edgeIntelligenceExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.tovli.edgeIntelligenceExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.tovli.edgeIntelligenceExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = { + isa = XCSwiftPackageProductDependency; + productName = FlutterGeneratedPluginSwiftPackage; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packaging/dart/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packaging/dart/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/packaging/dart/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packaging/dart/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packaging/dart/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/packaging/dart/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packaging/dart/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packaging/dart/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/packaging/dart/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packaging/dart/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packaging/dart/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..c3fedb2 --- /dev/null +++ b/packaging/dart/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packaging/dart/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packaging/dart/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/packaging/dart/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packaging/dart/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packaging/dart/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/packaging/dart/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packaging/dart/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packaging/dart/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/packaging/dart/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packaging/dart/example/ios/Runner/AppDelegate.swift b/packaging/dart/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..c30b367 --- /dev/null +++ b/packaging/dart/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,16 @@ +import Flutter +import UIKit + +@main +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } +} diff --git a/packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..dc9ada4725e9b0ddb1deab583e5b5102493aa332 GIT binary patch literal 10932 zcmeHN2~<R zh`|8`A_PQ1nSu(UMFx?8j8PC!!VDphaL#`F42fd#7Vlc`zIE4n%Y~eiz4y1j|NDpi z?<@|pSJ-HM`qifhf@m%MamgwK83`XpBA<+azdF#2QsT{X@z0A9Bq>~TVErigKH1~P zRX-!h-f0NJ4Mh++{D}J+K>~~rq}d%o%+4dogzXp7RxX4C>Km5XEI|PAFDmo;DFm6G zzjVoB`@qW98Yl0Kvc-9w09^PrsobmG*Eju^=3f?0o-t$U)TL1B3;sZ^!++3&bGZ!o-*6w?;oOhf z=A+Qb$scV5!RbG+&2S}BQ6YH!FKb0``VVX~T$dzzeSZ$&9=X$3)_7Z{SspSYJ!lGE z7yig_41zpQ)%5dr4ff0rh$@ky3-JLRk&DK)NEIHecf9c*?Z1bUB4%pZjQ7hD!A0r-@NF(^WKdr(LXj|=UE7?gBYGgGQV zidf2`ZT@pzXf7}!NH4q(0IMcxsUGDih(0{kRSez&z?CFA0RVXsVFw3^u=^KMtt95q z43q$b*6#uQDLoiCAF_{RFc{!H^moH_cmll#Fc^KXi{9GDl{>%+3qyfOE5;Zq|6#Hb zp^#1G+z^AXfRKaa9HK;%b3Ux~U@q?xg<2DXP%6k!3E)PA<#4$ui8eDy5|9hA5&{?v z(-;*1%(1~-NTQ`Is1_MGdQ{+i*ccd96ab$R$T3=% zw_KuNF@vI!A>>Y_2pl9L{9h1-C6H8<)J4gKI6{WzGBi<@u3P6hNsXG=bRq5c+z;Gc3VUCe;LIIFDmQAGy+=mRyF++u=drBWV8-^>0yE9N&*05XHZpPlE zxu@?8(ZNy7rm?|<+UNe0Vs6&o?l`Pt>P&WaL~M&#Eh%`rg@Mbb)J&@DA-wheQ>hRV z<(XhigZAT z>=M;URcdCaiO3d^?H<^EiEMDV+7HsTiOhoaMX%P65E<(5xMPJKxf!0u>U~uVqnPN7T!X!o@_gs3Ct1 zlZ_$5QXP4{Aj645wG_SNT&6m|O6~Tsl$q?nK*)(`{J4b=(yb^nOATtF1_aS978$x3 zx>Q@s4i3~IT*+l{@dx~Hst21fR*+5}S1@cf>&8*uLw-0^zK(+OpW?cS-YG1QBZ5q! zgTAgivzoF#`cSz&HL>Ti!!v#?36I1*l^mkrx7Y|K6L#n!-~5=d3;K<;Zqi|gpNUn_ z_^GaQDEQ*jfzh;`j&KXb66fWEk1K7vxQIMQ_#Wu_%3 z4Oeb7FJ`8I>Px;^S?)}2+4D_83gHEq>8qSQY0PVP?o)zAv3K~;R$fnwTmI-=ZLK`= zTm+0h*e+Yfr(IlH3i7gUclNH^!MU>id$Jw>O?2i0Cila#v|twub21@e{S2v}8Z13( zNDrTXZVgris|qYm<0NU(tAPouG!QF4ZNpZPkX~{tVf8xY690JqY1NVdiTtW+NqyRP zZ&;T0ikb8V{wxmFhlLTQ&?OP7 z;(z*<+?J2~z*6asSe7h`$8~Se(@t(#%?BGLVs$p``;CyvcT?7Y!{tIPva$LxCQ&4W z6v#F*);|RXvI%qnoOY&i4S*EL&h%hP3O zLsrFZhv&Hu5tF$Lx!8(hs&?!Kx5&L(fdu}UI5d*wn~A`nPUhG&Rv z2#ixiJdhSF-K2tpVL=)5UkXRuPAFrEW}7mW=uAmtVQ&pGE-&az6@#-(Te^n*lrH^m@X-ftVcwO_#7{WI)5v(?>uC9GG{lcGXYJ~Q8q zbMFl7;t+kV;|;KkBW2!P_o%Czhw&Q(nXlxK9ak&6r5t_KH8#1Mr-*0}2h8R9XNkr zto5-b7P_auqTJb(TJlmJ9xreA=6d=d)CVbYP-r4$hDn5|TIhB>SReMfh&OVLkMk-T zYf%$taLF0OqYF?V{+6Xkn>iX@TuqQ?&cN6UjC9YF&%q{Ut3zv{U2)~$>-3;Dp)*(? zg*$mu8^i=-e#acaj*T$pNowo{xiGEk$%DusaQiS!KjJH96XZ-hXv+jk%ard#fu=@Q z$AM)YWvE^{%tDfK%nD49=PI|wYu}lYVbB#a7wtN^Nml@CE@{Gv7+jo{_V?I*jkdLD zJE|jfdrmVbkfS>rN*+`#l%ZUi5_bMS<>=MBDNlpiSb_tAF|Zy`K7kcp@|d?yaTmB^ zo?(vg;B$vxS|SszusORgDg-*Uitzdi{dUV+glA~R8V(?`3GZIl^egW{a919!j#>f` znL1o_^-b`}xnU0+~KIFLQ)$Q6#ym%)(GYC`^XM*{g zv3AM5$+TtDRs%`2TyR^$(hqE7Y1b&`Jd6dS6B#hDVbJlUXcG3y*439D8MrK!2D~6gn>UD4Imctb z+IvAt0iaW73Iq$K?4}H`7wq6YkTMm`tcktXgK0lKPmh=>h+l}Y+pDtvHnG>uqBA)l zAH6BV4F}v$(o$8Gfo*PB>IuaY1*^*`OTx4|hM8jZ?B6HY;F6p4{`OcZZ(us-RVwDx zUzJrCQlp@mz1ZFiSZ*$yX3c_#h9J;yBE$2g%xjmGF4ca z&yL`nGVs!Zxsh^j6i%$a*I3ZD2SoNT`{D%mU=LKaEwbN(_J5%i-6Va?@*>=3(dQy` zOv%$_9lcy9+(t>qohkuU4r_P=R^6ME+wFu&LA9tw9RA?azGhjrVJKy&8=*qZT5Dr8g--d+S8zAyJ$1HlW3Olryt`yE zFIph~Z6oF&o64rw{>lgZISC6p^CBer9C5G6yq%?8tC+)7*d+ib^?fU!JRFxynRLEZ zj;?PwtS}Ao#9whV@KEmwQgM0TVP{hs>dg(1*DiMUOKHdQGIqa0`yZnHk9mtbPfoLx zo;^V6pKUJ!5#n`w2D&381#5#_t}AlTGEgDz$^;u;-vxDN?^#5!zN9ngytY@oTv!nc zp1Xn8uR$1Z;7vY`-<*?DfPHB;x|GUi_fI9@I9SVRv1)qETbNU_8{5U|(>Du84qP#7 z*l9Y$SgA&wGbj>R1YeT9vYjZuC@|{rajTL0f%N@>3$DFU=`lSPl=Iv;EjuGjBa$Gw zHD-;%YOE@<-!7-Mn`0WuO3oWuL6tB2cpPw~Nvuj|KM@))ixuDK`9;jGMe2d)7gHin zS<>k@!x;!TJEc#HdL#RF(`|4W+H88d4V%zlh(7#{q2d0OQX9*FW^`^_<3r$kabWAB z$9BONo5}*(%kx zOXi-yM_cmB3>inPpI~)duvZykJ@^^aWzQ=eQ&STUa}2uT@lV&WoRzkUoE`rR0)`=l zFT%f|LA9fCw>`enm$p7W^E@U7RNBtsh{_-7vVz3DtB*y#*~(L9+x9*wn8VjWw|Q~q zKFsj1Yl>;}%MG3=PY`$g$_mnyhuV&~O~u~)968$0b2!Jkd;2MtAP#ZDYw9hmK_+M$ zb3pxyYC&|CuAbtiG8HZjj?MZJBFbt`ryf+c1dXFuC z0*ZQhBzNBd*}s6K_G}(|Z_9NDV162#y%WSNe|FTDDhx)K!c(mMJh@h87@8(^YdK$&d*^WQe8Z53 z(|@MRJ$Lk-&ii74MPIs80WsOFZ(NX23oR-?As+*aq6b?~62@fSVmM-_*cb1RzZ)`5$agEiL`-E9s7{GM2?(KNPgK1(+c*|-FKoy}X(D_b#etO|YR z(BGZ)0Ntfv-7R4GHoXp?l5g#*={S1{u-QzxCGng*oWr~@X-5f~RA14b8~B+pLKvr4 zfgL|7I>jlak9>D4=(i(cqYf7#318!OSR=^`xxvI!bBlS??`xxWeg?+|>MxaIdH1U~#1tHu zB{QMR?EGRmQ_l4p6YXJ{o(hh-7Tdm>TAX380TZZZyVkqHNzjUn*_|cb?T? zt;d2s-?B#Mc>T-gvBmQZx(y_cfkXZO~{N zT6rP7SD6g~n9QJ)8F*8uHxTLCAZ{l1Y&?6v)BOJZ)=R-pY=Y=&1}jE7fQ>USS}xP#exo57uND0i*rEk@$;nLvRB@u~s^dwRf?G?_enN@$t* zbL%JO=rV(3Ju8#GqUpeE3l_Wu1lN9Y{D4uaUe`g>zlj$1ER$6S6@{m1!~V|bYkhZA z%CvrDRTkHuajMU8;&RZ&itnC~iYLW4DVkP<$}>#&(`UO>!n)Po;Mt(SY8Yb`AS9lt znbX^i?Oe9r_o=?})IHKHoQGKXsps_SE{hwrg?6dMI|^+$CeC&z@*LuF+P`7LfZ*yr+KN8B4{Nzv<`A(wyR@!|gw{zB6Ha ziwPAYh)oJ(nlqSknu(8g9N&1hu0$vFK$W#mp%>X~AU1ay+EKWcFdif{% z#4!4aoVVJ;ULmkQf!ke2}3hqxLK>eq|-d7Ly7-J9zMpT`?dxo6HdfJA|t)?qPEVBDv z{y_b?4^|YA4%WW0VZd8C(ZgQzRI5(I^)=Ub`Y#MHc@nv0w-DaJAqsbEHDWG8Ia6ju zo-iyr*sq((gEwCC&^TYBWt4_@|81?=B-?#P6NMff(*^re zYqvDuO`K@`mjm_Jd;mW_tP`3$cS?R$jR1ZN09$YO%_iBqh5ftzSpMQQtxKFU=FYmP zeY^jph+g<4>YO;U^O>-NFLn~-RqlHvnZl2yd2A{Yc1G@Ga$d+Q&(f^tnPf+Z7serIU};17+2DU_f4Z z@GaPFut27d?!YiD+QP@)T=77cR9~MK@bd~pY%X(h%L={{OIb8IQmf-!xmZkm8A0Ga zQSWONI17_ru5wpHg3jI@i9D+_Y|pCqVuHJNdHUauTD=R$JcD2K_liQisqG$(sm=k9;L* z!L?*4B~ql7uioSX$zWJ?;q-SWXRFhz2Jt4%fOHA=Bwf|RzhwqdXGr78y$J)LR7&3T zE1WWz*>GPWKZ0%|@%6=fyx)5rzUpI;bCj>3RKzNG_1w$fIFCZ&UR0(7S?g}`&Pg$M zf`SLsz8wK82Vyj7;RyKmY{a8G{2BHG%w!^T|Njr!h9TO2LaP^_f22Q1=l$QiU84ao zHe_#{S6;qrC6w~7{y(hs-?-j?lbOfgH^E=XcSgnwW*eEz{_Z<_xN#0001NP)t-s|Ns9~ z#rXRE|M&d=0au&!`~QyF`q}dRnBDt}*!qXo`c{v z{Djr|@Adh0(D_%#_&mM$D6{kE_x{oE{l@J5@%H*?%=t~i_`ufYOPkAEn!pfkr2$fs z652Tz0001XNklqeeKN4RM4i{jKqmiC$?+xN>3Apn^ z0QfuZLym_5b<*QdmkHjHlj811{If)dl(Z2K0A+ekGtrFJb?g|wt#k#pV-#A~bK=OT ts8>{%cPtyC${m|1#B1A6#u!Q;umknL1chzTM$P~L002ovPDHLkV1lTfnu!1a literal 0 HcmV?d00001 diff --git a/packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..797d452e458972bab9d994556c8305db4c827017 GIT binary patch literal 406 zcmV;H0crk;P))>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed2d933e1120817fe9182483a228007b18ab6ae GIT binary patch literal 450 zcmV;z0X_bSP)iGWQ_5NJQ_~rNh*z)}eT%KUb z`7gNk0#AwF^#0T0?hIa^`~Ck;!}#m+_uT050aTR(J!bU#|IzRL%^UsMS#KsYnTF*!YeDOytlP4VhV?b} z%rz_<=#CPc)tU1MZTq~*2=8~iZ!lSa<{9b@2Jl;?IEV8)=fG217*|@)CCYgFze-x? zIFODUIA>nWKpE+bn~n7;-89sa>#DR>TSlqWk*!2hSN6D~Qb#VqbP~4Fk&m`@1$JGr zXPIdeRE&b2Thd#{MtDK$px*d3-Wx``>!oimf%|A-&-q*6KAH)e$3|6JV%HX{Hig)k suLT-RhftRq8b9;(V=235Wa|I=027H2wCDra;{X5v07*qoM6N<$f;9x^2LJ#7 literal 0 HcmV?d00001 diff --git a/packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..4cd7b0099ca80c806f8fe495613e8d6c69460d76 GIT binary patch literal 282 zcmV+#0p(^bcu7P-R4C8Q z&e;xxFbF_Vrezo%_kH*OKhshZ6BFpG-Y1e10`QXJKbND7AMQ&cMj60B5TNObaZxYybcN07*qoM6N<$g3m;S%K!iX literal 0 HcmV?d00001 diff --git a/packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fe730945a01f64a61e2235dbe3f45b08f7729182 GIT binary patch literal 462 zcmV;<0WtoGP)-}iV`2<;=$?g5M=KQbZ{F&YRNy7Nn@%_*5{gvDM0aKI4?ESmw z{NnZg)A0R`+4?NF_RZexyVB&^^ZvN!{I28tr{Vje;QNTz`dG&Jz0~Ek&f2;*Z7>B|cg}xYpxEFY+0YrKLF;^Q+-HreN0P{&i zK~zY`?b7ECf-n?@;d<&orQ*Q7KoR%4|C>{W^h6@&01>0SKS`dn{Q}GT%Qj_{PLZ_& zs`MFI#j-(>?bvdZ!8^xTwlY{qA)T4QLbY@j(!YJ7aXJervHy6HaG_2SB`6CC{He}f zHVw(fJWApwPq!6VY7r1w-Fs)@ox~N+q|w~e;JI~C4Vf^@d>Wvj=fl`^u9x9wd9 zR%3*Q+)t%S!MU_`id^@&Y{y7-r98lZX0?YrHlfmwb?#}^1b{8g&KzmkE(L>Z&)179 zp<)v6Y}pRl100G2FL_t(o!|l{-Q-VMg#&MKg7c{O0 z2wJImOS3Gy*Z2Qifdv~JYOp;v+U)a|nLoc7hNH;I$;lzDt$}rkaFw1mYK5_0Q(Sut zvbEloxON7$+HSOgC9Z8ltuC&0OSF!-mXv5caV>#bc3@hBPX@I$58-z}(ZZE!t-aOG zpjNkbau@>yEzH(5Yj4kZiMH32XI!4~gVXNnjAvRx;Sdg^`>2DpUEwoMhTs_st8pKG z(%SHyHdU&v%f36~uERh!bd`!T2dw;z6PrOTQ7Vt*#9F2uHlUVnb#ev_o^fh}Dzmq} zWtlk35}k=?xj28uO|5>>$yXadTUE@@IPpgH`gJ~Ro4>jd1IF|(+IX>8M4Ps{PNvmI zNj4D+XgN83gPt_Gm}`Ybv{;+&yu-C(Grdiahmo~BjG-l&mWM+{e5M1sm&=xduwgM9 z`8OEh`=F3r`^E{n_;%9weN{cf2%7=VzC@cYj+lg>+3|D|_1C@{hcU(DyQG_BvBWe? zvTv``=%b1zrol#=R`JB)>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..502f463a9bc882b461c96aadf492d1729e49e725 GIT binary patch literal 586 zcmV-Q0=4~#P)+}#`wDE{8-2Mebf5<{{PqV{TgVcv*r8?UZ3{-|G?_}T*&y;@cqf{ z{Q*~+qr%%p!1pS*_Uicl#q9lc(D`!D`LN62sNwq{oYw(Wmhk)k<@f$!$@ng~_5)Ru z0Z)trIA5^j{DIW^c+vT2%lW+2<(RtE2wR;4O@)Tm`Xr*?A(qYoM}7i5Yxw>D(&6ou zxz!_Xr~yNF+waPe00049Nkl*;a!v6h%{rlvIH#gW3s8p;bFr=l}mRqpW2h zw=OA%hdyL~z+UHOzl0eKhEr$YYOL-c-%Y<)=j?(bzDweB7{b+%_ypvm_cG{SvM=DK zhv{K@m>#Bw>2W$eUI#iU)Wdgs8Y3U+A$Gd&{+j)d)BmGKx+43U_!tik_YlN)>$7G! zhkE!s;%oku3;IwG3U^2kw?z+HM)jB{@zFhK8P#KMSytSthr+4!c(5c%+^UBn`0X*2 zy3(k600_CSZj?O$Qu%&$;|TGUJrptR(HzyIx>5E(2r{eA(<6t3e3I0B)7d6s7?Z5J zZ!rtKvA{MiEBm&KFtoifx>5P^Z=vl)95XJn()aS5%ad(s?4-=Tkis9IGu{`Fy8r+H07*qoM6N<$f20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0ec303439225b78712f49115768196d8d76f6790 GIT binary patch literal 862 zcmV-k1EKthP)20Z)wqMt%V?S?~D#06};F zA3KcL`Wb+>5ObvgQIG&ig8(;V04hz?@cqy3{mSh8o!|U|)cI!1_+!fWH@o*8vh^CU z^ws0;(c$gI+2~q^tO#GDHf@=;DncUw00J^eL_t(&-tE|HQ`%4vfZ;WsBqu-$0nu1R zq^Vj;p$clf^?twn|KHO+IGt^q#a3X?w9dXC@*yxhv&l}F322(8Y1&=P&I}~G@#h6; z1CV9ecD9ZEe87{{NtI*)_aJ<`kJa z?5=RBtFF50s;jQLFil-`)m2wrb=6h(&brpj%nG_U&ut~$?8Rokzxi8zJoWr#2dto5 zOX_URcc<1`Iky+jc;A%Vzx}1QU{2$|cKPom2Vf1{8m`vja4{F>HS?^Nc^rp}xo+Nh zxd}eOm`fm3@MQC1< zIk&aCjb~Yh%5+Yq0`)D;q{#-Uqlv*o+Oor zE!I71Z@ASH3grl8&P^L0WpavHoP|UX4e?!igT`4?AZk$hu*@%6WJ;zDOGlw7kj@ zY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f~t1N9smFU literal 0 HcmV?d00001 diff --git a/packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..e9f5fea27c705180eb716271f41b582e76dcbd90 GIT binary patch literal 1674 zcmV;526g#~P){YQnis^a@{&-nmRmq)<&%Mztj67_#M}W?l>kYSliK<%xAp;0j{!}J0!o7b zE>q9${Lb$D&h7k=+4=!ek^n+`0zq>LL1O?lVyea53S5x`Nqqo2YyeuIrQrJj9XjOp z{;T5qbj3}&1vg1VK~#9!?b~^C5-}JC@Pyrv-6dSEqJqT}#j9#dJ@GzT@B8}x zU&J@bBI>f6w6en+CeI)3^kC*U?}X%OD8$Fd$H&LV$H&LV$H&LV#|K5~mLYf|VqzOc zkc7qL~0sOYuM{tG`rYEDV{DWY`Z8&)kW*hc2VkBuY+^Yx&92j&StN}Wp=LD zxoGxXw6f&8sB^u})h@b@z0RBeD`K7RMR9deyL(ZJu#39Z>rT)^>v}Khq8U-IbIvT> z?4pV9qGj=2)TNH3d)=De<+^w;>S7m_eFKTvzeaBeir45xY!^m!FmxnljbSS_3o=g( z->^wC9%qkR{kbGnW8MfFew_o9h3(r55Is`L$8KI@d+*%{=Nx+FXJ98L0PjFIu;rGnnfY zn1R5Qnp<{Jq0M1vX=X&F8gtLmcWv$1*M@4ZfF^9``()#hGTeKeP`1!iED ztNE(TN}M5}3Bbc*d=FIv`DNv&@|C6yYj{sSqUj5oo$#*0$7pu|Dd2TLI>t5%I zIa4Dvr(iayb+5x=j*Vum9&irk)xV1`t509lnPO0%skL8_1c#Xbamh(2@f?4yUI zhhuT5<#8RJhGz4%b$`PJwKPAudsm|at?u;*hGgnA zU1;9gnxVBC)wA(BsB`AW54N{|qmikJR*%x0c`{LGsSfa|NK61pYH(r-UQ4_JXd!Rsz)=k zL{GMc5{h138)fF5CzHEDM>+FqY)$pdN3}Ml+riTgJOLN0F*Vh?{9ESR{SVVg>*>=# zix;VJHPtvFFCRY$Ks*F;VX~%*r9F)W`PmPE9F!(&s#x07n2<}?S{(ygpXgX-&B&OM zONY&BRQ(#%0%jeQs?oJ4P!p*R98>qCy5p8w>_gpuh39NcOlp)(wOoz0sY-Qz55eB~ z7OC-fKBaD1sE3$l-6QgBJO!n?QOTza`!S_YK z_v-lm^7{VO^8Q@M_^8F)09Ki6%=s?2_5eupee(w1FB%aqSweusQ-T+CH0Xt{` zFjMvW{@C&TB)k25()nh~_yJ9coBRL(0oO@HK~z}7?bm5j;y@69;bvlHb2tf!$ReA~x{22wTq550 z?f?Hnw(;m3ip30;QzdV~7pi!wyMYhDtXW#cO7T>|f=bdFhu+F!zMZ2UFj;GUKX7tI z;hv3{q~!*pMj75WP_c}>6)IWvg5_yyg<9Op()eD1hWC19M@?_9_MHec{Z8n3FaF{8 z;u`Mw0ly(uE>*CgQYv{be6ab2LWhlaH1^iLIM{olnag$78^Fd}%dR7;JECQ+hmk|o z!u2&!3MqPfP5ChDSkFSH8F2WVOEf0(E_M(JL17G}Y+fg0_IuW%WQ zG(mG&u?|->YSdk0;8rc{yw2@2Z&GA}z{Wb91Ooz9VhA{b2DYE7RmG zjL}?eq#iX%3#k;JWMx_{^2nNax`xPhByFiDX+a7uTGU|otOvIAUy|dEKkXOm-`aWS z27pUzD{a)Ct<6p{{3)+lq@i`t@%>-wT4r?*S}k)58e09WZYP0{{R3FC5Sl00039P)t-s|Ns9~ z#rP?<_5oL$Q^olD{r_0T`27C={r>*`|Nj71npVa5OTzc(_WfbW_({R{p56NV{r*M2 z_xt?)2V0#0NsfV0u>{42ctGP(8vQj-Btk1n|O0ZD=YLwd&R{Ko41Gr9H= zY@z@@bOAMB5Ltl$E>bJJ{>JP30ZxkmI%?eW{k`b?Wy<&gOo;dS`~CR$Vwb@XWtR|N zi~t=w02?-0&j0TD{>bb6sNwsK*!p?V`RMQUl(*DVjk-9Cx+-z1KXab|Ka2oXhX5f% z`$|e!000AhNklrxs)5QTeTVRiEmz~MKK1WAjCw(c-JK6eox;2O)?`? zTG`AHia671e^vgmp!llKp|=5sVHk#C7=~epA~VAf-~%aPC=%Qw01h8mnSZ|p?hz91 z7p83F3%LVu9;S$tSI$C^%^yud1dfTM_6p2|+5Ejp$bd`GDvbR|xit>i!ZD&F>@CJrPmu*UjD&?DfZs=$@e3FQA(vNiU+$A*%a} z?`XcG2jDxJ_ZQ#Md`H{4Lpf6QBDp81_KWZ6Tk#yCy1)32zO#3<7>b`eT7UyYH1eGz z;O(rH$=QR*L%%ZcBpc=eGua?N55nD^K(8<#gl2+pN_j~b2MHs4#mcLmv%DkspS-3< zpI1F=^9siI0s-;IN_IrA;5xm~3?3!StX}pUv0vkxMaqm+zxrg7X7(I&*N~&dEd0kD z-FRV|g=|QuUsuh>-xCI}vD2imzYIOIdcCVV=$Bz@*u0+Bs<|L^)32nN*=wu3n%Ynw z@1|eLG>!8ruU1pFXUfb`j>(=Gy~?Rn4QJ-c3%3T|(Frd!bI`9u&zAnyFYTqlG#&J7 zAkD(jpw|oZLNiA>;>hgp1KX7-wxC~31II47gc zHcehD6Uxlf%+M^^uN5Wc*G%^;>D5qT{>=uxUhX%WJu^Z*(_Wq9y}npFO{Hhb>s6<9 zNi0pHXWFaVZnb)1+RS&F)xOv6&aeILcI)`k#0YE+?e)5&#r7J#c`3Z7x!LpTc01dx zrdC3{Z;joZ^KN&))zB_i)I9fWedoN>Zl-6_Iz+^G&*ak2jpF07*qoM6N<$f;w%0(f|Me literal 0 HcmV?d00001 diff --git a/packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packaging/dart/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0467bf12aa4d28f374bb26596605a46dcbb3e7c8 GIT binary patch literal 1418 zcmV;51$Fv~P)q zKfU)WzW*n(@|xWGCA9ScMt*e9`2kdxPQ&&>|-UCa7_51w+ zLUsW@ZzZSW0y$)Hp~e9%PvP|a03ks1`~K?q{u;6NC8*{AOqIUq{CL&;p56Lf$oQGq z^={4hPQv)y=I|4n+?>7Fim=dxt1 z2H+Dm+1+fh+IF>G0SjJMkQQre1x4|G*Z==(Ot&kCnUrL4I(rf(ucITwmuHf^hXiJT zkdTm&kdTm&kdTm&kdP`esgWG0BcWCVkVZ&2dUwN`cgM8QJb`Z7Z~e<&Yj2(}>Tmf` zm1{eLgw!b{bXkjWbF%dTkTZEJWyWOb##Lfw4EK2}<0d6%>AGS{po>WCOy&f$Tay_> z?NBlkpo@s-O;0V%Y_Xa-G#_O08q5LR*~F%&)}{}r&L%Sbs8AS4t7Y0NEx*{soY=0MZExqA5XHQkqi#4gW3 zqODM^iyZl;dvf)-bOXtOru(s)Uc7~BFx{w-FK;2{`VA?(g&@3z&bfLFyctOH!cVsF z7IL=fo-qBndRUm;kAdXR4e6>k-z|21AaN%ubeVrHl*<|s&Ax@W-t?LR(P-24A5=>a z*R9#QvjzF8n%@1Nw@?CG@6(%>+-0ASK~jEmCV|&a*7-GKT72W<(TbSjf)&Eme6nGE z>Gkj4Sq&2e+-G%|+NM8OOm5zVl9{Z8Dd8A5z3y8mZ=4Bv4%>as_{9cN#bm~;h>62( zdqY93Zy}v&c4n($Vv!UybR8ocs7#zbfX1IY-*w~)p}XyZ-SFC~4w>BvMVr`dFbelV{lLL0bx7@*ZZdebr3`sP;? zVImji)kG)(6Juv0lz@q`F!k1FE;CQ(D0iG$wchPbKZQELlsZ#~rt8#90Y_Xh&3U-< z{s<&cCV_1`^TD^ia9!*mQDq& zn2{r`j};V|uV%_wsP!zB?m%;FeaRe+X47K0e+KE!8C{gAWF8)lCd1u1%~|M!XNRvw zvtqy3iz0WSpWdhn6$hP8PaRBmp)q`#PCA`Vd#Tc$@f1tAcM>f_I@bC)hkI9|o(Iqv zo}Piadq!j76}004RBio<`)70k^`K1NK)q>w?p^C6J2ZC!+UppiK6&y3Kmbv&O!oYF z34$0Z;QO!JOY#!`qyGH<3Pd}Pt@q*A0V=3SVtWKRR8d8Z&@)3qLPA19LPA19LPEUC YUoZo%k(ykuW&i*H07*qoM6N<$f+CH{y8r+H literal 0 HcmV?d00001 diff --git a/packaging/dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packaging/dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/packaging/dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/packaging/dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packaging/dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/packaging/dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packaging/dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/packaging/dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packaging/dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9da19eacad3b03bb08bbddbbf4ac48dd78b3d838 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^j3CUx0wlM}@Gt=>Zci7-kcv6Uzs@r-FtIZ-&5|)J Q1PU{Fy85}Sb4q9e0B4a5jsO4v literal 0 HcmV?d00001 diff --git a/packaging/dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packaging/dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/packaging/dart/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packaging/dart/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packaging/dart/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/packaging/dart/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packaging/dart/example/ios/Runner/Base.lproj/Main.storyboard b/packaging/dart/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/packaging/dart/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packaging/dart/example/ios/Runner/Info.plist b/packaging/dart/example/ios/Runner/Info.plist new file mode 100644 index 0000000..d6e74cf --- /dev/null +++ b/packaging/dart/example/ios/Runner/Info.plist @@ -0,0 +1,70 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Edge Intelligence Example + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + edge_intelligence_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/packaging/dart/example/ios/Runner/Runner-Bridging-Header.h b/packaging/dart/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/packaging/dart/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/packaging/dart/example/ios/Runner/SceneDelegate.swift b/packaging/dart/example/ios/Runner/SceneDelegate.swift new file mode 100644 index 0000000..b9ce8ea --- /dev/null +++ b/packaging/dart/example/ios/Runner/SceneDelegate.swift @@ -0,0 +1,6 @@ +import Flutter +import UIKit + +class SceneDelegate: FlutterSceneDelegate { + +} diff --git a/packaging/dart/example/ios/RunnerTests/RunnerTests.swift b/packaging/dart/example/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/packaging/dart/example/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/packaging/dart/example/lib/main.dart b/packaging/dart/example/lib/main.dart new file mode 100644 index 0000000..e4aa71b --- /dev/null +++ b/packaging/dart/example/lib/main.dart @@ -0,0 +1,192 @@ +import 'package:edge_intelligence/edge_intelligence.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + + Object? initializationError; + try { + await initEdgeIntelligence(); + debugPrint('edge_intelligence runtime initialized'); + } catch (error) { + initializationError = error; + debugPrint('edge_intelligence runtime initialization failed: $error'); + } + + runApp(EdgeIntelligenceExample(initializationError: initializationError)); +} + +class EdgeIntelligenceExample extends StatefulWidget { + const EdgeIntelligenceExample({super.key, this.initializationError}); + + final Object? initializationError; + + @override + State createState() => + _EdgeIntelligenceExampleState(); +} + +class _EdgeIntelligenceExampleState extends State { + final _promptController = TextEditingController( + text: 'Summarize edge inference in one sentence.', + ); + + EdgeLlm? _sdk; + String? _modelName; + String _output = ''; + Object? _error; + bool _busy = false; + + @override + void initState() { + super.initState(); + _error = widget.initializationError; + } + + @override + void dispose() { + _promptController.dispose(); + if (widget.initializationError == null) { + disposeEdgeIntelligence(); + } + super.dispose(); + } + + Future _selectModel() async { + final result = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: const ['gguf'], + ); + final file = result?.files.single; + if (file?.path == null) { + return; + } + + setState(() { + _busy = true; + _error = null; + _output = ''; + }); + + try { + final sdk = await EdgeLlm.local(file!.path!); + if (!mounted) return; + setState(() { + _sdk = sdk; + _modelName = file.name; + }); + } catch (error) { + if (!mounted) return; + setState(() => _error = error); + } finally { + if (mounted) { + setState(() => _busy = false); + } + } + } + + Future _ask() async { + final sdk = _sdk; + final prompt = _promptController.text.trim(); + if (sdk == null || prompt.isEmpty || _busy) { + return; + } + + setState(() { + _busy = true; + _error = null; + _output = ''; + }); + + try { + await for (final token in sdk.askStream(prompt)) { + if (!mounted) return; + setState(() => _output += token); + } + } catch (error) { + if (!mounted) return; + setState(() => _error = error); + } finally { + if (mounted) { + setState(() => _busy = false); + } + } + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Edge Intelligence', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xff0f766e)), + useMaterial3: true, + ), + home: Scaffold( + appBar: AppBar(title: const Text('Edge Intelligence')), + body: SafeArea( + child: ListView( + padding: const EdgeInsets.all(20), + children: [ + Row( + children: [ + Expanded( + child: Text( + _modelName ?? 'No GGUF model selected', + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 12), + OutlinedButton.icon( + onPressed: _busy || widget.initializationError != null + ? null + : _selectModel, + icon: const Icon(Icons.folder_open), + label: const Text('Model'), + ), + ], + ), + const SizedBox(height: 20), + TextField( + controller: _promptController, + enabled: !_busy, + minLines: 3, + maxLines: 6, + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Prompt', + ), + ), + const SizedBox(height: 12), + Align( + alignment: Alignment.centerRight, + child: FilledButton.icon( + onPressed: _sdk == null || _busy ? null : _ask, + icon: _busy + ? const SizedBox.square( + dimension: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Icon(Icons.send), + label: const Text('Ask'), + ), + ), + if (_error != null) ...[ + const SizedBox(height: 20), + Text( + 'Error: $_error', + style: TextStyle(color: Theme.of(context).colorScheme.error), + ), + ], + if (_output.isNotEmpty) ...[ + const SizedBox(height: 20), + SelectableText(_output), + ], + ], + ), + ), + ), + ); + } +} diff --git a/packaging/dart/example/pubspec.yaml b/packaging/dart/example/pubspec.yaml new file mode 100644 index 0000000..a3ff003 --- /dev/null +++ b/packaging/dart/example/pubspec.yaml @@ -0,0 +1,23 @@ +name: edge_intelligence_example +description: Flutter Android and iOS example for edge_intelligence. +publish_to: none +version: 1.0.0+1 + +environment: + sdk: ">=3.4.0 <4.0.0" + +dependencies: + edge_intelligence: + path: .. + # 11.0.2 omits its Kotlin plugin classes under Flutter 3.44 / AGP 9. + file_picker: 10.3.10 + flutter: + sdk: flutter + +dev_dependencies: + flutter_lints: ^6.0.0 + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packaging/dart/example/test/widget_test.dart b/packaging/dart/example/test/widget_test.dart new file mode 100644 index 0000000..7f57d12 --- /dev/null +++ b/packaging/dart/example/test/widget_test.dart @@ -0,0 +1,15 @@ +import 'package:edge_intelligence_example/main.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + testWidgets('shows the mobile SDK controls', (tester) async { + await tester.pumpWidget( + const EdgeIntelligenceExample(initializationError: 'runtime unavailable'), + ); + + expect(find.text('Edge Intelligence'), findsOneWidget); + expect(find.text('Model'), findsOneWidget); + expect(find.text('Prompt'), findsOneWidget); + expect(find.text('Ask'), findsOneWidget); + }); +} diff --git a/packaging/dart/ios/Frameworks/README.md b/packaging/dart/ios/Frameworks/README.md new file mode 100644 index 0000000..ce4b1a9 --- /dev/null +++ b/packaging/dart/ios/Frameworks/README.md @@ -0,0 +1,2 @@ +Release assembly places `el_ffi.xcframework` here. The framework contains iOS +device and simulator slices and is generated rather than committed. diff --git a/packaging/dart/ios/edge_intelligence.podspec b/packaging/dart/ios/edge_intelligence.podspec new file mode 100644 index 0000000..300dcf2 --- /dev/null +++ b/packaging/dart/ios/edge_intelligence.podspec @@ -0,0 +1,19 @@ +Pod::Spec.new do |s| + s.name = 'edge_intelligence' + s.version = '0.1.0' + s.summary = 'Flutter mobile runtime for the Edge Intelligence Dart SDK.' + s.description = <<-DESC +Precompiled Rust runtime used by the framework-neutral Edge Intelligence Dart API. + DESC + s.homepage = 'https://github.com/Tovli/EdgeIntelligence' + s.license = { :file => '../LICENSE' } + s.author = { 'Tovli' => 'opensource@tovli.com' } + s.source = { :path => '.' } + s.vendored_frameworks = 'Frameworks/el_ffi.xcframework' + s.dependency 'Flutter' + s.platform = :ios, '13.0' + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' + } +end diff --git a/packaging/dart/ios/edge_intelligence/Package.swift b/packaging/dart/ios/edge_intelligence/Package.swift new file mode 100644 index 0000000..1adabe8 --- /dev/null +++ b/packaging/dart/ios/edge_intelligence/Package.swift @@ -0,0 +1,29 @@ +// swift-tools-version: 5.9 + +import PackageDescription + +let package = Package( + name: "edge_intelligence", + platforms: [ + .iOS("13.0") + ], + products: [ + .library(name: "edge-intelligence", targets: ["edge_intelligence"]) + ], + dependencies: [ + .package(name: "FlutterFramework", path: "../FlutterFramework") + ], + targets: [ + .target( + name: "edge_intelligence", + dependencies: [ + "el_ffi", + .product(name: "FlutterFramework", package: "FlutterFramework") + ] + ), + .binaryTarget( + name: "el_ffi", + path: "../Frameworks/el_ffi.xcframework" + ) + ] +) diff --git a/packaging/dart/ios/edge_intelligence/Sources/edge_intelligence/EdgeIntelligence.swift b/packaging/dart/ios/edge_intelligence/Sources/edge_intelligence/EdgeIntelligence.swift new file mode 100644 index 0000000..29911be --- /dev/null +++ b/packaging/dart/ios/edge_intelligence/Sources/edge_intelligence/EdgeIntelligence.swift @@ -0,0 +1 @@ +@_exported import el_ffi diff --git a/packaging/dart/lib/edge_intelligence.dart b/packaging/dart/lib/edge_intelligence.dart new file mode 100644 index 0000000..98e245b --- /dev/null +++ b/packaging/dart/lib/edge_intelligence.dart @@ -0,0 +1,83 @@ +/// Dart bindings for the Edge Intelligence SDK. +library; + +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart' + show ExternalLibrary; + +import 'src/dart_api.dart' as dart_api; +import 'src/frb_generated.dart' show RustLib; +import 'src/lib.dart' as generated; +import 'src/runtime_loader.dart' as runtime_loader; + +export 'src/frb_generated.dart' show RustLib, RustLibApi, RustLibApiImpl; + +/// Edge Intelligence LLM SDK handle. +final class EdgeLlm { + EdgeLlm._(this._sdk); + + final generated.EdgeLlm _sdk; + + /// Creates a local, air-gapped LLM provider. + static Future local(String modelUri) async { + return EdgeLlm._(await dart_api.edgeLlmLocal(modelUri: modelUri)); + } + + /// Creates an opt-in frontier cloud provider. + static Future cloud(String model, String apiKey) async { + return EdgeLlm._(await dart_api.edgeLlmCloud(model: model, apiKey: apiKey)); + } + + /// Runs a single prompt and returns the complete response. + Future ask(String prompt) { + return dart_api.edgeLlmAsk(sdk: _sdk, prompt: prompt); + } + + /// Streams response tokens for a prompt. + /// + /// If the provider fails after emitting tokens, the returned stream emits a + /// terminal error event before it closes. + Stream askStream(String prompt) { + return dart_api.edgeLlmAskStream(sdk: _sdk, prompt: prompt); + } + + /// Clears any cached session state. + /// + /// The local provider resets automatically at the start of each call, so + /// this is a no-op for the current backend. Call it as a forward-compatible + /// signal if your host logic requires an explicit boundary between sessions. + Future reset() { + return dart_api.edgeLlmReset(sdk: _sdk); + } +} + +/// Initializes the Edge Intelligence native runtime. +Future initEdgeIntelligence({ + ExternalLibrary? externalLibrary, + bool forceSameCodegenVersion = true, +}) async { + try { + await RustLib.init( + externalLibrary: + externalLibrary ?? await runtime_loader.loadPackagedExternalLibrary(), + forceSameCodegenVersion: forceSameCodegenVersion, + ); + } catch (error, stackTrace) { + if (error is UnsupportedError) { + Error.throwWithStackTrace(error, stackTrace); + } + + Error.throwWithStackTrace( + StateError( + 'Failed to initialize edge_intelligence native runtime. Ensure the ' + 'el_ffi native library is available to the host process, pass ' + 'externalLibrary explicitly, or set ' + 'FRB_DART_LOAD_EXTERNAL_LIBRARY_NATIVE_LIB_DIR. Original ' + '${error.runtimeType}: $error', + ), + stackTrace, + ); + } +} + +/// Releases the Edge Intelligence native runtime. +void disposeEdgeIntelligence() => RustLib.dispose(); diff --git a/packaging/dart/lib/src/runtime_loader.dart b/packaging/dart/lib/src/runtime_loader.dart new file mode 100644 index 0000000..79e53dd --- /dev/null +++ b/packaging/dart/lib/src/runtime_loader.dart @@ -0,0 +1,3 @@ +export 'runtime_loader_stub.dart' + if (dart.library.io) 'runtime_loader_io.dart' + if (dart.library.js_interop) 'runtime_loader_web.dart'; diff --git a/packaging/dart/lib/src/runtime_loader_io.dart b/packaging/dart/lib/src/runtime_loader_io.dart new file mode 100644 index 0000000..fcae715 --- /dev/null +++ b/packaging/dart/lib/src/runtime_loader_io.dart @@ -0,0 +1,75 @@ +import 'dart:ffi' show Abi; +import 'dart:io'; +import 'dart:isolate'; + +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart' + show ExternalLibrary; + +Future loadPackagedExternalLibrary() async { + if (Platform.isAndroid) { + return ExternalLibrary.open( + 'libel_ffi.so', + debugInfo: 'Flutter Android FFI plugin', + ); + } + if (Platform.isIOS) { + return ExternalLibrary.open( + 'el_ffi.framework/el_ffi', + debugInfo: 'Flutter iOS FFI plugin', + ); + } + + final libraryPath = _platformLibraryPath(); + final packageUri = await Isolate.resolvePackageUri( + Uri.parse('package:edge_intelligence/native/$libraryPath'), + ); + if (packageUri == null) { + throw UnsupportedError( + 'edge_intelligence: cannot locate packaged native library ' + '($libraryPath). This host does not expose package assets as files. ' + 'Pass externalLibrary explicitly.', + ); + } + if (!packageUri.isScheme('file')) { + throw UnsupportedError( + 'edge_intelligence: package URI resolved to a non-file scheme ' + '(${packageUri.scheme}); cannot load packaged native library. ' + 'Pass externalLibrary explicitly.', + ); + } + + final file = File.fromUri(packageUri); + if (!await file.exists()) { + throw UnsupportedError( + 'edge_intelligence: packaged native library is missing at ' + '${file.path}. Reinstall the package or pass externalLibrary explicitly.', + ); + } + + return ExternalLibrary.open(file.path, debugInfo: packageUri.toString()); +} + +String _platformLibraryPath() { + final abi = Abi.current(); + if (Platform.isWindows) { + if (abi == Abi.windowsX64) { + return 'windows/x64/el_ffi.dll'; + } + throw UnsupportedError(_unsupportedRuntimeMessage()); + } + if (Platform.isMacOS) { + return 'macos/libel_ffi.dylib'; + } + if (Platform.isLinux) { + if (abi == Abi.linuxX64) { + return 'linux/x64/libel_ffi.so'; + } + throw UnsupportedError(_unsupportedRuntimeMessage()); + } + throw UnsupportedError(_unsupportedRuntimeMessage()); +} + +String _unsupportedRuntimeMessage() => + 'edge_intelligence does not currently ship a pub.dev native runtime for ' + '${Platform.operatingSystem}/${Abi.current()}. Use a host-specific adapter ' + 'or pass externalLibrary explicitly.'; diff --git a/packaging/dart/lib/src/runtime_loader_stub.dart b/packaging/dart/lib/src/runtime_loader_stub.dart new file mode 100644 index 0000000..d60869f --- /dev/null +++ b/packaging/dart/lib/src/runtime_loader_stub.dart @@ -0,0 +1,10 @@ +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart' + show ExternalLibrary; + +Future loadPackagedExternalLibrary() async { + throw UnsupportedError( + 'edge_intelligence pub.dev does not support this runtime environment. ' + 'Expected a native desktop (dart.library.io) or web (dart.library.js_interop) ' + 'context. Pass externalLibrary explicitly if your platform is supported.', + ); +} diff --git a/packaging/dart/lib/src/runtime_loader_web.dart b/packaging/dart/lib/src/runtime_loader_web.dart new file mode 100644 index 0000000..f735c0e --- /dev/null +++ b/packaging/dart/lib/src/runtime_loader_web.dart @@ -0,0 +1,9 @@ +import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart' + show ExternalLibrary; + +Future loadPackagedExternalLibrary() async { + throw UnsupportedError( + 'edge_intelligence pub.dev does not currently ship a web/WASM ' + 'flutter_rust_bridge runtime. Use the npm/web package for browser hosts.', + ); +} diff --git a/packaging/dart/pubspec.yaml b/packaging/dart/pubspec.yaml new file mode 100644 index 0000000..666bace --- /dev/null +++ b/packaging/dart/pubspec.yaml @@ -0,0 +1,24 @@ +name: edge_intelligence +version: 0.1.0 +description: Dart bindings for the Edge Intelligence SDK, with Flutter mobile and native desktop runtimes. +repository: https://github.com/Tovli/EdgeIntelligence +environment: + sdk: ">=3.4.0 <4.0.0" + flutter: ">=3.22.0" +platforms: + android: + ios: + linux: + macos: + windows: +dependencies: + flutter_rust_bridge: ">=2.12.0 <2.12.1" +dev_dependencies: + test: ^1.25.0 +flutter: + plugin: + platforms: + android: + ffiPlugin: true + ios: + ffiPlugin: true diff --git a/packaging/dart/test/missing_runtime_test.dart b/packaging/dart/test/missing_runtime_test.dart new file mode 100644 index 0000000..c32ab44 --- /dev/null +++ b/packaging/dart/test/missing_runtime_test.dart @@ -0,0 +1,8 @@ +import 'package:edge_intelligence/edge_intelligence.dart'; +import 'package:test/test.dart'; + +void main() { + test('reports a missing packaged runtime clearly', () async { + await expectLater(initEdgeIntelligence(), throwsA(isA())); + }); +} diff --git a/packaging/dart/test/packaged_runtime_smoke_test.dart b/packaging/dart/test/packaged_runtime_smoke_test.dart new file mode 100644 index 0000000..f700f78 --- /dev/null +++ b/packaging/dart/test/packaged_runtime_smoke_test.dart @@ -0,0 +1,9 @@ +import 'package:edge_intelligence/edge_intelligence.dart'; +import 'package:test/test.dart'; + +void main() { + test('loads the packaged native runtime', () async { + await initEdgeIntelligence(); + addTearDown(disposeEdgeIntelligence); + }); +} diff --git a/packaging/flutter/README.md b/packaging/flutter/README.md deleted file mode 100644 index 85864cc..0000000 --- a/packaging/flutter/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# edge_intelligence - -Generated Flutter bindings for the Edge Intelligence SDK. - -The package contains Dart bindings generated with flutter_rust_bridge and native -libraries assembled by the release pipeline. - -## Usage - -Copy the Qwen2.5 0.5B GGUF into app storage, then open the local SDK facade with -that model path. - -```dart -import "dart:io"; -import "package:edge_intelligence/edge_intelligence.dart"; - -final qwen05b = "/path/to/app/models/qwen2.5-0.5b-instruct-q4_k_m.gguf"; -final sdk = await EdgeLlm.local(qwen05b); - -final reply = await sdk.ask("Summarize edge inference in one sentence."); -await for (final token in sdk.askStream("Give me two deployment tips.")) { - stdout.write(token); -} -``` diff --git a/scripts/build-ios-xcframework.sh b/scripts/build-ios-xcframework.sh new file mode 100644 index 0000000..3f39b20 --- /dev/null +++ b/scripts/build-ios-xcframework.sh @@ -0,0 +1,84 @@ +#!/usr/bin/env bash +set -euo pipefail + +crate_manifest="crates/adapters/el-ffi/Cargo.toml" +output_root="target/ios-xcframework" +device_framework="$output_root/device/el_ffi.framework" +simulator_framework="$output_root/simulator/el_ffi.framework" + +rm -rf "$output_root" + +cargo build --manifest-path "$crate_manifest" --target aarch64-apple-ios --release +cargo build --manifest-path "$crate_manifest" --target aarch64-apple-ios-sim --release +cargo build --manifest-path "$crate_manifest" --target x86_64-apple-ios --release + +create_framework() { + local framework_path="$1" + local library_path="$2" + + mkdir -p "$framework_path/Headers" "$framework_path/Modules" + cp "$library_path" "$framework_path/el_ffi" + chmod +x "$framework_path/el_ffi" + install_name_tool -id '@rpath/el_ffi.framework/el_ffi' "$framework_path/el_ffi" + + cat > "$framework_path/Headers/el_ffi.h" <<'HEADER' +#ifndef EL_FFI_H +#define EL_FFI_H + +// Dart FFI resolves the exported bridge symbols dynamically. + +#endif +HEADER + + cat > "$framework_path/Modules/module.modulemap" <<'MODULEMAP' +framework module el_ffi { + umbrella header "el_ffi.h" + export * +} +MODULEMAP + + cat > "$framework_path/Info.plist" <<'PLIST' + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + el_ffi + CFBundleIdentifier + com.tovli.edge-intelligence.el-ffi + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + el_ffi + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + MinimumOSVersion + 13.0 + + +PLIST +} + +create_framework \ + "$device_framework" \ + "target/aarch64-apple-ios/release/libel_ffi.dylib" + +mkdir -p "$output_root/simulator" +lipo -create \ + target/aarch64-apple-ios-sim/release/libel_ffi.dylib \ + target/x86_64-apple-ios/release/libel_ffi.dylib \ + -output "$output_root/simulator/libel_ffi.dylib" +create_framework \ + "$simulator_framework" \ + "$output_root/simulator/libel_ffi.dylib" + +xcodebuild -create-xcframework \ + -framework "$device_framework" \ + -framework "$simulator_framework" \ + -output "$output_root/el_ffi.xcframework" diff --git a/scripts/verify-android-page-alignment.py b/scripts/verify-android-page-alignment.py new file mode 100644 index 0000000..98596d3 --- /dev/null +++ b/scripts/verify-android-page-alignment.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""Fail when a 64-bit Android ELF has a LOAD segment below 16 KiB alignment.""" + +from pathlib import Path +import subprocess +import sys + + +MIN_ALIGNMENT = 16 * 1024 + + +def load_alignments(readelf: Path, library: Path) -> list[int]: + output = subprocess.check_output( + [str(readelf), "-lW", str(library)], + text=True, + ) + return [ + int(line.split()[-1], 16) + for line in output.splitlines() + if line.lstrip().startswith("LOAD") + ] + + +def main(arguments: list[str]) -> int: + if len(arguments) < 2: + print( + "usage: verify-android-page-alignment.py ...", + file=sys.stderr, + ) + return 2 + + readelf = Path(arguments[0]) + errors: list[str] = [] + for library_arg in arguments[1:]: + library = Path(library_arg) + alignments = load_alignments(readelf, library) + if not alignments: + errors.append(f"{library}: no ELF LOAD segments found") + continue + minimum = min(alignments) + print(f"{library}: minimum LOAD alignment = {minimum} bytes") + if minimum < MIN_ALIGNMENT: + errors.append( + f"{library}: {minimum}-byte LOAD alignment is below 16 KiB" + ) + + if errors: + print("\n".join(errors), file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/tests/check_ci_workflows.py b/tests/check_ci_workflows.py index df88ec1..c532b0d 100644 --- a/tests/check_ci_workflows.py +++ b/tests/check_ci_workflows.py @@ -2,6 +2,7 @@ """Validate CI dependencies that are easy to accidentally leave implicit.""" from pathlib import Path +import re import sys @@ -13,6 +14,18 @@ RN_RETRY_COMMAND = "bash scripts/retry-command.sh make codegen-rn" WASM_RETRY_COMMAND = "bash scripts/retry-command.sh curl -fsSL" BINDINGS_UPLOAD_IF = "if: github.event_name != 'pull_request'" +DART_FRB_STALENESS_CHECK = ( + "git diff --exit-code -- crates/adapters/el-ffi/src/frb_generated.rs" +) +PUB_DESKTOP_ARTIFACTS = [ + "linux-x64-lib", + "macos-universal-lib", + "windows-x64-lib", +] +PUB_MOBILE_ARTIFACTS = [ + "android-libs", + "ios-xcframework", +] REQUIRED_INSTALLS = [ ( @@ -29,26 +42,57 @@ def check_workflow(path: Path) -> list[str]: text = path.read_text(encoding="utf-8") errors: list[str] = [] - codegen_pos = text.find("make codegen-flutter") + codegen_pos = text.find("make codegen-dart") rn_codegen_pos = text.find("make codegen-rn") if codegen_pos < 0: - return [f"{path}: missing make codegen-flutter step"] + return [f"{path}: missing make codegen-dart step"] + + if DART_FRB_STALENESS_CHECK not in text[codegen_pos:]: + errors.append( + f"{path}: missing Dart FRB Rust glue staleness check after codegen step" + ) for label, needle in REQUIRED_INSTALLS: install_pos = text.find(needle) if install_pos < 0: - errors.append(f"{path}: missing explicit {label} install before Flutter codegen") + errors.append(f"{path}: missing explicit {label} install before Dart codegen") elif install_pos > codegen_pos: - errors.append(f"{path}: installs {label} after Flutter codegen") + errors.append(f"{path}: installs {label} after Dart codegen") if rn_codegen_pos >= 0 and RN_RETRY_COMMAND not in text: errors.append(f"{path}: React Native codegen must run through retry wrapper") + if "aarch64-linux-android" in text: + for mobile_build_requirement in ( + "armv7-linux-androideabi", + "x86_64-linux-android", + "ndk-version: r28c", + "armv7a-linux-androideabi24-clang", + "aarch64-linux-android24-clang", + "x86_64-linux-android24-clang", + "bash scripts/build-ios-xcframework.sh", + ): + if mobile_build_requirement not in text: + errors.append( + f"{path}: mobile validation missing {mobile_build_requirement!r}" + ) + if "Verify Android 16 KB page alignment" not in text: + errors.append(f"{path}: missing Android 16 KB page-alignment gate") + if "Install wasm-pack" in text and WASM_RETRY_COMMAND not in text: errors.append(f"{path}: wasm-pack download must run through retry wrapper") if path.name == "bindings.yml": + for trigger_path in ( + "packaging/dart/**", + "scripts/build-ios-xcframework.sh", + "scripts/verify-android-page-alignment.py", + ): + if text.count(trigger_path) < 2: + errors.append( + f"{path}: push and pull_request filters must include {trigger_path}" + ) step_blocks = text.split("\n - ") for block in step_blocks: if "uses: actions/upload-artifact@v4" in block and BINDINGS_UPLOAD_IF not in block: @@ -57,6 +101,122 @@ def check_workflow(path: Path) -> list[str]: f"{BINDINGS_UPLOAD_IF!r}" ) + if path.name == "release.yml": + if "Validate Dart package resolves without Flutter SDK" in text: + errors.append( + f"{path}: Flutter plugin packages cannot claim standalone Dart resolution" + ) + if text.count("flutter pub get --no-example") < 2: + errors.append(f"{path}: Flutter plugin resolution must use flutter pub get") + if "flutter pub publish --dry-run" not in text: + errors.append(f"{path}: pub.dev validation must run in Flutter context") + if "flutter pub publish --force" not in text: + errors.append(f"{path}: pub.dev publishing must run in Flutter context") + if "verify-flutter-ios:" not in text: + errors.append(f"{path}: missing Flutter iOS package verification job") + if "Run Flutter iOS package smoke build" not in text: + errors.append(f"{path}: missing Flutter iOS simulator smoke build") + parts = text.split(" assemble-dart:", 1) + if len(parts) < 2: + errors.append(f"{path}: missing assemble-dart job") + return errors + assemble_dart = parts[1].split("\n publish-crates:", 1)[0] + for artifact in PUB_DESKTOP_ARTIFACTS + PUB_MOBILE_ARTIFACTS: + if artifact not in assemble_dart: + errors.append(f"{path}: pub.dev assembly must include {artifact}") + for artifact in ("wasm-output",): + if artifact in assemble_dart: + errors.append( + f"{path}: pub.dev assembly must not include unsupported {artifact}" + ) + if "Check pub.dev package size" not in assemble_dart: + errors.append(f"{path}: pub.dev assembly must check package size") + if "Run Dart package runtime smoke tests" not in assemble_dart: + errors.append( + f"{path}: pub.dev assembly must run packaged Dart runtime smoke tests" + ) + if "Run Flutter Android package smoke build" not in assemble_dart: + errors.append( + f"{path}: pub.dev assembly must smoke-build the Flutter Android example" + ) + + required_mobile_paths = ( + "assembly/android/src/main/jniLibs/armeabi-v7a/libel_ffi.so", + "assembly/android/src/main/jniLibs/arm64-v8a/libel_ffi.so", + "assembly/android/src/main/jniLibs/x86_64/libel_ffi.so", + "assembly/ios/Frameworks/el_ffi.xcframework", + ) + for mobile_path in required_mobile_paths: + if mobile_path not in assemble_dart: + errors.append( + f"{path}: pub.dev assembly must package {mobile_path}" + ) + + for android_build_requirement in ( + "ndk-version: r28c", + "armv7a-linux-androideabi24-clang", + "aarch64-linux-android24-clang", + "x86_64-linux-android24-clang", + ): + if android_build_requirement not in text: + errors.append( + f"{path}: Android build missing {android_build_requirement!r}" + ) + + return errors + + +def check_dart_mobile_package() -> list[str]: + errors: list[str] = [] + pubspec = Path("packaging/dart/pubspec.yaml").read_text(encoding="utf-8") + loader = Path("packaging/dart/lib/src/runtime_loader_io.dart").read_text( + encoding="utf-8" + ) + makefile = Path("Makefile").read_text(encoding="utf-8") + + required_pubspec_fragments = ( + "android:", + "ios:", + "flutter:", + "ffiPlugin: true", + 'flutter_rust_bridge: ">=2.12.0 <2.12.1"', + ) + for fragment in required_pubspec_fragments: + if fragment not in pubspec: + errors.append( + f"packaging/dart/pubspec.yaml: missing Flutter mobile declaration {fragment!r}" + ) + + required_files = ( + Path("packaging/dart/android/build.gradle"), + Path("packaging/dart/android/src/main/AndroidManifest.xml"), + Path("packaging/dart/ios/edge_intelligence.podspec"), + Path("packaging/dart/ios/edge_intelligence/Package.swift"), + Path("packaging/dart/example/pubspec.yaml"), + Path("packaging/dart/example/lib/main.dart"), + ) + for path in required_files: + if not path.is_file(): + errors.append(f"{path}: required Flutter mobile package file is missing") + + required_mobile_loaders = ( + r"if \(Platform\.isAndroid\).*?ExternalLibrary\.open\(\s*'libel_ffi\.so'", + r"if \(Platform\.isIOS\).*?ExternalLibrary\.open\(\s*'el_ffi\.framework/el_ffi'", + ) + for loader_pattern in required_mobile_loaders: + if re.search(loader_pattern, loader, re.DOTALL) is None: + errors.append( + "packaging/dart/lib/src/runtime_loader_io.dart: " + f"missing automatic mobile loader matching {loader_pattern!r}" + ) + + for package_dir in ("android", "ios"): + copy_command = f"packaging/dart/{package_dir}" + if copy_command not in makefile: + errors.append( + f"Makefile: codegen-dart must copy the Flutter {package_dir} plugin layout" + ) + return errors @@ -64,12 +224,13 @@ def main() -> int: errors: list[str] = [] for workflow in WORKFLOWS: errors.extend(check_workflow(workflow)) + errors.extend(check_dart_mobile_package()) if errors: print("\n".join(errors), file=sys.stderr) return 1 - print("OK: CI workflows guard Flutter tools and retry external codegen downloads") + print("OK: CI workflows guard Dart codegen tools and retry external codegen downloads") return 0