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

Commit 1505fc6

Browse files
committed
Add device selection to et run
- `et run` now detects the target device automatically and builds the related engine builds. - `et run -- -d <device-id>` also works.
1 parent 3e8b0de commit 1505fc6

File tree

6 files changed

+395
-17
lines changed

6 files changed

+395
-17
lines changed

tools/engine_tool/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ The tool has the following commands.
1616
* `help` - Prints helpful information about commands and usage.
1717
* `format` - Formats files in the engine tree using various off-the-shelf
1818
formatters.
19+
* `run` - Runs a flutter application with a local build of the engine.
1920
* `query builds` - Lists the CI builds described under `ci/builders` that the
2021
host platform is capable of executing.
2122

tools/engine_tool/lib/src/commands/run_command.dart

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import 'package:engine_build_configs/engine_build_configs.dart';
88
import 'package:process_runner/process_runner.dart';
99

1010
import '../build_utils.dart';
11+
import '../run_utils.dart';
1112
import 'command.dart';
1213
import 'flags.dart';
1314

@@ -48,7 +49,7 @@ final class RunCommand extends CommandBase {
4849
String get description => 'Run a flutter app with a local engine build'
4950
'All arguments after -- are forwarded to flutter run, e.g.: '
5051
'et run -- --profile'
51-
'et run -- -d chrome'
52+
'et run -- -d macos'
5253
'See `flutter run --help` for a listing';
5354

5455
Build? _lookup(String configName) {
@@ -76,26 +77,47 @@ final class RunCommand extends CommandBase {
7677
return null;
7778
}
7879

79-
String _selectTargetConfig() {
80-
final String configName = argResults![configFlag] as String;
81-
if (configName.isNotEmpty) {
82-
return configName;
80+
String _getDeviceId() {
81+
if (argResults!.rest.contains('-d')) {
82+
final int index = argResults!.rest.indexOf('-d') + 1;
83+
if (index < argResults!.rest.length) {
84+
return argResults!.rest[index];
85+
}
8386
}
84-
// TODO(johnmccutchan): We need a way to invoke flutter tool and be told
85-
// which OS and CPU architecture the selected device requires, for now
86-
// use some hard coded values:
87-
const String targetOS = 'android';
88-
const String cpuArch = 'arm64';
87+
if (argResults!.rest.contains('--device-id')) {
88+
final int index = argResults!.rest.indexOf('--device-id') + 1;
89+
if (index < argResults!.rest.length) {
90+
return argResults!.rest[index];
91+
}
92+
}
93+
return '';
94+
}
8995

96+
String _getMode() {
9097
// Sniff the build mode from the args that will be passed to flutter run.
9198
String mode = 'debug';
9299
if (argResults!.rest.contains('--profile')) {
93100
mode = 'profile';
94101
} else if (argResults!.rest.contains('--release')) {
95102
mode = 'release';
96103
}
104+
return mode;
105+
}
97106

98-
return '${targetOS}_${mode}_$cpuArch';
107+
Future<String?> _selectTargetConfig() async {
108+
final String configName = argResults![configFlag] as String;
109+
if (configName.isNotEmpty) {
110+
return configName;
111+
}
112+
final String deviceId = _getDeviceId();
113+
final RunTarget? target =
114+
await detectAndSelectRunTarget(environment, deviceId);
115+
if (target == null) {
116+
return 'host_debug';
117+
}
118+
environment.logger.status(
119+
'Building to run on "${target.name}" running ${target.targetPlatform}');
120+
return target.buildConfigFor(_getMode());
99121
}
100122

101123
@override
@@ -104,7 +126,11 @@ final class RunCommand extends CommandBase {
104126
environment.logger.error('Cannot find flutter command in your path');
105127
return 1;
106128
}
107-
final String configName = _selectTargetConfig();
129+
final String? configName = await _selectTargetConfig();
130+
if (configName == null) {
131+
environment.logger.error('Could not find target config');
132+
return 1;
133+
}
108134
final Build? build = _lookup(configName);
109135
final Build? hostBuild = _findHostBuild(build);
110136
if (build == null) {
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
void _appendTypeError(
6+
Map<String, Object?> map,
7+
String field,
8+
String expected,
9+
List<String> errors, {
10+
Object? element,
11+
}) {
12+
if (element == null) {
13+
final Type actual = map[field]!.runtimeType;
14+
errors.add(
15+
'For field "$field", expected type: $expected, actual type: $actual.',
16+
);
17+
} else {
18+
final Type actual = element.runtimeType;
19+
errors.add(
20+
'For element "$element" of "$field", '
21+
'expected type: $expected, actual type: $actual',
22+
);
23+
}
24+
}
25+
26+
/// Type safe getter of a List<String> field from map.
27+
List<String>? stringListOfJson(
28+
Map<String, Object?> map,
29+
String field,
30+
List<String> errors,
31+
) {
32+
if (map[field] == null) {
33+
return <String>[];
34+
}
35+
if (map[field]! is! List<Object?>) {
36+
_appendTypeError(map, field, 'list', errors);
37+
return null;
38+
}
39+
for (final Object? obj in map[field]! as List<Object?>) {
40+
if (obj is! String) {
41+
_appendTypeError(map, field, element: obj, 'string', errors);
42+
return null;
43+
}
44+
}
45+
return (map[field]! as List<Object?>).cast<String>();
46+
}
47+
48+
/// Type safe getter of a String field from map.
49+
String? stringOfJson(
50+
Map<String, Object?> map,
51+
String field,
52+
List<String> errors,
53+
) {
54+
if (map[field] == null) {
55+
return '<undef>';
56+
}
57+
if (map[field]! is! String) {
58+
_appendTypeError(map, field, 'string', errors);
59+
return null;
60+
}
61+
return map[field]! as String;
62+
}
63+
64+
/// Type safe getter of an int field from map.
65+
int? intOfJson(
66+
Map<String, Object?> map,
67+
String field,
68+
List<String> errors, {
69+
int fallback = 0,
70+
}) {
71+
if (map[field] == null) {
72+
return fallback;
73+
}
74+
if (map[field]! is! int) {
75+
_appendTypeError(map, field, 'int', errors);
76+
return null;
77+
}
78+
return map[field]! as int;
79+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:convert';
6+
7+
import 'package:process_runner/process_runner.dart';
8+
9+
import 'environment.dart';
10+
import 'json_utils.dart';
11+
12+
const String _targetPlatformKey = 'targetPlatform';
13+
const String _nameKey = 'name';
14+
const String _idKey = 'id';
15+
16+
/// Target to run a flutter application on.
17+
class RunTarget {
18+
/// Construct a RunTarget from a JSON map.
19+
factory RunTarget.fromJson(Map<String, Object> map) {
20+
final List<String> errors = <String>[];
21+
final String name = stringOfJson(map, _nameKey, errors)!;
22+
final String id = stringOfJson(map, _idKey, errors)!;
23+
final String targetPlatform =
24+
stringOfJson(map, _targetPlatformKey, errors)!;
25+
26+
if (errors.isNotEmpty) {
27+
throw FormatException('Failed to parse RunTarget: ${errors.join('\n')}');
28+
}
29+
return RunTarget._(name, id, targetPlatform);
30+
}
31+
32+
RunTarget._(this.name, this.id, this.targetPlatform);
33+
34+
/// Name of target device.
35+
final String name;
36+
37+
/// Id of target device.
38+
final String id;
39+
40+
/// Target platform of device.
41+
final String targetPlatform;
42+
43+
/// BuildConfig name for compilation mode.
44+
String buildConfigFor(String mode) {
45+
switch (targetPlatform) {
46+
case 'android-arm64':
47+
return 'android_${mode}_arm64';
48+
case 'darwin':
49+
return 'host_$mode';
50+
case 'web-javascript':
51+
return 'chrome_$mode';
52+
default:
53+
throw UnimplementedError('No mapping for $targetPlatform');
54+
}
55+
}
56+
}
57+
58+
/// Parse the raw output of `flutter devices --machine`.
59+
List<RunTarget> parseDevices(Environment env, String flutterDevicesMachine) {
60+
late final List<dynamic> decoded;
61+
try {
62+
decoded = jsonDecode(flutterDevicesMachine) as List<dynamic>;
63+
} on FormatException catch (e) {
64+
env.logger.error(
65+
'Failed to parse flutter devices output: $e\n\n$flutterDevicesMachine\n\n');
66+
return <RunTarget>[];
67+
}
68+
69+
final List<RunTarget> r = <RunTarget>[];
70+
for (final dynamic device in decoded) {
71+
if (device is! Map<String, Object?>) {
72+
return <RunTarget>[];
73+
}
74+
if (!device.containsKey(_nameKey) || !device.containsKey(_idKey)) {
75+
env.logger.error('device is missing required fields:\n$device\n');
76+
return <RunTarget>[];
77+
}
78+
if (!device.containsKey(_targetPlatformKey)) {
79+
env.logger.warning('Skipping ${device[_nameKey]}: '
80+
'Could not find $_targetPlatformKey in device description.');
81+
continue;
82+
}
83+
late final RunTarget target;
84+
try {
85+
target = RunTarget.fromJson(device.cast<String, Object>());
86+
} on FormatException catch (e) {
87+
env.logger.error(e);
88+
return <RunTarget>[];
89+
}
90+
r.add(target);
91+
}
92+
93+
return r;
94+
}
95+
96+
/// Return the default device to be used.
97+
RunTarget? defaultDevice(Environment env, List<RunTarget> targets) {
98+
if (targets.isEmpty) {
99+
return null;
100+
}
101+
return targets.first;
102+
}
103+
104+
/// Select a run target.
105+
RunTarget? selectRunTarget(Environment env, String flutterDevicesMachine,
106+
[String? idPrefix]) {
107+
final List<RunTarget> targets = parseDevices(env, flutterDevicesMachine);
108+
if (idPrefix != null && idPrefix.isNotEmpty) {
109+
for (final RunTarget target in targets) {
110+
if (target.id.startsWith(idPrefix)) {
111+
return target;
112+
}
113+
}
114+
}
115+
return defaultDevice(env, targets);
116+
}
117+
118+
/// Detects available targets and then selects one.
119+
Future<RunTarget?> detectAndSelectRunTarget(Environment env,
120+
[String? idPrefix]) async {
121+
final ProcessRunnerResult result = await env.processRunner
122+
.runProcess(<String>['flutter', 'devices', '--machine']);
123+
if (result.exitCode != 0) {
124+
env.logger.error('flutter devices --machine failed:\n'
125+
'EXIT_CODE:${result.exitCode}\n'
126+
'STDOUT:\n${result.stdout}'
127+
'STDERR:\n${result.stderr}');
128+
return null;
129+
}
130+
return selectRunTarget(env, result.stdout, idPrefix);
131+
}

tools/engine_tool/test/fixtures.dart

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,76 @@ String testConfig(String os) => '''
110110
]
111111
}
112112
''';
113+
114+
String attachedDevices() => '''
115+
[
116+
{
117+
"name": "sdk gphone64 arm64",
118+
"id": "emulator-5554",
119+
"isSupported": true,
120+
"targetPlatform": "android-arm64",
121+
"emulator": true,
122+
"sdk": "Android 14 (API 34)",
123+
"capabilities": {
124+
"hotReload": true,
125+
"hotRestart": true,
126+
"screenshot": true,
127+
"fastStart": true,
128+
"flutterExit": true,
129+
"hardwareRendering": true,
130+
"startPaused": true
131+
}
132+
},
133+
{
134+
"name": "macOS",
135+
"id": "macos",
136+
"isSupported": true,
137+
"targetPlatform": "darwin",
138+
"emulator": false,
139+
"sdk": "macOS 14.3.1 23D60 darwin-arm64",
140+
"capabilities": {
141+
"hotReload": true,
142+
"hotRestart": true,
143+
"screenshot": false,
144+
"fastStart": false,
145+
"flutterExit": true,
146+
"hardwareRendering": false,
147+
"startPaused": true
148+
}
149+
},
150+
{
151+
"name": "Mac Designed for iPad",
152+
"id": "mac-designed-for-ipad",
153+
"isSupported": true,
154+
"targetPlatform": "darwin",
155+
"emulator": false,
156+
"sdk": "macOS 14.3.1 23D60 darwin-arm64",
157+
"capabilities": {
158+
"hotReload": true,
159+
"hotRestart": true,
160+
"screenshot": false,
161+
"fastStart": false,
162+
"flutterExit": true,
163+
"hardwareRendering": false,
164+
"startPaused": true
165+
}
166+
},
167+
{
168+
"name": "Chrome",
169+
"id": "chrome",
170+
"isSupported": true,
171+
"targetPlatform": "web-javascript",
172+
"emulator": false,
173+
"sdk": "Google Chrome 122.0.6261.94",
174+
"capabilities": {
175+
"hotReload": true,
176+
"hotRestart": true,
177+
"screenshot": false,
178+
"fastStart": false,
179+
"flutterExit": false,
180+
"hardwareRendering": false,
181+
"startPaused": true
182+
}
183+
}
184+
]
185+
''';

0 commit comments

Comments
 (0)