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 e1d1911ba..abd06f420 100644 --- a/test/unit/tooling/litert_lm_library_smoke_test.dart +++ b/test/unit/tooling/litert_lm_library_smoke_test.dart @@ -201,4 +201,83 @@ 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', ['-DSMOKE_SYMBOLS']), + ('zero', ['-DSMOKE_SYMBOLS', '-DIN_PROCESS_LITERT_ABI=0']), + ('one', ['-DSMOKE_SYMBOLS', '-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); + + 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); + }, skip: skip); + }); } diff --git a/tool/litert_lm_library_smoke.dart b/tool/litert_lm_library_smoke.dart index 2c770ae7b..34796edb3 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'; @@ -26,19 +28,57 @@ 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); } - 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. 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 null; + } + 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.', + ); + } + 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 75c6abf3d..6013400ce 100644 --- a/website/docs/maintainers/native-and-web-sync.md +++ b/website/docs/maintainers/native-and-web-sync.md @@ -396,7 +396,12 @@ 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 or Windows +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