build function
- List<
String> arguments, - Future<
void> builder(- BuildInput input,
- BuildOutputBuilder output
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:
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:
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(),
),
);
});
}
User-defines
Build hooks can read custom, package-specific configuration settings passed
by the end-user from the root package pubspec.yaml (or the root package
pub workspace pubspec.yaml if using a workspace) via the
input.userDefines property.
See HookInput.userDefines for detailed documentation, configuration schema, and code snippets.
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.APPDATA,LOCALAPPDATA: NuGet, dart_data_home, and pub on Windows.SYSTEMDRIVE,SYSTEMROOT,WINDIR: Process invocations and CMake on Windows.PROGRAMDATA: Forvswhere.exeon Windows.PROCESSOR_ARCHITECTURE: CMake Android 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'sbindgen+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_.
- Any variable starting with
- Nix:
- Any variable starting with
NIX_.
- Any variable starting with
- .NET and NuGet:
- Any variable starting with
DOTNET_. - Any variable starting with
NUGET_.
- Any variable starting with
Any changes to these environment variables will cause cache invalidation for hooks.
All other environment variables are stripped.
Caching
Hook execution is automatically cached by the hooks runner to avoid unnecessary runs.
An execution of this hook is skipped, and the cached output is reused, if and only if:
- The input to the hook didn't change (including the configuration fields
accessed via BuildInput.config and the
user-definesin the workspacepubspec.yaml). - No environment variables (that are not filtered out) changed.
- None of the files or directories declared in HookOutputBuilder.dependencies changed.
- None of the transitive Dart sources of the hook script itself changed.
- The workspace
package_config.jsondidn't change. - The Dart SDK version didn't change.
If any of these conditions are not met, the hook is re-run.
Hook Output Dependencies
To ensure cache correctness when external files or assets are modified,
hooks must explicitly declare their file and directory dependencies using
HookOutputBuilder.dependencies (e.g., via
output.dependencies.add(uri)).
If your hook resolves and reads local files referenced in user-defines (e.g.
using input.userDefines.path('key')), you must manually register those
files in HookOutputBuilder.dependencies to ensure the hook is re-run
when the referenced files' contents change.
Cache Isolation
Outputs are cached in a configuration-specific subdirectory inside
.dart_tool/hooks_runner/. This directory is unique per hook and is
derived from the configuration fields in BuildInput.config. Therefore,
different configurations (e.g., building for a different target OS or
architecture) do not collide.
The cache is reused for identical configurations across different builds, even when inputs outside the configuration or environment variables change.
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:
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:
{
"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.
Implementation
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);
}