Add --create option to `flutter emulators` command (#18235) * Add --create option to flutter emulators * Tweaks to error message * Simplify emulator search logic * Make name optional * Add a note about this option being used with --create * Tweaks to help information * Switch to processManager for easier testing * Don't crash on missing files or missing properties in Android Emulator * Move name suffixing into emulator manager This allows it to be tested in the EmulatorManager tests and also used by daemon later if desired. * Pass the context's android SDK through so it can be mocked by tests * Misc fixes * Add tests around emulator creation Process calls are mocked to avoid needing a real SDK (and to be fast). Full integration tests may be useful, but may require ensuring all build environments etc. are set up correctly. * Simplify avdManagerPath Previous changes were to emulatorPath! * Fix lint errors * Fix incorrect file exgtension for Windows * Fix an issue where no system images would crash reduce throws on an empty collection. * Fix "null" appearing in error messages The name we attempted to use will now always be returned, even in the case of failure. * Add additional info to missing-system-image failure message On Windows after installing Andriod Studio I didn't have any of these and got this message. Installing with sdkmanager fixed the issue. * Fix thrown errors runResult had a toString() but we moved to ProcessResult when switching to ProcessManager to this ended up throwing "Instance of ProcessResult". * Fix package import * Fix more package imports * Move mock implementation into Mock class There seemed to be issues using Lists in args with Mockito that I couldn't figure out (docs say to use typed() but I couldn't make this compile with these lists still).. * Rename method that's ambigious now we have create * Handle where there's no avd path * Add another toList() :( * Remove comment that was rewritten * Fix forbidden import * Make optional arg more obviously optional * Reformat doc * Note that we create a pixel device in help text * Make this a named arg
diff --git a/packages/flutter_tools/lib/src/android/android_emulator.dart b/packages/flutter_tools/lib/src/android/android_emulator.dart index adb1ee7..582bf0f 100644 --- a/packages/flutter_tools/lib/src/android/android_emulator.dart +++ b/packages/flutter_tools/lib/src/android/android_emulator.dart
@@ -9,7 +9,8 @@ import '../android/android_sdk.dart'; import '../android/android_workflow.dart'; import '../base/file_system.dart'; -import '../base/process.dart'; +import '../base/io.dart'; +import '../base/process_manager.dart'; import '../emulator.dart'; import 'android_sdk.dart'; @@ -31,21 +32,23 @@ Map<String, String> _properties; @override - String get name => _properties['hw.device.name']; + String get name => _prop('hw.device.name'); @override - String get manufacturer => _properties['hw.device.manufacturer']; + String get manufacturer => _prop('hw.device.manufacturer'); @override String get label => _properties['avd.ini.displayname']; + String _prop(String name) => _properties != null ? _properties[name] : null; + @override Future<void> launch() async { final Future<void> launchResult = - runAsync(<String>[getEmulatorPath(), '-avd', id]) - .then((RunResult runResult) { + processManager.run(<String>[getEmulatorPath(), '-avd', id]) + .then((ProcessResult runResult) { if (runResult.exitCode != 0) { - throw '$runResult'; + throw '${runResult.stdout}\n${runResult.stderr}'.trimRight(); } }); // emulator continues running on a successful launch so if we @@ -65,10 +68,12 @@ return <AndroidEmulator>[]; } - final String listAvdsOutput = runSync(<String>[emulatorPath, '-list-avds']); + final String listAvdsOutput = processManager.runSync(<String>[emulatorPath, '-list-avds']).stdout; final List<AndroidEmulator> emulators = <AndroidEmulator>[]; - extractEmulatorAvdInfo(listAvdsOutput, emulators); + if (listAvdsOutput != null) { + extractEmulatorAvdInfo(listAvdsOutput, emulators); + } return emulators; } @@ -76,21 +81,26 @@ /// of emulators by reading information from the relevant ini files. void extractEmulatorAvdInfo(String text, List<AndroidEmulator> emulators) { for (String id in text.trim().split('\n').where((String l) => l != '')) { - emulators.add(_createEmulator(id)); + emulators.add(_loadEmulatorInfo(id)); } } -AndroidEmulator _createEmulator(String id) { +AndroidEmulator _loadEmulatorInfo(String id) { id = id.trim(); - final File iniFile = fs.file(fs.path.join(getAvdPath(), '$id.ini')); - final Map<String, String> ini = parseIniLines(iniFile.readAsLinesSync()); - - if (ini['path'] != null) { - final File configFile = fs.file(fs.path.join(ini['path'], 'config.ini')); - if (configFile.existsSync()) { - final Map<String, String> properties = - parseIniLines(configFile.readAsLinesSync()); - return new AndroidEmulator(id, properties); + final String avdPath = getAvdPath(); + if (avdPath != null) { + final File iniFile = fs.file(fs.path.join(avdPath, '$id.ini')); + if (iniFile.existsSync()) { + final Map<String, String> ini = parseIniLines(iniFile.readAsLinesSync()); + if (ini['path'] != null) { + final File configFile = + fs.file(fs.path.join(ini['path'], 'config.ini')); + if (configFile.existsSync()) { + final Map<String, String> properties = + parseIniLines(configFile.readAsLinesSync()); + return new AndroidEmulator(id, properties); + } + } } }
diff --git a/packages/flutter_tools/lib/src/android/android_sdk.dart b/packages/flutter_tools/lib/src/android/android_sdk.dart index 79a5d67..2dcc5bf 100644 --- a/packages/flutter_tools/lib/src/android/android_sdk.dart +++ b/packages/flutter_tools/lib/src/android/android_sdk.dart
@@ -64,16 +64,8 @@ /// will work for those users who have Android Tools installed but /// not the full SDK. String getEmulatorPath([AndroidSdk existingSdk]) { - if (existingSdk?.emulatorPath != null) - return existingSdk.emulatorPath; - - final AndroidSdk sdk = AndroidSdk.locateAndroidSdk(); - - if (sdk?.latestVersion == null) { - return os.which('emulator')?.path; - } else { - return sdk.emulatorPath; - } + return existingSdk?.emulatorPath ?? + AndroidSdk.locateAndroidSdk()?.emulatorPath; } /// Locate the path for storing AVD emulator images. Returns null if none found. @@ -104,6 +96,15 @@ ); } +/// Locate 'avdmanager'. Prefer to use one from an Android SDK, if we can locate that. +/// This should be used over accessing androidSdk.avdManagerPath directly because it +/// will work for those users who have Android Tools installed but +/// not the full SDK. +String getAvdManagerPath([AndroidSdk existingSdk]) { + return existingSdk?.avdManagerPath ?? + AndroidSdk.locateAndroidSdk()?.avdManagerPath; +} + class AndroidNdkSearchError { AndroidNdkSearchError(this.reason); @@ -314,6 +315,8 @@ String get emulatorPath => getEmulatorPath(); + String get avdManagerPath => getAvdManagerPath(); + /// Validate the Android SDK. This returns an empty list if there are no /// issues; otherwise, it returns a list of issues found. List<String> validateSdkWellFormed() { @@ -343,6 +346,14 @@ return null; } + String getAvdManagerPath() { + final String binaryName = platform.isWindows ? 'avdmanager.bat' : 'avdmanager'; + final String path = fs.path.join(directory, 'tools', 'bin', binaryName); + if (fs.file(path).existsSync()) + return path; + return null; + } + void _init() { Iterable<Directory> platforms = <Directory>[]; // android-22, ...
diff --git a/packages/flutter_tools/lib/src/commands/emulators.dart b/packages/flutter_tools/lib/src/commands/emulators.dart index 78b1284..0904bc4 100644 --- a/packages/flutter_tools/lib/src/commands/emulators.dart +++ b/packages/flutter_tools/lib/src/commands/emulators.dart
@@ -16,13 +16,18 @@ EmulatorsCommand() { argParser.addOption('launch', help: 'The full or partial ID of the emulator to launch.'); + argParser.addFlag('create', + help: 'Creates a new Android emulator based on a Pixel device.', + negatable: false); + argParser.addOption('name', + help: 'Used with flag --create. Specifies a name for the emulator being created.'); } @override final String name = 'emulators'; @override - final String description = 'List and launch available emulators.'; + final String description = 'List, launch and create emulators.'; @override final List<String> aliases = <String>['emulator']; @@ -40,6 +45,8 @@ if (argResults.wasParsed('launch')) { await _launchEmulator(argResults['launch']); + } else if (argResults.wasParsed('create')) { + await _createEmulator(name: argResults['name']); } else { final String searchText = argResults.rest != null && argResults.rest.isNotEmpty @@ -70,17 +77,27 @@ } } + Future<Null> _createEmulator({String name}) async { + final CreateEmulatorResult createResult = + await emulatorManager.createEmulator(name: name); + + if (createResult.success) { + printStatus("Emulator '${createResult.emulatorName}' created successfully."); + } else { + printStatus("Failed to create emulator '${createResult.emulatorName}'.\n"); + printStatus(createResult.error.trim()); + _printAdditionalInfo(); + } + } + Future<void> _listEmulators(String searchText) async { final List<Emulator> emulators = searchText == null ? await emulatorManager.getAllAvailableEmulators() : await emulatorManager.getEmulatorsMatching(searchText); if (emulators.isEmpty) { - printStatus('No emulators available.\n\n' - // TODO(dantup): Change these when we support creation - // 'You may need to create images using "flutter emulators --create"\n' - 'You may need to create one using Android Studio ' - 'or visit https://flutter.io/setup/ for troubleshooting tips.'); + printStatus('No emulators available.'); + _printAdditionalInfo(showCreateInstruction: true); } else { _printEmulatorList( emulators, @@ -92,7 +109,28 @@ void _printEmulatorList(List<Emulator> emulators, String message) { printStatus('$message\n'); Emulator.printEmulators(emulators); - printStatus( - "\nTo run an emulator, run 'flutter emulators --launch <emulator id>'."); + _printAdditionalInfo(showCreateInstruction: true, showRunInstruction: true); + } + + void _printAdditionalInfo({ bool showRunInstruction = false, + bool showCreateInstruction = false }) { + printStatus(''); + if (showRunInstruction) { + printStatus( + "To run an emulator, run 'flutter emulators --launch <emulator id>'."); + } + if (showCreateInstruction) { + printStatus( + "To create a new emulator, run 'flutter emulators --create [--name xyz]'."); + } + + if (showRunInstruction || showCreateInstruction) { + printStatus(''); + } + // TODO(dantup): Update this link to flutter.io if/when we have a better page. + // That page can then link out to these places if required. + printStatus('You can find more information on managing emulators at the links below:\n' + ' https://developer.android.com/studio/run/managing-avds\n' + ' https://developer.android.com/studio/command-line/avdmanager'); } }
diff --git a/packages/flutter_tools/lib/src/emulator.dart b/packages/flutter_tools/lib/src/emulator.dart index 81bbe2a..5fa6b2a 100644 --- a/packages/flutter_tools/lib/src/emulator.dart +++ b/packages/flutter_tools/lib/src/emulator.dart
@@ -6,7 +6,10 @@ import 'dart:math' as math; import 'android/android_emulator.dart'; +import 'android/android_sdk.dart'; import 'base/context.dart'; +import 'base/io.dart' show ProcessResult; +import 'base/process_manager.dart'; import 'globals.dart'; import 'ios/ios_emulators.dart'; @@ -34,8 +37,8 @@ emulator.id?.toLowerCase()?.startsWith(searchText) == true || emulator.name?.toLowerCase()?.startsWith(searchText) == true; - final Emulator exactMatch = emulators.firstWhere( - exactlyMatchesEmulatorId, orElse: () => null); + final Emulator exactMatch = + emulators.firstWhere(exactlyMatchesEmulatorId, orElse: () => null); if (exactMatch != null) { return <Emulator>[exactMatch]; } @@ -57,6 +60,133 @@ return emulators; } + /// Return the list of all available emulators. + Future<CreateEmulatorResult> createEmulator({String name}) async { + if (name == null || name == '') { + const String autoName = 'flutter_emulator'; + // Don't use getEmulatorsMatching here, as it will only return one + // if there's an exact match and we need all those with this prefix + // so we can keep adding suffixes until we miss. + final List<Emulator> all = await getAllAvailableEmulators(); + final Set<String> takenNames = all + .map((Emulator e) => e.id) + .where((String id) => id.startsWith(autoName)) + .toSet(); + int suffix = 1; + name = autoName; + while (takenNames.contains(name)) { + name = '${autoName}_${++suffix}'; + } + } + + final String device = await _getPreferredAvailableDevice(); + if (device == null) + return new CreateEmulatorResult(name, + success: false, error: 'No device definitions are available'); + + final String sdkId = await _getPreferredSdkId(); + if (sdkId == null) + return new CreateEmulatorResult(name, + success: false, + error: + 'No suitable Android AVD system images are available. You may need to install these' + ' using sdkmanager, for example:\n' + ' sdkmanager "system-images;android-27;google_apis_playstore;x86"'); + + // Cleans up error output from avdmanager to make it more suitable to show + // to flutter users. Specifically: + // - Removes lines that say "null" (!) + // - Removes lines that tell the user to use '--force' to overwrite emulators + String cleanError(String error) { + return (error ?? '') + .split('\n') + .where((String l) => l.trim() != 'null') + .where((String l) => + l.trim() != 'Use --force if you want to replace it.') + .join('\n'); + } + + final List<String> args = <String>[ + getAvdManagerPath(androidSdk), + 'create', + 'avd', + '-n', name, + '-k', sdkId, + '-d', device + ]; + final ProcessResult runResult = processManager.runSync(args); + return new CreateEmulatorResult( + name, + success: runResult.exitCode == 0, + output: runResult.stdout, + error: cleanError(runResult.stderr), + ); + } + + static const List<String> preferredDevices = const <String>[ + 'pixel', + 'pixel_xl', + ]; + Future<String> _getPreferredAvailableDevice() async { + final List<String> args = <String>[ + getAvdManagerPath(androidSdk), + 'list', + 'device', + '-c' + ]; + final ProcessResult runResult = processManager.runSync(args); + if (runResult.exitCode != 0) + return null; + + final List<String> availableDevices = runResult.stdout + .split('\n') + .where((String l) => preferredDevices.contains(l.trim())) + .toList(); + + return preferredDevices.firstWhere( + (String d) => availableDevices.contains(d), + orElse: () => null, + ); + } + + RegExp androidApiVersion = new RegExp(r';android-(\d+);'); + Future<String> _getPreferredSdkId() async { + // It seems that to get the available list of images, we need to send a + // request to create without the image and it'll provide us a list :-( + final List<String> args = <String>[ + getAvdManagerPath(androidSdk), + 'create', + 'avd', + '-n', 'temp', + ]; + final ProcessResult runResult = processManager.runSync(args); + + // Get the list of IDs that match our criteria + final List<String> availableIDs = runResult.stderr + .split('\n') + .where((String l) => androidApiVersion.hasMatch(l)) + .where((String l) => l.contains('system-images')) + .where((String l) => l.contains('google_apis_playstore')) + .toList(); + + final List<int> availableApiVersions = availableIDs + .map((String id) => androidApiVersion.firstMatch(id).group(1)) + .map((String apiVersion) => int.parse(apiVersion)) + .toList(); + + // Get the highest Android API version or whats left + final int apiVersion = availableApiVersions.isNotEmpty + ? availableApiVersions.reduce(math.max) + : -1; // Don't match below + + // We're out of preferences, we just have to return the first one with the high + // API version. + return availableIDs.firstWhere( + (String id) => id.contains(';android-$apiVersion;'), + orElse: () => null, + ); + } + /// Whether we're capable of listing any emulators given the current environment configuration. bool get canListAnything { return _platformDiscoverers.any((EmulatorDiscovery discoverer) => discoverer.canListAnything); @@ -124,11 +254,13 @@ // Join columns into lines of text final RegExp whiteSpaceAndDots = new RegExp(r'[•\s]+$'); - return table.map((List<String> row) { - return indices - .map((int i) => row[i].padRight(widths[i])) - .join(' • ') + ' • ${row.last}'; - }) + return table + .map((List<String> row) { + return indices + .map((int i) => row[i].padRight(widths[i])) + .join(' • ') + + ' • ${row.last}'; + }) .map((String line) => line.replaceAll(whiteSpaceAndDots, '')) .toList(); } @@ -137,3 +269,12 @@ descriptions(emulators).forEach(printStatus); } } + +class CreateEmulatorResult { + final bool success; + final String emulatorName; + final String output; + final String error; + + CreateEmulatorResult(this.emulatorName, {this.success, this.output, this.error}); +}
diff --git a/packages/flutter_tools/lib/src/ios/ios_emulators.dart b/packages/flutter_tools/lib/src/ios/ios_emulators.dart index cd85c2f..3608bad 100644 --- a/packages/flutter_tools/lib/src/ios/ios_emulators.dart +++ b/packages/flutter_tools/lib/src/ios/ios_emulators.dart
@@ -71,6 +71,8 @@ } String getSimulatorPath() { + if (xcode.xcodeSelectPath == null) + return null; final List<String> searchPaths = <String>[ fs.path.join(xcode.xcodeSelectPath, 'Applications', 'Simulator.app'), ];