blob: dc6887f74afed71f740dfe7cc9a3c637cc9d4e5f [file] [log] [blame]
// Copyright 2013 The Flutter Authors. 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:convert';
import 'dart:io';
import 'package:engine_build_configs/engine_build_configs.dart';
import 'package:path/path.dart' as p;
import 'package:process_runner/process_runner.dart';
import 'build_utils.dart';
import 'environment.dart';
import 'json_utils.dart';
import 'proc_utils.dart';
/// Canonicalized build targets start with this prefix.
const String buildTargetPrefix = '//';
/// A suffix to build targets that recursively selects all child build targets.
const String _buildTargetGlobSuffix = '/...';
/// The type of a BuildTarget
enum BuildTargetType {
/// Produces an executable program.
executable,
/// Produces a shared library.
sharedLibrary,
/// Produces a static library.
staticLibrary,
}
BuildTargetType? _buildTargetTypeFromString(String type) {
switch (type) {
case 'executable':
return BuildTargetType.executable;
case 'shared_library':
return BuildTargetType.sharedLibrary;
case 'static_library':
return BuildTargetType.staticLibrary;
default:
// We ignore a number of types here.
return null;
}
}
// TODO(johnmccutchan): What should we do about source_sets and other
// output-less targets? Also, what about action targets which are kind of
// "internal" build steps? For now we are ignoring them.
/// Information about a build target.
final class BuildTarget {
//// Construct a build target.
BuildTarget(this.type, this.label, {this.executable, this.testOnly = false});
/// The type of build target.
final BuildTargetType type;
/// The build target label. `//flutter/fml:fml_unittests`.
final String label;
/// The executable file produced after the build target is built.
final File? executable;
/// Is this a target that is only used by tests?
final bool testOnly;
@override
String toString() {
return 'target=$label type=$type testOnly=$testOnly executable=${executable ?? "N/A"}';
}
}
/// Returns all targets for a given build directory.
Future<Map<String, BuildTarget>> findTargets(
Environment environment, Directory buildDir) async {
final Map<String, BuildTarget> r = <String, BuildTarget>{};
final List<String> getBuildInfoCommandLine = <String>[
gnBinPath(environment),
'desc',
buildDir.path,
'*',
'--format=json',
];
final ProcessRunnerResult result = await environment.processRunner.runProcess(
getBuildInfoCommandLine,
workingDirectory: environment.engine.srcDir,
failOk: true);
// Handle any process failures.
fatalIfFailed(environment, getBuildInfoCommandLine, result);
late final Map<String, Object?> jsonResult;
try {
jsonResult = jsonDecode(result.stdout) as Map<String, Object?>;
} catch (e) {
environment.logger.fatal(
'gn desc output could not be parsed:\nE=$e\nIN=${result.stdout}\n');
}
for (final MapEntry<String, Object?> targetEntry in jsonResult.entries) {
final String label = targetEntry.key;
if (targetEntry.value == null) {
environment.logger
.fatal('gn desc output is malformed $label has no value.');
}
final Map<String, Object?> properties =
targetEntry.value! as Map<String, Object?>;
final String? typeString = getString(properties, 'type');
if (typeString == null) {
environment.logger.fatal('gn desc is missing target type: $properties');
}
final BuildTargetType? type = _buildTargetTypeFromString(typeString!);
if (type == null) {
// Target is a type that we don't support.
continue;
}
final bool testOnly = getBool(properties, 'testonly');
final List<String> outputs =
getListOfString(properties, 'outputs') ?? <String>[];
File? executable;
if (type == BuildTargetType.executable) {
if (outputs.isEmpty) {
environment.logger.fatal('gn executable target $label has no outputs.');
}
executable = File(p.join(buildDir.path, outputs.first));
}
final BuildTarget target =
BuildTarget(type, label, testOnly: testOnly, executable: executable);
r[label] = target;
}
return r;
}
/// Process selectors and filter allTargets for matches.
///
/// We support:
/// 1) Exact label matches (the '//' prefix will be stripped off).
/// 2) '/...' suffix which selects all targets that match the prefix.
///
/// NOTE: if selectors is empty all targets will be selected.
Set<BuildTarget> selectTargets(
List<String> selectors, Map<String, BuildTarget> allTargets) {
final Set<BuildTarget> selected = <BuildTarget>{};
if (selectors.isEmpty) {
// Default to all if no selectors are specified.
// TODO(johnmccutchan): Reconsider this default or at least lift
// this logic up to the caller.
allTargets.values.forEach(selected.add);
return selected;
}
for (String selector in selectors) {
if (!selector.startsWith(buildTargetPrefix)) {
// Insert the prefix when necessary.
selector = '$buildTargetPrefix$selector';
}
final bool recursiveMatch = selector.endsWith(_buildTargetGlobSuffix);
if (recursiveMatch) {
// Remove the /... suffix.
selector = selector.substring(
0, selector.length - _buildTargetGlobSuffix.length);
// TODO(johnmccutchan): Accelerate this by using a trie.
for (final BuildTarget target in allTargets.values) {
if (target.label.startsWith(selector)) {
selected.add(target);
}
}
} else {
for (final BuildTarget target in allTargets.values) {
if (target.label == selector) {
selected.add(target);
}
}
}
}
return selected;
}
/// Given a list of target specifications from the command line, return a list
/// of [BuildTarget]s from the [Build] that match those specifications.
///
/// If `commandLineTargets` is empty, by default this will return the empty
/// list, which indicates that the caller should delegate the selection of
/// build targets to whatever is specified by the [Build] object. However,
/// if `defaultToAll` is `true`, then this function will return the list of
/// all build targets.
Future<List<BuildTarget>?> targetsFromCommandLine(
Environment environment,
Build build,
List<String> commandLineTargets, {
bool defaultToAll = false,
}) async {
// If there are no targets specified on the command line, then delegate to
// the default targets specified in the Build object unless directed
// otherwise by the defaultToAll argument.
if (commandLineTargets.isEmpty && !defaultToAll) {
return <BuildTarget>[];
}
final Directory buildDir = Directory(p.join(
environment.engine.outDir.path,
build.ninja.config,
));
// If the expected build output directory doesn't exist yet, eagerly run
// the build's GN step to try to produce it.
if (!buildDir.existsSync()) {
environment.logger.status(
'Build output directory at ${buildDir.path} not found. Running GN.',
);
final int gnResult = await runGn(environment, build);
if (gnResult != 0 || !buildDir.existsSync()) {
environment.logger.error(
'The specified build did not produce the expected build '
'output directory.',
);
return null;
}
}
// Find all targets buildable in the configuration.
final Map<String, BuildTarget> allTargets = await findTargets(
environment,
buildDir,
);
// Use the targets specified on the command line to filter the list of
// all targets.
final Set<BuildTarget> selectedTargets = selectTargets(
commandLineTargets,
allTargets,
);
// Report an error if applying the filter yields no results.
if (selectedTargets.isEmpty) {
environment.logger.error(
'No build targets matched ${commandLineTargets.join(',')}\n'
'Run `et query targets` to see list of targets.',
);
return null;
}
return selectedTargets.toList();
}