From fb5a632636a3f6a0203f28842e10da8bfefcfe19 Mon Sep 17 00:00:00 2001 From: Jhin Lee Date: Mon, 9 Nov 2026 06:06:09 -0500 Subject: [PATCH 1/3] Fail the LiteRT-LM smoke when a desktop runtime lacks the in-process LiteRT marker --- .github/workflows/litert_lm_smoke.yml | 9 ++- .../tooling/litert_lm_library_smoke_test.dart | 58 +++++++++++++++++++ .../litert_lm_smoke_workflow_test.dart | 28 +++++++++ tool/litert_lm_library_smoke.dart | 23 ++++++++ .../docs/maintainers/native-and-web-sync.md | 4 +- 5 files changed, 119 insertions(+), 3 deletions(-) diff --git a/.github/workflows/litert_lm_smoke.yml b/.github/workflows/litert_lm_smoke.yml index 2bacc7158..f3597deae 100644 --- a/.github/workflows/litert_lm_smoke.yml +++ b/.github/workflows/litert_lm_smoke.yml @@ -55,6 +55,9 @@ jobs: litert_lib_subdir: linux/x64 - os: windows-latest litert_lib_subdir: windows/x64 + - os: ubuntu-24.04-arm + litert_lib_subdir: linux/arm64 + library_only: true steps: - uses: actions/checkout@v7 @@ -122,12 +125,14 @@ jobs: dart run tool/litert_lm_library_smoke.dart - name: Cache tiny LiteRT-LM model + if: ${{ !matrix.library_only }} uses: actions/cache@v6 with: path: .dart_tool/litert_lm_models/Qwen3-0.6B.litertlm key: litert-lm-smoke-qwen3-0.6b-${{ runner.os }}-${{ env.LITERT_LM_SMOKE_MODEL_REVISION }}-${{ env.LITERT_LM_SMOKE_MODEL_SHA256 }} - name: Download tiny LiteRT-LM model + if: ${{ !matrix.library_only }} id: download-model continue-on-error: true shell: bash @@ -174,7 +179,7 @@ jobs: echo "Downloaded $model_path (sha256 $actual_sha256)" - name: Run LiteRT-LM engine and chat smoke - if: steps.download-model.outcome == 'success' + if: ${{ !matrix.library_only && steps.download-model.outcome == 'success' }} shell: bash env: LLAMADART_LITERT_LM_LIB_DIR: ${{ github.workspace }}/.dart_tool/llamadart/litert_lm/${{ steps.litert-version.outputs.version }}/${{ matrix.litert_lib_subdir }} @@ -195,7 +200,7 @@ jobs: --model-path "$LITERT_LM_MODEL_PATH" --backend cpu - name: Note skipped LiteRT-LM model smokes - if: steps.download-model.outcome != 'success' + if: ${{ !matrix.library_only && steps.download-model.outcome != 'success' }} shell: bash run: | echo "::warning::Skipped LiteRT-LM engine/chat smokes because the external model download failed. The library/native-asset smoke already passed." diff --git a/test/unit/tooling/litert_lm_library_smoke_test.dart b/test/unit/tooling/litert_lm_library_smoke_test.dart index e1d1911ba..245da16fa 100644 --- a/test/unit/tooling/litert_lm_library_smoke_test.dart +++ b/test/unit/tooling/litert_lm_library_smoke_test.dart @@ -201,4 +201,62 @@ void main() { throwsA(same(failure)), ); }); + + group('in-process LiteRT marker', () { + final skip = Platform.isWindows ? 'compiles fixtures with cc' : null; + final libraries = {}; + late Directory fixtures; + + setUpAll(() { + if (skip != null) return; + fixtures = Directory.systemTemp.createTempSync('litert_marker_'); + for (final (name, defines) in [ + ('absent', []), + ('zero', ['-DIN_PROCESS_LITERT_ABI=0']), + ('one', ['-DIN_PROCESS_LITERT_ABI=1']), + ]) { + final output = path.join( + fixtures.path, + '$name.${Platform.isMacOS ? 'dylib' : 'so'}', + ); + final result = Process.runSync('cc', [ + '-shared', + '-fPIC', + ...defines, + 'test/fixtures/litert_lm/c_api_version.c', + '-o', + output, + ]); + expect(result.exitCode, 0, reason: '${result.stderr}'); + libraries[name] = DynamicLibrary.open(output); + } + }); + + tearDownAll(() { + if (skip == null) fixtures.deleteSync(recursive: true); + }); + + test('desktop runtimes must export the marker with value 1', () { + for (final abi in [Abi.linuxX64, Abi.linuxArm64, Abi.windowsX64]) { + requireInProcessLiteRtMarker(libraries['one']!, abi); + for (final name in ['absent', 'zero']) { + expect( + () => requireInProcessLiteRtMarker(libraries[name]!, abi), + throwsA( + isA().having( + (error) => error.message, + 'message', + contains('litert_lm_native_in_process_litert_abi_version'), + ), + ), + ); + } + } + }, skip: skip); + + test('Apple and Android runtimes are not required to export it', () { + requireInProcessLiteRtMarker(libraries['absent']!, Abi.macosArm64); + requireInProcessLiteRtMarker(libraries['absent']!, Abi.androidArm64); + }, skip: skip); + }); } diff --git a/test/unit/tooling/litert_lm_smoke_workflow_test.dart b/test/unit/tooling/litert_lm_smoke_workflow_test.dart index 5abb0b44a..891867910 100644 --- a/test/unit/tooling/litert_lm_smoke_workflow_test.dart +++ b/test/unit/tooling/litert_lm_smoke_workflow_test.dart @@ -28,6 +28,34 @@ void main() { final bash = Platform.isWindows ? null : Process.runSync('which', ['bash']); final skip = bash == null || bash.exitCode != 0 ? 'requires bash' : null; + test('library smoke covers every bundled desktop runtime', () { + final jobs = _workflow()['jobs'] as YamlMap; + final include = + (((jobs['native-smoke'] as YamlMap)['strategy'] as YamlMap)['matrix'] + as YamlMap)['include'] + as YamlList; + final subdirs = include + .cast() + .map((entry) => entry['litert_lib_subdir']) + .toSet(); + expect(subdirs, {'linux/x64', 'linux/arm64', 'windows/x64'}); + + for (final entry in include.cast()) { + if (entry['litert_lib_subdir'] == 'linux/arm64') { + expect(entry['library_only'], isTrue); + } + } + for (final name in [ + 'Cache tiny LiteRT-LM model', + 'Download tiny LiteRT-LM model', + 'Run LiteRT-LM engine and chat smoke', + 'Note skipped LiteRT-LM model smokes', + ]) { + expect(_step(name)['if'], contains('!matrix.library_only')); + } + expect(_step('Run LiteRT-LM library smoke')['if'], isNull); + }); + test( 'cache key and download URL derive from the pinned revision and sha', () { diff --git a/tool/litert_lm_library_smoke.dart b/tool/litert_lm_library_smoke.dart index 2c770ae7b..2dc110ece 100644 --- a/tool/litert_lm_library_smoke.dart +++ b/tool/litert_lm_library_smoke.dart @@ -2,6 +2,8 @@ import 'dart:convert'; import 'dart:ffi'; import 'dart:io'; +import 'package:llamadart/src/backends/litert_lm/litert_lm_c_api_version.dart'; +import 'package:llamadart/src/backends/litert_lm/litert_lm_platform.dart'; import 'package:llamadart/src/backends/litert_lm/litert_lm_runtime.dart' as runtime; import 'package:llamadart/src/hook/native_release_pins.dart'; @@ -36,11 +38,32 @@ void main() { library.lookup>(symbol); } + requireInProcessLiteRtMarker(library, Abi.current()); + print( 'RESULT litert_lm_library ${jsonEncode({'abi': Abi.current().toString(), 'library': opened.path, 'symbols': _requiredSymbols})}', ); } +/// Fails when a bundled Linux or Windows runtime does not report in-process +/// LiteRT, which gates native embeddings there; a repin that drops the +/// `litert_lm_native_in_process_litert_abi_version` export would otherwise +/// surface only on hardware. +void requireInProcessLiteRtMarker(DynamicLibrary library, Abi abi) { + if (abi != Abi.linuxX64 && abi != Abi.linuxArm64 && abi != Abi.windowsX64) { + return; + } + final reported = readLiteRtLmInProcessLiteRtAbiVersion(library); + if (reported != liteRtLmInProcessLiteRtAbiVersion) { + throw StateError( + 'The $abi LiteRT-LM runtime reports ' + 'litert_lm_native_in_process_litert_abi_version ' + '${reported ?? '(not exported)'}, expected ' + '$liteRtLmInProcessLiteRtAbiVersion.', + ); + } +} + bool _isSupportedHost() { final abi = Abi.current(); return (Platform.isLinux && (abi == Abi.linuxX64 || abi == Abi.linuxArm64)) || diff --git a/website/docs/maintainers/native-and-web-sync.md b/website/docs/maintainers/native-and-web-sync.md index 75c6abf3d..101eb3227 100644 --- a/website/docs/maintainers/native-and-web-sync.md +++ b/website/docs/maintainers/native-and-web-sync.md @@ -396,7 +396,9 @@ hook and Apple companion source now pin published [v0.19.1-2](https://github.com/leehack/litert-lm-native/releases/tag/v0.19.1-2), owner `0b15fb34` / upstream `7a03724c`, which keeps C API 1.0.0 and the `v0.19.0` exports and adds the in-process LiteRT marker -`litert_lm_native_in_process_litert_abi_version` (1 on every platform). The +`litert_lm_native_in_process_litert_abi_version` (1 on every platform); the +`LiteRT-LM Smoke` library step fails when the bundled Linux x64/arm64 or +Windows x64 runtime lacks the export or does not report 1. The earlier `-1` rebuild links LiteRT into the Linux and Windows `libLiteRtLm` ([litert-lm-native#112](https://github.com/leehack/litert-lm-native/pull/112)); its Apple and Android binaries are identical to `v0.19.1`. Upstream From dc058a2a748c0cb851207f06281e356e40616818 Mon Sep 17 00:00:00 2001 From: Jhin Lee Date: Mon, 9 Nov 2026 06:35:55 -0500 Subject: [PATCH 2/3] Drop the Linux arm64 smoke entry: no Flutter 3.47.1 arm64 build for flutter-action --- .github/workflows/litert_lm_smoke.yml | 9 ++---- .../litert_lm_smoke_workflow_test.dart | 28 ------------------- .../docs/maintainers/native-and-web-sync.md | 5 ++-- 3 files changed, 5 insertions(+), 37 deletions(-) diff --git a/.github/workflows/litert_lm_smoke.yml b/.github/workflows/litert_lm_smoke.yml index f3597deae..2bacc7158 100644 --- a/.github/workflows/litert_lm_smoke.yml +++ b/.github/workflows/litert_lm_smoke.yml @@ -55,9 +55,6 @@ jobs: litert_lib_subdir: linux/x64 - os: windows-latest litert_lib_subdir: windows/x64 - - os: ubuntu-24.04-arm - litert_lib_subdir: linux/arm64 - library_only: true steps: - uses: actions/checkout@v7 @@ -125,14 +122,12 @@ jobs: dart run tool/litert_lm_library_smoke.dart - name: Cache tiny LiteRT-LM model - if: ${{ !matrix.library_only }} uses: actions/cache@v6 with: path: .dart_tool/litert_lm_models/Qwen3-0.6B.litertlm key: litert-lm-smoke-qwen3-0.6b-${{ runner.os }}-${{ env.LITERT_LM_SMOKE_MODEL_REVISION }}-${{ env.LITERT_LM_SMOKE_MODEL_SHA256 }} - name: Download tiny LiteRT-LM model - if: ${{ !matrix.library_only }} id: download-model continue-on-error: true shell: bash @@ -179,7 +174,7 @@ jobs: echo "Downloaded $model_path (sha256 $actual_sha256)" - name: Run LiteRT-LM engine and chat smoke - if: ${{ !matrix.library_only && steps.download-model.outcome == 'success' }} + if: steps.download-model.outcome == 'success' shell: bash env: LLAMADART_LITERT_LM_LIB_DIR: ${{ github.workspace }}/.dart_tool/llamadart/litert_lm/${{ steps.litert-version.outputs.version }}/${{ matrix.litert_lib_subdir }} @@ -200,7 +195,7 @@ jobs: --model-path "$LITERT_LM_MODEL_PATH" --backend cpu - name: Note skipped LiteRT-LM model smokes - if: ${{ !matrix.library_only && steps.download-model.outcome != 'success' }} + if: steps.download-model.outcome != 'success' shell: bash run: | echo "::warning::Skipped LiteRT-LM engine/chat smokes because the external model download failed. The library/native-asset smoke already passed." diff --git a/test/unit/tooling/litert_lm_smoke_workflow_test.dart b/test/unit/tooling/litert_lm_smoke_workflow_test.dart index 891867910..5abb0b44a 100644 --- a/test/unit/tooling/litert_lm_smoke_workflow_test.dart +++ b/test/unit/tooling/litert_lm_smoke_workflow_test.dart @@ -28,34 +28,6 @@ void main() { final bash = Platform.isWindows ? null : Process.runSync('which', ['bash']); final skip = bash == null || bash.exitCode != 0 ? 'requires bash' : null; - test('library smoke covers every bundled desktop runtime', () { - final jobs = _workflow()['jobs'] as YamlMap; - final include = - (((jobs['native-smoke'] as YamlMap)['strategy'] as YamlMap)['matrix'] - as YamlMap)['include'] - as YamlList; - final subdirs = include - .cast() - .map((entry) => entry['litert_lib_subdir']) - .toSet(); - expect(subdirs, {'linux/x64', 'linux/arm64', 'windows/x64'}); - - for (final entry in include.cast()) { - if (entry['litert_lib_subdir'] == 'linux/arm64') { - expect(entry['library_only'], isTrue); - } - } - for (final name in [ - 'Cache tiny LiteRT-LM model', - 'Download tiny LiteRT-LM model', - 'Run LiteRT-LM engine and chat smoke', - 'Note skipped LiteRT-LM model smokes', - ]) { - expect(_step(name)['if'], contains('!matrix.library_only')); - } - expect(_step('Run LiteRT-LM library smoke')['if'], isNull); - }); - test( 'cache key and download URL derive from the pinned revision and sha', () { diff --git a/website/docs/maintainers/native-and-web-sync.md b/website/docs/maintainers/native-and-web-sync.md index 101eb3227..b56b0abd5 100644 --- a/website/docs/maintainers/native-and-web-sync.md +++ b/website/docs/maintainers/native-and-web-sync.md @@ -397,8 +397,9 @@ hook and Apple companion source now pin published owner `0b15fb34` / upstream `7a03724c`, which keeps C API 1.0.0 and the `v0.19.0` exports and adds the in-process LiteRT marker `litert_lm_native_in_process_litert_abi_version` (1 on every platform); the -`LiteRT-LM Smoke` library step fails when the bundled Linux x64/arm64 or -Windows x64 runtime lacks the export or does not report 1. The +`LiteRT-LM Smoke` library step fails when the bundled Linux x64 or Windows +x64 runtime lacks the export or does not report 1; Linux arm64 has no CI +runner for it. The earlier `-1` rebuild links LiteRT into the Linux and Windows `libLiteRtLm` ([litert-lm-native#112](https://github.com/leehack/litert-lm-native/pull/112)); its Apple and Android binaries are identical to `v0.19.1`. Upstream From b068961a2a8a66348055901135bc17d2bad54a53 Mon Sep 17 00:00:00 2001 From: Jhin Lee Date: Mon, 9 Nov 2026 09:27:29 -0500 Subject: [PATCH 3/3] Pin the smoke's in-process LiteRT marker check and report its value --- test/fixtures/litert_lm/c_api_version.c | 8 ++++ .../tooling/litert_lm_library_smoke_test.dart | 27 +++++++++++-- tool/litert_lm_library_smoke.dart | 39 +++++++++++++------ .../docs/maintainers/native-and-web-sync.md | 6 ++- 4 files changed, 64 insertions(+), 16 deletions(-) diff --git a/test/fixtures/litert_lm/c_api_version.c b/test/fixtures/litert_lm/c_api_version.c index df782fa95..622eeb68d 100644 --- a/test/fixtures/litert_lm/c_api_version.c +++ b/test/fixtures/litert_lm/c_api_version.c @@ -33,3 +33,11 @@ int litert_lm_native_in_process_litert_abi_version(void) { // A missing gate must fail the test process before an incompatible call can // accidentally look like a supported engine initialization. void *litert_lm_engine_settings_create(void) { abort(); } + +#ifdef SMOKE_SYMBOLS +void litert_lm_engine_create(void) { abort(); } +void litert_lm_engine_delete(void) { abort(); } +void stream_proxy_create(void) { abort(); } +void stream_proxy_free_string(void) { abort(); } +void stream_proxy_delete(void) { abort(); } +#endif diff --git a/test/unit/tooling/litert_lm_library_smoke_test.dart b/test/unit/tooling/litert_lm_library_smoke_test.dart index 245da16fa..abd06f420 100644 --- a/test/unit/tooling/litert_lm_library_smoke_test.dart +++ b/test/unit/tooling/litert_lm_library_smoke_test.dart @@ -211,9 +211,9 @@ void main() { if (skip != null) return; fixtures = Directory.systemTemp.createTempSync('litert_marker_'); for (final (name, defines) in [ - ('absent', []), - ('zero', ['-DIN_PROCESS_LITERT_ABI=0']), - ('one', ['-DIN_PROCESS_LITERT_ABI=1']), + ('absent', ['-DSMOKE_SYMBOLS']), + ('zero', ['-DSMOKE_SYMBOLS', '-DIN_PROCESS_LITERT_ABI=0']), + ('one', ['-DSMOKE_SYMBOLS', '-DIN_PROCESS_LITERT_ABI=1']), ]) { final output = path.join( fixtures.path, @@ -254,6 +254,27 @@ void main() { } }, skip: skip); + Map runSmoke(String fixture, Abi abi) => + runLiteRtLmLibrarySmoke( + abi: abi, + directory: null, + releaseTag: 'v0.19.1-2', + openLibrary: (_) => libraries[fixture]!, + ); + + test('the smoke run enforces the marker and reports its value', () { + for (final abi in [Abi.linuxX64, Abi.windowsX64]) { + expect(runSmoke('one', abi)['inProcessLiteRtAbi'], 1); + for (final name in ['absent', 'zero']) { + expect(() => runSmoke(name, abi), throwsStateError); + } + } + }, skip: skip); + + test('the smoke run reports no marker where it is not required', () { + expect(runSmoke('absent', Abi.macosArm64)['inProcessLiteRtAbi'], isNull); + }, skip: skip); + test('Apple and Android runtimes are not required to export it', () { requireInProcessLiteRtMarker(libraries['absent']!, Abi.macosArm64); requireInProcessLiteRtMarker(libraries['absent']!, Abi.androidArm64); diff --git a/tool/litert_lm_library_smoke.dart b/tool/litert_lm_library_smoke.dart index 2dc110ece..34796edb3 100644 --- a/tool/litert_lm_library_smoke.dart +++ b/tool/litert_lm_library_smoke.dart @@ -28,30 +28,46 @@ void main() { return; } + print( + 'RESULT litert_lm_library ${jsonEncode(runLiteRtLmLibrarySmoke(abi: Abi.current(), directory: Platform.environment[_litertLmLibDirEnv], releaseTag: liteRtLmReleaseTag))}', + ); +} + +/// Opens the runtime, checks its symbols and in-process LiteRT marker, and +/// returns the RESULT payload. +Map runLiteRtLmLibrarySmoke({ + required Abi abi, + required String? directory, + required String releaseTag, + DynamicLibrary Function(String)? openLibrary, +}) { final opened = openLiteRtLmSmokeLibrary( - directory: Platform.environment[_litertLmLibDirEnv], - abi: Abi.current(), - cApiV1: liteRtLmSmokeUsesCApiV1(liteRtLmReleaseTag), + directory: directory, + abi: abi, + cApiV1: liteRtLmSmokeUsesCApiV1(releaseTag), + openLibrary: openLibrary, ); final library = opened.library; for (final symbol in _requiredSymbols) { library.lookup>(symbol); } - requireInProcessLiteRtMarker(library, Abi.current()); - - print( - 'RESULT litert_lm_library ${jsonEncode({'abi': Abi.current().toString(), 'library': opened.path, 'symbols': _requiredSymbols})}', - ); + return { + 'abi': abi.toString(), + 'library': opened.path, + 'symbols': _requiredSymbols, + 'inProcessLiteRtAbi': requireInProcessLiteRtMarker(library, abi), + }; } /// Fails when a bundled Linux or Windows runtime does not report in-process /// LiteRT, which gates native embeddings there; a repin that drops the /// `litert_lm_native_in_process_litert_abi_version` export would otherwise -/// surface only on hardware. -void requireInProcessLiteRtMarker(DynamicLibrary library, Abi abi) { +/// surface only on hardware. Returns the reported value, or null where the +/// marker is not required. +int? requireInProcessLiteRtMarker(DynamicLibrary library, Abi abi) { if (abi != Abi.linuxX64 && abi != Abi.linuxArm64 && abi != Abi.windowsX64) { - return; + return null; } final reported = readLiteRtLmInProcessLiteRtAbiVersion(library); if (reported != liteRtLmInProcessLiteRtAbiVersion) { @@ -62,6 +78,7 @@ void requireInProcessLiteRtMarker(DynamicLibrary library, Abi abi) { '$liteRtLmInProcessLiteRtAbiVersion.', ); } + return reported; } bool _isSupportedHost() { diff --git a/website/docs/maintainers/native-and-web-sync.md b/website/docs/maintainers/native-and-web-sync.md index b56b0abd5..6013400ce 100644 --- a/website/docs/maintainers/native-and-web-sync.md +++ b/website/docs/maintainers/native-and-web-sync.md @@ -398,8 +398,10 @@ owner `0b15fb34` / upstream `7a03724c`, which keeps C API 1.0.0 and the `v0.19.0` exports and adds the in-process LiteRT marker `litert_lm_native_in_process_litert_abi_version` (1 on every platform); the `LiteRT-LM Smoke` library step fails when the bundled Linux x64 or Windows -x64 runtime lacks the export or does not report 1; Linux arm64 has no CI -runner for it. The +x64 runtime lacks the export or does not report 1, and prints the value as +`inProcessLiteRtAbi`; Linux arm64 has no CI runner for it. Rolling the pin back +to a pre-marker tag (`v0.18.1-4`, `v0.19.0`, `v0.19.1`, `v0.19.1-1`) therefore +fails the smoke on Linux and Windows by design. The earlier `-1` rebuild links LiteRT into the Linux and Windows `libLiteRtLm` ([litert-lm-native#112](https://github.com/leehack/litert-lm-native/pull/112)); its Apple and Android binaries are identical to `v0.19.1`. Upstream