In Dart, the foundation for custom build systems is the build package โ itโs the same system used by tools like build_runner, json_serializable, and freezed. It allows you to define your own builders that transform source files into generated outputs.
When Iโve needed a custom build setup, I typically follow this process:
First, I add the necessary dependencies in pubspec.yaml:
dev_dependencies:
build_runner: ^2.4.0
build: ^2.4.0
Then, I create my own builder definition โ this is a Dart class that specifies how to read input assets (like .dart files, JSON, or YAML) and produce outputs (like generated Dart code).
For example, if I want to automatically generate boilerplate configuration code from a JSON file, Iโd create a builder like this:
// lib/builders/config_builder.dart
import 'dart:async';
import 'package:build/build.dart';
class ConfigBuilder implements Builder {
@override
Map<String, List> get buildExtensions => {
'.json': ['.config.dart']
};
@override
Future build(BuildStep buildStep) async {
final inputId = buildStep.inputId;
final content = await buildStep.readAsString(inputId);
final outputId = inputId.changeExtension('.config.dart');
final generatedCode = '''
// GENERATED CODE - DO NOT MODIFY BY HAND
const configData = $content;
''';
await buildStep.writeAsString(outputId, generatedCode);
}
}
This builder reads any .json file and generates a corresponding .config.dart file containing a Dart constant.
Next, I register the builder in a build.yaml file:
targets:
$default:
builders:
my_custom_builder:
generate_for:
- lib/**.json
builders:
my_custom_builder:
import: "package:my_project/builders/config_builder.dart"
builder_factories: ["configBuilderFactory"]
build_extensions: {".json": [".config.dart"]}
auto_apply: root_package
And I create a small factory method:
Builder configBuilderFactory(BuilderOptions options) => ConfigBuilder();
Then I can run:
dart run build_runner build
This will scan the source tree, run my builder for matching inputs, and output generated code.
I used a similar setup in a Flutter project where we needed to dynamically generate environment-specific configurations (like API URLs, feature toggles, etc.) from JSON templates. Instead of manually maintaining environment files, we automated it through a custom builder โ it saved time and ensured consistency across environments.
One challenge I faced was managing build dependencies and incremental rebuilds. The build_runner system caches outputs intelligently, so if inputs havenโt changed, it wonโt rebuild. However, sometimes stale outputs caused issues โ to fix that, Iโd clean the cache using:
dart run build_runner clean
before running another build.
Another challenge was parallel execution and watch mode. When I needed continuous builds (like during active development), Iโd run:
dart run build_runner watch
This keeps the builder running and automatically regenerates files when sources change โ very useful for large apps with frequent schema or config updates.
In terms of limitations, the build_runner system is very flexible but can get slow for large projects, especially when multiple builders are chained. For lightweight or one-time code generation, I sometimes write a custom Dart CLI script using dart:io to perform similar tasks without the full build system overhead.
For example, if I just need to preprocess files before packaging, Iโll create a script like:
import 'dart:io';
void main() {
final dir = Directory('assets/raw');
for (var file in dir.listSync()) {
// Custom processing logic
}
}
and run it before build via CI/CD scripts.
In larger modular projects, Iโve also customized build systems with build.yaml per package โ allowing specific packages to have their own code generators and build rules, keeping things modular and maintainable.
In summary, my approach to configuring custom build systems in Dart involves:
- Using the
buildandbuild_runnerpackages for structured, reusable build logic - Defining custom builders and
build.yamlconfigurations for targeted file transformations - Using watch mode for continuous development, and clean builds to avoid stale caches
- Falling back to lightweight custom scripts (
dart:io) for simpler preprocessing needs - Keeping build logic modular across packages to improve maintainability and performance
This approach gives me a flexible and powerful build system tailored exactly to the projectโs needs โ whether itโs automating code generation, asset transformation, or environment configuration.
