[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', ); }