blob: d79b3f109ff6428c692a27b1a19edbedf7e34c9b [file] [view]
Defines the basic pieces of how a build happens and how they interact.
## [`Builder`][dartdoc:Builder]
The business logic for code generation. Most consumers of the `build` package
will create custom implementations of `Builder`.
## [`BuildStep`][dartdoc:BuildStep]
The way a `Builder` interacts with the outside world. Defines the unit of work
and allows reading/writing files and resolving Dart source code.
## [`Resolver`][dartdoc:Resolver] class
An interface into the dart [analyzer][pub:analyzer] to allow resolution of code
that needs static analysis and/or code generation.
## Implementing your own Builders
A `Builder` gets invoked one by one on it's inputs, and may read other files and
output new files based on those inputs.
The basic API looks like this:
```dart
abstract class Builder {
/// You can only output files that are configured here by suffix substitution.
/// You are not required to output all of these files, but no other builder
/// may declare the same outputs.
Map<String, List<String>> get buildExtensions;
/// This is where you build and output files.
FutureOr<void> build(BuildStep buildStep);
}
```
Here is an implementation of a `Builder` which just copies files to other files
with the same name, but an additional extension:
```dart
import 'package:build/build.dart';
/// A really simple [Builder], it just makes copies of .txt files!
class CopyBuilder implements Builder {
@override
final buildExtensions = const {
'.txt': ['.txt.copy']
};
@override
Future<void> build(BuildStep buildStep) async {
// Each `buildStep` has a single input.
var inputId = buildStep.inputId;
// Create a new target `AssetId` based on the old one.
var copy = inputId.addExtension('.copy');
var contents = await buildStep.readAsString(inputId);
// Write out the new asset.
await buildStep.writeAsString(copy, contents);
}
}
```
It should be noted that you should _never_ touch the file system directly. Go
through the `buildStep#readAsString` and `buildStep#writeAsString` methods in
order to read and write assets. This is what enables the package to track all of
your dependencies and do incremental rebuilds. It is also what enables your
[`Builder`][dartdoc:Builder] to run on different environments.
### Using the analyzer
If you need to do analyzer resolution, you can use the `BuildStep#resolver`
object. This makes sure that all `Builder`s in the system share the same
analysis context, which greatly speeds up the overall system when multiple
`Builder`s are doing resolution.
Here is an example of a `Builder` which uses the `resolve` method:
```dart
import 'package:build/build.dart';
class ResolvingCopyBuilder implements Builder {
// Take a `.dart` file as input so that the Resolver has code to resolve
@override
final buildExtensions = const {
'.dart': ['.dart.copy']
};
@override
Future<void> build(BuildStep buildStep) async {
// Get the `LibraryElement` for the primary input.
var entryLib = await buildStep.inputLibrary;
// Resolves all libraries reachable from the primary input.
var resolver = buildStep.resolver;
// Get a `LibraryElement` for another asset.
var libFromAsset = await resolver.libraryFor(
AssetId.resolve(Uri.parse('some_import.dart'),
from: buildStep.inputId));
// Or get a `LibraryElement` by name.
var libByName = await resolver.findLibraryByName('my.library');
}
}
```
Once you have gotten a `LibraryElement` using one of the methods on `Resolver`,
you are now just using the regular `analyzer` package to explore your app.
### Sharing expensive objects across build steps
The build package includes a `Resource` class, which can give you an instance
of an expensive object that is guaranteed to be unique across builds, but may
be re-used by multiple build steps within a single build (to the extent that
the implementation allows). It also gives you a way of disposing of your
resource at the end of its lifecycle.
The `Resource<T>` constructor takes a single required argument which is a
factory function that returns a `FutureOr<T>`. There is also a named argument
`dispose` which is called at the end of life for the resource, with the
instance that should be disposed. This returns a `FutureOr<dynamic>`.
So a simple example `Resource` would look like this:
```dart
final resource = Resource(
() => createMyExpensiveResource(),
dispose: (instance) async {
await instance.doSomeCleanup();
});
```
You can get an instance of the underlying resource by using the
`BuildStep#fetchResource` method, whose type signature looks like
`Future<T> fetchResource<T>(Resource<T>)`.
**Important Note**: It may be tempting to try and use a `Resource` instance to
cache information from previous build steps (or even assets), but this should
be avoided because it can break the soundness of the build, and may introduce
subtle bugs for incremental builds (remember the whole build doesn't run every
time!). The `build` package relies on the `BuildStep#canRead` and
`BuildStep#readAs*` methods to track build step dependencies, so sidestepping
those can and will break the dependency tracking, resulting in inconsistent and
stale assets.
## Features and bugs
Please file feature requests and bugs at the [issue tracker][tracker].
[tracker]: https://github.com/dart-lang/build/issues
[dartdoc:Builder]: https://pub.dev/documentation/build/latest/build/Builder-class.html
[dartdoc:BuildStep]: https://pub.dev/documentation/build/latest/build/BuildStep-class.html
[dartdoc:Resolver]: https://pub.dev/documentation/build/latest/build/Resolver-class.html
[pub:analyzer]: https://pub.dev/packages/analyzer