blob: 2cf9736ee739ae25ece6f66e9e66e3c0987fb945 [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 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:typed_data';
import 'package:build/build.dart';
import 'package:crypto/crypto.dart';
import 'package:glob/glob.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as p;
import 'package:pedantic/pedantic.dart';
import 'package:pool/pool.dart';
import 'package:watcher/watcher.dart';
import '../asset/cache.dart';
import '../asset/finalized_reader.dart';
import '../asset/reader.dart';
import '../asset/writer.dart';
import '../asset_graph/graph.dart';
import '../asset_graph/node.dart';
import '../asset_graph/optional_output_tracker.dart';
import '../changes/build_script_updates.dart';
import '../environment/build_environment.dart';
import '../logging/build_for_input_logger.dart';
import '../logging/failure_reporter.dart';
import '../logging/human_readable_duration.dart';
import '../logging/logging.dart';
import '../package_graph/apply_builders.dart';
import '../package_graph/package_graph.dart';
import '../performance_tracking/performance_tracking_resolvers.dart';
import '../util/async.dart';
import '../util/build_dirs.dart';
import '../util/constants.dart';
import 'build_definition.dart';
import 'build_directory.dart';
import 'build_result.dart';
import 'finalized_assets_view.dart';
import 'heartbeat.dart';
import 'options.dart';
import 'performance_tracker.dart';
import 'phase.dart';
final _logger = Logger('Build');
List<String> _buildPaths(Set<BuildDirectory> buildDirs) =>
// The empty string means build everything.
buildDirs.any((b) => b.directory == '')
? []
: buildDirs.map((b) => b.directory).toList();
class BuildImpl {
final FinalizedReader _finalizedReader;
FinalizedReader get finalizedReader => _finalizedReader;
final AssetGraph assetGraph;
final BuildScriptUpdates _buildScriptUpdates;
BuildScriptUpdates get buildScriptUpdates => _buildScriptUpdates;
final List<BuildPhase> _buildPhases;
final PackageGraph _packageGraph;
final AssetReader _reader;
final Resolvers _resolvers;
final ResourceManager _resourceManager;
final RunnerAssetWriter _writer;
final bool _trackPerformance;
final bool _verbose;
final BuildEnvironment _environment;
final String _logPerformanceDir;
Future<void> beforeExit() => _resourceManager.beforeExit();
BuildImpl._(BuildDefinition buildDefinition, BuildOptions options,
this._buildPhases, this._finalizedReader)
: _buildScriptUpdates = buildDefinition.buildScriptUpdates,
_packageGraph = buildDefinition.packageGraph,
_reader = options.enableLowResourcesMode
? buildDefinition.reader
: CachingAssetReader(buildDefinition.reader),
_resolvers = options.resolvers,
_writer = buildDefinition.writer,
assetGraph = buildDefinition.assetGraph,
_resourceManager = buildDefinition.resourceManager,
_verbose = options.verbose,
_environment = buildDefinition.environment,
_trackPerformance = options.trackPerformance,
_logPerformanceDir = options.logPerformanceDir;
Future<BuildResult> run(Map<AssetId, ChangeType> updates,
{Set<BuildDirectory> buildDirs}) {
buildDirs ??= Set<BuildDirectory>();
finalizedReader.reset(_buildPaths(buildDirs));
return _SingleBuild(this, buildDirs).run(updates)
..whenComplete(_resolvers.reset);
}
static Future<BuildImpl> create(
BuildOptions options,
BuildEnvironment environment,
List<BuilderApplication> builders,
Map<String, Map<String, dynamic>> builderConfigOverrides,
{bool isReleaseBuild = false}) async {
// Don't allow any changes to the generated asset directory after this
// point.
lockGeneratedOutputDirectory();
var buildPhases = await createBuildPhases(
options.targetGraph, builders, builderConfigOverrides, isReleaseBuild);
if (buildPhases.isEmpty) {
_logger.severe('Nothing can be built, yet a build was requested.');
}
var buildDefinition = await BuildDefinition.prepareWorkspace(
environment, options, buildPhases);
var singleStepReader = SingleStepReader(
buildDefinition.reader,
buildDefinition.assetGraph,
buildPhases.length,
options.packageGraph.root.name,
_isReadableAfterBuildFactory(buildPhases));
var finalizedReader = FinalizedReader(
singleStepReader,
buildDefinition.assetGraph,
buildPhases,
options.packageGraph.root.name);
var build =
BuildImpl._(buildDefinition, options, buildPhases, finalizedReader);
return build;
}
static bool Function(AssetNode, int, String) _isReadableAfterBuildFactory(
List<BuildPhase> buildPhases) =>
(AssetNode node, int phaseNum, String package) {
if (node is GeneratedAssetNode) {
return node.wasOutput && !node.isFailure;
}
return node.isReadable && node.isValidInput;
};
}
/// Performs a single build and manages state that only lives for a single
/// build.
class _SingleBuild {
final AssetGraph _assetGraph;
final List<BuildPhase> _buildPhases;
final List<Pool> _buildPhasePool;
final BuildEnvironment _environment;
final _lazyPhases = <String, Future<Iterable<AssetId>>>{};
final _lazyGlobs = <AssetId, Future<void>>{};
final PackageGraph _packageGraph;
final BuildPerformanceTracker _performanceTracker;
final AssetReader _reader;
final Resolvers _resolvers;
final ResourceManager _resourceManager;
final bool _verbose;
final RunnerAssetWriter _writer;
final Set<BuildDirectory> _buildDirs;
final String _logPerformanceDir;
final _failureReporter = FailureReporter();
int actionsCompletedCount = 0;
int actionsStartedCount = 0;
final pendingActions = SplayTreeMap<int, Set<String>>();
/// Can't be final since it needs access to [pendingActions].
HungActionsHeartbeat hungActionsHeartbeat;
_SingleBuild(BuildImpl buildImpl, Set<BuildDirectory> buildDirs)
: _assetGraph = buildImpl.assetGraph,
_buildPhases = buildImpl._buildPhases,
_buildPhasePool = List(buildImpl._buildPhases.length),
_environment = buildImpl._environment,
_packageGraph = buildImpl._packageGraph,
_performanceTracker = buildImpl._trackPerformance
? BuildPerformanceTracker()
: BuildPerformanceTracker.noOp(),
_reader = buildImpl._reader,
_resolvers = buildImpl._resolvers,
_resourceManager = buildImpl._resourceManager,
_verbose = buildImpl._verbose,
_writer = buildImpl._writer,
_buildDirs = buildDirs,
_logPerformanceDir = buildImpl._logPerformanceDir {
hungActionsHeartbeat = HungActionsHeartbeat(() {
final message = StringBuffer();
const actionsToLogMax = 5;
var descriptions = pendingActions.values.fold(
<String>[],
(combined, actions) =>
combined..addAll(actions)).take(actionsToLogMax);
for (final description in descriptions) {
message.writeln(' - $description');
}
var additionalActionsCount =
actionsStartedCount - actionsCompletedCount - actionsToLogMax;
if (additionalActionsCount > 0) {
message.writeln(' .. and $additionalActionsCount more');
}
return '$message';
});
}
Future<BuildResult> run(Map<AssetId, ChangeType> updates) async {
var watch = Stopwatch()..start();
var result = await _safeBuild(updates);
var optionalOutputTracker = OptionalOutputTracker(
_assetGraph, _buildPaths(_buildDirs), _buildPhases);
if (result.status == BuildStatus.success) {
final failures = _assetGraph.failedOutputs
.where((n) => optionalOutputTracker.isRequired(n.id));
if (failures.isNotEmpty) {
await _failureReporter.reportErrors(failures);
result = BuildResult(BuildStatus.failure, result.outputs,
performance: result.performance);
}
}
await _resourceManager.disposeAll();
result = await _environment.finalizeBuild(
result,
FinalizedAssetsView(_assetGraph, optionalOutputTracker),
_reader,
_buildDirs);
if (result.status == BuildStatus.success) {
_logger.info('Succeeded after ${humanReadable(watch.elapsed)} with '
'${result.outputs.length} outputs '
'($actionsCompletedCount actions)\n');
} else {
_logger.severe('Failed after ${humanReadable(watch.elapsed)}');
}
return result;
}
Future<void> _updateAssetGraph(Map<AssetId, ChangeType> updates) async {
await logTimedAsync(_logger, 'Updating asset graph', () async {
var invalidated = await _assetGraph.updateAndInvalidate(
_buildPhases, updates, _packageGraph.root.name, _delete, _reader);
if (_reader is CachingAssetReader) {
(_reader as CachingAssetReader).invalidate(invalidated);
}
});
}
/// Runs a build inside a zone with an error handler and stack chain
/// capturing.
Future<BuildResult> _safeBuild(Map<AssetId, ChangeType> updates) {
var done = Completer<BuildResult>();
var heartbeat = HeartbeatLogger(
transformLog: (original) => '$original, ${_buildProgress()}',
waitDuration: Duration(seconds: 1))
..start();
hungActionsHeartbeat.start();
done.future.whenComplete(() {
heartbeat.stop();
hungActionsHeartbeat.stop();
});
runZoned(() async {
if (updates.isNotEmpty) {
await _updateAssetGraph(updates);
}
// Run a fresh build.
var result = await logTimedAsync(_logger, 'Running build', _runPhases);
// Write out the dependency graph file.
await logTimedAsync(_logger, 'Caching finalized dependency graph',
() async {
await _writer.writeAsBytes(
AssetId(_packageGraph.root.name, assetGraphPath),
_assetGraph.serialize());
});
// Log performance information if requested
if (_logPerformanceDir != null) {
assert(result.performance != null);
var logPath =
p.join(_logPerformanceDir, DateTime.now().toIso8601String());
await logTimedAsync(_logger, 'Writing performance log to $logPath', () {
var performanceLogId = AssetId(_packageGraph.root.name, logPath);
var serialized = jsonEncode(result.performance);
return _writer.writeAsString(performanceLogId, serialized);
});
}
if (!done.isCompleted) done.complete(result);
}, onError: (e, StackTrace st) {
if (!done.isCompleted) {
_logger.severe('Unhandled build failure!', e, st);
done.complete(BuildResult(BuildStatus.failure, []));
}
});
return done.future;
}
/// Returns a message describing the progress of the current build.
String _buildProgress() =>
'$actionsCompletedCount/$actionsStartedCount actions completed.';
/// Runs the actions in [_buildPhases] and returns a [Future<BuildResult>]
/// which completes once all [BuildPhase]s are done.
Future<BuildResult> _runPhases() {
return _performanceTracker.track(() async {
final outputs = <AssetId>[];
for (var phaseNum = 0; phaseNum < _buildPhases.length; phaseNum++) {
var phase = _buildPhases[phaseNum];
if (phase.isOptional) continue;
outputs
.addAll(await _performanceTracker.trackBuildPhase(phase, () async {
if (phase is InBuildPhase) {
var primaryInputs =
await _matchingPrimaryInputs(phase.package, phaseNum);
return _runBuilder(phaseNum, phase, primaryInputs);
} else if (phase is PostBuildPhase) {
return _runPostProcessPhase(phaseNum, phase);
} else {
throw StateError('Unrecognized BuildPhase type $phase');
}
}));
}
await Future.forEach(
_lazyPhases.values,
(Future<Iterable<AssetId>> lazyOuts) async =>
outputs.addAll(await lazyOuts));
// Assume success, `_assetGraph.failedOutputs` will be checked later.
return BuildResult(BuildStatus.success, outputs,
performance: _performanceTracker);
});
}
/// Gets a list of all inputs matching the [phaseNumber], as well as
/// its [Builder]s primary inputs.
///
/// Lazily builds any optional build actions that might potentially produce
/// a primary input to this phase.
Future<Set<AssetId>> _matchingPrimaryInputs(
String package, int phaseNumber) async {
var ids = Set<AssetId>();
var phase = _buildPhases[phaseNumber];
await Future.wait(
_assetGraph.outputsForPhase(package, phaseNumber).map((node) async {
if (!shouldBuildForDirs(node.id, _buildPaths(_buildDirs), phase)) {
return;
}
var input = _assetGraph.get(node.primaryInput);
if (input is GeneratedAssetNode) {
if (input.state != NodeState.upToDate) {
await _runLazyPhaseForInput(input.phaseNumber, input.primaryInput);
}
if (!input.wasOutput) return;
if (input.isFailure) return;
}
ids.add(input.id);
}));
return ids;
}
/// Runs a normal builder with [primaryInputs] as inputs and returns only the
/// outputs that were newly created.
///
/// Does not return outputs that didn't need to be re-ran or were declared
/// but not output.
Future<Iterable<AssetId>> _runBuilder(int phaseNumber, InBuildPhase action,
Iterable<AssetId> primaryInputs) async {
var outputLists = await Future.wait(
primaryInputs.map((input) => _runForInput(phaseNumber, action, input)));
return outputLists.fold<List<AssetId>>(
<AssetId>[], (combined, next) => combined..addAll(next));
}
/// Lazily runs [phaseNumber] with [input]..
Future<Iterable<AssetId>> _runLazyPhaseForInput(
int phaseNumber, AssetId input) {
return _lazyPhases.putIfAbsent('$phaseNumber|$input', () async {
// First check if `input` is generated, and whether or not it was
// actually output. If it wasn't then we just return an empty list here.
var inputNode = _assetGraph.get(input);
if (inputNode is GeneratedAssetNode) {
// Make sure the `inputNode` is up to date, and rebuild it if not.
if (inputNode.state != NodeState.upToDate) {
await _runLazyPhaseForInput(
inputNode.phaseNumber, inputNode.primaryInput);
}
if (!inputNode.wasOutput || inputNode.isFailure) return <AssetId>[];
}
// We can never lazily build `PostProcessBuildAction`s.
var action = _buildPhases[phaseNumber] as InBuildPhase;
return _runForInput(phaseNumber, action, input);
});
}
/// Checks whether [node] can be read by this step - attempting to build the
/// asset if necessary.
FutureOr<bool> _isReadableNode(
AssetNode node, int phaseNum, String fromPackage) {
if (node is GeneratedAssetNode) {
if (node.phaseNumber >= phaseNum) return false;
return doAfter(
_ensureAssetIsBuilt(node), (_) => node.wasOutput && !node.isFailure);
}
return node.isReadable && node.isValidInput;
}
FutureOr<void> _ensureAssetIsBuilt(AssetNode node) {
if (node is GeneratedAssetNode && node.state != NodeState.upToDate) {
return _runLazyPhaseForInput(node.phaseNumber, node.primaryInput);
}
return null;
}
Future<Iterable<AssetId>> _runForInput(
int phaseNumber, InBuildPhase phase, AssetId input) {
var pool = _buildPhasePool[phaseNumber] ??= Pool(buildPhasePoolSize);
return pool.withResource(() {
final builder = phase.builder;
var tracker =
_performanceTracker.addBuilderAction(input, phase.builderLabel);
return tracker.track(() async {
var builderOutputs = expectedOutputs(builder, input);
// Add `builderOutputs` to the primary outputs of the input.
var inputNode = _assetGraph.get(input);
assert(inputNode != null,
'Inputs should be known in the static graph. Missing $input');
assert(
inputNode.primaryOutputs.containsAll(builderOutputs),
'input $input with builder $builder missing primary outputs: \n'
'Got ${inputNode.primaryOutputs.join(', ')} '
'which was missing:\n' +
builderOutputs
.where((id) => !inputNode.primaryOutputs.contains(id))
.join(', '));
var wrappedReader = SingleStepReader(_reader, _assetGraph, phaseNumber,
input.package, _isReadableNode, _getUpdatedGlobNode);
if (!await tracker.trackStage(
'Setup', () => _buildShouldRun(builderOutputs, wrappedReader))) {
return <AssetId>[];
}
await _cleanUpStaleOutputs(builderOutputs);
await FailureReporter.clean(phaseNumber, input);
// We may have read some inputs in the call to `_buildShouldRun`, we want
// to remove those.
wrappedReader.assetsRead.clear();
var wrappedWriter = AssetWriterSpy(_writer);
var actionDescription =
_actionLoggerName(phase, input, _packageGraph.root.name);
var logger = BuildForInputLogger(Logger(actionDescription));
actionsStartedCount++;
pendingActions
.putIfAbsent(phaseNumber, () => Set<String>())
.add(actionDescription);
await tracker.trackStage(
'Build',
() => runBuilder(builder, [input], wrappedReader, wrappedWriter,
PerformanceTrackingResolvers(_resolvers, tracker),
logger: logger,
resourceManager: _resourceManager,
stageTracker: tracker)
.catchError((_) {
// Errors tracked through the logger
}));
actionsCompletedCount++;
hungActionsHeartbeat.ping();
pendingActions[phaseNumber].remove(actionDescription);
// Reset the state for all the `builderOutputs` nodes based on what was
// read and written.
await tracker.trackStage(
'Finalize',
() => _setOutputsState(builderOutputs, wrappedReader, wrappedWriter,
actionDescription, logger.errorsSeen));
return wrappedWriter.assetsWritten;
});
});
}
Future<Iterable<AssetId>> _runPostProcessPhase(
int phaseNum, PostBuildPhase phase) async {
var actionNum = 0;
var outputLists = await Future.wait(phase.builderActions
.map((action) => _runPostProcessAction(phaseNum, actionNum++, action)));
return outputLists.fold<List<AssetId>>(
<AssetId>[], (combined, next) => combined..addAll(next));
}
Future<Iterable<AssetId>> _runPostProcessAction(
int phaseNum, int actionNum, PostBuildAction action) async {
var anchorNodes = _assetGraph.packageNodes(action.package).where((node) {
if (node is PostProcessAnchorNode && node.actionNumber == actionNum) {
var inputNode = _assetGraph.get(node.primaryInput);
if (inputNode is SourceAssetNode) {
return true;
} else if (inputNode is GeneratedAssetNode) {
return inputNode.wasOutput &&
!inputNode.isFailure &&
inputNode.state == NodeState.upToDate;
}
}
return false;
}).cast<PostProcessAnchorNode>();
var outputLists = await Future.wait(anchorNodes.map((anchorNode) =>
_runPostProcessBuilderForAnchor(
phaseNum, actionNum, action.builder, anchorNode)));
return outputLists.fold<List<AssetId>>(
<AssetId>[], (combined, next) => combined..addAll(next));
}
Future<Iterable<AssetId>> _runPostProcessBuilderForAnchor(
int phaseNum,
int actionNum,
PostProcessBuilder builder,
PostProcessAnchorNode anchorNode) async {
var input = anchorNode.primaryInput;
var inputNode = _assetGraph.get(input);
assert(inputNode != null,
'Inputs should be known in the static graph. Missing $input');
var wrappedReader = SingleStepReader(
_reader, _assetGraph, phaseNum, input.package, _isReadableNode);
if (!await _postProcessBuildShouldRun(anchorNode, wrappedReader)) {
return <AssetId>[];
}
// We may have read some inputs in the call to `_buildShouldRun`, we want
// to remove those.
wrappedReader.assetsRead.clear();
// Clean out the impacts of the previous run
await FailureReporter.clean(phaseNum, input);
await _cleanUpStaleOutputs(anchorNode.outputs);
anchorNode.outputs
..toList().forEach(_assetGraph.remove)
..clear();
inputNode.deletedBy.remove(anchorNode.id);
var wrappedWriter = AssetWriterSpy(_writer);
var actionDescription = '$builder on $input';
var logger = BuildForInputLogger(Logger(actionDescription));
actionsStartedCount++;
pendingActions
.putIfAbsent(phaseNum, () => Set<String>())
.add(actionDescription);
await runPostProcessBuilder(
builder, input, wrappedReader, wrappedWriter, logger,
addAsset: (assetId) {
if (_assetGraph.contains(assetId)) {
throw InvalidOutputException(assetId, 'Asset already exists');
}
var node = GeneratedAssetNode(assetId,
primaryInput: input,
builderOptionsId: anchorNode.builderOptionsId,
isHidden: true,
phaseNumber: phaseNum,
wasOutput: true,
isFailure: false,
state: NodeState.upToDate);
_assetGraph.add(node);
anchorNode.outputs.add(assetId);
}, deleteAsset: (assetId) {
if (!_assetGraph.contains(assetId)) {
throw AssetNotFoundException(assetId);
}
if (assetId != input) {
throw InvalidOutputException(assetId, 'Can only delete primary input');
}
_assetGraph.get(assetId).deletedBy.add(anchorNode.id);
}).catchError((_) {
// Errors tracked through the logger
});
actionsCompletedCount++;
hungActionsHeartbeat.ping();
pendingActions[phaseNum].remove(actionDescription);
var assetsWritten = wrappedWriter.assetsWritten.toSet();
// Reset the state for all the output nodes based on what was read and
// written.
inputNode.primaryOutputs.addAll(assetsWritten);
await _setOutputsState(assetsWritten, wrappedReader, wrappedWriter,
actionDescription, logger.errorsSeen);
return assetsWritten;
}
/// Checks and returns whether any [outputs] need to be updated.
Future<bool> _buildShouldRun(
Iterable<AssetId> outputs, AssetReader reader) async {
assert(
outputs.every(_assetGraph.contains),
'Outputs should be known statically. Missing '
'${outputs.where((o) => !_assetGraph.contains(o)).toList()}');
assert(outputs.isNotEmpty, 'Can\'t run a build with no outputs');
// We only check the first output, because all outputs share the same inputs
// and invalidation state.
var firstOutput = outputs.first;
var node = _assetGraph.get(firstOutput) as GeneratedAssetNode;
assert(
outputs.skip(1).every((output) =>
(_assetGraph.get(output) as GeneratedAssetNode)
.inputs
.difference(node.inputs)
.isEmpty),
'All outputs of a build action should share the same inputs.');
// No need to build an up to date output
if (node.state == NodeState.upToDate) return false;
// Early bail out condition, this is a forced update.
if (node.state == NodeState.definitelyNeedsUpdate) return true;
// This is a fresh build or the first time we've seen this output.
if (node.previousInputsDigest == null) return true;
var digest = await _computeCombinedDigest(
node.inputs, node.builderOptionsId, reader);
if (digest != node.previousInputsDigest) {
return true;
} else {
// Make sure to update the `state` field for all outputs.
for (var id in outputs) {
(_assetGraph.get(id) as NodeWithInputs).state = NodeState.upToDate;
}
return false;
}
}
/// Checks if a post process build should run based on [anchorNode].
Future<bool> _postProcessBuildShouldRun(
PostProcessAnchorNode anchorNode, AssetReader reader) async {
var inputsDigest = await _computeCombinedDigest(
[anchorNode.primaryInput], anchorNode.builderOptionsId, reader);
if (inputsDigest != anchorNode.previousInputsDigest) {
anchorNode.previousInputsDigest = inputsDigest;
return true;
}
return false;
}
/// Deletes any of [outputs] which previously were output.
///
/// This should be called after deciding that an asset really needs to be
/// regenerated based on its inputs hash changing. All assets in [outputs]
/// must correspond to a [GeneratedAssetNode].
Future<void> _cleanUpStaleOutputs(Iterable<AssetId> outputs) =>
Future.wait(outputs
.map(_assetGraph.get)
.cast<GeneratedAssetNode>()
.where((n) => n.wasOutput)
.map((n) => _delete(n.id)));
Future<GlobAssetNode> _getUpdatedGlobNode(
Glob glob, String package, int phaseNum) {
var globNodeId = GlobAssetNode.createId(package, glob, phaseNum);
var globNode = _assetGraph.get(globNodeId) as GlobAssetNode;
if (globNode == null) {
globNode = GlobAssetNode(
globNodeId, glob, phaseNum, NodeState.definitelyNeedsUpdate);
_assetGraph.add(globNode);
}
return toFuture(
doAfter(_updateGlobNodeIfNecessary(globNode), (_) => globNode));
}
FutureOr<void> _updateGlobNodeIfNecessary(GlobAssetNode globNode) {
if (globNode.state == NodeState.upToDate) return null;
return _lazyGlobs.putIfAbsent(globNode.id, () async {
var potentialNodes = _assetGraph
.packageNodes(globNode.id.package)
.where((n) => n.isReadable && n.isValidInput)
.where((n) =>
n is! GeneratedAssetNode ||
(n as GeneratedAssetNode).phaseNumber < globNode.phaseNumber)
.where((n) => globNode.glob.matches(n.id.path))
.toList();
await Future.wait(potentialNodes
.whereType<GeneratedAssetNode>()
.map(_ensureAssetIsBuilt)
.map(toFuture));
var actualMatches = <AssetId>[];
for (var node in potentialNodes) {
node.outputs.add(globNode.id);
if (node is GeneratedAssetNode && (!node.wasOutput || node.isFailure)) {
continue;
}
actualMatches.add(node.id);
}
globNode
..results = actualMatches
..inputs = HashSet.of(potentialNodes.map((n) => n.id))
..state = NodeState.upToDate
..lastKnownDigest =
md5.convert(utf8.encode(globNode.results.join(' ')));
unawaited(_lazyGlobs.remove(globNode.id));
});
}
/// Computes a single [Digest] based on the combined [Digest]s of [ids] and
/// [builderOptionsId].
Future<Digest> _computeCombinedDigest(Iterable<AssetId> ids,
AssetId builderOptionsId, AssetReader reader) async {
var combinedBytes = Uint8List.fromList(List.filled(16, 0));
void _combine(Uint8List other) {
assert(other.length == 16);
assert(other is Uint8List);
for (var i = 0; i < 16; i++) {
combinedBytes[i] ^= other[i];
}
}
var builderOptionsNode = _assetGraph.get(builderOptionsId);
_combine(builderOptionsNode.lastKnownDigest.bytes as Uint8List);
// Limit the total number of digests we are computing at a time. Otherwise
// this can overload the event queue.
await Future.wait(ids.map((id) async {
var node = _assetGraph.get(id);
if (node is GlobAssetNode) {
await _updateGlobNodeIfNecessary(node);
} else if (!await reader.canRead(id)) {
// We want to add something here, a missing/unreadable input should be
// different from no input at all.
//
// This needs to be unique per input so we use the md5 hash of the id.
_combine(md5.convert(id.toString().codeUnits).bytes as Uint8List);
return;
} else {
node.lastKnownDigest ??= await reader.digest(id);
}
_combine(node.lastKnownDigest.bytes as Uint8List);
}));
return Digest(combinedBytes);
}
/// Sets the state for all [outputs] of a build step, by:
///
/// - Setting `needsUpdate` to `false` for each output
/// - Setting `wasOutput` based on `writer.assetsWritten`.
/// - Setting `isFailed` based on action success.
/// - Adding `outputs` as outputs to all `reader.assetsRead`.
/// - Setting the `lastKnownDigest` on each output based on the new contents.
/// - Setting the `previousInputsDigest` on each output based on the inputs.
/// - Storing the error message with the [_failureReporter].
Future<void> _setOutputsState(
Iterable<AssetId> outputs,
SingleStepReader reader,
AssetWriterSpy writer,
String actionDescription,
Iterable<ErrorReport> errors) async {
if (outputs.isEmpty) return;
final inputsDigest = await _computeCombinedDigest(
reader.assetsRead,
(_assetGraph.get(outputs.first) as GeneratedAssetNode).builderOptionsId,
reader);
final isFailure = errors.isNotEmpty;
for (var output in outputs) {
var wasOutput = writer.assetsWritten.contains(output);
var digest = wasOutput ? await _reader.digest(output) : null;
var node = _assetGraph.get(output) as GeneratedAssetNode;
// **IMPORTANT**: All updates to `node` must be synchronous. With lazy
// builders we can run arbitrary code between updates otherwise, at which
// time a node might not be in a valid state.
_removeOldInputs(node, reader.assetsRead);
_addNewInputs(node, reader.assetsRead);
node
..state = NodeState.upToDate
..wasOutput = wasOutput
..isFailure = isFailure
..lastKnownDigest = digest
..previousInputsDigest = inputsDigest;
if (isFailure) {
await _failureReporter.markReported(actionDescription, node, errors);
var needsMarkAsFailure = Queue.of(node.primaryOutputs);
var allSkippedFailures = <GeneratedAssetNode>[];
while (needsMarkAsFailure.isNotEmpty) {
var output = needsMarkAsFailure.removeLast();
var outputNode = _assetGraph.get(output) as GeneratedAssetNode
..state = NodeState.upToDate
..wasOutput = false
..isFailure = true
..lastKnownDigest = null
..previousInputsDigest = null;
allSkippedFailures.add(outputNode);
needsMarkAsFailure.addAll(outputNode.primaryOutputs);
// Make sure output invalidation follows primary outputs for builds
// that won't run.
node.outputs.add(output);
outputNode.inputs.add(node.id);
}
await _failureReporter.markSkipped(allSkippedFailures);
}
}
}
/// Removes old inputs from [node] based on [updatedInputs], and cleans up all
/// the old edges.
void _removeOldInputs(GeneratedAssetNode node, Set<AssetId> updatedInputs) {
var removedInputs = node.inputs.difference(updatedInputs);
node.inputs.removeAll(removedInputs);
for (var input in removedInputs) {
var inputNode = _assetGraph.get(input);
assert(inputNode != null, 'Asset Graph is missing $input');
inputNode.outputs.remove(node.id);
}
}
/// Adds new inputs to [node] based on [updatedInputs], and adds the
/// appropriate edges.
void _addNewInputs(GeneratedAssetNode node, Set<AssetId> updatedInputs) {
var newInputs = updatedInputs.difference(node.inputs);
node.inputs.addAll(newInputs);
for (var input in newInputs) {
var inputNode = _assetGraph.get(input);
assert(inputNode != null, 'Asset Graph is missing $input');
inputNode.outputs.add(node.id);
}
}
Future _delete(AssetId id) => _writer.delete(id);
}
String _actionLoggerName(
InBuildPhase phase, AssetId primaryInput, String rootPackageName) {
var asset = primaryInput.package == rootPackageName
? primaryInput.path
: primaryInput.uri;
return '${phase.builderLabel} on $asset';
}