Saltar al contenido principal

Hooks

Ejecutar scripts de construcción personalizados.

Esta guía explica qué son los hooks y cómo usarlos con un paquete.

Introducción

#

Actualmente puedes usar hooks para hacer cosas como compilar o descargar assets nativos (código escrito en otros lenguajes que se compila en código máquina), y luego llamar a estos assets desde el código Dart de un paquete.

Los hooks son scripts de Dart ubicados en el directorio hook/ de tu paquete Dart. Tienen un formato predefinido para su entrada y salida, lo que permite al SDK de Dart:

  1. Descubrir los hooks.
  2. Ejecutar los hooks con la entrada necesaria.
  3. Consumir la salida producida por los hooks.

Proyecto de ejemplo con un hook de construcción:

  • example_project/// Proyecto con hooks.
    • hook/// Agrega scripts de hook aquí.
      • build.dart
    • lib/ // Usa tus assets aquí.
      • example.dart
    • src/ // Agrega fuentes nativas aquí.
      • example_native_library.c
      • example_native_library.h
    • test/// Prueba tus assets aquí.
      • example_test.dart

Hooks

#

Currently, build hooks and link hooks are available. To learn more, see the following.

Hooks de construcción

#

Con los hooks de construcción, un paquete puede hacer cosas como compilar o descargar assets nativos, como bibliotecas de C o Rust. Después, estos assets se pueden llamar desde el código Dart de un paquete.

El SDK de Dart invoca automáticamente el hook de construcción de un paquete en el momento adecuado durante el proceso de construcción. Los hooks de construcción se ejecutan en paralelo con la compilación de Dart y podrían realizar operaciones de mayor duración, como descargar o llamar a un compilador nativo.

Usa la función build para analizar la entrada del hook con BuildInput y luego escribe la salida del hook con BuildOutputBuilder. El hook debe colocar los assets descargados y generados en BuildInput.sharedOutputDirectory.

Los assets producidos para tu paquete podrían depender de los assets o los metadatos producidos por los hooks de construcción de los paquetes en las dependencias directas en el pubspec. Por lo tanto, los hooks de construcción se ejecutan en el orden de las dependencias en el pubspec, y las dependencias cíclicas entre paquetes no son compatibles cuando se usan 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.

Variables de entorno

#

Los hooks se ejecutan en un entorno semihermético. Esto significa que Platform.environment no expone todas las variables de entorno del proceso principal. Esto garantiza que las invocaciones de los hooks sean reproducibles y almacenables en caché, y no dependan de variables de entorno accidentales.

Sin embargo, algunas variables de entorno son necesarias para localizar herramientas (como compiladores) o configurar el acceso a la red. Las siguientes variables de entorno se pasan al proceso del hook:

  • Path and system roots:
    • PATH: Invocar herramientas nativas.
    • HOME, USERPROFILE: Buscar herramientas en las ubicaciones de instalación predeterminadas.
    • SYSTEMDRIVE, SYSTEMROOT, WINDIR: Invocaciones de procesos y CMake en Windows.
    • PROGRAMDATA: Para vswhere.exe en Windows.
  • Temporary directories:
    • TEMP, TMP, TMPDIR: Directorios temporales.
  • HTTP proxies:
    • HTTP_PROXY, HTTPS_PROXY, NO_PROXY: Acceso a la red detrás de proxies.
  • Clang/LLVM:
    • LIBCLANG_PATH: bindgen de Rust + clang-sys.
  • Android NDK:
    • ANDROID_HOME: Ubicación estándar para el SDK/NDK de Android.
    • ANDROID_NDK, ANDROID_NDK_HOME, ANDROID_NDK_LATEST_HOME, ANDROID_NDK_ROOT: Ubicaciones alternativas para el NDK.
  • Nix:
    • Cualquier variable que comience con NIX_.

Cualquier cambio en estas variables de entorno provoca la invalidación de la caché de los hooks.

Todas las demás variables de entorno se eliminan.

Assets

#

Los assets son los archivos producidos por un hook y luego empaquetados en una aplicación Dart. Se puede acceder a los assets en tiempo de ejecución desde el código Dart. Actualmente, el SDK de Dart puede usar el tipo CodeAsset, pero se planean más tipos de assets. Para obtener más información, consulta lo siguiente.

Tipo CodeAsset

#

Un CodeAsset representa un asset de código. Un asset de código es una biblioteca dinámica compilada a partir de un lenguaje que no sea Dart, como C, C++, Rust o Go. CodeAsset es parte del paquete code_asset. Se accede a las APIs proporcionadas por los assets de código en tiempo de ejecución a través de los correspondientes miembros externos de Dart anotados con la anotación @Native de dart:ffi.

Usar un hook

#

Para agregar assets a tu proyecto, usa un hook. Para más detalles, consulta las siguientes secciones.

Agregar dependencias

#

Para usar un hook, primero debes agregar los paquetes auxiliares hooks y code_assets a las dependencias de tu pubspec.yaml:

  1. dart pub add hooks code_assets.

Si necesitas compilar fuentes en C, también necesitarás el paquete native_toolchain_c:

  1. dart pub add native_toolchain_c.

Dependencias de ejemplo para un hook de construcción:

pubspec.yaml
yaml
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

Crear un hook de construcción para generar assets nativos

#

Si deseas usar un hook de construcción para compilar de forma transparente assets nativos (como bibliotecas de C o Rust), que luego estén disponibles para ser llamados desde el código Dart de un paquete, crea un script build.dart similar al siguiente:

  1. En tu proyecto Dart, crea o abre hook/build.dart.

  2. En el método main, llama a la función build de package:hooks/hooks.dart y usa la cadena de herramientas (toolchain) adecuada para compilar la biblioteca nativa. Por ejemplo:

    hook/build.dart
    dart
    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 cLibrary = CLibrary(
          name: packageName,
          assetName: '$packageName.dart',
          sources: ['src/$packageName.c'],
        );
        await cLibrary.build(
          input: input,
          output: output,
        );
      });
    }
    

    El segundo parámetro de build espera una función a la que le pasará dos argumentos:

    • input: La entrada de solo lectura para el hook. Incluye información para que el hook produzca el tipo de asset correcto (por ejemplo, SO de destino, arquitectura de destino, directorio de salida y más). Para más detalles, consulta la clase BuildInput.

    • output: El constructor de solo escritura para la salida del hook. Después de que el hook de construcción lee la entrada, produce un asset y luego proporciona lo que produjo como salida. Para más detalles, consulta la clase BuildOutputBuilder.

#

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.

  1. In your Dart project, create or open hook/link.dart.

  2. In the main method, call the link function from package:hooks/hooks.dart and pass tree-shaking options to CLibrary.link. For example:

    hook/link.dart
    dart
    import '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.

Assets empaquetados automáticamente

#

Los hooks se ejecutan automáticamente al invocar los comandos run, build o test. Los assets resultantes se almacenan en el directorio de salida especificado en la entrada del hook. El SDK de Dart luego empaqueta automáticamente esos assets con tu aplicación Dart para que se pueda acceder a ellos en tiempo de ejecución.

Usar 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.

El siguiente ejemplo ilustra cómo vincular a la función nativa de C add de native_add_library.c y llamarla:

my_package/lib/my_package.dart
dart
import 'dart:ffi';

@Native<Int32 Function(Int32, Int32)>()
external int add(int a, int b);
my_package/bin/my_package.dart
dart
import 'package:my_package/my_package.dart';

void main() {
  print(add(24, 18));
}

El ID de asset en @Native es opcional y por defecto es la URI de la biblioteca. En el ejemplo anterior, esto es package:native_add_library/native_add_library.dart, que es el mismo ID de asset producido por el hook de construcción. Esto permite a Dart conectar un asset referenciado en tiempo de ejecución con el proporcionado por el hook durante el proceso de construcción.

Probar assets

#

Después de escribir un hook que genera un asset y de haber usado ese asset en tu código Dart, considera escribir una prueba para verificar que el hook y el asset generado funcionen como se espera.

En el siguiente ejemplo, se crea una prueba para native_add_library.dart, un script que hace referencia a una función nativa de C llamada add:

test/native_add_library_test.dart
dart
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);
  });
}

Configuración del hook

#

Pasa parámetros personalizados o rutas de archivos locales a los hooks de construcción y enlace desde el entorno de construcción de tu proyecto. Configura estos parámetros bajo la clave hooks en tu archivo pubspec.yaml. Actualmente, este bloque solo admite la opción user_defines.

Configurar user-defines

#

Configura parámetros personalizados dentro del archivo pubspec.yaml del paquete raíz, o en el archivo pubspec.yaml del espacio de trabajo si usas uno. Solo los usuarios finales (los autores de la aplicación raíz o el paquete que consume las dependencias) pueden configurar user-defines. Las dependencias no pueden proporcionar sus propios user-defines predeterminados.

Los valores configurados bajo user_defines pueden ser de cualquier tipo compatible con JSON, como booleanos, cadenas, números, mapas anidados o listas. Los user-defines se filtran por paquete. Un hook dentro de my_package solo puede acceder a las claves configuradas bajo hooks.user_defines.my_package. No puede acceder a los user-defines de otros paquetes.

El siguiente es un ejemplo de cómo pasar dos user-defines a los hooks del paquete my_package:

pubspec.yaml
yaml
hooks:
  user_defines:
    my_package:
      enable_experimental: true
      custom_lib: assets/libnative.so

Acceder a user-defines en un hook

#

Para acceder a los user-defines configurados en tu script de hook build.dart o link.dart, usa el objeto input.userDefines:

  1. Lee valores sin procesar (raw) usando el operador de corchetes, como input.userDefines['key'].
  2. Resuelve rutas relativas a una Uri usando el método path(), como input.userDefines.path('key'). Esto resuelve la ruta relativa con respecto al directorio del pubspec.yaml donde se declaran los user-defines.

Si el hook lee un archivo o directorio resuelto, regístralo como una dependencia usando output.dependencies.add(). Esto garantiza que el sistema de construcción invalide la caché y vuelva a ejecutar el hook cuando el archivo cambie.

El siguiente es un ejemplo de script de hook que accede a los user-defines enable_experimental y custom_lib:

hook/build.dart
dart
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...
    }
  });
}

Proyectos de ejemplo

#

Hay varios proyectos de ejemplo para ayudarte a comenzar con los hooks y los assets de código:

ProjectDescription
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 Un paquete que usa una biblioteca del sistema nativa.
native_add_library Un paquete que compila, empaqueta y usa algo de código C simple.
native_add_app Una aplicación CLI de Dart que depende de native_add_library.
download_asset Un paquete que empaqueta y usa assets preconstruidos que se descargan en el hook de construcción.
native_dynamic_linking Un paquete que compila, empaqueta y usa tres bibliotecas nativas que dependen entre sí.
use_dart_api Un paquete que usa la API C de la Dart VM.

Más información

#

Consulta los siguientes enlaces para más información: