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--verboseflagLevel.WARNINGtoLevel.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(...)
}