| // Copyright (c) 2024, 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:convert'; |
| import 'dart:io'; |
| |
| import '../args_parser.dart'; |
| import '../config.dart'; |
| import '../validation.dart'; |
| |
| /// Builds assets in a `hook/build.dart`. |
| /// |
| /// If a build hook is defined (`hook/build.dart`) then `build` must be called |
| /// by that hook, to write the [BuildInput.outputFile], even if the [builder] |
| /// function has no work to do. |
| /// |
| /// Can build native assets which are not already available, or expose existing |
| /// files. Each individual asset is assigned a unique asset ID. |
| /// |
| /// Example using `package:native_toolchain_c`: |
| /// |
| /// <!-- file://./../../../example/api/build_snippet_1.dart --> |
| /// ```dart |
| /// import 'package:hooks/hooks.dart'; |
| /// import 'package:native_toolchain_c/native_toolchain_c.dart'; |
| /// |
| /// void main(List<String> args) async { |
| /// await build(args, (input, output) async { |
| /// final packageName = input.packageName; |
| /// final cbuilder = CBuilder.library( |
| /// name: packageName, |
| /// assetName: '$packageName.dart', |
| /// sources: ['src/$packageName.c'], |
| /// ); |
| /// await cbuilder.run(input: input, output: output); |
| /// }); |
| /// } |
| /// ``` |
| /// |
| /// Example outputting assets manually: |
| /// |
| /// <!-- file://./../../../example/api/build_snippet_2.dart --> |
| /// ```dart |
| /// import 'dart:io'; |
| /// |
| /// import 'package:code_assets/code_assets.dart'; |
| /// import 'package:hooks/hooks.dart'; |
| /// |
| /// const assetName = 'asset.txt'; |
| /// final packageAssetPath = Uri.file('data/$assetName'); |
| /// |
| /// void main(List<String> args) async { |
| /// await build(args, (input, output) async { |
| /// if (input.config.code.linkModePreference == .static) { |
| /// // Simulate that this hook only supports dynamic libraries. |
| /// throw UnsupportedError('LinkModePreference.static is not supported.'); |
| /// } |
| /// |
| /// final packageName = input.packageName; |
| /// final assetPath = input.outputDirectory.resolve(assetName); |
| /// final assetSourcePath = input.packageRoot.resolveUri(packageAssetPath); |
| /// // Insert code that downloads or builds the asset to `assetPath`. |
| /// await File.fromUri(assetSourcePath).copy(assetPath.toFilePath()); |
| /// |
| /// output.dependencies.add(assetSourcePath); |
| /// |
| /// output.assets.code.add( |
| /// // TODO: Change to DataAsset once the Dart/Flutter SDK can consume it. |
| /// CodeAsset( |
| /// package: packageName, |
| /// name: 'asset.txt', |
| /// file: assetPath, |
| /// linkMode: DynamicLoadingBundled(), |
| /// ), |
| /// ); |
| /// }); |
| /// } |
| /// ``` |
| /// |
| /// ## Environment |
| /// |
| /// Build hooks are executed in a semi-hermetic environment. This means that |
| /// `Platform.environment` does not expose all environment variables from the |
| /// parent process. This ensures that hook invocations are reproducible and |
| /// cacheable, and do not depend on accidental environment variables. |
| /// |
| /// However, some environment variables are necessary for locating tools (like |
| /// compilers) or configuring network access. The following environment |
| /// variables are passed through to the hook process: |
| /// |
| /// * **Path and system roots:** |
| /// * `PATH`: Invoke native tools. |
| /// * `HOME`, `USERPROFILE`: Find tools in default install locations. |
| /// * `SYSTEMDRIVE`, `SYSTEMROOT`, `WINDIR`: Process invocations and CMake |
| /// on Windows. |
| /// * `PROGRAMDATA`: For `vswhere.exe` on Windows. |
| /// * **Temporary directories:** |
| /// * `TEMP`, `TMP`, `TMPDIR`: Temporary directories. |
| /// * **HTTP proxies:** |
| /// * `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`: Network access behind |
| /// proxies. |
| /// * **Clang/LLVM:** |
| /// * `LIBCLANG_PATH`: Rust's `bindgen` + `clang-sys`. |
| /// * **Android NDK:** |
| /// * `ANDROID_HOME`: Standard location for the Android SDK/NDK. |
| /// * `ANDROID_NDK`, `ANDROID_NDK_HOME`, `ANDROID_NDK_LATEST_HOME`, |
| /// `ANDROID_NDK_ROOT`: Alternative locations for the NDK. |
| /// * **Ccache:** |
| /// * Any variable starting with `CCACHE_`. |
| /// * **Nix:** |
| /// * Any variable starting with `NIX_`. |
| /// |
| /// Any changes to these environment variables will cause cache invalidation for |
| /// hooks. |
| /// |
| /// All other environment variables are stripped. |
| /// |
| /// ## Debugging |
| /// |
| /// When a build hook doesn't work as expected, you can investigate the |
| /// intermediate files generated by the Dart and Flutter SDK build process. |
| /// |
| /// The most important files for debugging are located in a subdirectory |
| /// specific to your hook's execution. The path is of the form |
| /// `.dart_tool/hooks_runner/<package_name>/<some_hash>/`, where |
| /// `<package_name>` is the name of the package containing the hook. Inside, you |
| /// will find: |
| /// |
| /// * `input.json`: The configuration and data passed into your build hook. |
| /// * `output.json`: The JSON data that your build hook produced. |
| /// * `stdout.txt`: Any standard output from your build hook. |
| /// * `stderr.txt`: Any error messages or exceptions. |
| /// |
| /// When you run a build, hooks for all dependencies are executed, so you might |
| /// see multiple package directories. |
| /// |
| /// The `<some_hash>` is a checksum of the [BuildConfig] in the `input.json`. If |
| /// you are unsure which hash directory to inspect within your package's hook |
| /// directory, you can delete the `.dart_tool/hooks_runner/<package_name>/` |
| /// directory and re-run the command that failed. The newly created directory |
| /// will be for the latest invocation. |
| /// |
| /// You can step through your code with a debugger by running the build hook |
| /// from its source file and providing the `input.json` via the `--config` flag: |
| /// |
| /// ```sh |
| /// dart run hook/build.dart --config .dart_tool/hooks_runner/<package_name>/<some_hash>/input.json |
| /// ``` |
| /// |
| /// To debug in VS Code, you can create a `launch.json` file in a `.vscode` |
| /// directory in your project root. This allows you to run your hook with a |
| /// debugger attached. |
| /// |
| /// Here is an example configuration: |
| /// |
| /// ```json |
| /// { |
| /// "version": "0.2.0", |
| /// "configurations": [ |
| /// { |
| /// "name": "Debug Build Hook", |
| /// "type": "dart", |
| /// "request": "launch", |
| /// "program": "hook/build.dart", |
| /// "args": [ |
| /// "--config", |
| /// ".dart_tool/hooks_runner/your_package_name/some_hash/input.json" |
| /// ] |
| /// } |
| /// ] |
| /// } |
| /// ``` |
| /// |
| /// Again, make sure to replace `your_package_name`, and `some_hash` with the |
| /// actual paths from your project. After setting this up, you can run the |
| /// "Debug Build Hook" configuration from the "Run and Debug" view in VS Code. |
| Future<void> build( |
| List<String> arguments, |
| Future<void> Function(BuildInput input, BuildOutputBuilder output) builder, |
| ) async { |
| final inputPath = getInputArgument(arguments); |
| final bytes = File(inputPath).readAsBytesSync(); |
| final jsonInput = |
| const Utf8Decoder().fuse(const JsonDecoder()).convert(bytes) |
| as Map<String, Object?>; |
| final input = BuildInput(jsonInput); |
| final outputFile = input.outputFile; |
| final output = BuildOutputBuilder(); |
| try { |
| await builder(input, output); |
| // ignore: avoid_catching_errors |
| } on HookError catch (e, st) { |
| output.setFailure(e.failureType); |
| await _writeOutput(output, outputFile); |
| _exitViaHookException(e, st); |
| } |
| final errors = await ProtocolBase.validateBuildOutput( |
| input, |
| BuildOutput(output.json), |
| ); |
| if (errors.isNotEmpty) { |
| final message = [ |
| 'The output contained unsupported output:', |
| for (final error in errors) '- $error', |
| ].join('\n'); |
| stderr.writeln(message); |
| output.setFailure(.build); |
| await _writeOutput(output, outputFile); |
| exit(BuildError(message: message).exitCode); |
| } |
| |
| await _writeOutput(output, outputFile); |
| } |
| |
| /// Links assets in a `hook/link.dart`. |
| /// |
| /// If a link hook is defined (`hook/link.dart`) then `link` must be called by |
| /// that hook, to write the [BuildInput.outputFile], even if the [linker] |
| /// function has no work to do. |
| /// |
| /// Can link native assets which are not already available, or expose existing |
| /// files. Each individual asset is assigned a unique asset ID. |
| /// |
| /// The linking script may receive assets from build scripts, which are accessed |
| /// through [LinkInputAssets.encodedAssets]. They will only be bundled with the |
| /// final application if included in the [LinkOutput]. |
| /// |
| /// |
| /// <!-- file://./../../../example/api/link_snippet.dart --> |
| /// ```dart |
| /// import 'package:data_assets/data_assets.dart'; |
| /// import 'package:hooks/hooks.dart'; |
| /// |
| /// void main(List<String> args) async { |
| /// await link(args, (input, output) async { |
| /// final dataEncodedAssets = input.assets.encodedAssets.where( |
| /// (e) => e.isDataAsset, |
| /// ); |
| /// output.assets.addEncodedAssets(dataEncodedAssets); |
| /// }); |
| /// } |
| /// ``` |
| /// If the [linker] fails, it must `throw` a [HookError]. Link hooks are |
| /// guaranteed to be invoked with a process invocation and should return a |
| /// non-zero exit code on failure. Throwing will lead to an uncaught exception, |
| /// causing a non-zero exit code. |
| /// |
| /// ## Environment |
| /// |
| /// Link hooks are executed in a semi-hermetic environment. This means that |
| /// `Platform.environment` does not expose all environment variables from the |
| /// parent process. This ensures that hook invocations are reproducible and |
| /// cacheable, and do not depend on accidental environment variables. |
| /// |
| /// However, some environment variables are necessary for locating tools (like |
| /// compilers) or configuring network access. The following environment |
| /// variables are passed through to the hook process: |
| /// |
| /// * **Path and system roots:** |
| /// * `PATH`: Invoke native tools. |
| /// * `HOME`, `USERPROFILE`: Find tools in default install locations. |
| /// * `SYSTEMDRIVE`, `SYSTEMROOT`, `WINDIR`: Process invocations and CMake |
| /// on Windows. |
| /// * `PROGRAMDATA`: For `vswhere.exe` on Windows. |
| /// * **Temporary directories:** |
| /// * `TEMP`, `TMP`, `TMPDIR`: Temporary directories. |
| /// * **HTTP proxies:** |
| /// * `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`: Network access behind |
| /// proxies. |
| /// * **Clang/LLVM:** |
| /// * `LIBCLANG_PATH`: Rust's `bindgen` + `clang-sys`. |
| /// * **Android NDK:** |
| /// * `ANDROID_HOME`: Standard location for the Android SDK/NDK. |
| /// * `ANDROID_NDK`, `ANDROID_NDK_HOME`, `ANDROID_NDK_LATEST_HOME`, |
| /// `ANDROID_NDK_ROOT`: Alternative locations for the NDK. |
| /// * **Ccache:** |
| /// * Any variable starting with `CCACHE_`. |
| /// * **Nix:** |
| /// * Any variable starting with `NIX_`. |
| /// |
| /// Any changes to these environment variables will cause cache invalidation for |
| /// hooks. |
| /// |
| /// All other environment variables are stripped. |
| /// |
| /// ## Debugging |
| /// |
| /// When a link hook doesn't work as expected, you can investigate the |
| /// intermediate files generated by the Dart and Flutter SDK build process. |
| /// |
| /// The most important files for debugging are located in a subdirectory |
| /// specific to your hook's execution. The path is of the form |
| /// `.dart_tool/hooks_runner/<package_name>/<some_hash>/`, where |
| /// `<package_name>` is the name of the package containing the hook. Inside, you |
| /// will find: |
| /// |
| /// * `input.json`: The configuration and data passed into your link hook. |
| /// * `output.json`: The JSON data that your link hook produced. |
| /// * `stdout.txt`: Any standard output from your link hook. |
| /// * `stderr.txt`: Any error messages or exceptions. |
| /// |
| /// When you run a build, hooks for all dependencies are executed, so you might |
| /// see multiple package directories. |
| /// |
| /// The `<some_hash>` is a checksum of the [LinkConfig] in the `input.json`. If |
| /// you are unsure which hash directory to inspect within your package's hook |
| /// directory, you can delete the `.dart_tool/hooks_runner/<package_name>/` |
| /// directory and re-run the command that failed. The newly created directory |
| /// will be for the latest invocation. |
| /// |
| /// You can step through your code with a debugger by running the link hook from |
| /// its source file and providing the `input.json` via the `--config` flag: |
| /// |
| /// ```sh |
| /// dart run hook/link.dart --config .dart_tool/hooks_runner/<package_name>/<some_hash>/input.json |
| /// ``` |
| /// |
| /// To debug in VS Code, you can create a `launch.json` file in a `.vscode` |
| /// directory in your project root. This allows you to run your hook with a |
| /// debugger attached. |
| /// |
| /// Here is an example configuration: |
| /// |
| /// ```json |
| /// { |
| /// "version": "0.2.0", |
| /// "configurations": [ |
| /// { |
| /// "name": "Debug Link Hook", |
| /// "type": "dart", |
| /// "request": "launch", |
| /// "program": "hook/link.dart", |
| /// "args": [ |
| /// "--config", |
| /// ".dart_tool/hooks_runner/your_package_name/some_hash/input.json" |
| /// ] |
| /// } |
| /// ] |
| /// } |
| /// ``` |
| /// |
| /// Again, make sure to replace `your_package_name`, and `some_hash` with the |
| /// actual paths from your project. After setting this up, you can run the |
| /// "Debug Link Hook" configuration from the "Run and Debug" view in VS Code. |
| Future<void> link( |
| List<String> arguments, |
| Future<void> Function(LinkInput input, LinkOutputBuilder output) linker, |
| ) async { |
| final inputPath = getInputArgument(arguments); |
| final bytes = File(inputPath).readAsBytesSync(); |
| final jsonInput = |
| const Utf8Decoder().fuse(const JsonDecoder()).convert(bytes) |
| as Map<String, Object?>; |
| final input = LinkInput(jsonInput); |
| final outputFile = input.outputFile; |
| final output = LinkOutputBuilder(); |
| try { |
| await linker(input, output); |
| // ignore: avoid_catching_errors |
| } on HookError catch (e, st) { |
| output.setFailure(e.failureType); |
| await _writeOutput(output, outputFile); |
| _exitViaHookException(e, st); |
| } |
| final errors = await ProtocolBase.validateLinkOutput( |
| input, |
| LinkOutput(output.json), |
| ); |
| if (errors.isNotEmpty) { |
| final message = [ |
| 'The output contained unsupported output:', |
| for (final error in errors) '- $error', |
| ].join('\n'); |
| stderr.writeln(message); |
| output.setFailure(.build); |
| await _writeOutput(output, outputFile); |
| exit(BuildError(message: message).exitCode); |
| } |
| |
| await _writeOutput(output, outputFile); |
| } |
| |
| Future<void> _writeOutput(HookOutputBuilder output, Uri outputFile) async { |
| final jsonOutput = const JsonEncoder.withIndent( |
| ' ', |
| ).fuse(const Utf8Encoder()).convert(output.json); |
| await File.fromUri(outputFile).writeAsBytes(jsonOutput); |
| } |
| |
| Never _exitViaHookException(HookError exception, StackTrace stackTrace) { |
| stderr.writeln(exception.message); |
| stderr.writeln(stackTrace); |
| if (exception.wrappedException != null) { |
| stderr.writeln('Wrapped exception:'); |
| stderr.writeln(exception.wrappedException); |
| stderr.writeln(exception.wrappedTrace); |
| } |
| exit(exception.exitCode); |
| } |