Hooks
Run custom build scripts.
This guide explains what hooks are and how to use them with a package.
Introduction
#You can currently use hooks to do things such as compile or download native assets (code written in other languages that are compiled into machine code), and then call these assets from the Dart code of a package.
Hooks are Dart scripts placed in the
hook/ directory of your Dart package. They have
a predefined format for their input and output, which allows
the Dart SDK to:
- Discover the hooks.
- Execute the hooks with the necessary input.
- Consume the output produced by the hooks.
Example project with a build hook:
-
example_project/// Project with hooks.
-
hook/// Add hook scripts here.
- build.dart
-
lib/ // Use your assets here.
- example.dart
-
src/ // Add native sources here.
- example_native_library.c
- example_native_library.h
-
test/// Test your assets here.
- example_test.dart
-
Hooks
#Currently, build hooks and link hooks are available. To learn more, see the following.
Build hooks
#With build hooks, a package can do things such as compile or download native assets such as C or Rust libraries. Afterwards, these assets can be called from the Dart code of a package.
A package's build hook is automatically invoked by the Dart SDK at an appropriate time during the build process. Build hooks are run in parallel with Dart compilation and might do longer running operations such as downloading or calling a native compiler.
Use the
build
function to parse the hook input with
BuildInput
and then write the hook output with
BuildOutputBuilder. The hook should place downloaded and generated assets in
BuildInput.sharedOutputDirectory.
The assets produced for your package might depend on
assets
or
metadata
produced by the build hooks from the packages in the direct
dependencies in the pubspec. Therefore, build hooks are run
in the order of dependencies in the pubspec, and cyclic
dependencies between packages are not supported when using
hooks.
Link hooks
#With link hooks, a package can filter, optimize, or tree-shake code assets generated by build hooks before bundling them into an application.
The Dart SDK automatically invokes a package's link hook
(hook/link.dart) during the application
bundling phase. Unlike build hooks, which run for each
package, link hooks run with application-level context so
they can inspect which symbols the application actually
uses.
Use the
link
function to parse the hook input with
LinkInput
and write the hook output with
LinkOutputBuilder. If tree-shaking removes all symbols for an asset, the
link hook can omit the asset entirely to avoid bundling it
with the application.
Environment variables
#
Hooks are executed in a semi-hermetic environment. This
means that Platform.environment doesn't expose
all environment variables from the parent process. This
ensures that hook invocations are reproducible and
cacheable, and don't 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: Forvswhere.exeon 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.
-
-
Nix:
- Any variable starting with
NIX_.
- Any variable starting with
Any changes to these environment variables cause cache invalidation for hooks.
All other environment variables are stripped.
Assets
#
Assets are the files that are produced by a hook and then
bundled in a Dart application. Assets can be accessed at run
time from the Dart code. Currently, the Dart SDK can use the
CodeAsset type, but more asset types are
planned. To learn more, see the following.
CodeAsset type
#
A
CodeAsset
represents a code asset. A code asset is a dynamic library
compiled from a language other than Dart, such as C, C++,
Rust, or Go.
CodeAsset is part of the
code_asset package. APIs provided by code
assets are accessed at run time through corresponding
external Dart members annotated with the
@Native
annotation from dart:ffi.
Use a hook
#To add assets to your project, use a hook. For details, see the following sections.
Add dependencies
#
To use a hook, you must first add the helper packages
hooks and code_assets to your
pubspec.yaml
dependencies:
dart pub add hooks code_assets.
If you need to compile C sources, you'll also need package
native_toolchain_c:
dart pub add native_toolchain_c.
Example dependencies for a build hook:
name: native_add_library
description: Sums two numbers with native code.
version: 0.1.0
environment:
sdk: '^3.10.0'
dependencies:
# ...
code_assets: any
hooks: any
native_toolchain_c: any
dev_dependencies:
# ...
ffigen: ^18.0.0
Create a build hook to generate native assets
#
If you want to use a build hook to transparently compile
native assets (such as C or Rust libraries), which are then
made available to be called from the Dart code of a package,
create a build.dart script similar to the
following:
-
In your Dart project, create or open
hook/build.dart. -
In the
mainmethod, call thebuildfunction frompackage:hooks/hooks.dartand use the appropriate toolchain to compile the native library. For example:hook/build.dartdartimport '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 cLibrary = CLibrary( name: packageName, assetName: '$packageName.dart', sources: ['src/$packageName.c'], ); await cLibrary.build( input: input, output: output, ); }); }The second parameter of
buildexpects a function that it will pass two arguments to:-
input: The read-only input for the hook. Includes information for the hook to produce the right asset type (for example, target OS, target architecture, output directory, and more). For details, see theBuildInputclass. -
output: The write-only builder for the hook output. After the build hook reads the input, it produces an asset and then provides what it produced as the output. For details, see theBuildOutputBuilderclass.
-
Create a link hook to tree-shake native assets
#
If your package generates native assets with a build hook
(hook/build.dart), you can add a link hook
(hook/link.dart) to tree-shake unused code
assets before bundling.
During compilation, the Dart compiler records which
@Native symbols are actually referenced by the
application and provides them to link hooks through
LinkInput.recordedUses. If input.recordedUses is null,
the link hook disables tree-shaking and keeps all symbols.
-
In your Dart project, create or open
hook/link.dart. -
In the
mainmethod, call thelinkfunction frompackage:hooks/hooks.dartand pass tree-shaking options toCLibrary.link. For example:hook/link.dartdartimport 'package:hooks/hooks.dart'; import 'package:native_toolchain_c/native_toolchain_c.dart'; import 'package:record_use/record_use.dart'; import 'record_use_mapping.dart'; void main(List<String> args) async { await link(args, (input, output) async { final packageName = input.packageName; final cLibrary = CLibrary( name: packageName, assetName: '$packageName.dart', sources: ['src/$packageName.c'], ); final linkerOptions = LinkerOptions.treeshake( symbolsToKeep: input.recordedUses?.calls.keys .cast<Method>() .map((e) => recordUseMapping[e.name]!), ); await cLibrary.link( input: input, output: output, linkerOptions: linkerOptions, ); }); }
Dart symbols in bindings generated by tools such as
ffigen don't always map one-to-one to native C
symbol names. When generating FFI bindings,
ffigen can automatically generate a mapping
(recordUseMapping) from Dart method identifiers
to native symbols. Map each recorded Dart method call
(input.recordedUses?.calls.keys) using
recordUseMapping to pass the corresponding
native symbols to LinkerOptions.treeshake.
When symbolsToKeep is null (for
example, when input.recordedUses
is
null),
LinkerOptions.treeshake preserves all symbols
in the native library. When symbolsToKeep is an
empty list ([]), meaning the application
doesn't reference any symbols from the library,
CLibrary.link automatically skips compiling and
bundling the dynamic library entirely.
Automatically bundled assets
#
The hooks are run automatically when invoking the
run, build, or test
commands. The resulting assets are stored in the output
directory specified in the hook input. The Dart SDK then
automatically bundles those assets with your Dart app so
that they can be accessed at run time.
Use assets
#
Assets are the files that hooks create. Once an asset is
created, you can reference it in your code and at run time
with its asset ID (assetId). Asset IDs are structured as
package:<package-name>/<asset-name>. Build hooks can only output assets in their own package.
CLibrary in the build hook in the previous
example outputs the asset ID
package:native_add_library/native_add_library.dart, and is based on the packageName and
assetName.
The following example illustrates how to bind to the native
C function add from
native_add_library.c and call it:
import 'dart:ffi';
@Native<Int32 Function(Int32, Int32)>()
external int add(int a, int b);
import 'package:my_package/my_package.dart';
void main() {
print(add(24, 18));
}
The asset ID in @Native is optional and
defaults to the library URI. In the previous example, this
is
package:native_add_library/native_add_library.dart, which is the same asset ID as output from the build hook.
This enables Dart to connect an asset referenced at run time
to the one provided by the hook during the build process.
Test assets
#After you've written a hook that generates an asset and you've used that asset in your Dart code, consider writing a test to verify that the hook and the generated asset works as expected.
In the following example, a test is created for
native_add_library.dart, a script that
references a native C function called add:
import 'package:native_add_library/native_add_library.dart';
import 'package:test/test.dart';
void main() {
test('invoke native function', () {
expect(add(24, 18), 42);
});
}
Hook configuration
#
Pass custom parameters or local file paths to build and link
hooks from your project's build environment. Configure these
parameters under the hooks key in your
pubspec.yaml file. Currently, this block
supports only the user_defines option.
Configure user-defines
#
Configure custom parameters inside the root package
pubspec.yaml file, or in the workspace
pubspec.yaml file if you use a workspace. Only
end-users—the authors of the root app or package consuming
the dependencies—can configure user-defines. Dependencies
can't supply their own default user-defines.
The configured values under user_defines can be
any JSON-compatible type, such as booleans, strings,
numbers, nested maps, or lists. User-defines are filtered
per package. A hook inside my_package can only
access keys configured under
hooks.user_defines.my_package. It can't access
the user-defines of other packages.
The following is an example of passing two user-defines to
the hooks of the my_package package:
hooks:
user_defines:
my_package:
enable_experimental: true
custom_lib: assets/libnative.so
Access user-defines in a hook
#
To access configured user-defines in your
build.dart or link.dart hook
script, use the input.userDefines object:
-
Read raw values using the bracket operator, such as
input.userDefines['key']. -
Resolve relative paths to a
Uriusing thepath()method, such asinput.userDefines.path('key'). This resolves the relative path against the directory of thepubspec.yamlwhere the user-defines are declared.
If the hook reads a resolved file or directory, register it
as a dependency using
output.dependencies.add(). This ensures that
the build system invalidates the cache and re-runs the hook
when the file changes.
The following is an example hook script that accesses the
enable_experimental and
custom_lib user-defines:
import 'dart:io';
import 'package:hooks/hooks.dart';
void main(List<String> args) async {
await build(args, (input, output) async {
final experimental = input.userDefines['enable_experimental'];
if (experimental is! bool?) {
throw const FormatException(
'hooks.user_defines.my_package.enable_experimental must be a '
'boolean (or omitted)',
);
}
if (experimental ?? false) {
print('Experimental features enabled.');
}
final customLibUri = input.userDefines.path('custom_lib');
if (customLibUri != null) {
final file = File.fromUri(customLibUri);
output.dependencies.add(file.uri);
// Use the file...
}
});
}
Example projects
#There are several example projects to help you get started with hooks and code assets:
| Project | Description |
|---|---|
sqlite
|
A package compiling, bundling, tree-shaking, and using a native database engine. |
mini_audio
|
A package compiling, bundling, tree-shaking, and using a native audio player. |
stb_image
|
A package compiling, bundling, tree-shaking, and using a native image library. |
host_name
|
A package using a native system library. |
native_add_library
|
A package compiling, bundling, and using some simple C code. |
native_add_app
|
A Dart CLI application that depends on
native_add_library.
|
download_asset
|
A package bundling and using prebuilt assets that are downloaded in the build hook. |
native_dynamic_linking
|
A package compiling, bundling, and using three native libraries that depend on each other. |
use_dart_api
|
A package that uses the C API of the Dart VM. |
More information
#See the following links for more information: