| // 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.', |
| ), |
| }; |
| } |
| } |