[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',