LogoDart Code Generation

Build System Core#

Source of truth: build/build/lib/src/ — package version 4.0.5, Dart SDK ^3.7.0


Overview#

The build package defines the low-level interfaces for Dart code generation. build_runner implements these interfaces and orchestrates the build. You implement Builder (and optionally PostProcessBuilder) and declare them in build.yaml.


Builder Interface#

// build/build/lib/src/builder.dart
abstract class Builder {
  /// The core method: called once per matching input file.
  /// Read from buildStep.inputId, write to declared outputs.
  FutureOr<void> build(BuildStep buildStep);

  /// Maps input extensions to output extensions.
  /// Key: input suffix (e.g. ".dart")
  /// Value: list of output suffixes (e.g. [".g.dart"])
  Map<String, List<String>> get buildExtensions;
}

BuilderFactory typedef#

typedef BuilderFactory = Builder Function(BuilderOptions options);

Every builder you expose to build_runner must be a top-level function matching this signature:

// lib/builder.dart
Builder myBuilder(BuilderOptions options) => MyBuilder(options);

BuilderOptions#

class BuilderOptions {
  static const empty = BuilderOptions({});
  static const forRoot = BuilderOptions({}, isRoot: true);

  /// Config from build.yaml `options:` section — values are strings/bools/maps
  final Map<String, dynamic> config;

  /// true when the builder is running on the root package (the one being built)
  final bool isRoot;

  const BuilderOptions(this.config, {this.isRoot = false});

  /// Merge two options; other takes precedence
  BuilderOptions overrideWith(BuilderOptions? other);
}

Reading options in your builder:

Builder myBuilder(BuilderOptions options) {
  final outputDir = options.config['output_dir'] as String? ?? 'lib/generated';
  final verbose = (options.config['verbose'] as bool?) ?? false;
  return MyBuilder(outputDir: outputDir, verbose: verbose);
}

BuildStep Interface#

// build/build/lib/src/build_step.dart
abstract class BuildStep implements AssetReader, AssetWriter {
  /// The primary input file this build step is processing
  AssetId get inputId;

  /// Resolves the Dart library for inputId (requires analyzer dep)
  Future<LibraryElement> get inputLibrary;

  /// A Resolver that can parse/resolve any Dart source visible to this step
  Resolver get resolver;

  // --- Reading ---
  Future<List<int>> readAsBytes(AssetId id);
  Future<String> readAsString(AssetId id, {Encoding encoding = utf8});
  Future<bool> canRead(AssetId id);

  /// Find assets matching a glob pattern within the current package
  Stream<AssetId> findAssets(Glob glob);

  // --- Writing ---
  /// All AssetIds you're allowed to write (declared in buildExtensions)
  Iterable<AssetId> get allowedOutputs;
  Future<void> writeAsBytes(AssetId id, FutureOr<List<int>> bytes);
  Future<void> writeAsString(AssetId id, FutureOr<String> contents, {Encoding encoding = utf8});

  // --- Resources ---
  /// Fetch a shared Resource (e.g. a shared cache object)
  Future<T> fetchResource<T>(Resource<T> resource);

  /// Performance tracking (optional)
  T trackStage<T>(String label, T Function() action, {bool isExternal = false});

  /// Report assets that were read but don't affect output (for caching)
  void reportUnusedAssets(Iterable<AssetId> ids);

  /// The current package config (for resolving package paths)
  Future<PackageConfig> get packageConfig;
}

Typical build() implementation pattern#

@override
Future<void> build(BuildStep buildStep) async {
  // 1. Get the output AssetId
  final inputId = buildStep.inputId;
  final outputId = inputId.changeExtension('.g.dart');

  // 2. Read the input library
  final library = await buildStep.inputLibrary;

  // 3. Process and generate code
  final code = _generateCode(library);

  // 4. Write output
  await buildStep.writeAsString(outputId, code);
}

AssetId#

// build/build/lib/src/asset_id.dart
class AssetId implements Comparable<AssetId> {
  final String package;  // e.g. 'my_package'
  final String path;     // e.g. 'lib/src/model.dart' (always relative, forward slashes)

  // Constructors
  AssetId(this.package, String path);                    // AssetId('pkg', 'lib/model.dart')
  factory AssetId.resolve(Uri uri, {AssetId? from});     // Resolve package: or asset: URI
  factory AssetId.parse(String id);                      // Parse 'pkg|lib/model.dart'

  // Properties
  String get extension;                    // '.dart'
  List<String> get pathSegments;          // ['lib', 'src', 'model.dart']
  Uri get uri;                             // package:pkg/src/model.dart

  // Common operations
  AssetId addExtension(String extension);         // 'model.dart' + '.copy' → 'model.dart.copy'
  AssetId changeExtension(String newExtension);   // 'model.dart' → 'model.g.dart'

  // Serialization
  Object serialize();                             // ['pkg', 'lib/model.dart']
  AssetId.deserialize(List<dynamic> serialized);

  @override
  String toString() => '$package|$path';         // 'pkg|lib/model.dart'
}

Common AssetId patterns#

// Get output path from input
final outputId = buildStep.inputId.changeExtension('.g.dart');

// Compute output in different directory
final inputId = buildStep.inputId;
final outputId = AssetId(
  inputId.package,
  inputId.path.replaceFirst('assets/', 'lib/generated/').replaceFirst('.json', '.dart'),
);

// Find all dart files in a package
await for (final id in buildStep.findAssets(Glob('lib/**.dart'))) {
  // process id
}

// Check if an asset exists before reading
if (await buildStep.canRead(someAssetId)) {
  final content = await buildStep.readAsString(someAssetId);
}

AssetReader and AssetWriter (interfaces)#

abstract class AssetReader {
  Future<List<int>> readAsBytes(AssetId id);
  Future<String> readAsString(AssetId id, {Encoding encoding = utf8});
  Future<bool> canRead(AssetId id);
  Stream<AssetId> findAssets(Glob glob);
  Future<Digest> digest(AssetId id);  // SHA256 hash for cache invalidation
}

abstract class AssetWriter {
  Future<void> writeAsBytes(AssetId id, List<int> bytes);
  Future<void> writeAsString(AssetId id, String contents, {Encoding encoding = utf8});
}

Resolver#

// build/build/lib/src/resolver.dart
abstract class Resolver {
  /// Check if an AssetId is a Dart library (not a part file)
  Future<bool> isLibrary(AssetId assetId);

  /// Stream of all resolved libraries visible to this build step
  Stream<LibraryElement> get libraries;

  /// Resolve a Dart library from an AssetId
  Future<LibraryElement> libraryFor(
    AssetId assetId, {
    bool allowSyntaxErrors = false,
  });

  /// Parse (but don't fully resolve) a Dart file
  Future<CompilationUnit> compilationUnitFor(
    AssetId assetId, {
    bool allowSyntaxErrors = false,
  });

  /// Find the AssetId that contains a given Element
  Future<AssetId> assetIdForElement(Element element);

  /// Find a library by its name (package:pkg/lib.dart)
  Future<LibraryElement?> findLibraryByName(String libraryName);

  /// Get the AST node for a Fragment (use element.firstFragment to get the Fragment)
  Future<AstNode?> astNodeFor(Fragment fragment, {bool resolve = false});
}

Resolver usage example#

Future<void> build(BuildStep buildStep) async {
  // Get the library from the input file
  final library = await buildStep.inputLibrary;

  // Or resolve another library
  final otherLib = await buildStep.resolver.libraryFor(
    AssetId('other_package', 'lib/other.dart'),
  );

  // Traverse all visible libraries
  await for (final lib in buildStep.resolver.libraries) {
    // process lib
  }

  // Find where an element is defined
  final assetId = await buildStep.resolver.assetIdForElement(someElement);
}

PostProcessBuilder#

Runs after all regular builders. Cannot read arbitrary assets — only its primary input. Outputs don't affect other builders.

// build/build/lib/src/post_process_builder.dart
abstract class PostProcessBuilder {
  /// Extensions of files this builder processes (e.g. ['.dart'])
  Iterable<String> get inputExtensions;

  FutureOr<void> build(PostProcessBuildStep buildStep);
}

typedef PostProcessBuilderFactory = PostProcessBuilder Function(BuilderOptions);

PostProcessBuildStep#

// build/build/lib/src/post_process_build_step.dart
abstract class PostProcessBuildStep {
  AssetId get inputId;

  Future<Digest> digest(AssetId id);

  Future<List<int>> readInputAsBytes();
  Future<String> readInputAsString({Encoding encoding = utf8});

  Future<void> writeAsBytes(AssetId id, FutureOr<List<int>> bytes);
  Future<void> writeAsString(AssetId id, FutureOr<String> content, {Encoding encoding = utf8});

  /// Mark the primary input for deletion
  void deletePrimaryInput();

  /// Must be called when done
  Future<void> complete();
}

PostProcessBuilders are declared in build.yaml under post_process_builders: (see reference 02).


Resource#

Resources allow expensive objects to be shared across build steps within a build, with lifecycle management.

// build/build/lib/src/resource.dart
class Resource<T> {
  /// [create] is called once per build phase to create the resource
  /// [dispose] is called at the end of the build phase
  /// [beforeExit] is called before the Dart VM exits
  Resource(
    FutureOr<T> Function() create, {
    FutureOr<void> Function(T)? dispose,
    FutureOr<void> Function()? beforeExit,
  });
}

Usage in a builder:

// Declare as a package-level variable
final _analyzerResource = Resource<MyAnalyzer>(
  () => MyAnalyzer.create(),
  dispose: (analyzer) => analyzer.dispose(),
);

class MyBuilder implements Builder {
  @override
  Future<void> build(BuildStep buildStep) async {
    // Get the shared instance (created once per build)
    final analyzer = await buildStep.fetchResource(_analyzerResource);
    // use analyzer...
  }
}

Logging#

// build/build/lib/src/logging.dart
// Use the 'logging' package's Logger
import 'package:logging/logging.dart';

// In your builder, use the global 'log' logger from package:build
// It's available via: import 'package:build/build.dart';
log.fine('Debug info (shown with --verbose)');
log.info('Regular info (shown with --verbose)');
log.warning('Warning message (always shown)');
log.severe('Error message (always shown, fails build)');

Log level semantics:

  • < Level.WARNING (fine, info, config): only shown with --verbose flag
  • Level.WARNING to Level.SEVERE: always shown
  • >= Level.SEVERE: always shown AND causes build to fail

Exceptions#

// build/build/lib/src/exceptions.dart

/// The requested asset could not be found
class AssetNotFoundException implements Exception {
  final AssetId assetId;
}

/// A package referenced in an AssetId could not be found
class PackageNotFoundException implements Exception {
  final String name;
}

/// An output was written to an AssetId not declared in buildExtensions
class UnexpectedOutputException implements Exception {
  final AssetId id;
}

/// An asset was read that is not public (not in lib/)
class InvalidInputException implements Exception {
  final AssetId id;
}

/// A BuildStep was used after it was completed
class BuildStepCompletedException implements Exception {}

/// An asset could not be resolved (not in SDK summaries or source)
class UnresolvableAssetException implements Exception {
  final String message;
}

buildExtensions Patterns#

The buildExtensions map on Builder controls which inputs are processed and what outputs are declared.

Simple suffix matching (most common)#

Map<String, List<String>> get buildExtensions => const {
  '.dart': ['.g.dart'],         // foo.dart → foo.g.dart
  '.json': ['.dart'],           // config.json → config.dart
};

Path-level matching (prefix with ^)#

Map<String, List<String>> get buildExtensions => const {
  '^pubspec.yaml': ['lib/generated.dart'],  // Only matches pubspec.yaml exactly
  '^lib/template.txt': ['lib/output.dart'], // Only one input
};

Capture groups (use {{}})#

Map<String, List<String>> get buildExtensions => const {
  '^assets/{{}}.json': ['lib/generated/{{}}.dart'],
  // assets/config.json → lib/generated/config.dart
  // assets/users.json → lib/generated/users.dart
};

Package-level input (runs once per package)#

Map<String, List<String>> get buildExtensions => const {
  r'$package$': ['lib/all_routes.dart'],  // Runs exactly once per package
};

Note: When using r'$package$', the output AssetId is built manually since the "input" is virtual:

Future<void> build(BuildStep buildStep) async {
  final outputId = AssetId(buildStep.inputId.package, 'lib/all_routes.dart');
  // Find all relevant assets via buildStep.findAssets(...)
}