blob: 3aab6ec0f02b661dc8c70f83366c49732bfa87fa [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 'package:args/args.dart';
import 'package:collection/collection.dart';
import 'package:engine_build_configs/engine_build_configs.dart';
import 'package:meta/meta.dart';
import 'package:platform/platform.dart';
import 'build_utils.dart';
import 'environment.dart';
import 'logger.dart';
const _flagConfig = 'config';
const _flagConcurrency = 'concurrency';
const _flagStrategy = 'build-strategy';
const _flagRbe = 'rbe';
const _flagLto = 'lto';
const _flagExtraGnArgs = 'gn-args';
/// Describes what (platform, targets) and how (strategy, options) to build.
///
/// Multiple commands in `et` are used to indirectly run builds, which often
/// means running some combination of `gn`, `ninja`, and RBE bootstrap scripts;
/// this class encapsulates the information needed to do so.
@immutable
final class BuildPlan {
/// Creates a new build plan from parsed command-line arguments.
///
/// The [ArgParser] that produced [args] must have been configured with
/// [configureArgParser].
factory BuildPlan.fromArgResults(
ArgResults args,
Environment environment, {
required List<Build> builds,
String? Function() defaultBuild = _defaultHostDebug,
}) {
final build = () {
final name = args.option(_flagConfig) ?? defaultBuild();
final config = builds.firstWhereOrNull(
(b) => mangleConfigName(environment, b.name) == name,
);
if (config == null) {
if (name == null) {
throw FatalError('No build configuration specified.');
}
throw FatalError('Unknown build configuration: $name');
}
return config;
}();
return BuildPlan._(
build: build,
strategy: BuildStrategy.values.byName(args.option(_flagStrategy)!),
useRbe: () {
final useRbe = args.flag(_flagRbe);
if (useRbe && !environment.hasRbeConfigInTree()) {
throw FatalError(
'RBE requested but configuration not found.\n\n$_rbeInstructions',
);
}
return useRbe;
}(),
useLto: () {
if (args.wasParsed(_flagLto)) {
return args.flag(_flagLto);
}
return !build.gn.contains('--no-lto');
}(),
concurrency: () {
final value = args.option(_flagConcurrency);
if (value == null) {
return null;
}
if (int.tryParse(value) case final value? when value >= 0) {
return value;
}
throw FatalError('Invalid value for --$_flagConcurrency: $value');
}(),
extraGnArgs: () {
final value = args.multiOption(_flagExtraGnArgs);
_checkExtraGnArgs(value);
return value;
}(),
);
}
BuildPlan._({
required this.build,
required this.strategy,
required this.useRbe,
required this.useLto,
required this.concurrency,
required Iterable<String> extraGnArgs,
}) : extraGnArgs = List.unmodifiable(extraGnArgs) {
if (!useRbe && strategy == BuildStrategy.remote) {
throw FatalError(
'Cannot use remote builds without RBE enabled.\n\n$_rbeInstructions',
);
}
}
/// Arguments that cannot be provided to [BuildPlan.extraGnArgs].
///
/// Instead, provide them explicitly as other [BuildPlan] arguments.
@visibleForTesting
static const reservedGnArgs = {
_flagRbe,
_flagLto,
'no-$_flagRbe',
'no-$_flagLto',
// If we are to expand this list to include flags that are not a 1:1 mapping
// - for example we want to reserve "--foo-bar" but it's called "--use-baz"
// in "et", let's (a) re-think having these arguments named differently and
// (b) if necessary, consider changing this set to a map instead so a clear
// error can be presented below.
};
/// Error thrown when [reservedGnArgs] are used as [extraGnArgs].
@visibleForTesting
static final reservedGnArgsError = FatalError(
'Flags such as ${reservedGnArgs.join(', ')} should be specified as '
'direct arguments to "et" and not using "--gn-args". For example, '
'`et build --no-lto` instead of `et build --gn-args="--no-lto"`.',
);
/// Error thrown when a non-flag argument is provided as [extraGnArgs].
@visibleForTesting
static final argumentsMustBeFlagsError = FatalError(
'Arguments provided to --gn-args must be flags (booleans) and be '
'specified as either in the format "--flag" or "--no-flag". Options '
'that are not flags or are abberviated ("-F") are not currently '
'supported; consider filing a request: '
'https://fluter.dev/to/engine-tool-bug.',
);
static void _checkExtraGnArgs(Iterable<String> gnArgs) {
for (final arg in gnArgs) {
if (!arg.startsWith('--') || arg.contains('=') || arg.contains(' ')) {
throw argumentsMustBeFlagsError;
}
// Strip off the prefix and compare it to reserved flags.
final withoutPrefix = arg.replaceFirst('--', '');
if (reservedGnArgs.contains(withoutPrefix)) {
throw reservedGnArgsError;
}
}
}
static String _defaultHostDebug() {
return 'host_debug';
}
/// Adds options to [parser] for configuring a [BuildPlan].
///
/// Returns the list of builds that can be configured.
@useResult
static List<Build> configureArgParser(
ArgParser parser,
Environment environment, {
required bool help,
required Map<String, BuilderConfig> configs,
}) {
// Add --config.
final builds = _extractBuilds(
environment.platform,
runnableConfigs: _runnableBuildConfigs(
environment.platform,
configsByName: configs,
),
hideCiSpecificBuilds: help && !environment.verbose,
);
debugCheckBuilds(builds);
parser.addOption(
_flagConfig,
abbr: 'c',
help: ''
'Selects a build configuration for the current platform.\n'
'\n'
'If omitted, et attempts '
'to default to a suitable target platform. This is typically a '
'"host_debug" build when building on a supported desktop OS, or a '
'suitable build when targeting (via "et run") a flutter app.\n'
'\n'
'${environment.verbose ? ''
'Since verbose mode was selected, both local development '
'configurations and configurations that are typically only '
'used on CI will be visible, including possible duplicates.' : ''
'Configurations include (use --verbose for more details):'}',
allowed: [
for (final config in builds) mangleConfigName(environment, config.name),
]..sort(),
allowedHelp: environment.verbose
? {
for (final config in builds)
mangleConfigName(environment, config.name): config.description,
}
: null,
);
// Add --lto.
parser.addFlag(
_flagLto,
help: ''
'Whether LTO should be enabled for a build.\n'
"If omitted, defaults to the configuration's specified value, "
'which is typically (but not always) --no-lto.',
defaultsTo: null,
hide: !environment.verbose,
);
// Add --rbe.
final hasRbeConfigInTree = environment.hasRbeConfigInTree();
parser.addFlag(
_flagRbe,
defaultsTo: hasRbeConfigInTree,
help: () {
var rbeHelp = 'Enable pre-configured remote build execution.';
if (!hasRbeConfigInTree || environment.verbose) {
rbeHelp += '\n\n$_rbeInstructions';
}
return rbeHelp;
}(),
);
// Add --build-strategy.
parser.addOption(
_flagStrategy,
defaultsTo: _defaultStrategy.name,
allowed: BuildStrategy.values.map((e) => e.name),
allowedHelp: {
for (final e in BuildStrategy.values) e.name: e._help,
},
help: 'How to prefer remote or local builds.',
hide: !hasRbeConfigInTree && !environment.verbose,
);
// Add --concurrency.
parser.addOption(
_flagConcurrency,
abbr: 'j',
help: 'How many jobs to run in parallel.',
);
// Add --gn-args.
parser.addMultiOption(
_flagExtraGnArgs,
help: ''
'Additional arguments to provide to "gn".\n'
'GN arguments change the parameters of the compiler and invalidate '
'the current build, and should be used sparingly. If there is an '
'engine build that should be reused and tested on CI prefer adding '
'the arguments to "//flutter/ci/builders/local_engine.json".',
hide: !environment.verbose,
);
return builds;
}
/// The build configuration to use.
final Build build;
/// How to prefer remote or local builds.
final BuildStrategy strategy;
static const _defaultStrategy = BuildStrategy.auto;
/// Whether to configure the build plan to use RBE (remote build execution).
final bool useRbe;
static const _rbeInstructions = ''
'Google employees can follow the instructions at '
'https://flutter.dev/to/engine-rbe to enable RBE, which can '
'parallelize builds and reduce build times on faster internet '
'connections.';
/// How many jobs to run in parallel.
///
/// If `null`, the build system will use the default number of jobs.
final int? concurrency;
/// Whether to build with LTO (link-time optimization).
final bool useLto;
/// Additional GN arguments to use for a build.
///
/// By contract, these arguments are always strictly _flags_ (not options),
/// and specified as either `--flag`, `-F`, or as the negative variant (such
/// as `--no-flag`).
final List<String> extraGnArgs;
@override
bool operator ==(Object other) {
return other is BuildPlan &&
build.name == other.build.name &&
strategy == other.strategy &&
useRbe == other.useRbe &&
useLto == other.useLto &&
concurrency == other.concurrency &&
const ListEquality<Object?>().equals(extraGnArgs, other.extraGnArgs);
}
@override
int get hashCode {
return Object.hash(
build.name,
strategy,
useRbe,
useLto,
concurrency,
Object.hashAll(extraGnArgs),
);
}
/// Converts this build plan to its equivalent [RbeConfig].
RbeConfig toRbeConfig() {
switch (strategy) {
case BuildStrategy.auto:
return const RbeConfig();
case BuildStrategy.local:
return const RbeConfig(
execStrategy: RbeExecStrategy.local,
remoteDisabled: true,
);
case BuildStrategy.remote:
return const RbeConfig(execStrategy: RbeExecStrategy.remote);
}
}
/// Converts this build plan into extra GN arguments to pass to the build.
List<String> toGnArgs() {
return [
if (!useRbe) '--no-rbe',
if (useLto) '--lto' else '--no-lto',
...extraGnArgs,
];
}
@override
String toString() {
final buffer = StringBuffer('BuildPlan <');
buffer.writeln();
buffer.writeln(' build: ${build.name}');
buffer.writeln(' useLto: $useLto');
buffer.writeln(' useRbe: $useRbe');
buffer.writeln(' strategy: $strategy');
buffer.writeln(' concurrency: $concurrency');
buffer.writeln(' extraGnArgs: $extraGnArgs');
buffer.write('>');
return buffer.toString();
}
}
/// User-specified strategy for executing a build.
enum BuildStrategy {
/// Automatically determine the best build strategy.
auto(
'Prefer remote builds and fallback silently to local builds.',
),
/// Build locally.
local(
'Use local builds.'
'\n'
'No internet connection is required.',
),
/// Build remotely.
remote(
'Use remote builds.'
'\n'
'If --$_flagRbe is not specified, the build will fail.',
);
const BuildStrategy(this._help);
final String _help;
}
typedef _ConfigsByName = Iterable<MapEntry<String, BuilderConfig>>;
/// Computes a list of build configs that can can execute on [environment].
_ConfigsByName _runnableBuildConfigs(
Platform platform, {
required Map<String, BuilderConfig> configsByName,
}) {
return configsByName.entries.where((entry) {
return entry.value.canRunOn(platform);
});
}
/// Extracts [Build]s from [runnableConfigs] that can execute on [platform].
///
/// If [hideCiSpecificBuilds], builds that are unlikely to be picked for local
/// development (i.e. start with the prefix `ci/` by convention) are not
/// returned in order to make command-line _help_ text shorter.
List<Build> _extractBuilds(
Platform platform, {
required _ConfigsByName runnableConfigs,
required bool hideCiSpecificBuilds,
}) {
return [
for (final buildConfig in runnableConfigs)
...buildConfig.value.builds.where(
(build) {
if (!build.canRunOn(platform)) {
return false;
}
if (!hideCiSpecificBuilds) {
return true;
}
return build.name.startsWith(platform.operatingSystem);
},
),
];
}