link function

Future<void> link(
  1. List<String> arguments,
  2. Future<void> linker(
    1. LinkInput input,
    2. LinkOutputBuilder output
    )
)

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.

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.

Custom Configurations (User-Defines)

Link 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

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.
    • APPDATA, LOCALAPPDATA: NuGet, dart_data_home, and pub on Windows.
    • SYSTEMDRIVE, SYSTEMROOT, WINDIR: Process invocations and CMake on Windows.
    • PROGRAMDATA: For vswhere.exe on 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'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_.
  • .NET and NuGet:
    • Any variable starting with DOTNET_.
    • Any variable starting with NUGET_.

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 LinkInput.config and the user-defines in the workspace pubspec.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.json didn'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 LinkInput.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 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:

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:

{
  "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.

Implementation

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);
}