[prebuilt_code_assets] Distinguish disabled record use from no used functions The docs and messages said "no recorded uses" for two different cases: - Record use is disabled (`input.recordedUses` is `null`): it is unknown which functions the application uses. They now say "record use is disabled". - The application uses none of the functions: `link` now explicitly bundles no library and returns before setting up the linker, instead of relying on `CLinker` skipping an empty `symbolsToKeep`. This also holds with `treeshake: on`. Adds unit tests for the second case on Linux and Windows in the `auto` and `on` modes, and an integration test that a `dart build cli` app that calls none of the functions runs without the library.
diff --git a/pkgs/prebuilt_code_assets/CHANGELOG.md b/pkgs/prebuilt_code_assets/CHANGELOG.md index 0e42611..6c5d45e 100644 --- a/pkgs/prebuilt_code_assets/CHANGELOG.md +++ b/pkgs/prebuilt_code_assets/CHANGELOG.md
@@ -41,7 +41,7 @@ - **Breaking:** Without `usedSymbols`, `build` bundles the dynamic library directly instead of routing a static library to the link hook, and `treeshake: on` fails the build. -- **Breaking:** Without recorded uses, `link` bundles the prebuilt dynamic +- **Breaking:** If record use is disabled, `link` bundles the prebuilt dynamic library in the `fetch` build mode instead of linking the static library. In other build modes, it still links the static library keeping all functions, except on Windows, where it throws a `BuildError` (a DLL only exports the
diff --git a/pkgs/prebuilt_code_assets/README.md b/pkgs/prebuilt_code_assets/README.md index 307b080..db9622a 100644 --- a/pkgs/prebuilt_code_assets/README.md +++ b/pkgs/prebuilt_code_assets/README.md
@@ -19,7 +19,8 @@ - `auto` (default): Tree-shakes when possible. If no static library is released for a target, or if linking fails in `fetch` mode (e.g. no C toolchain for the target), prints a warning and bundles the prebuilt dynamic library instead. - `on`: Always tree-shakes, and fails the build if that is not possible. - `off`: Never tree-shakes; bundles the dynamic library directly without running the C linker. - - Without recorded uses (for example, when record use is disabled), nothing can be tree-shaken. Then `fetch` mode bundles the prebuilt dynamic library. When building from source, the link hook links the static library keeping all functions, except on Windows, where a DLL only exports the functions it lists; that fails the build (use `treeshake: off` instead). + - If the application uses none of the library's functions, the link hook bundles no library at all. + - If record use is disabled, it is unknown which functions the application uses, so nothing can be tree-shaken. Then `fetch` mode bundles the prebuilt dynamic library. When building from source, the link hook links the static library keeping all functions, except on Windows, where a DLL only exports the functions it lists; that fails the build (use `treeshake: off` instead). - On Windows, `package:native_toolchain_c` only exports the used functions that the `.lib` defines. - On Windows, don't mark functions with `__declspec(dllexport)` when compiling the static library: the linker then exports, and so keeps, every such function in each object file it links, even unused ones. The `.def` file already determines the exports. - Any other assets routed to the package's link hook are forwarded unchanged.
diff --git a/pkgs/prebuilt_code_assets/lib/src/prebuilt_library.dart b/pkgs/prebuilt_code_assets/lib/src/prebuilt_library.dart index 5eec9d1..403afcc 100644 --- a/pkgs/prebuilt_code_assets/lib/src/prebuilt_library.dart +++ b/pkgs/prebuilt_code_assets/lib/src/prebuilt_library.dart
@@ -334,13 +334,15 @@ /// Runs the link hook (`hook/link.dart`) to link and tree-shake the static /// library emitted by [build] into a dynamic library containing only the - /// functions referenced in `input.recordedUses`. + /// functions referenced in `input.recordedUses`. If the application uses + /// none of them, no library is bundled. /// - /// Without recorded uses, nothing can be tree-shaken. Then [link] bundles - /// the prebuilt dynamic library in [NativeBuildMode.fetch] if available. - /// Otherwise it links the static library keeping all functions, except on - /// Windows, where it throws a [BuildError] because a DLL only exports the - /// functions it lists. + /// If record use is disabled (`input.recordedUses` is `null`), it is unknown + /// which functions the application uses, so nothing can be tree-shaken. Then + /// [link] bundles the prebuilt dynamic library in [NativeBuildMode.fetch] if + /// available. Otherwise it links the static library keeping all functions, + /// except on Windows, where it throws a [BuildError] because a DLL only + /// exports the functions it lists. /// /// All other assets sent to this link hook are forwarded unchanged. /// @@ -348,8 +350,8 @@ /// - [TreeshakeMode.auto] (default): Tries to tree-shake, and if linking /// fails in [NativeBuildMode.fetch], prints a warning and falls back to /// bundling the prebuilt dynamic library. - /// - [TreeshakeMode.on]: Always tries to tree-shake, and throws if there are - /// no recorded uses or linking fails. + /// - [TreeshakeMode.on]: Always tries to tree-shake, and throws if record use + /// is disabled or linking fails. /// - [TreeshakeMode.off]: Never tree-shakes. [build] then bundles the /// dynamic library directly; if a static library still reaches [link], the /// prebuilt dynamic library is bundled instead. @@ -399,22 +401,26 @@ final resolver = usedSymbols; final List<String>? symbols; if (recordedUses == null || resolver == null) { + // It's unknown which functions the application uses, so nothing can be + // tree-shaken. [build] only routes a static library here if there is a + // `usedSymbols`, so record use is disabled. if (buildOptions.treeshake == TreeshakeMode.on) { throw BuildError( message: resolver == null ? _noUsedSymbolsMessage(pkg) - : '$pkg: treeshake is on, but the link hook received no ' - 'recorded uses, so it cannot tell which functions the ' - 'application uses. Build with record use enabled, or set ' - '`treeshake` to `auto`.', + : '$pkg: treeshake is on, but record use is disabled, so the ' + 'link hook cannot tell which functions the application ' + 'uses. Build with record use enabled, or set `treeshake` ' + 'to `auto`.', ); } - // Nothing can be tree-shaken, so the pre-built dynamic library is - // equivalent, and doesn't need a C toolchain. + // The pre-built dynamic library is equivalent, and doesn't need a C + // toolchain. if (buildOptions.buildMode == NativeBuildMode.fetch && await _bundlePrebuiltDynamicLibrary(input, output, pkg, log)) { log.info( - '$pkg: no recorded uses, bundled the pre-built dynamic library.', + '$pkg: record use is disabled, bundled the pre-built dynamic ' + 'library.', ); return; } @@ -422,16 +428,23 @@ throw BuildError( message: '$pkg: cannot link the static library for ${_target(input)} ' - 'without recorded uses, because a DLL only exports the ' + 'while record use is disabled, because a DLL only exports the ' 'functions it lists. Build with record use enabled, or set ' '`treeshake: off` under `hooks.user_defines.$pkg` to bundle a ' 'dynamic library instead.', ); } - log.info('$pkg: no recorded uses, keeping all functions.'); + log.info('$pkg: record use is disabled, keeping all functions.'); symbols = null; } else { symbols = resolver(recordedUses); + if (symbols.isEmpty) { + log.info( + '$pkg: the application uses none of the functions, so no library is ' + 'bundled.', + ); + return; + } log.info( '$pkg: keeping the ${symbols.length} functions the application ' 'uses:\n ${symbols.join('\n ')}',
diff --git a/pkgs/prebuilt_code_assets/test/integration_test.dart b/pkgs/prebuilt_code_assets/test/integration_test.dart index 508d0fb..131193b 100644 --- a/pkgs/prebuilt_code_assets/test/integration_test.dart +++ b/pkgs/prebuilt_code_assets/test/integration_test.dart
@@ -461,6 +461,30 @@ ); test( + 'dart build cli bundles no library if the application uses none of its ' + 'functions, even with treeshake: on', + () async { + final pkgDir = await createPackage(buildMode: 'fetch', treeshake: 'on'); + await writeFile(pkgDir, 'bin/main.dart', ''' +void main() { + print('No FFI calls'); +} +'''); + final bundle = await buildCli(pkgDir); + final result = await Process.run( + bundle.resolve(exeName).toFilePath(), + [], + ); + expect(result.exitCode, 0, reason: '${result.stdout}\n${result.stderr}'); + expect(result.stdout, contains('No FFI calls')); + expect( + File.fromUri(bundle.resolve('lib/$dylibFileName')).existsSync(), + isFalse, + ); + }, + ); + + test( 'treeshake: off bundles the dynamic library without tree-shaking', () async { final pkgDir = await createPackage(buildMode: 'fetch', treeshake: 'off');
diff --git a/pkgs/prebuilt_code_assets/test/prebuilt_code_assets_test.dart b/pkgs/prebuilt_code_assets/test/prebuilt_code_assets_test.dart index f297be8..d7ec574 100644 --- a/pkgs/prebuilt_code_assets/test/prebuilt_code_assets_test.dart +++ b/pkgs/prebuilt_code_assets/test/prebuilt_code_assets_test.dart
@@ -619,10 +619,12 @@ List<EncodedAsset> assets, { Map<String, Object?> defines = const {}, CodeAssetExtension? extension, - bool withRecordedUses = false, + bool recordUseEnabled = false, }) { + // With record use enabled, the link hook gets recorded uses even if + // the application uses nothing. Uri? recordedUsesFile; - if (withRecordedUses) { + if (recordUseEnabled) { recordedUsesFile = tempDir.uri.resolve('recorded_uses.json'); File.fromUri(recordedUsesFile).writeAsStringSync( jsonEncode( @@ -712,11 +714,11 @@ LinkInput staticLinkInput( OS os, { Map<String, Object?> defines = const {}, - bool withRecordedUses = false, + bool recordUseEnabled = false, }) => createLinkInput( [staticAsset('demo.dart').encode()], defines: defines, - withRecordedUses: withRecordedUses, + recordUseEnabled: recordUseEnabled, extension: CodeAssetExtension( targetOS: os, targetArchitecture: Architecture.x64, @@ -741,7 +743,7 @@ return const []; }, ).link( - input: staticLinkInput(OS.windows, withRecordedUses: true), + input: staticLinkInput(OS.windows, recordUseEnabled: true), output: output, ); expect(linked, isTrue); @@ -750,13 +752,50 @@ expect(requestedPaths, ['/releases/1.0.0/$dll']); }); + for (final os in [OS.linux, OS.windows]) { + for (final treeshake in ['auto', 'on']) { + test( + 'on $os with treeshake: $treeshake, bundles no library if the ' + 'application uses none of its functions', + () async { + var linked = false; + final input = staticLinkInput( + os, + defines: {'treeshake': treeshake}, + recordUseEnabled: true, + ); + final output = LinkOutputBuilder(); + await makeLibrary( + makeReleaseConfig(bothHashes), + usedSymbols: SymbolsResolvers.fromRecordUseMapping( + const record_use.Library('package:demo/demo.dart'), + const {'demoAdd': 'demo_add'}, + ), + libraries: (_) { + linked = true; + return const []; + }, + ).link(input: input, output: output); + final linkOutput = LinkOutput(output.json); + expect( + await ProtocolBase.validateLinkOutput(input, linkOutput), + isEmpty, + ); + expect(linkOutput.assets.encodedAssets, isEmpty); + expect(linked, isFalse, reason: 'Needs no C toolchain'); + expect(requestedPaths, isEmpty); + }, + ); + } + } + for (final (os, prebuilt) in [ (OS.linux, dylibAsset), (OS.windows, dll), ]) { test( - 'on $os without recorded uses, bundles the prebuilt dynamic library ' - 'in fetch mode without linking', + 'on $os with record use disabled, bundles the prebuilt dynamic ' + 'library in fetch mode without linking', () async { var linked = false; final output = LinkOutputBuilder(); @@ -776,7 +815,7 @@ } test( - 'without recorded uses, links keeping all functions when building ' + 'with record use disabled, links keeping all functions when building ' 'from source', () async { var linked = false; @@ -800,7 +839,7 @@ ); test( - 'on Windows without recorded uses, throws when building from source', + 'on Windows with record use disabled, throws when building from source', () async { await expectLater( makeLibrary(makeReleaseConfig({dll: dylibHash})).link( @@ -822,7 +861,7 @@ }, ); - test('treeshake: on throws without recorded uses', () async { + test('treeshake: on throws with record use disabled', () async { var linked = false; await expectLater( makeLibrary( @@ -839,7 +878,7 @@ isA<BuildError>().having( (e) => e.message, 'message', - contains('no recorded uses'), + contains('record use is disabled'), ), ), ); @@ -859,7 +898,7 @@ staticAsset('demo.dart').encode(), ], defines: {'treeshake': 'on'}, - withRecordedUses: true, + recordUseEnabled: true, ); final library = PrebuiltLibrary( name: 'demo',