blob: c1325e4711d8e0d2e28760a975266f224bf993ea [file] [log] [blame]
// Copyright (c) 2019, 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 'package:build/build.dart';
import 'package:build_config/build_config.dart';
import 'package:build_runner_core/build_runner_core.dart';
import 'package:build_runner_core/src/generate/phase.dart';
import 'package:io/io.dart';
import 'package:logging/logging.dart';
import '../logging/std_io_logging.dart';
import '../package_graph/build_config_overrides.dart';
import 'base_command.dart';
/// A command that validates the build environment.
class DoctorCommand extends BuildRunnerCommand {
@override
String get name => 'doctor';
@override
bool get hidden => true;
@override
String get description => 'Check for misconfiguration of the build.';
@override
Future<int> run() async {
final options = readOptions();
final verbose = options.verbose ?? false;
Logger.root.level = verbose ? Level.ALL : Level.INFO;
final logSubscription =
Logger.root.onRecord.listen(stdIOLogListener(verbose: verbose));
final config = await _loadBuilderDefinitions();
var isOk = true;
for (final builderApplication in builderApplications) {
final builderOk = _checkBuildExtensions(builderApplication, config);
isOk = isOk && builderOk;
}
if (isOk) {
logger.info('No problems found!\n');
}
await logSubscription.cancel();
return isOk ? ExitCode.success.code : ExitCode.config.code;
}
Future<Map<String, BuilderDefinition>> _loadBuilderDefinitions() async {
final packageGraph = PackageGraph.forThisPackage();
final buildConfigOverrides =
await findBuildConfigOverrides(packageGraph, null);
Future<BuildConfig> _packageBuildConfig(PackageNode package) async {
if (buildConfigOverrides.containsKey(package.name)) {
return buildConfigOverrides[package.name];
}
try {
return await BuildConfig.fromBuildConfigDir(package.name,
package.dependencies.map((n) => n.name), package.path);
} on ArgumentError catch (e) {
logger.severe(
'Failed to parse a `build.yaml` file for ${package.name}', e);
return BuildConfig.useDefault(
package.name, package.dependencies.map((n) => n.name));
}
}
final allConfig = await Future.wait(
packageGraph.allPackages.values.map(_packageBuildConfig));
final allBuilders = <String, BuilderDefinition>{};
for (final config in allConfig) {
allBuilders.addAll(config.builderDefinitions);
}
return allBuilders;
}
/// Returns true of [builderApplication] has sane build extension
/// configuration.
///
/// If there are any problems they will be logged and `false` returned.
bool _checkBuildExtensions(BuilderApplication builderApplication,
Map<String, BuilderDefinition> config) {
var phases = builderApplication.buildPhaseFactories
.map((f) => f(PackageNode(null, null, null, isRoot: true),
BuilderOptions.empty, InputSet.anything, InputSet.anything, true))
.whereType<InBuildPhase>()
.toList();
if (phases.isEmpty) return true;
if (!config.containsKey(builderApplication.builderKey)) return false;
var problemFound = false;
var allowed = Map.of(config[builderApplication.builderKey].buildExtensions);
for (final phase in phases.whereType<InBuildPhase>()) {
final extensions = phase.builder.buildExtensions;
for (final extension in extensions.entries) {
if (!allowed.containsKey(extension.key)) {
logger.warning('Builder ${builderApplication.builderKey} '
'uses input extension ${extension.key} '
'which is not specified in the `build.yaml`');
problemFound = true;
continue;
}
final allowedOutputs = List.of(allowed[extension.key]);
for (final output in extension.value) {
if (!allowedOutputs.contains(output)) {
logger.warning('Builder ${builderApplication.builderKey} '
'outputs $output from ${extension.key} '
'which is not specified in the `build.yaml`');
problemFound = true;
}
// Allow subsequent phases to use these outputs as inputs
if (allowedOutputs.length > 1) {
allowed.putIfAbsent(output, () => []).addAll(allowedOutputs);
}
}
}
}
return !problemFound;
}
}