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.

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:

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