[boring] Use package:prebuilt_code_assets in the build and link hooks

Replace the hook helpers for fetching, verifying, building and linking
with package:prebuilt_code_assets, which implements the same build modes
and tree shaking for any package with prebuilt native code. What is
specific to package:boring, the release configuration and the CMake
build, is in lib/src/hook_helpers/library.dart, which both hooks and
the release tools share.

- tool/precompile_binaries.dart and tool/regenerate_hashes.dart use the
  package's CLI runners with the same options, so the binaries workflow
  is unchanged.
- test/tree_shaking_test.dart uses its ELF helpers.
- Drop the dependencies only the replaced helpers used, and with them
  dart_dependency_validator.yaml, which is no longer needed.
diff --git a/pkgs/boring/CHANGELOG.md b/pkgs/boring/CHANGELOG.md
index f3556c3..2f0280e 100644
--- a/pkgs/boring/CHANGELOG.md
+++ b/pkgs/boring/CHANGELOG.md
@@ -5,6 +5,7 @@
   [dart-lang/tools](https://github.com/dart-lang/tools) repository.
 - The package is now licensed under the BSD 3-Clause license used by other
   Dart packages.
+- Refactored build and link hooks to use `package:prebuilt_code_assets`.
 
 ## 0.4.1
 
diff --git a/pkgs/boring/dart_dependency_validator.yaml b/pkgs/boring/dart_dependency_validator.yaml
deleted file mode 100644
index de98338..0000000
--- a/pkgs/boring/dart_dependency_validator.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-# Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
-# for details. All rights reserved. Use of this source code is governed by a
-# BSD-style license that can be found in the LICENSE file.
-
-# Only imported by the hooks, which run when a package depending on boring is
-# built, so they have to be regular dependencies.
-ignore:
-  - logging
-  - native_toolchain_c
-  - native_toolchain_cmake
-  - record_use
diff --git a/pkgs/boring/hook/build.dart b/pkgs/boring/hook/build.dart
index 35ff8b2..e40d84e 100644
--- a/pkgs/boring/hook/build.dart
+++ b/pkgs/boring/hook/build.dart
@@ -2,192 +2,17 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-import 'dart:io';
-
-import 'package:boring/src/hook_helpers/build_options.dart'
-    show BuildModeEnum, BuildOptions;
-import 'package:boring/src/hook_helpers/fetch.dart' show fetchPrebuiltLibrary;
-import 'package:boring/src/hook_helpers/targets.dart' show libraryFileName;
-import 'package:code_assets/code_assets.dart';
+import 'package:boring/src/hook_helpers/library.dart';
 import 'package:hooks/hooks.dart';
-import 'package:native_toolchain_cmake/native_toolchain_cmake.dart';
-
-const _assetName = 'boring.dart';
 
 Future<void> main(List<String> args) async {
   await build(args, (input, output) async {
-    if (!input.config.buildCodeAssets) {
-      stdout.writeln(
-        'boring: skipping native asset build (code assets not requested).',
-      );
-      return;
-    }
-
-    final buildOptions = BuildOptions.fromDefines(input.userDefines);
-    stdout.writeln('boring: build options: $buildOptions');
-
-    // When linking is enabled (`dart build`, and Flutter's profile and release
-    // builds), hook/link.dart links a dynamic library with only the functions
-    // the application uses from the static library.
-    final static = input.config.linkingEnabled;
-
-    switch (buildOptions.buildMode) {
-      case BuildModeEnum.fetch:
-        await _fetchPrebuiltBinary(input, output, static: static);
-      case BuildModeEnum.build:
-      case BuildModeEnum.checkout:
-        await _buildLocalCMake(input, output, static: static);
-      case BuildModeEnum.local:
-        await _useLocalBinary(input, output, buildOptions.localPath);
-    }
-    output.dependencies.addAll([
-      input.packageRoot.resolve('pubspec.yaml'),
-      input.packageRoot.resolve('hook/build.dart'),
-      input.packageRoot.resolve('lib/src/hook_helpers/hashes.dart'),
-    ]);
+    await boringLibrary.build(
+      input: input,
+      output: output,
+      additionalDependencies: [
+        input.packageRoot.resolve('lib/src/hook_helpers/hashes.dart'),
+      ],
+    );
   });
 }
-
-/// Adds [library] as the `package:boring/boring.dart` code asset.
-///
-/// A [static] library is sent to hook/link.dart, which links it into the
-/// dynamic library that is bundled.
-void _addLibrary(
-  BuildInput input,
-  BuildOutputBuilder output,
-  Uri library, {
-  required bool static,
-}) {
-  output.assets.code.add(
-    CodeAsset(
-      package: input.packageName,
-      name: _assetName,
-      linkMode: static ? StaticLinking() : DynamicLoadingBundled(),
-      file: library,
-    ),
-    routing: static ? ToLinkHook(input.packageName) : const ToAppBundle(),
-  );
-}
-
-Future<void> _fetchPrebuiltBinary(
-  BuildInput input,
-  BuildOutputBuilder output, {
-  required bool static,
-}) async {
-  final cachedLibrary = await fetchPrebuiltLibrary(input, static: static);
-  if (cachedLibrary == null) {
-    stdout.writeln(
-      'boring: falling back to building from local source via CMake.',
-    );
-    await _buildLocalCMake(input, output, static: static);
-    return;
-  }
-  _addLibrary(input, output, cachedLibrary, static: static);
-}
-
-/// Bundles the dynamic library at [localPath] as is, even when linking is
-/// enabled.
-Future<void> _useLocalBinary(
-  BuildInput input,
-  BuildOutputBuilder output,
-  Uri? localPath,
-) async {
-  if (localPath == null) {
-    throw BuildError(
-      message:
-          'buildMode is set to `local`, but `localPath` was not specified '
-          'under `hooks.user_defines.boring`.',
-    );
-  }
-  final file = File.fromUri(localPath);
-  if (!file.existsSync()) {
-    throw BuildError(
-      message:
-          'Specified local binary does not exist at ${localPath.toFilePath()}',
-    );
-  }
-  final dylibFileName = input.config.code.targetOS.dylibFileName('bssl_dart');
-  final destFile = File.fromUri(input.outputDirectory.resolve(dylibFileName));
-  await file.copy(destFile.path);
-
-  _addLibrary(input, output, destFile.uri, static: false);
-  output.dependencies.add(localPath);
-}
-
-Future<void> _buildLocalCMake(
-  BuildInput input,
-  BuildOutputBuilder output, {
-  required bool static,
-}) async {
-  final packageRoot = input.packageRoot;
-  final installDir = input.outputDirectory.resolve('install/');
-  final sourceDir = packageRoot.resolve('src/');
-  final targetOS = input.config.code.targetOS;
-  final targetArch = input.config.code.targetArchitecture;
-
-  stdout.writeln(
-    'boring: building native asset with CMake for $targetOS-$targetArch.',
-  );
-
-  // Installs both the dynamic and the static library, see src/CMakeLists.txt.
-  final builder = CMakeBuilder.create(
-    name: 'bssl_dart',
-    sourceDir: sourceDir,
-    defines: {
-      'CMAKE_BUILD_TYPE': 'Release',
-      'CMAKE_INSTALL_PREFIX': installDir.toFilePath(),
-    },
-    targets: ['install'],
-    parallelUseAllProcessors: true,
-  );
-
-  await builder.run(input: input, output: output);
-
-  final fileName = libraryFileName(targetOS, static: static);
-  final library = installDir.resolve(fileName);
-  if (!File.fromUri(library).existsSync()) {
-    throw BuildError(
-      message:
-          'Failed to locate the built $fileName in '
-          '${installDir.toFilePath()}',
-    );
-  }
-  _addLibrary(input, output, library, static: static);
-
-  output.dependencies.addAll(_buildDependencies(packageRoot));
-}
-
-final _buildDependencyExtensions = {
-  '.S',
-  '.asm',
-  '.c',
-  '.cc',
-  '.cmake',
-  '.cpp',
-  '.h',
-  '.s',
-  'CMakeLists.txt',
-};
-
-Iterable<Uri> _buildDependencies(Uri packageRoot) sync* {
-  yield* _filesForBuild(Directory.fromUri(packageRoot.resolve('src/')));
-  yield* _filesForBuild(
-    Directory.fromUri(packageRoot.resolve('third_party/boringssl/')),
-  );
-}
-
-Iterable<Uri> _filesForBuild(Directory root) sync* {
-  if (!root.existsSync()) {
-    return;
-  }
-
-  for (final entity in root.listSync(recursive: true, followLinks: false)) {
-    if (entity is! File) {
-      continue;
-    }
-    if (!_buildDependencyExtensions.any(entity.uri.path.endsWith)) {
-      continue;
-    }
-    yield entity.uri;
-  }
-}
diff --git a/pkgs/boring/hook/link.dart b/pkgs/boring/hook/link.dart
index 398d38f..f65184f 100644
--- a/pkgs/boring/hook/link.dart
+++ b/pkgs/boring/hook/link.dart
@@ -2,23 +2,8 @@
 // for details. All rights reserved. Use of this source code is governed by a
 // BSD-style license that can be found in the LICENSE file.
 
-import 'dart:io';
-import 'dart:typed_data';
-
-import 'package:boring/src/bindings/record_use_mapping.g.dart';
-import 'package:boring/src/hook_helpers/build_options.dart'
-    show BuildModeEnum, BuildOptions;
-import 'package:boring/src/hook_helpers/fetch.dart' show fetchPrebuiltLibrary;
-import 'package:boring/src/hook_helpers/hashes.dart' show version;
-import 'package:code_assets/code_assets.dart';
+import 'package:boring/src/hook_helpers/library.dart';
 import 'package:hooks/hooks.dart';
-import 'package:logging/logging.dart';
-import 'package:native_toolchain_c/native_toolchain_c.dart';
-import 'package:record_use/record_use.dart' as record_use;
-
-const _bindings = record_use.Library(
-  'package:boring/src/bindings/boringssl.g.dart',
-);
 
 /// Links the static library from hook/build.dart into a dynamic library with
 /// only the BoringSSL functions that the application uses.
@@ -27,210 +12,6 @@
 /// pre-built dynamic library.
 Future<void> main(List<String> args) async {
   await link(args, (input, output) async {
-    final staticLibrary = input.assets.code
-        .where(
-          (asset) => asset.id == 'package:${input.packageName}/boring.dart',
-        )
-        .firstOrNull;
-    if (staticLibrary == null) {
-      // hook/build.dart bundled a dynamic library instead.
-      return;
-    }
-    final staticLibraryFile = staticLibrary.file!;
-
-    final recordedUses = input.recordedUses;
-    final List<String>? symbols;
-    if (recordedUses == null) {
-      // For example with `flutter config --no-enable-record-use`.
-      stdout.writeln('boring: no recorded uses, keeping all functions.');
-      symbols = null;
-    } else {
-      symbols = _usedSymbols(recordedUses);
-      stdout.writeln(
-        'boring: keeping the ${symbols.length} functions the application '
-        'uses:\n  ${symbols.join('\n  ')}',
-      );
-    }
-
-    final LinkerOptions linkerOptions;
-    if (input.config.code.targetOS == OS.windows) {
-      linkerOptions = await _windowsLinkerOptions(
-        input,
-        staticLibraryFile,
-        symbols,
-      );
-    } else {
-      linkerOptions = LinkerOptions.treeshake(symbolsToKeep: symbols);
-    }
-
-    try {
-      await CLinker.library(
-        name: 'bssl_dart',
-        packageName: input.packageName,
-        assetName: 'boring.dart',
-        sources: [staticLibraryFile.toFilePath()],
-        frameworks: const [],
-        linkerOptions: linkerOptions,
-        linkModePreference: LinkModePreference.dynamic,
-      ).run(
-        input: input,
-        output: output,
-        logger: Logger('')
-          ..level = Level.ALL
-          ..onRecord.listen((record) => stdout.writeln(record.message)),
-      );
-    } catch (e, s) {
-      // Tree-shaking only makes the library smaller, so a missing or broken C
-      // toolchain should not fail the build if there is an equivalent
-      // pre-built library. This also catches errors, as native_toolchain_c
-      // throws a `ToolError`, which extends `Error` and is not exported, if it
-      // finds no toolchain.
-      stdout.writeln('boring: linking failed: $e\n$s');
-      if (!await _fallBackToPrebuiltLibrary(input, output, e)) rethrow;
-    }
+    await boringLibrary.link(input: input, output: output);
   });
 }
-
-/// Bundles the pre-built, not tree-shaken, dynamic library after linking the
-/// static library failed with [linkError].
-///
-/// Returns `false` if there is no equivalent pre-built library to fall back
-/// to.
-Future<bool> _fallBackToPrebuiltLibrary(
-  LinkInput input,
-  LinkOutputBuilder output,
-  Object linkError,
-) async {
-  final code = input.config.code;
-  final target = '${code.targetOS}_${code.targetArchitecture}';
-  final buildMode = BuildOptions.fromDefines(input.userDefines).buildMode;
-  if (buildMode != BuildModeEnum.fetch) {
-    // The static library was not fetched, but for example built from a local
-    // checkout. Falling back would silently swap in different BoringSSL code.
-    stderr.writeln(
-      'package:boring could not link the static library built in the '
-      '`${buildMode.name}` build mode for $target. Install a C toolchain '
-      '(compiler and linker) for $target. Only the `fetch` build mode falls '
-      'back to the pre-built dynamic library, which could differ from the '
-      'library built in other modes.',
-    );
-    return false;
-  }
-  final library = await fetchPrebuiltLibrary(input, static: false);
-  if (library == null) {
-    return false;
-  }
-  final reason = switch (linkError) {
-    // native_toolchain_c puts the whole linker command, which has an argument
-    // for each used symbol, into the message.
-    ProcessException(:final executable) =>
-      'ProcessException: $executable failed',
-    // The first line only, to keep the warning to one paragraph.
-    _ => linkError.toString().split('\n').first,
-  };
-  stderr.writeln(
-    'Warning: package:boring could not tree-shake its native library for '
-    '$target, so it bundles the pre-built dynamic library of the boring '
-    '$version release instead, which is not tree-shaken and therefore '
-    'larger. To enable tree-shaking, install a C toolchain (compiler and '
-    'linker) for $target. Linking failed with: $reason',
-  );
-  output.assets.code.add(
-    CodeAsset(
-      package: input.packageName,
-      name: 'boring.dart',
-      linkMode: DynamicLoadingBundled(),
-      file: library,
-    ),
-  );
-  return true;
-}
-
-/// The symbols of the bound functions that the application calls, tears off,
-/// or takes the address of with `addresses`.
-List<String> _usedSymbols(record_use.Recordings recordedUses) => [
-  for (final definition in recordedUses.calls.keys)
-    if (definition.library == _bindings) ?recordUseMapping[definition.name],
-]..sort();
-
-/// The options for linking a DLL exporting [symbols], or all bound functions
-/// if [symbols] is null.
-///
-/// Only exports the functions that the [staticLibrary] defines. BoringSSL
-/// doesn't compile some for Windows, such as
-/// `RAND_enable_fork_unsafe_buffering`, and exporting them would fail the link.
-Future<LinkerOptions> _windowsLinkerOptions(
-  LinkInput input,
-  Uri staticLibrary,
-  List<String>? symbols,
-) async {
-  final defined = _definedBindings(
-    await File.fromUri(staticLibrary).readAsBytes(),
-  );
-  final exports = symbols?.where(defined.contains).toList() ?? [...defined];
-
-  // `LinkerOptions.treeshake` passes an `/INCLUDE:<symbol>` per symbol, and
-  // Windows limits command lines to 32767 characters. Leave room for the
-  // paths and other flags.
-  final includesLength = exports.fold(0, (sum, s) => sum + s.length + 10);
-  if (symbols != null && includesLength < 24000) {
-    return LinkerOptions.treeshake(symbolsToKeep: exports);
-  }
-
-  // Link the whole archive instead, which keeps the assembly the DLL doesn't
-  // use. The objects aren't compiled with `__declspec(dllexport)`, so the
-  // module-definition file determines what the DLL exports.
-  final moduleDefinition = input.outputDirectory.resolve('bssl_dart.def');
-  await File.fromUri(moduleDefinition).writeAsString(
-    ['EXPORTS', for (final symbol in exports) '    $symbol', ''].join('\n'),
-  );
-  return LinkerOptions.manual(
-    linkerScript: moduleDefinition,
-    symbolsToKeep: null,
-  );
-}
-
-/// The symbols of the bound functions that the COFF [archive] defines.
-Set<String> _definedBindings(Uint8List archive) {
-  final defined = _archiveSymbols(archive);
-  return {
-    for (final symbol in recordUseMapping.values)
-      // x86 prefixes C symbols with an underscore, which the module-definition
-      // file omits.
-      if (defined.contains(symbol) || defined.contains('_$symbol')) symbol,
-  };
-}
-
-/// The symbols that the members of the COFF [archive] define, read from its
-/// first linker member.
-///
-/// See https://learn.microsoft.com/windows/win32/debug/pe-format#first-linker-member.
-Set<String> _archiveSymbols(Uint8List archive) {
-  const signature = '!<arch>\n';
-  const memberHeaderSize = 60;
-  const start = signature.length + memberHeaderSize;
-  if (archive.length < start + 4 ||
-      String.fromCharCodes(archive, 0, signature.length) != signature ||
-      String.fromCharCodes(archive, signature.length, 24).trimRight() != '/') {
-    throw const FormatException('Not an archive with a linker member.');
-  }
-  final count = ByteData.sublistView(archive).getUint32(start, Endian.big);
-  // The member offsets are followed by the NUL-terminated symbol names.
-  var offset = start + 4 + 4 * count;
-  if (offset > archive.length) {
-    throw const FormatException('Archive is truncated or corrupted.');
-  }
-  final symbols = <String>{};
-  for (var i = 0; i < count; i++) {
-    if (offset >= archive.length) {
-      throw const FormatException('Archive is truncated or corrupted.');
-    }
-    final end = archive.indexOf(0, offset);
-    if (end == -1) {
-      throw const FormatException('Archive symbol name is not NUL-terminated.');
-    }
-    symbols.add(String.fromCharCodes(archive, offset, end));
-    offset = end + 1;
-  }
-  return symbols;
-}
diff --git a/pkgs/boring/lib/src/hook_helpers/build_options.dart b/pkgs/boring/lib/src/hook_helpers/build_options.dart
deleted file mode 100644
index 283a015..0000000
--- a/pkgs/boring/lib/src/hook_helpers/build_options.dart
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-import 'package:hooks/hooks.dart' show HookInputUserDefines;
-
-enum BuildModeEnum { fetch, build, checkout, local }
-
-class BuildOptions {
-  final BuildModeEnum buildMode;
-  final Uri? localPath;
-
-  BuildOptions({required this.buildMode, this.localPath});
-
-  factory BuildOptions.fromDefines(HookInputUserDefines defines) {
-    final modeString = defines['buildMode'] as String? ?? 'fetch';
-    return BuildOptions(
-      buildMode: BuildModeEnum.values.firstWhere(
-        (element) => element.name == modeString,
-        orElse: () => BuildModeEnum.fetch,
-      ),
-      localPath: defines.path('localPath'),
-    );
-  }
-
-  @override
-  String toString() =>
-      'BuildOptions(buildMode: $buildMode, localPath: $localPath)';
-}
diff --git a/pkgs/boring/lib/src/hook_helpers/fetch.dart b/pkgs/boring/lib/src/hook_helpers/fetch.dart
deleted file mode 100644
index 8ddc853..0000000
--- a/pkgs/boring/lib/src/hook_helpers/fetch.dart
+++ /dev/null
@@ -1,110 +0,0 @@
-// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-import 'dart:io';
-
-import 'package:code_assets/code_assets.dart';
-import 'package:crypto/crypto.dart' show sha256;
-import 'package:hooks/hooks.dart';
-
-import 'hashes.dart' show fileHashes, version;
-import 'targets.dart' show libraryFileName, releaseAssetName;
-
-/// Downloads and verifies the pre-built library for the target of [input] from
-/// the GitHub release [version], caching it in
-/// [HookInput.outputDirectoryShared].
-///
-/// Returns `null` if no hash is registered in [fileHashes] or if downloading
-/// fails due to an HTTP or network error. Throws a [BuildError] if the
-/// downloaded file's SHA-256 checksum does not match [fileHashes].
-Future<Uri?> fetchPrebuiltLibrary(
-  HookInput input, {
-  required bool static,
-}) async {
-  final targetOS = input.config.code.targetOS;
-  final targetArch = input.config.code.targetArchitecture;
-  final iosSdk = targetOS == OS.iOS ? input.config.code.iOS.targetSdk : null;
-
-  final assetRemoteName = releaseAssetName(
-    targetOS,
-    targetArch,
-    iosSdk: iosSdk,
-    static: static,
-  );
-  final expectedHash = fileHashes[assetRemoteName];
-
-  if (expectedHash == null || expectedHash.isEmpty) {
-    stdout.writeln(
-      'boring: no prebuilt binary hash registered for $assetRemoteName.',
-    );
-    return null;
-  }
-
-  final fileName = libraryFileName(targetOS, static: static);
-  final cachedFile = File.fromUri(
-    input.outputDirectoryShared
-        .resolve('boring-$version/$assetRemoteName/')
-        .resolve(fileName),
-  );
-  if (await cachedFile.exists()) {
-    final cachedHash = sha256
-        .convert(await cachedFile.readAsBytes())
-        .toString();
-    if (cachedHash == expectedHash) {
-      stdout.writeln(
-        'boring: using cached prebuilt binary ($assetRemoteName).',
-      );
-      return cachedFile.uri;
-    }
-  }
-
-  final binaryUrl = Uri.parse(
-    'https://github.com/mosuem/boring/releases/download/v$version/$assetRemoteName',
-  );
-
-  stdout.writeln('boring: fetching prebuilt binary from $binaryUrl...');
-
-  final client = HttpClient();
-  final List<int> bytes;
-  try {
-    final request = await client.getUrl(binaryUrl);
-    final response = await request.close();
-    if (response.statusCode != 200) {
-      stdout.writeln(
-        'boring: failed to download from $binaryUrl '
-        '(status: ${response.statusCode}).',
-      );
-      await response.drain<void>();
-      return null;
-    }
-    bytes = await response.fold<List<int>>([], (a, b) => a..addAll(b));
-  } on IOException catch (e) {
-    stdout.writeln(
-      'boring: network error downloading prebuilt binary ($e).',
-    );
-    return null;
-  } finally {
-    client.close();
-  }
-
-  final actualHash = sha256.convert(bytes).toString();
-
-  if (actualHash != expectedHash) {
-    throw BuildError(
-      message:
-          'SHA256 hash mismatch for prebuilt binary $assetRemoteName.\n'
-          'Expected: $expectedHash\n'
-          'Actual:   $actualHash\n'
-          'To build boring locally from source instead, set '
-          '`buildMode: checkout` in your pubspec.yaml under '
-          '`hooks.user_defines.boring`.',
-    );
-  }
-
-  stdout.writeln('boring: verified SHA256 checksum ($actualHash).');
-
-  await cachedFile.parent.create(recursive: true);
-  await cachedFile.writeAsBytes(bytes);
-  return cachedFile.uri;
-}
diff --git a/pkgs/boring/lib/src/hook_helpers/hashes.dart b/pkgs/boring/lib/src/hook_helpers/hashes.dart
index d42ddbc..ef1a408 100644
--- a/pkgs/boring/lib/src/hook_helpers/hashes.dart
+++ b/pkgs/boring/lib/src/hook_helpers/hashes.dart
@@ -9,8 +9,7 @@
 
 const version = releaseVersion;
 
-/// Mapping from release asset name (see `releaseAssetName`) to
-/// SHA-256 hash.
+/// Mapping from release asset name to SHA-256 hash.
 const fileHashes = <String, String>{
   'boring-linux-arm64-libbssl_dart.so':
       '7a43ec9d900beac0117fb587f52fd18a08aeaadf78338ee7f5dbda06e0f0cefe',
diff --git a/pkgs/boring/lib/src/hook_helpers/library.dart b/pkgs/boring/lib/src/hook_helpers/library.dart
new file mode 100644
index 0000000..f94e7b3
--- /dev/null
+++ b/pkgs/boring/lib/src/hook_helpers/library.dart
@@ -0,0 +1,101 @@
+// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:code_assets/code_assets.dart';
+import 'package:hooks/hooks.dart';
+import 'package:native_toolchain_cmake/native_toolchain_cmake.dart';
+import 'package:prebuilt_code_assets/prebuilt_code_assets.dart';
+import 'package:record_use/record_use.dart' as record_use;
+
+import '../bindings/record_use_mapping.g.dart';
+import 'hashes.dart' show fileHashes, version;
+
+const _bindings = record_use.Library(
+  'package:boring/src/bindings/boringssl.g.dart',
+);
+
+/// Creates the [PrebuiltReleaseConfig] for a given `package:boring` release
+/// [releaseVersion].
+PrebuiltReleaseConfig boringReleaseConfig(
+  String releaseVersion, {
+  Map<String, String> hashes = fileHashes,
+}) => PrebuiltReleaseConfig.github(
+  owner: 'mosuem',
+  repo: 'boring',
+  version: releaseVersion,
+  fileHashes: hashes,
+  libraryName: 'bssl_dart',
+  staticLibraryName: 'bssl_dart_static',
+);
+
+/// Shared build and link specification for `package:boring`.
+final boringLibrary = PrebuiltLibrary(
+  name: 'bssl_dart',
+  packageName: 'boring',
+  assetName: 'boring.dart',
+  envVarPrefix: 'BORING',
+  releaseConfig: boringReleaseConfig(version),
+  buildFromSource: buildBoringFromCMake,
+  usedSymbols: SymbolsResolvers.fromRecordUseMapping(
+    _bindings,
+    recordUseMapping,
+  ),
+  allKnownSymbols: recordUseMapping.values,
+);
+
+/// Compiles BoringSSL and the `bssl_dart` wrapper from `src/CMakeLists.txt`.
+Future<Uri> buildBoringFromCMake(
+  BuildInput input,
+  BuildOutputBuilder output, {
+  required bool static,
+  Uri? checkoutPath,
+}) async {
+  final packageRoot = checkoutPath ?? input.packageRoot;
+  final installDir = input.outputDirectory.resolve('install/');
+  final sourceDir = packageRoot.resolve('src/');
+  final targetOS = input.config.code.targetOS;
+  final targetArch = input.config.code.targetArchitecture;
+
+  stdout.writeln(
+    'boring: building native asset with CMake for $targetOS-$targetArch.',
+  );
+
+  // Installs both the dynamic and the static library, see src/CMakeLists.txt.
+  final builder = CMakeBuilder.create(
+    name: 'bssl_dart',
+    sourceDir: sourceDir,
+    defines: {
+      'CMAKE_BUILD_TYPE': 'Release',
+      'CMAKE_INSTALL_PREFIX': installDir.toFilePath(),
+    },
+    targets: ['install'],
+    parallelUseAllProcessors: true,
+  );
+
+  await builder.run(input: input, output: output);
+
+  final fileName = boringReleaseConfig(version).resolveLibraryFileName(
+    targetOS,
+    static: static,
+  );
+  final library = installDir.resolve(fileName);
+  if (!File.fromUri(library).existsSync()) {
+    throw BuildError(
+      message:
+          'Failed to locate the built $fileName in '
+          '${installDir.toFilePath()}',
+    );
+  }
+
+  output.dependencies.addAll(
+    findSourceBuildDependencies(packageRoot, const [
+      'src/',
+      'third_party/boringssl/',
+    ]),
+  );
+
+  return library;
+}
diff --git a/pkgs/boring/lib/src/hook_helpers/targets.dart b/pkgs/boring/lib/src/hook_helpers/targets.dart
deleted file mode 100644
index 4287209..0000000
--- a/pkgs/boring/lib/src/hook_helpers/targets.dart
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
-// for details. All rights reserved. Use of this source code is governed by a
-// BSD-style license that can be found in the LICENSE file.
-
-import 'package:code_assets/code_assets.dart';
-
-const supportedTargets = [
-  (OS.android, Architecture.arm, null),
-  (OS.android, Architecture.arm64, null),
-  (OS.android, Architecture.ia32, null),
-  (OS.android, Architecture.riscv64, null),
-  (OS.android, Architecture.x64, null),
-  (OS.iOS, Architecture.arm64, IOSSdk.iPhoneOS),
-  (OS.iOS, Architecture.arm64, IOSSdk.iPhoneSimulator),
-  (OS.iOS, Architecture.x64, IOSSdk.iPhoneSimulator),
-  (OS.linux, Architecture.arm, null),
-  (OS.linux, Architecture.arm64, null),
-  (OS.linux, Architecture.ia32, null),
-  (OS.linux, Architecture.riscv64, null),
-  (OS.linux, Architecture.x64, null),
-  (OS.macOS, Architecture.arm64, null),
-  (OS.macOS, Architecture.x64, null),
-  (OS.windows, Architecture.arm64, null),
-  (OS.windows, Architecture.ia32, null),
-  (OS.windows, Architecture.x64, null),
-];
-
-/// Formats a canonical target identifier, including [iosSdk] when targeting iOS
-/// so device (`iphoneos`) and simulator (`iphonesimulator`) binaries do not
-/// collide on `ios-arm64`.
-String targetTripleFor(OS os, Architecture arch, {IOSSdk? iosSdk}) {
-  if (os == OS.iOS && iosSdk != null) {
-    return '${os.name}-${arch.name}-${iosSdk.type}';
-  }
-  return '${os.name}-${arch.name}';
-}
-
-/// The file name of the library built by `src/CMakeLists.txt` for [os].
-///
-/// The static library is linked into a dynamic library by `hook/link.dart`,
-/// and the dynamic library is bundled as is when linking is disabled.
-String libraryFileName(OS os, {required bool static}) => static
-    ? os.staticlibFileName('bssl_dart_static')
-    : os.dylibFileName('bssl_dart');
-
-/// The name of the GitHub release asset with the prebuilt library for [os] and
-/// [arch].
-String releaseAssetName(
-  OS os,
-  Architecture arch, {
-  IOSSdk? iosSdk,
-  required bool static,
-}) =>
-    'boring-${targetTripleFor(os, arch, iosSdk: iosSdk)}-'
-    '${libraryFileName(os, static: static)}';
diff --git a/pkgs/boring/pubspec.yaml b/pkgs/boring/pubspec.yaml
index 8a8ca79..de3786d 100644
--- a/pkgs/boring/pubspec.yaml
+++ b/pkgs/boring/pubspec.yaml
@@ -13,16 +13,13 @@
 
 dependencies:
   code_assets: ^2.1.0
-  crypto: ^3.0.6
   hooks: ^2.2.0
-  logging: ^1.3.0
   meta: ^1.19.0
-  native_toolchain_c: ^0.19.4
   native_toolchain_cmake: ^0.3.2
+  prebuilt_code_assets: ^0.1.0
   record_use: ^1.1.0
 
 dev_dependencies:
-  args: ^2.7.0
   dart_flutter_team_lints: ^3.5.2
   ffi: ^2.2.0
   ffigen: ^22.0.0
diff --git a/pkgs/boring/test/tree_shaking_test.dart b/pkgs/boring/test/tree_shaking_test.dart
index 4f85114..853c31a 100644
--- a/pkgs/boring/test/tree_shaking_test.dart
+++ b/pkgs/boring/test/tree_shaking_test.dart
@@ -11,9 +11,9 @@
 
 import 'dart:ffi';
 import 'dart:io';
-import 'dart:typed_data';
 
 import 'package:code_assets/code_assets.dart';
+import 'package:prebuilt_code_assets/testing.dart';
 import 'package:test/test.dart';
 
 const _usedFunctions = ['EVP_sha256', 'EVP_DigestFinal', 'OPENSSL_free'];
@@ -117,13 +117,12 @@
       });
 
       test('builds a linux-arm64 executable and dynamic library', () {
-        // EM_AARCH64 = 183.
-        expect(_elfMachine(executable.readAsBytesSync()), 183);
-        expect(_elfMachine(library.readAsBytesSync()), 183);
+        expect(elfMachine(executable.readAsBytesSync()), elfMachineAarch64);
+        expect(elfMachine(library.readAsBytesSync()), elfMachineAarch64);
       });
 
       test('the library only exports the functions the example uses', () {
-        final symbols = _elfDefinedDynamicSymbols(library.readAsBytesSync());
+        final symbols = elfDefinedDynamicSymbols(library.readAsBytesSync());
         for (final used in _usedFunctions) {
           expect(symbols, contains('bssl_dart_$used'), reason: used);
         }
@@ -137,7 +136,7 @@
         expect(library.lengthSync(), lessThan(1024 * 1024));
       });
     },
-    skip: Platform.environment['CI'] != 'true' && !_hasAarch64LinuxToolchain()
+    skip: Platform.environment['CI'] != 'true' && !hasAarch64LinuxToolchain()
         ? 'aarch64-linux-gnu-gcc is not installed'
         : null,
   );
@@ -148,8 +147,9 @@
     // On Linux (`x64`), we target `linux-arm64` with a stub on PATH that masks
     // `aarch64-linux-gnu-gcc` if installed.
     final targetArch = Platform.isLinux ? 'arm64' : 'x64';
-    // EM_AARCH64 = 183, EM_X86_64 = 62.
-    final expectedElfMachine = Platform.isLinux ? 183 : 62;
+    final expectedElfMachine = Platform.isLinux
+        ? elfMachineAarch64
+        : elfMachineX86_64;
 
     late Uri bundle;
     late File executable;
@@ -202,15 +202,15 @@
     });
 
     test('builds a linux-$targetArch executable and dynamic library', () {
-      expect(_elfMachine(executable.readAsBytesSync()), expectedElfMachine);
-      expect(_elfMachine(library.readAsBytesSync()), expectedElfMachine);
+      expect(elfMachine(executable.readAsBytesSync()), expectedElfMachine);
+      expect(elfMachine(library.readAsBytesSync()), expectedElfMachine);
     });
 
     test(
       'falls back to bundling the pre-built dynamic library without '
       'tree-shaking',
       () {
-        final symbols = _elfDefinedDynamicSymbols(library.readAsBytesSync());
+        final symbols = elfDefinedDynamicSymbols(library.readAsBytesSync());
         for (final used in _usedFunctions) {
           expect(symbols, contains('bssl_dart_$used'), reason: used);
         }
@@ -261,59 +261,3 @@
   );
   return appDir;
 }
-
-bool _hasAarch64LinuxToolchain() {
-  final which = Platform.isWindows ? 'where' : 'which';
-  if (Process.runSync(which, ['aarch64-linux-gnu-gcc']).exitCode == 0) {
-    return true;
-  }
-  return Platform.isMacOS &&
-      (File('/opt/homebrew/bin/aarch64-linux-gnu-gcc').existsSync() ||
-          File('/usr/local/bin/aarch64-linux-gnu-gcc').existsSync());
-}
-
-/// Returns the ELF `e_machine` field of [bytes].
-int _elfMachine(Uint8List bytes) {
-  expect(bytes.length, greaterThanOrEqualTo(64));
-  // 0x7f 'E' 'L' 'F', 64-bit (2), little-endian (1).
-  expect(bytes.sublist(0, 6), [0x7f, 0x45, 0x4c, 0x46, 2, 1]);
-  return ByteData.sublistView(bytes).getUint16(18, Endian.little);
-}
-
-/// Returns the names of all defined symbols in the `.dynsym` section of a
-/// 64-bit little-endian ELF binary.
-Set<String> _elfDefinedDynamicSymbols(Uint8List bytes) {
-  final data = ByteData.sublistView(bytes);
-  final shoff = data.getUint64(40, Endian.little);
-  final shentsize = data.getUint16(58, Endian.little);
-  final shnum = data.getUint16(60, Endian.little);
-
-  const shtDynsym = 11;
-  for (var i = 0; i < shnum; i++) {
-    final shdr = shoff + i * shentsize;
-    final shType = data.getUint32(shdr + 4, Endian.little);
-    if (shType != shtDynsym) continue;
-
-    final symOffset = data.getUint64(shdr + 24, Endian.little);
-    final symSize = data.getUint64(shdr + 32, Endian.little);
-    final strTabIndex = data.getUint32(shdr + 40, Endian.little);
-    final symEntSize = data.getUint64(shdr + 56, Endian.little);
-
-    final strShdr = shoff + strTabIndex * shentsize;
-    final strOffset = data.getUint64(strShdr + 24, Endian.little);
-
-    final symbols = <String>{};
-    final count = symSize ~/ symEntSize;
-    for (var j = 0; j < count; j++) {
-      final sym = symOffset + j * symEntSize;
-      final stName = data.getUint32(sym, Endian.little);
-      final stShndx = data.getUint16(sym + 6, Endian.little);
-      if (stName == 0 || stShndx == 0) continue;
-      final start = strOffset + stName;
-      final end = bytes.indexOf(0, start);
-      symbols.add(String.fromCharCodes(bytes, start, end));
-    }
-    return symbols;
-  }
-  throw StateError('No .dynsym section found in ELF binary.');
-}
diff --git a/pkgs/boring/tool/precompile_binaries.dart b/pkgs/boring/tool/precompile_binaries.dart
index 433fcc6..18663c2 100644
--- a/pkgs/boring/tool/precompile_binaries.dart
+++ b/pkgs/boring/tool/precompile_binaries.dart
@@ -6,131 +6,18 @@
 
 import 'dart:io';
 
-import 'package:args/args.dart';
-import 'package:boring/src/hook_helpers/targets.dart';
-import 'package:code_assets/code_assets.dart';
+import 'package:boring/src/hook_helpers/library.dart';
+import 'package:prebuilt_code_assets/tools.dart';
 
 /// Builds the dynamic and the static library for a release.
 ///
 /// hook/build.dart bundles the dynamic library when linking is disabled, and
 /// hook/link.dart links the static library into a dynamic library with only
 /// the functions an application uses when linking is enabled.
-void main(List<String> args) async {
-  final parser = ArgParser()
-    ..addOption(
-      'target-os',
-      abbr: 'o',
-      allowed: ['linux', 'macos', 'windows', 'current'],
-      defaultsTo: 'current',
-      help: 'Target OS to build for.',
-    )
-    ..addOption(
-      'target-arch',
-      abbr: 'a',
-      allowed: ['x64', 'arm64', 'arm', 'ia32', 'riscv64', 'current'],
-      defaultsTo: 'current',
-      help: 'Target architecture to build for.',
-    )
-    ..addOption(
-      'out-dir',
-      abbr: 'd',
-      defaultsTo: 'bin',
-      help: 'Output directory for built release binaries.',
-    );
-
-  ArgResults results;
-  try {
-    results = parser.parse(args);
-  } catch (e) {
-    stderr.writeln('Error parsing arguments: $e\n');
-    stderr.writeln(parser.usage);
-    exit(1);
-  }
-
-  final targetOS = results['target-os'] == 'current'
-      ? OS.current
-      : OS.values.firstWhere((o) => o.name == results['target-os']);
-
-  final targetArch = results['target-arch'] == 'current'
-      ? Architecture.current
-      : Architecture.values.firstWhere((a) => a.name == results['target-arch']);
-
-  final packageRoot = Platform.script.resolve('../');
-  final outDir = Directory.fromUri(
-    packageRoot.resolve('${results['out-dir']}/'),
+Future<void> main(List<String> args) async {
+  await runPrecompileBinariesCli(
+    args,
+    library: boringLibrary,
+    packageRoot: Platform.script.resolve('../'),
   );
-  await outDir.create(recursive: true);
-
-  final targetTriple = targetTripleFor(targetOS, targetArch);
-  stdout.writeln('==> Building BoringSSL for $targetTriple...');
-
-  final buildDir = Directory.fromUri(
-    packageRoot.resolve('build/precompile-$targetTriple/'),
-  );
-  final installDir = Directory.fromUri(buildDir.uri.resolve('install/'));
-  await buildDir.create(recursive: true);
-
-  await _run('cmake', [
-    '-S',
-    Directory.fromUri(packageRoot.resolve('src/')).path,
-    '-B',
-    buildDir.path,
-    // The default generators: Visual Studio's on Windows, like
-    // native_toolchain_cmake, which finds MSVC without a Developer Command
-    // Prompt, and Makefiles elsewhere.
-    if (targetOS == OS.windows) ...['-A', _visualStudioPlatforms[targetArch]!],
-    if (targetOS == OS.macOS)
-      '-DCMAKE_OSX_ARCHITECTURES=${_macOSArchitectures[targetArch]!}',
-    '-DCMAKE_BUILD_TYPE=Release',
-    '-DCMAKE_INSTALL_PREFIX=${installDir.path}',
-  ]);
-  // Installs both libraries into installDir, see src/CMakeLists.txt.
-  await _run('cmake', [
-    '--build',
-    buildDir.path,
-    '--config',
-    'Release',
-    '--target',
-    'install',
-    '--parallel',
-    '${Platform.numberOfProcessors}',
-  ]);
-
-  for (final static in [false, true]) {
-    final builtLibrary = File.fromUri(
-      installDir.uri.resolve(libraryFileName(targetOS, static: static)),
-    );
-    final releaseAsset = File.fromUri(
-      outDir.uri.resolve(
-        releaseAssetName(targetOS, targetArch, static: static),
-      ),
-    );
-    await builtLibrary.copy(releaseAsset.path);
-    stdout.writeln('==> Created release binary: ${releaseAsset.path}');
-  }
-}
-
-final _visualStudioPlatforms = {
-  Architecture.arm64: 'ARM64',
-  Architecture.ia32: 'Win32',
-  Architecture.x64: 'x64',
-};
-
-final _macOSArchitectures = {
-  Architecture.arm64: 'arm64',
-  Architecture.x64: 'x86_64',
-};
-
-Future<void> _run(String executable, List<String> arguments) async {
-  stdout.writeln('==> $executable ${arguments.join(' ')}');
-  final process = await Process.start(
-    executable,
-    arguments,
-    mode: ProcessStartMode.inheritStdio,
-  );
-  final exitCode = await process.exitCode;
-  if (exitCode != 0) {
-    stderr.writeln('$executable failed with exit code $exitCode');
-    exit(exitCode);
-  }
 }
diff --git a/pkgs/boring/tool/regenerate_hashes.dart b/pkgs/boring/tool/regenerate_hashes.dart
index 4c98178..981b9b4 100644
--- a/pkgs/boring/tool/regenerate_hashes.dart
+++ b/pkgs/boring/tool/regenerate_hashes.dart
@@ -4,115 +4,22 @@
 
 // coverage:ignore-file
 
-import 'dart:io';
-import 'dart:typed_data';
-
-import 'package:boring/src/hook_helpers/targets.dart';
+import 'package:boring/src/hook_helpers/library.dart';
 import 'package:boring/src/hook_helpers/version.dart';
-import 'package:crypto/crypto.dart' show sha256;
+import 'package:prebuilt_code_assets/tools.dart';
 
 /// Writes the SHA-256 hashes of the release assets built by
 /// tool/precompile_binaries.dart to lib/src/hook_helpers/hashes.dart.
 Future<void> main(List<String> args) async {
-  final version = args.isNotEmpty ? args[0] : releaseVersion;
-  final localDir = args.length > 1 ? Directory(args[1]) : null;
-  final httpClient = localDir == null ? HttpClient() : null;
-
-  stdout.writeln('Checking hashes for version $version...');
-  final fileHashes = <String, String>{};
-
-  for (final (os, arch, iosSdk) in supportedTargets) {
-    for (final static in [false, true]) {
-      final assetName = releaseAssetName(
-        os,
-        arch,
-        iosSdk: iosSdk,
-        static: static,
-      );
-      if (localDir != null) {
-        final file = File('${localDir.path}/$assetName');
-        if (!await file.exists()) {
-          stdout.writeln('  Skipping missing local file: ${file.path}');
-          continue;
-        }
-        final fileHash = sha256.convert(await file.readAsBytes()).toString();
-        fileHashes[assetName] = fileHash;
-        stdout.writeln('  $assetName: $fileHash');
-        continue;
-      }
-
-      final uri = Uri.parse(
-        'https://github.com/mosuem/boring/releases/download/v$version/$assetName',
-      );
-
-      stdout.writeln('Fetching from $uri...');
-      try {
-        final request = await httpClient!.getUrl(uri);
-        final response = await request.close();
-        if (response.statusCode != 200) {
-          stdout.writeln('  Skipping: status ${response.statusCode}');
-          await response.drain<void>();
-          continue;
-        }
-        final builder = BytesBuilder(copy: false);
-        await response.forEach(builder.add);
-        final fileHash = sha256.convert(builder.takeBytes()).toString();
-        fileHashes[assetName] = fileHash;
-        stdout.writeln('  $assetName: $fileHash');
-      } catch (e) {
-        stdout.writeln('  Error fetching $uri: $e');
-      }
-    }
-  }
-  httpClient?.close(force: true);
-
-  await File('lib/src/hook_helpers/version.dart').writeAsString(
-    '// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS '
-    'file\n'
-    '// for details. All rights reserved. Use of this source code is governed '
-    'by a\n'
-    '// BSD-style license that can be found in the LICENSE file.\n'
-    '\n'
-    "const releaseVersion = '$version';\n",
-  );
-
-  final buffer = StringBuffer()
-    ..writeln(
-      '// Copyright (c) 2026, the Dart project authors. Please see the AUTHORS '
-      'file',
-    )
-    ..writeln(
-      '// for details. All rights reserved. Use of this source code is '
-      'governed by a',
-    )
-    ..writeln('// BSD-style license that can be found in the LICENSE file.')
-    ..writeln()
-    ..writeln('// coverage:ignore-file')
-    ..writeln('// THIS FILE IS GENERATED BY `tool/regenerate_hashes.dart`.')
-    ..writeln()
-    ..writeln("import 'version.dart';")
-    ..writeln()
-    ..writeln('const version = releaseVersion;')
-    ..writeln()
-    ..writeln('/// Mapping from release asset name (see `releaseAssetName`) to')
-    ..writeln('/// SHA-256 hash.')
-    ..writeln('const fileHashes = <String, String>{');
-
-  for (final entry in fileHashes.entries) {
-    buffer.writeln("  '${entry.key}':");
-    buffer.writeln("      '${entry.value}',");
-  }
-  buffer.writeln('};');
-
-  await File(
-    'lib/src/hook_helpers/hashes.dart',
-  ).writeAsString(buffer.toString());
-  await Process.run(Platform.resolvedExecutable, [
-    'format',
-    'lib/src/hook_helpers/version.dart',
-    'lib/src/hook_helpers/hashes.dart',
-  ]);
-  stdout.writeln(
-    'Updated lib/src/hook_helpers/hashes.dart with ${fileHashes.length} hashes.',
+  await runRegenerateHashesCli(
+    args,
+    defaultVersion: releaseVersion,
+    releaseConfigForVersion: boringReleaseConfig,
+    licenseHeader:
+        '// Copyright (c) 2026, the Dart project authors. Please see the '
+        'AUTHORS file\n'
+        '// for details. All rights reserved. Use of this source code is '
+        'governed by a\n'
+        '// BSD-style license that can be found in the LICENSE file.\n',
   );
 }