Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Add device selection to et run #51184

Merged
merged 1 commit into from
Mar 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions tools/engine_tool/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ The tool has the following commands.
* `help` - Prints helpful information about commands and usage.
* `format` - Formats files in the engine tree using various off-the-shelf
formatters.
* `run` - Runs a flutter application with a local build of the engine.
* `query builds` - Lists the CI builds described under `ci/builders` that the
host platform is capable of executing.

Expand Down
50 changes: 38 additions & 12 deletions tools/engine_tool/lib/src/commands/run_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:engine_build_configs/engine_build_configs.dart';
import 'package:process_runner/process_runner.dart';

import '../build_utils.dart';
import '../run_utils.dart';
import 'command.dart';
import 'flags.dart';

Expand Down Expand Up @@ -48,7 +49,7 @@ final class RunCommand extends CommandBase {
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 chrome'
'et run -- -d macos'
'See `flutter run --help` for a listing';

Build? _lookup(String configName) {
Expand Down Expand Up @@ -76,26 +77,47 @@ final class RunCommand extends CommandBase {
return null;
}

String _selectTargetConfig() {
final String configName = argResults![configFlag] as String;
if (configName.isNotEmpty) {
return configName;
String _getDeviceId() {
if (argResults!.rest.contains('-d')) {
final int index = argResults!.rest.indexOf('-d') + 1;
if (index < argResults!.rest.length) {
return argResults!.rest[index];
}
}
// TODO(johnmccutchan): We need a way to invoke flutter tool and be told
// which OS and CPU architecture the selected device requires, for now
// use some hard coded values:
const String targetOS = 'android';
const String cpuArch = 'arm64';
if (argResults!.rest.contains('--device-id')) {
final int 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.
String mode = 'debug';
if (argResults!.rest.contains('--profile')) {
mode = 'profile';
} else if (argResults!.rest.contains('--release')) {
mode = 'release';
}
return mode;
}

return '${targetOS}_${mode}_$cpuArch';
Future<String?> _selectTargetConfig() async {
final String configName = argResults![configFlag] as String;
if (configName.isNotEmpty) {
return configName;
}
final String deviceId = _getDeviceId();
final RunTarget? target =
await detectAndSelectRunTarget(environment, deviceId);
if (target == null) {
return 'host_debug';
}
environment.logger.status(
'Building to run on "${target.name}" running ${target.targetPlatform}');
return target.buildConfigFor(_getMode());
}

@override
Expand All @@ -104,7 +126,11 @@ final class RunCommand extends CommandBase {
environment.logger.error('Cannot find flutter command in your path');
return 1;
}
final String configName = _selectTargetConfig();
final String? configName = await _selectTargetConfig();
if (configName == null) {
environment.logger.error('Could not find target config');
return 1;
}
final Build? build = _lookup(configName);
final Build? hostBuild = _findHostBuild(build);
if (build == null) {
Expand Down
79 changes: 79 additions & 0 deletions tools/engine_tool/lib/src/json_utils.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// 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.

void _appendTypeError(
Map<String, Object?> map,
String field,
String expected,
List<String> errors, {
Object? element,
}) {
if (element == null) {
final Type actual = map[field]!.runtimeType;
errors.add(
'For field "$field", expected type: $expected, actual type: $actual.',
);
} else {
final Type actual = element.runtimeType;
errors.add(
'For element "$element" of "$field", '
'expected type: $expected, actual type: $actual',
);
}
}

/// Type safe getter of a List<String> field from map.
List<String>? stringListOfJson(
Map<String, Object?> map,
String field,
List<String> errors,
) {
if (map[field] == null) {
return <String>[];
}
if (map[field]! is! List<Object?>) {
_appendTypeError(map, field, 'list', errors);
return null;
}
for (final Object? obj in map[field]! as List<Object?>) {
if (obj is! String) {
_appendTypeError(map, field, element: obj, 'string', errors);
return null;
}
}
return (map[field]! as List<Object?>).cast<String>();
}

/// Type safe getter of a String field from map.
String? stringOfJson(
Map<String, Object?> map,
String field,
List<String> errors,
) {
if (map[field] == null) {
return '<undef>';
}
if (map[field]! is! String) {
_appendTypeError(map, field, 'string', errors);
return null;
}
return map[field]! as String;
}

/// Type safe getter of an int field from map.
int? intOfJson(
Map<String, Object?> map,
String field,
List<String> errors, {
int fallback = 0,
}) {
if (map[field] == null) {
return fallback;
}
if (map[field]! is! int) {
_appendTypeError(map, field, 'int', errors);
return null;
}
return map[field]! as int;
}
131 changes: 131 additions & 0 deletions tools/engine_tool/lib/src/run_utils.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// 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 'package:process_runner/process_runner.dart';

import 'environment.dart';
import 'json_utils.dart';

const String _targetPlatformKey = 'targetPlatform';
const String _nameKey = 'name';
const String _idKey = 'id';

/// Target to run a flutter application on.
class RunTarget {
/// Construct a RunTarget from a JSON map.
factory RunTarget.fromJson(Map<String, Object> map) {
final List<String> errors = <String>[];
final String name = stringOfJson(map, _nameKey, errors)!;
final String id = stringOfJson(map, _idKey, errors)!;
final String targetPlatform =
stringOfJson(map, _targetPlatformKey, errors)!;

if (errors.isNotEmpty) {
throw FormatException('Failed to parse RunTarget: ${errors.join('\n')}');
}
return RunTarget._(name, id, targetPlatform);
}

RunTarget._(this.name, this.id, this.targetPlatform);

/// Name of target device.
final String name;

/// Id of target device.
final String id;

/// Target platform of device.
final String targetPlatform;

/// BuildConfig name for compilation mode.
String buildConfigFor(String mode) {
switch (targetPlatform) {
case 'android-arm64':
return 'android_${mode}_arm64';
case 'darwin':
return 'host_$mode';
case 'web-javascript':
return 'chrome_$mode';
default:
throw UnimplementedError('No mapping for $targetPlatform');
}
}
}

/// Parse the raw output of `flutter devices --machine`.
List<RunTarget> parseDevices(Environment env, String flutterDevicesMachine) {
late final List<dynamic> decoded;
try {
decoded = jsonDecode(flutterDevicesMachine) as List<dynamic>;
} on FormatException catch (e) {
env.logger.error(
'Failed to parse flutter devices output: $e\n\n$flutterDevicesMachine\n\n');
return <RunTarget>[];
}

final List<RunTarget> r = <RunTarget>[];
for (final dynamic device in decoded) {
if (device is! Map<String, Object?>) {
return <RunTarget>[];
}
if (!device.containsKey(_nameKey) || !device.containsKey(_idKey)) {
env.logger.error('device is missing required fields:\n$device\n');
return <RunTarget>[];
}
if (!device.containsKey(_targetPlatformKey)) {
env.logger.warning('Skipping ${device[_nameKey]}: '
'Could not find $_targetPlatformKey in device description.');
continue;
}
late final RunTarget target;
try {
target = RunTarget.fromJson(device.cast<String, Object>());
} on FormatException catch (e) {
env.logger.error(e);
return <RunTarget>[];
}
r.add(target);
}

return r;
}

/// Return the default device to be used.
RunTarget? defaultDevice(Environment env, List<RunTarget> targets) {
if (targets.isEmpty) {
return null;
}
return targets.first;
}

/// Select a run target.
RunTarget? selectRunTarget(Environment env, String flutterDevicesMachine,
[String? idPrefix]) {
final List<RunTarget> targets = parseDevices(env, flutterDevicesMachine);
if (idPrefix != null && idPrefix.isNotEmpty) {
for (final RunTarget target in targets) {
if (target.id.startsWith(idPrefix)) {
return target;
}
}
}
return defaultDevice(env, targets);
}

/// Detects available targets and then selects one.
Future<RunTarget?> detectAndSelectRunTarget(Environment env,
[String? idPrefix]) async {
final ProcessRunnerResult result = await env.processRunner
.runProcess(<String>['flutter', 'devices', '--machine']);
if (result.exitCode != 0) {
env.logger.error('flutter devices --machine failed:\n'
'EXIT_CODE:${result.exitCode}\n'
'STDOUT:\n${result.stdout}'
'STDERR:\n${result.stderr}');
return null;
}
return selectRunTarget(env, result.stdout, idPrefix);
}
73 changes: 73 additions & 0 deletions tools/engine_tool/test/fixtures.dart
Original file line number Diff line number Diff line change
Expand Up @@ -110,3 +110,76 @@ String testConfig(String os) => '''
]
}
''';

String attachedDevices() => '''
[
{
"name": "sdk gphone64 arm64",
"id": "emulator-5554",
"isSupported": true,
"targetPlatform": "android-arm64",
"emulator": true,
"sdk": "Android 14 (API 34)",
"capabilities": {
"hotReload": true,
"hotRestart": true,
"screenshot": true,
"fastStart": true,
"flutterExit": true,
"hardwareRendering": true,
"startPaused": true
}
},
{
"name": "macOS",
"id": "macos",
"isSupported": true,
"targetPlatform": "darwin",
"emulator": false,
"sdk": "macOS 14.3.1 23D60 darwin-arm64",
"capabilities": {
"hotReload": true,
"hotRestart": true,
"screenshot": false,
"fastStart": false,
"flutterExit": true,
"hardwareRendering": false,
"startPaused": true
}
},
{
"name": "Mac Designed for iPad",
"id": "mac-designed-for-ipad",
"isSupported": true,
"targetPlatform": "darwin",
"emulator": false,
"sdk": "macOS 14.3.1 23D60 darwin-arm64",
"capabilities": {
"hotReload": true,
"hotRestart": true,
"screenshot": false,
"fastStart": false,
"flutterExit": true,
"hardwareRendering": false,
"startPaused": true
}
},
{
"name": "Chrome",
"id": "chrome",
"isSupported": true,
"targetPlatform": "web-javascript",
"emulator": false,
"sdk": "Google Chrome 122.0.6261.94",
"capabilities": {
"hotReload": true,
"hotRestart": true,
"screenshot": false,
"fastStart": false,
"flutterExit": false,
"hardwareRendering": false,
"startPaused": true
}
}
]
''';
Loading