[hooks] Document user-defines in API (#3407)
diff --git a/pkgs/hooks/CHANGELOG.md b/pkgs/hooks/CHANGELOG.md
index f3cf4fe..ebd0ee3 100644
--- a/pkgs/hooks/CHANGELOG.md
+++ b/pkgs/hooks/CHANGELOG.md
@@ -1,3 +1,7 @@
+## 2.0.2
+
+- Update documentation for user-defines.
+
## 2.0.1
- Updated documentation for hook caching behavior and semi-hermetic environment variables.
diff --git a/pkgs/hooks/README.md b/pkgs/hooks/README.md
index 47bb64d..4305ded 100644
--- a/pkgs/hooks/README.md
+++ b/pkgs/hooks/README.md
@@ -83,6 +83,57 @@
For more information see [dart.dev/tools/hooks](https://dart.dev/tools/hooks).
+## User-defines
+
+Because build hooks execute in a semi-hermetic environment where most environment variables are stripped for reproducibility and caching purposes, you should use **user-defines** to pass custom configurations, flags, or paths to your hooks from `pubspec.yaml`.
+
+### 1. Define in `pubspec.yaml`
+In your package (or workspace root `pubspec.yaml` if using workspaces), add the `hooks.user_defines` section:
+
+```yaml
+hooks:
+ user_defines:
+ my_package_name:
+ enable_debug_logging: true
+ custom_asset: assets/data.json
+```
+
+> [!IMPORTANT]
+> **Workspace Scope:** If a project is set up as a **pub workspace**, the `hooks.user_defines` configuration block must be placed in the **workspace root** `pubspec.yaml` file. Defines inside member package `pubspec.yaml` files are ignored when workspace resolution is active.
+
+### 2. Read in `hook/build.dart` or `hook/link.dart`
+Access the values using `input.userDefines`:
+
+<!-- file://./example/api/config_snippet_6.dart -->
+```dart
+import 'dart:io';
+import 'package:hooks/hooks.dart';
+
+void main(List<String> args) async {
+ await build(args, (input, output) async {
+ // Access raw user-defines value
+ final debugLogging = input.userDefines['enable_debug_logging'];
+ if (debugLogging is! bool?) {
+ throw const FormatException(
+ 'hooks.user_defines.my_package.enable_debug_logging must be a '
+ 'boolean (or omitted)',
+ );
+ }
+ if (debugLogging == true) {
+ print('Debug logging is enabled.');
+ }
+
+ // Resolve relative path against pubspec.yaml base path
+ final customAssetUri = input.userDefines.path('custom_asset');
+ if (customAssetUri != null) {
+ final file = File.fromUri(customAssetUri);
+ output.dependencies.add(file.uri); // Declare cache dependency
+ // Use the file...
+ }
+ });
+}
+```
+
## Documentation
For detailed documentation on debugging and the configuration schema, see the
diff --git a/pkgs/hooks/example/api/config_snippet_6.dart b/pkgs/hooks/example/api/config_snippet_6.dart
new file mode 100644
index 0000000..93510ea
--- /dev/null
+++ b/pkgs/hooks/example/api/config_snippet_6.dart
@@ -0,0 +1,34 @@
+// Copyright (c) 2026, 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.
+
+// dart format width=74
+
+// snippet-start
+import 'dart:io';
+import 'package:hooks/hooks.dart';
+
+void main(List<String> args) async {
+ await build(args, (input, output) async {
+ // Access raw user-defines value
+ final debugLogging = input.userDefines['enable_debug_logging'];
+ if (debugLogging is! bool?) {
+ throw const FormatException(
+ 'hooks.user_defines.my_package.enable_debug_logging must be a '
+ 'boolean (or omitted)',
+ );
+ }
+ if (debugLogging == true) {
+ print('Debug logging is enabled.');
+ }
+
+ // Resolve relative path against pubspec.yaml base path
+ final customAssetUri = input.userDefines.path('custom_asset');
+ if (customAssetUri != null) {
+ final file = File.fromUri(customAssetUri);
+ output.dependencies.add(file.uri); // Declare cache dependency
+ // Use the file...
+ }
+ });
+}
+// snippet-end
diff --git a/pkgs/hooks/example/api/config_snippet_7.dart b/pkgs/hooks/example/api/config_snippet_7.dart
new file mode 100644
index 0000000..92b4b27
--- /dev/null
+++ b/pkgs/hooks/example/api/config_snippet_7.dart
@@ -0,0 +1,19 @@
+// Copyright (c) 2026, 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.
+
+// dart format width=74
+
+import 'package:hooks/hooks.dart';
+
+void main(List<String> args) async {
+ await build(args, (input, output) async {
+ // snippet-start
+ final assetsUri = input.userDefines.path('prebuilt_assets_dir');
+ if (assetsUri != null) {
+ output.dependencies.add(assetsUri);
+ // Read assets from the directory...
+ }
+ // snippet-end
+ });
+}
diff --git a/pkgs/hooks/lib/src/api/build_and_link.dart b/pkgs/hooks/lib/src/api/build_and_link.dart
index 5b46e03..3669612 100644
--- a/pkgs/hooks/lib/src/api/build_and_link.dart
+++ b/pkgs/hooks/lib/src/api/build_and_link.dart
@@ -78,6 +78,16 @@
/// }
/// ```
///
+/// ## 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
@@ -131,7 +141,8 @@
/// and only if:
///
/// * The input to the hook didn't change (including the configuration fields
-/// in [BuildConfig] and the `user-defines` in the workspace `pubspec.yaml`).
+/// accessed via [BuildInput.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.
@@ -148,13 +159,18 @@
/// [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 [HookConfig]/[BuildConfig] structure. Therefore, different
-/// configurations (e.g., building for a different target OS or architecture)
-/// do not collide.
+/// `.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.
@@ -289,6 +305,16 @@
/// 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
@@ -342,7 +368,8 @@
/// and only if:
///
/// * The input to the hook didn't change (including the configuration fields
-/// in [LinkConfig] and the `user-defines` in the workspace `pubspec.yaml`).
+/// 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.
@@ -359,13 +386,18 @@
/// [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 [HookConfig]/[LinkConfig] structure. Therefore, different
-/// configurations (e.g., building for a different target OS or architecture)
-/// do not collide.
+/// `.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.
diff --git a/pkgs/hooks/lib/src/config.dart b/pkgs/hooks/lib/src/config.dart
index e1a5c4a..a11ee3f 100644
--- a/pkgs/hooks/lib/src/config.dart
+++ b/pkgs/hooks/lib/src/config.dart
@@ -106,7 +106,101 @@
/// The configuration for this hook input.
HookConfig get config => HookConfig._(this);
- /// The user-defines for this hook input.
+ /// Custom configurations specified in the root package `pubspec.yaml`
+ /// (or root package pub workspace `pubspec.yaml` if using a workspace) under
+ /// the `hooks.user_defines` block.
+ ///
+ /// These are used to pass custom parameters or local file paths to build and
+ /// link hooks from the project's build environment.
+ ///
+ /// ### Workspace Scope
+ /// The SDK hook runner reads user-defines from the root package's
+ /// `pubspec.yaml` (or the root package pub workspace's `pubspec.yaml` if
+ /// using a workspace). As a result, only end-users (the authors of the root
+ /// app/package consuming the dependencies) can configure user-defines.
+ /// Dependencies cannot supply their own default user-defines.
+ ///
+ /// ### Caching
+ /// Hook user-defines are workspace-wide. If any user-defines inside the
+ /// root package `pubspec.yaml` (or the root package pub workspace
+ /// `pubspec.yaml` if using a workspace) change, the hook is re-run.
+ /// However, if the user-defines for the package are identical, the hook is
+ /// not re-run.
+ ///
+ /// ### Supported Types
+ /// Configured values inside `pubspec.yaml` can be any JSON-compatible type,
+ /// such as booleans, strings, numbers, nested maps, or lists:
+ /// ```yaml
+ /// hooks:
+ /// user_defines:
+ /// my_package:
+ /// supported_archs:
+ /// - arm64
+ /// - x64
+ /// ```
+ ///
+ /// ### Package Filtering
+ /// User-defines are filtered per package. A hook inside `my_package` can only
+ /// access keys configured under `hooks.user_defines.my_package`. It cannot
+ /// access defines of other packages.
+ ///
+ /// ### Common Use Cases
+ /// End-users can configure user-defines to:
+ /// - Select whether to download a prebuilt binary or build from source:
+ /// ```yaml
+ /// hooks:
+ /// user_defines:
+ /// my_package:
+ /// local_build: false
+ /// ```
+ /// - Enable/disable debug options or configure a custom compiler flag:
+ /// ```yaml
+ /// hooks:
+ /// user_defines:
+ /// my_package:
+ /// debug_mode: true
+ /// ```
+ ///
+ /// ### Example Hook Usage
+ /// In `pubspec.yaml`:
+ /// ```yaml
+ /// hooks:
+ /// user_defines:
+ /// my_package:
+ /// enable_experimental: true
+ /// custom_lib: assets/libnative.so
+ /// ```
+ ///
+ /// In `hook/build.dart`:
+ /// <!-- file://./../../example/api/config_snippet_6.dart -->
+ /// ```dart
+ /// import 'dart:io';
+ /// import 'package:hooks/hooks.dart';
+ ///
+ /// void main(List<String> args) async {
+ /// await build(args, (input, output) async {
+ /// // Access raw user-defines value
+ /// final debugLogging = input.userDefines['enable_debug_logging'];
+ /// if (debugLogging is! bool?) {
+ /// throw const FormatException(
+ /// 'hooks.user_defines.my_package.enable_debug_logging must be a '
+ /// 'boolean (or omitted)',
+ /// );
+ /// }
+ /// if (debugLogging == true) {
+ /// print('Debug logging is enabled.');
+ /// }
+ ///
+ /// // Resolve relative path against pubspec.yaml base path
+ /// final customAssetUri = input.userDefines.path('custom_asset');
+ /// if (customAssetUri != null) {
+ /// final file = File.fromUri(customAssetUri);
+ /// output.dependencies.add(file.uri); // Declare cache dependency
+ /// // Use the file...
+ /// }
+ /// });
+ /// }
+ /// ```
HookInputUserDefines get userDefines => HookInputUserDefines._(this);
}
@@ -118,8 +212,20 @@
/// The value for the user-define for [key] for this package.
///
- /// This can be arbitrary JSON/YAML if provided from the SDK from such source.
+ /// This can be arbitrary JSON if provided from the SDK from such source.
/// If it's provided from command-line arguments, it's likely a string.
+ ///
+ /// For example, if a project's `pubspec.yaml` contains:
+ /// ```yaml
+ /// hooks:
+ /// user_defines:
+ /// my_package:
+ /// enable_experimental_features: true
+ /// optimization_level: "O3"
+ /// ```
+ /// Then:
+ /// - `input.userDefines['enable_experimental_features']` returns `true`.
+ /// - `input.userDefines['optimization_level']` returns `"O3"`.
Object? operator [](String key) {
final syntaxNode = _input._syntax.userDefines;
if (syntaxNode == null) {
@@ -132,14 +238,37 @@
return pubspecSource?.defines[key];
}
- /// The absolute path for user-defines for [key] for this package.key
+ /// Resolves the relative path provided in the user-define for [key] to an
+ /// absolute [Uri] pointing to the file or directory on the host filesystem.
///
- /// The relative path passed as user-define is resolved against the base path.
- /// For user-defines originating from a JSON/YAML, the base path is this
- /// JSON/YAML. For user-defines originating from command-line arguments, the
- /// base path is the working directory of the command-line invocation.
+ /// The relative path is resolved against the directory containing the
+ /// `pubspec.yaml` where the user-define was declared (or the command-line
+ /// working directory if provided via command-line arguments).
///
/// If the user-define is `null` or not a [String], returns `null`.
+ ///
+ /// > If the hook reads the resolved file or directory, the hook author
+ /// > **must** register it as a dependency in
+ /// > [HookOutputBuilder.dependencies] (e.g. using
+ /// > `output.dependencies.add(resolvedUri)`) to ensure the build cache is
+ /// > invalidated and the hook is re-run when the file changes.
+ ///
+ /// For example, if a project's `pubspec.yaml` contains:
+ /// ```yaml
+ /// hooks:
+ /// user_defines:
+ /// my_package:
+ /// prebuilt_assets_dir: assets/prebuilt/
+ /// ```
+ /// The resolved path can be accessed and registered as a dependency:
+ /// <!-- file://./../../example/api/config_snippet_7.dart -->
+ /// ```dart
+ /// final assetsUri = input.userDefines.path('prebuilt_assets_dir');
+ /// if (assetsUri != null) {
+ /// output.dependencies.add(assetsUri);
+ /// // Read assets from the directory...
+ /// }
+ /// ```
Uri? path(String key) {
final syntaxNode = _input._syntax.userDefines;
if (syntaxNode == null) {
diff --git a/pkgs/hooks/pubspec.yaml b/pkgs/hooks/pubspec.yaml
index 01e5965..431bb3c 100644
--- a/pkgs/hooks/pubspec.yaml
+++ b/pkgs/hooks/pubspec.yaml
@@ -3,7 +3,7 @@
A library that contains a Dart API for the JSON-based protocol for
`hook/build.dart` and `hook/link.dart`.
-version: 2.0.1
+version: 2.0.2
repository: https://github.com/dart-lang/native/tree/main/pkgs/hooks