blob: b6b83b022d6ea845e5ea68b71a0d1bfce81ade13 [file] [log] [blame]
// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'package:build/build.dart';
import 'package:build_config/build_config.dart';
import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
import 'input_matcher.dart';
/// A "phase" in the build graph, which represents running a one or more
/// builders on a set of sources.
abstract class BuildPhase {
/// Whether to run lazily when an output is read.
///
/// An optional build action will only run if one of its outputs is read by
/// a later [Builder], or is used as a primary input to a later [Builder].
///
/// If no build actions read the output of an optional action, then it will
/// never run.
bool get isOptional;
/// Whether generated assets should be placed in the build cache.
///
/// When this is `false` the Builder may not run on any package other than
/// the root.
bool get hideOutput;
/// The identity of this action in terms of a build graph. If the identity of
/// any action changes the build will be invalidated.
///
/// This should take into account everything except for the builderOptions,
/// which are tracked separately via a `BuilderOptionsNode` which supports
/// more fine grained invalidation.
int get identity;
}
/// An "action" in the build graph which represents running a single builder
/// on a set of sources.
abstract class BuildAction {
/// Either a [Builder] or a [PostProcessBuilder].
dynamic get builder;
String get builderLabel;
BuilderOptions get builderOptions;
InputMatcher get generateFor;
String get package;
InputMatcher get targetSources;
}
/// A [BuildPhase] that uses a single [Builder] to generate files.
class InBuildPhase extends BuildPhase implements BuildAction {
@override
final Builder builder;
@override
final String builderLabel;
@override
final BuilderOptions builderOptions;
@override
final InputMatcher generateFor;
@override
final String package;
@override
final InputMatcher targetSources;
@override
final bool isOptional;
@override
final bool hideOutput;
InBuildPhase._(this.package, this.builder, this.builderOptions,
{@required this.targetSources,
@required this.generateFor,
@required this.builderLabel,
bool isOptional,
bool hideOutput})
: isOptional = isOptional ?? false,
hideOutput = hideOutput ?? false;
/// Creates an [BuildPhase] for a normal [Builder].
///
/// The build target is defined by [package] as well as [targetSources]. By
/// default all sources in the target are used as primary inputs to the
/// builder, but it can be further filtered with [generateFor].
///
/// [isOptional] specifies that a Builder may not be run unless some other
/// Builder in a later phase attempts to read one of the potential outputs.
///
/// [hideOutput] specifies that the generated asses should be placed in the
/// build cache rather than the source tree.
factory InBuildPhase(
Builder builder,
String package, {
String builderKey,
InputSet targetSources,
InputSet generateFor,
BuilderOptions builderOptions,
bool isOptional,
bool hideOutput,
}) {
var targetSourceMatcher = InputMatcher(targetSources ?? const InputSet());
var generateForMatcher = InputMatcher(generateFor ?? const InputSet());
builderOptions ??= const BuilderOptions({});
return InBuildPhase._(package, builder, builderOptions,
targetSources: targetSourceMatcher,
generateFor: generateForMatcher,
builderLabel: builderKey == null || builderKey.isEmpty
? _builderLabel(builder)
: _simpleBuilderKey(builderKey),
isOptional: isOptional,
hideOutput: hideOutput);
}
@override
String toString() {
final settings = <String>[];
if (isOptional) settings.add('optional');
if (hideOutput) settings.add('hidden');
var result = '$builderLabel on $targetSources in $package';
if (settings.isNotEmpty) result += ' $settings';
return result;
}
@override
int get identity => _deepEquals.hash([
builderLabel,
builder.buildExtensions,
package,
targetSources,
generateFor,
isOptional,
hideOutput
]);
}
/// A [BuildPhase] that can run multiple [PostBuildAction]s to
/// generate files.
///
/// There should only be one of these per build, and it should be the final
/// phase.
class PostBuildPhase extends BuildPhase {
final List<PostBuildAction> builderActions;
@override
bool get hideOutput => true;
@override
bool get isOptional => false;
PostBuildPhase(this.builderActions);
@override
String toString() =>
'${builderActions.map((a) => a.builderLabel).join(', ')}';
@override
int get identity =>
_deepEquals.hash(builderActions.map<dynamic>((b) => b.identity).toList()
..addAll([
isOptional,
hideOutput,
]));
}
/// Part of a larger [PostBuildPhase], applies a single
/// [PostProcessBuilder] to a single [package] with some additional options.
class PostBuildAction implements BuildAction {
@override
final PostProcessBuilder builder;
@override
final String builderLabel;
@override
final BuilderOptions builderOptions;
@override
final InputMatcher generateFor;
@override
final String package;
@override
final InputMatcher targetSources;
PostBuildAction(this.builder, this.package,
{String builderKey,
@required BuilderOptions builderOptions,
@required InputSet targetSources,
@required InputSet generateFor})
: builderLabel = builderKey == null || builderKey.isEmpty
? _builderLabel(builder)
: _simpleBuilderKey(builderKey),
builderOptions = builderOptions ?? const BuilderOptions({}),
targetSources = InputMatcher(targetSources ?? const InputSet()),
generateFor = InputMatcher(generateFor ?? const InputSet());
int get identity => _deepEquals.hash([
builderLabel,
builder.inputExtensions.toList(),
generateFor,
package,
targetSources,
]);
}
/// If we have no key find a human friendly name for the Builder.
String _builderLabel(Object builder) {
var label = '$builder';
if (label.startsWith('Instance of \'')) {
label = label.substring(13, label.length - 1);
}
return label;
}
/// Change "angular|angular" to "angular".
String _simpleBuilderKey(String builderKey) {
if (!builderKey.contains('|')) return builderKey;
var parts = builderKey.split('|');
if (parts[0] == parts[1]) return parts[0];
return builderKey;
}
final _deepEquals = const DeepCollectionEquality();