blob: 75bb4f85f04411de8ec6baef843a8f8137d85bc9 [file] [log] [blame]
// Copyright (c) 2018, 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:convert';
import 'dart:io';
import 'package:build/build.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as p;
import '../asset_graph/node.dart';
import '../util/constants.dart';
/// A tracker for the errors which have already been reported during the current
/// build.
///
/// Errors that occur due to actions that are run within this build will be
/// reported directly as they happen. When an action is skipped and remains a
/// failure the error will not have been reported by the time we check wether
/// the build is failed.
///
/// The lifetime of this class should be a single build.
class FailureReporter {
/// Removes all stored errors from previous builds.
///
/// This should be called any time the build phases change since the naming
/// scheme is dependent on the build phases.
static Future<void> cleanErrorCache() async {
final errorCacheDirectory = Directory(p.fromUri(errorCachePath));
if (await errorCacheDirectory.exists()) {
await errorCacheDirectory.delete(recursive: true);
}
}
/// Remove the stored error for [phaseNumber] runnon on [primaryInput].
///
/// This should be called anytime the action is being run.
static Future<void> clean(int phaseNumber, AssetId primaryInput) async {
final errorFile =
File(_errorPathForPrimaryInput(phaseNumber, primaryInput));
if (await errorFile.exists()) {
await errorFile.delete();
}
}
/// A set of Strings which uniquely identify a particular build action and
/// it's primary input.
final _reportedActions = <String>{};
/// Indicate that a failure reason for the build step which would produce
/// [output] and all other outputs from the same build step has been printed.
Future<void> markReported(String actionDescription, GeneratedAssetNode output,
Iterable<ErrorReport> errors) async {
if (!_reportedActions.add(_actionKey(output))) return;
final errorFile =
await File(_errorPathForOutput(output)).create(recursive: true);
await errorFile.writeAsString(jsonEncode(<dynamic>[actionDescription]
.followedBy(errors
.map((e) => [e.message, e.error, e.stackTrace?.toString() ?? '']))
.toList()));
}
/// Indicate that the build steps which would produce [outputs] are failing
/// due to a dependency and being skipped so no actuall error will be
/// produced.
Future<void> markSkipped(Iterable<GeneratedAssetNode> outputs) =>
Future.wait(outputs.map((output) async {
if (!_reportedActions.add(_actionKey(output))) return;
await clean(output.phaseNumber, output.primaryInput);
}));
/// Log stored errors for any build steps which would output nodes in
/// [failingNodes] which haven't already been reported.
Future<void> reportErrors(Iterable<GeneratedAssetNode> failingNodes) {
final errorFiles = <File>[];
for (final failure in failingNodes) {
final key = _actionKey(failure);
if (!_reportedActions.add(key)) continue;
errorFiles.add(File(_errorPathForOutput(failure)));
}
return Future.wait(errorFiles.map((errorFile) async {
if (await errorFile.exists()) {
final errorReports = jsonDecode(await errorFile.readAsString());
final actionDescription = '${(errorReports as List).first} (cached)';
final logger = Logger(actionDescription);
for (final List error in errorReports.skip(1)) {
final stackTraceString = error[2] as String;
final stackTrace = stackTraceString.isEmpty
? null
: StackTrace.fromString(stackTraceString);
logger.severe(error[0], error[1], stackTrace);
}
}
}));
}
}
/// Matches the call to [Logger.severe] except the [message] and [error] are
/// eagerly converted to String.
class ErrorReport {
final String message;
final String error;
final StackTrace stackTrace;
ErrorReport(this.message, this.error, this.stackTrace);
}
String _actionKey(GeneratedAssetNode node) =>
'${node.builderOptionsId} on ${node.primaryInput}';
String _errorPathForOutput(GeneratedAssetNode output) => p.join(
p.fromUri(errorCachePath),
output.id.package,
'${output.phaseNumber}',
p.fromUri(output.primaryInput.path));
String _errorPathForPrimaryInput(int phaseNumber, AssetId primaryInput) =>
p.join(p.fromUri(errorCachePath), primaryInput.package, '$phaseNumber',
p.fromUri(primaryInput.path));