blob: a692f4a5404080f0279bc2c72246fc8a1e43bd70 [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:io' show ProcessStartMode;
import 'package:collection/collection.dart';
import 'package:engine_build_configs/engine_build_configs.dart';
import 'package:meta/meta.dart';
import '../build_plan.dart';
import '../build_utils.dart';
import '../flutter_tool_interop/device.dart';
import '../flutter_tool_interop/flutter_tool.dart';
import '../flutter_tool_interop/target_platform.dart';
import '../label.dart';
import '../logger.dart';
import 'command.dart';
/// The root 'run' command.
final class RunCommand extends CommandBase {
/// Constructs the 'run' command.
RunCommand({
required super.environment,
required Map<String, BuilderConfig> configs,
super.help = false,
super.usageLineLength,
@visibleForTesting FlutterTool? flutterTool,
}) {
builds = BuildPlan.configureArgParser(
argParser,
environment,
configs: configs,
help: help,
);
_flutterTool = flutterTool ?? FlutterTool.fromEnvironment(environment);
}
/// Flutter tool.
late final FlutterTool _flutterTool;
/// List of compatible builds.
late final List<Build> builds;
@override
String get name => 'run';
@override
String get description => '''
Run a Flutter app with a local engine build.
All arguments after -- are forwarded to flutter run, e.g.:
et run -- --profile
et run -- -d macos
See `flutter run --help` for a listing
''';
Build? _findTargetBuild(String configName) {
final demangledName = demangleConfigName(environment, configName);
return builds.firstWhereOrNull((build) => build.name == demangledName);
}
Build? _findHostBuild(Build? targetBuild) {
if (targetBuild == null) {
return null;
}
final mangledName = mangleConfigName(environment, targetBuild.name);
if (mangledName.contains('host_')) {
return targetBuild;
}
// TODO(johnmccutchan): This is brittle, it would be better if we encoded
// the host config name in the target config.
final String ci =
mangledName.startsWith('ci') ? mangledName.substring(0, 3) : '';
if (mangledName.contains('_debug')) {
return _findTargetBuild('${ci}host_debug');
} else if (mangledName.contains('_profile')) {
return _findTargetBuild('${ci}host_profile');
} else if (mangledName.contains('_release')) {
return _findTargetBuild('${ci}host_release');
}
return null;
}
String _getDeviceId() {
if (argResults!.rest.contains('-d')) {
final index = argResults!.rest.indexOf('-d') + 1;
if (index < argResults!.rest.length) {
return argResults!.rest[index];
}
}
if (argResults!.rest.contains('--device-id')) {
final index = argResults!.rest.indexOf('--device-id') + 1;
if (index < argResults!.rest.length) {
return argResults!.rest[index];
}
}
return '';
}
String _getMode() {
// Sniff the build mode from the args that will be passed to flutter run.
var mode = 'debug';
if (argResults!.rest.contains('--profile')) {
mode = 'profile';
} else if (argResults!.rest.contains('--release')) {
mode = 'release';
}
return mode;
}
late final Future<RunTarget?> _runTarget = (() async {
final devices = await _flutterTool.devices();
return RunTarget.detectAndSelect(devices, idPrefix: _getDeviceId());
})();
@override
Future<int> run() async {
if (!environment.processRunner.processManager.canRun('flutter')) {
throw FatalError('Cannot find the "flutter" command in your PATH');
}
final target = await _runTarget;
final plan = BuildPlan.fromArgResults(
argResults!,
environment,
builds: builds,
defaultBuild: () => target?.buildConfigFor(_getMode()),
);
final hostBuild = _findHostBuild(plan.build);
if (hostBuild == null) {
throw FatalError('Could not find host build for ${plan.build.name}');
}
final buildTargetsForShell = target?.buildTargetsForShell ?? [];
// First build the host.
var r = await runBuild(
environment,
hostBuild,
concurrency: plan.concurrency ?? 0,
extraGnArgs: plan.toGnArgs(),
enableRbe: plan.useRbe,
rbeConfig: plan.toRbeConfig(),
);
if (r != 0) {
throw FatalError('Failed to build host (${hostBuild.name})');
}
// Now build the target if it isn't the same.
if (hostBuild.name != plan.build.name) {
r = await runBuild(
environment,
plan.build,
concurrency: plan.concurrency ?? 0,
extraGnArgs: plan.toGnArgs(),
enableRbe: plan.useRbe,
targets: buildTargetsForShell,
rbeConfig: plan.toRbeConfig(),
);
if (r != 0) {
throw FatalError('Failed to build target (${plan.build.name})');
}
}
final mangledBuildName = mangleConfigName(environment, plan.build.name);
final mangledHostBuildName = mangleConfigName(environment, hostBuild.name);
final command = <String>[
'flutter',
'run',
'--local-engine-src-path',
environment.engine.srcDir.path,
'--local-engine',
mangledBuildName,
'--local-engine-host',
mangledHostBuildName,
...argResults!.rest
];
// TODO(johnmccutchan): Be smart and if the user requested a profile
// config, add the '--profile' flag when invoking flutter run.
final result = await environment.processRunner.runProcess(
command,
runInShell: true,
startMode: ProcessStartMode.inheritStdio,
);
return result.exitCode;
}
}
/// Metadata about a target to run `flutter run` on.
///
/// This class translates between the `flutter devices` output and the build
/// configurations supported by the engine, including the build targets needed
/// to build the shell for the target platform.
@visibleForTesting
@immutable
final class RunTarget {
/// Construct a run target from a device returned by `flutter devices`.
@visibleForTesting
const RunTarget.fromDevice(this.device);
/// Device to run on.
final Device device;
/// Given a list of devices, returns a build target for the first matching.
///
/// If [idPrefix] is provided, only devices with an id that starts with the
/// prefix will be considered, otherwise the first device is selected. If no
/// devices are available, or none match the prefix, `null` is returned.
@visibleForTesting
static RunTarget? detectAndSelect(
Iterable<Device> devices, {
String idPrefix = '',
}) {
if (devices.isEmpty) {
return null;
}
for (final device in devices) {
if (idPrefix.isNotEmpty) {
if (device.id.startsWith(idPrefix)) {
return RunTarget.fromDevice(device);
}
}
}
if (idPrefix.isNotEmpty) {
return null;
}
return RunTarget.fromDevice(devices.first);
}
/// Returns the build configuration for the current platform and given [mode].
///
/// The [mode] is typically one of `debug`, `profile`, or `release`.
///
/// Throws a [FatalError] if the target platform is not supported.
String buildConfigFor(String mode) {
return switch (device.targetPlatform) {
// Supported platforms with known mappings.
// -----------------------------------------------------------------------
// ANDROID
TargetPlatform.androidUnspecified => 'android_$mode',
TargetPlatform.androidX86 => 'android_${mode}_x86',
TargetPlatform.androidX64 => 'android_${mode}_x64',
TargetPlatform.androidArm64 => 'android_${mode}_arm64',
// DESKTOP (MacOS, Linux, Windows)
// We do not support cross-builds, so implicitly assume the host platform.
TargetPlatform.darwinUnspecified ||
TargetPlatform.darwinX64 ||
TargetPlatform.linuxX64 ||
TargetPlatform.windowsX64 =>
'host_$mode',
TargetPlatform.darwinArm64 ||
TargetPlatform.linuxArm64 ||
TargetPlatform.windowsArm64 =>
'host_${mode}_arm64',
// WEB
TargetPlatform.webJavascript => 'chrome_$mode',
// Unsupported platforms.
// -----------------------------------------------------------------------
// iOS.
// TODO(matanlurey): https://github.com/flutter/flutter/issues/155960
TargetPlatform.iOSUnspecified ||
TargetPlatform.iOSX64 ||
TargetPlatform.iOSArm64 =>
throw FatalError(
'iOS targets are currently unsupported.\n\nIf you are an '
'iOS engine developer, and have a need for this, please either +1 or '
'help us implement https://github.com/flutter/flutter/issues/155960.',
),
// LEGACY ANDROID
TargetPlatform.androidArm => throw FatalError(
'Legacy Android targets are not supported. '
'Please use android-arm64 or android-x64.',
),
// FUCHSIA
TargetPlatform.fuchsiaArm64 ||
TargetPlatform.fuchsiaX64 =>
throw FatalError('Fuchsia is not supported.'),
// TESTER
TargetPlatform.tester =>
throw FatalError('flutter_tester is not supported.'),
// Platforms that maybe could be supported, but we don't know about.
_ => throw FatalError(
'Unknown target platform: ${device.targetPlatform.identifier}.\n\nIf '
'this is a new platform that should be supported, please file a bug: '
'https://github.com/flutter/flutter/issues/new?labels=e:%20engine-tool.',
),
};
}
/// Minimal build targets needed to build the shell for the target platform.
List<Label> get buildTargetsForShell {
return switch (device.targetPlatform) {
// Supported platforms with known mappings.
// -----------------------------------------------------------------------
// ANDROID
TargetPlatform.androidUnspecified ||
TargetPlatform.androidX86 ||
TargetPlatform.androidX64 ||
TargetPlatform.androidArm64 =>
[Label.parseGn('//flutter/shell/platform/android:android_jar')],
// iOS.
TargetPlatform.iOSUnspecified ||
TargetPlatform.iOSX64 ||
TargetPlatform.iOSArm64 =>
[
Label.parseGn('//flutter/shell/platform/darwin/ios:flutter_framework')
],
// Desktop (MacOS).
TargetPlatform.darwinUnspecified ||
TargetPlatform.darwinX64 ||
TargetPlatform.darwinArm64 =>
[
Label.parseGn(
'//flutter/shell/platform/darwin/macos:flutter_framework',
)
],
// Desktop (Linux).
TargetPlatform.linuxX64 || TargetPlatform.linuxArm64 => [
Label.parseGn(
'//flutter/shell/platform/linux:flutter_linux_gtk',
)
],
// Desktop (Windows).
TargetPlatform.windowsX64 || TargetPlatform.windowsArm64 => [
Label.parseGn(
'//flutter/shell/platform/windows',
)
],
// Web.
TargetPlatform.webJavascript => [
Label.parseGn(
'//flutter/web_sdk:flutter_web_sdk_archive',
)
],
// Unsupported platforms.
// -----------------------------------------------------------------------
_ => throw FatalError(
'Unknown target platform: ${device.targetPlatform.identifier}.\n\nIf '
'this is a new platform that should be supported, please file a bug: '
'https://github.com/flutter/flutter/issues/new?labels=e:%20engine-tool.',
),
};
}
}