diff --git a/packages/flutter_tools/lib/src/commands/apk.dart b/packages/flutter_tools/lib/src/commands/apk.dart index ac0f5dc41b9..9e762277e96 100644 --- a/packages/flutter_tools/lib/src/commands/apk.dart +++ b/packages/flutter_tools/lib/src/commands/apk.dart @@ -8,15 +8,19 @@ import 'dart:io'; import 'package:path/path.dart' as path; import 'package:yaml/yaml.dart'; +import 'package:xml/xml.dart' as xml; import '../android/device_android.dart'; +import '../application_package.dart'; import '../artifacts.dart'; import '../base/context.dart'; import '../base/file_system.dart'; import '../base/process.dart'; import '../build_configuration.dart'; +import '../device.dart'; import '../flx.dart' as flx; import '../runner/flutter_command.dart'; +import '../toolchain.dart'; import 'start.dart'; const String _kDefaultAndroidManifestPath = 'apk/AndroidManifest.xml'; @@ -25,6 +29,7 @@ const String _kDefaultResourcesPath = 'apk/res'; const String _kFlutterManifestPath = 'flutter.yaml'; const String _kPubspecYamlPath = 'pubspec.yaml'; +const String _kPackagesStatusPath = '.packages'; // Alias of the key provided in the Chromium debug keystore const String _kDebugKeystoreKeyAlias = "chromiumdebugkey"; @@ -132,6 +137,14 @@ class _ApkComponents { Directory resources; } +class ApkKeystoreInfo { + String keystore; + String password; + String keyAlias; + String keyPassword; + ApkKeystoreInfo({ this.keystore, this.password, this.keyAlias, this.keyPassword }); +} + // TODO(mpcomplete): find a better home for this. dynamic _loadYamlFile(String path) { if (!FileSystemEntity.isFileSync(path)) @@ -179,227 +192,352 @@ class ApkCommand extends FlutterCommand { help: 'Password for the entry within the keystore.'); } - Future _findServices(_ApkComponents components) async { - if (!ArtifactStore.isPackageRootValid) - return; - - dynamic manifest = _loadYamlFile(_kFlutterManifestPath); - if (manifest['services'] == null) - return; - - for (String service in manifest['services']) { - String serviceRoot = '${ArtifactStore.packageRoot}/$service/apk'; - dynamic serviceConfig = _loadYamlFile('$serviceRoot/config.yaml'); - if (serviceConfig == null || serviceConfig['jars'] == null) - continue; - components.services.addAll(serviceConfig['services']); - for (String jar in serviceConfig['jars']) { - if (jar.startsWith("android-sdk:")) { - // Jar is something shipped in the standard android SDK. - jar = jar.replaceAll('android-sdk:', '${components.androidSdk.path}/'); - components.jars.add(new File(jar)); - } else if (jar.startsWith("http")) { - // Jar is a URL to download. - String cachePath = await ArtifactStore.getThirdPartyFile(jar, service); - components.jars.add(new File(cachePath)); - } else { - // Assume jar is a path relative to the service's root dir. - components.jars.add(new File(path.join(serviceRoot, jar))); - } - } - } - } - - Future<_ApkComponents> _findApkComponents(BuildConfiguration config) async { - String androidSdkPath; - List artifactPaths; - if (runner.enginePath != null) { - androidSdkPath = '${runner.enginePath}/third_party/android_tools/sdk'; - artifactPaths = [ - '${runner.enginePath}/third_party/icu/android/icudtl.dat', - '${config.buildDir}/gen/sky/shell/shell/classes.dex.jar', - '${config.buildDir}/gen/sky/shell/shell/shell/libs/armeabi-v7a/libsky_shell.so', - '${runner.enginePath}/build/android/ant/chromium-debug.keystore', - ]; - } else { - androidSdkPath = AndroidDevice.getAndroidSdkPath(); - if (androidSdkPath == null) { - return null; - } - List artifactTypes = [ - ArtifactType.androidIcuData, - ArtifactType.androidClassesJar, - ArtifactType.androidLibSkyShell, - ArtifactType.androidKeystore, - ]; - Iterable> pathFutures = artifactTypes.map( - (ArtifactType type) => ArtifactStore.getPath(ArtifactStore.getArtifact( - type: type, targetPlatform: TargetPlatform.android))); - artifactPaths = await Future.wait(pathFutures); - } - - _ApkComponents components = new _ApkComponents(); - components.androidSdk = new Directory(androidSdkPath); - components.manifest = new File(argResults['manifest']); - components.icuData = new File(artifactPaths[0]); - components.jars = [new File(artifactPaths[1])]; - components.libSkyShell = new File(artifactPaths[2]); - components.debugKeystore = new File(artifactPaths[3]); - components.resources = new Directory(argResults['resources']); - - await _findServices(components); - - if (!components.resources.existsSync()) { - // TODO(eseidel): This level should be higher when path is manually set. - printStatus('Can not locate Resources: ${components.resources}, ignoring.'); - components.resources = null; - } - - if (!components.androidSdk.existsSync()) { - printError('Can not locate Android SDK: $androidSdkPath'); - return null; - } - if (!(new _ApkBuilder(components.androidSdk.path).checkSdkPath())) { - printError('Can not locate expected Android SDK tools at $androidSdkPath'); - printError('You must install version $_kAndroidPlatformVersion of the SDK platform'); - printError('and version $_kBuildToolsVersion of the build tools.'); - return null; - } - for (File f in [components.manifest, components.icuData, - components.libSkyShell, components.debugKeystore] - ..addAll(components.jars)) { - if (!f.existsSync()) { - printError('Can not locate file: ${f.path}'); - return null; - } - } - - return components; - } - - // Outputs a services.json file for the flutter engine to read. Format: - // { - // services: [ - // { name: string, class: string }, - // ... - // ] - // } - void _generateServicesConfig(File servicesConfig, List> servicesIn) { - List> services = - servicesIn.map((Map service) => { - 'name': service['name'], - 'class': service['registration-class'] - }).toList(); - - Map json = { 'services': services }; - servicesConfig.writeAsStringSync(JSON.encode(json), mode: FileMode.WRITE, flush: true); - } - - int _buildApk(_ApkComponents components, String flxPath) { - Directory tempDir = Directory.systemTemp.createTempSync('flutter_tools'); - try { - _ApkBuilder builder = new _ApkBuilder(components.androidSdk.path); - - File classesDex = new File('${tempDir.path}/classes.dex'); - builder.compileClassesDex(classesDex, components.jars); - - File servicesConfig = new File('${tempDir.path}/services.json'); - _generateServicesConfig(servicesConfig, components.services); - - _AssetBuilder assetBuilder = new _AssetBuilder(tempDir, 'assets'); - assetBuilder.add(components.icuData, 'icudtl.dat'); - assetBuilder.add(new File(flxPath), 'app.flx'); - assetBuilder.add(servicesConfig, 'services.json'); - - _AssetBuilder artifactBuilder = new _AssetBuilder(tempDir, 'artifacts'); - artifactBuilder.add(classesDex, 'classes.dex'); - artifactBuilder.add(components.libSkyShell, 'lib/armeabi-v7a/libsky_shell.so'); - - File unalignedApk = new File('${tempDir.path}/app.apk.unaligned'); - builder.package(unalignedApk, components.manifest, assetBuilder.directory, - artifactBuilder.directory, components.resources); - - int signResult = _signApk(builder, components, unalignedApk); - if (signResult != 0) - return signResult; - - File finalApk = new File(argResults['output-file']); - ensureDirectoryExists(finalApk.path); - builder.align(unalignedApk, finalApk); - - printStatus('APK generated: ${finalApk.path}'); - - return 0; - } finally { - tempDir.deleteSync(recursive: true); - } - } - - int _signApk(_ApkBuilder builder, _ApkComponents components, File apk) { - File keystore; - String keystorePassword; - String keyAlias; - String keyPassword; - - if (argResults['keystore'].isEmpty) { - printError('Signing the APK using the debug keystore'); - keystore = components.debugKeystore; - keystorePassword = _kDebugKeystorePassword; - keyAlias = _kDebugKeystoreKeyAlias; - keyPassword = _kDebugKeystorePassword; - } else { - keystore = new File(argResults['keystore']); - keystorePassword = argResults['keystore-password']; - keyAlias = argResults['keystore-key-alias']; - if (keystorePassword.isEmpty || keyAlias.isEmpty) { - printError('Must provide a keystore password and a key alias'); - return 1; - } - keyPassword = argResults['keystore-key-password']; - if (keyPassword.isEmpty) - keyPassword = keystorePassword; - } - - builder.sign(keystore, keystorePassword, keyAlias, keyPassword, apk); - - return 0; - } - @override Future runInProject() async { - BuildConfiguration config = buildConfigurations.firstWhere( - (BuildConfiguration bc) => bc.targetPlatform == TargetPlatform.android + await downloadToolchain(); + return await buildAndroid( + toolchain: toolchain, + configs: buildConfigurations, + enginePath: runner.enginePath, + force: true, + manifest: argResults['manifest'], + resources: argResults['resources'], + outputFile: argResults['output-file'], + target: argResults['target'], + flxPath: argResults['flx'], + keystore: argResults['keystore'].isEmpty ? null : new ApkKeystoreInfo( + keystore: argResults['keystore'], + password: argResults['keystore-password'], + keyAlias: argResults['keystore-key-alias'], + keyPassword: argResults['keystore-key-password'] + ) ); + } +} - _ApkComponents components = await _findApkComponents(config); - if (components == null) { - printError('Unable to build APK.'); - return 1; - } +Future _findServices(_ApkComponents components) async { + if (!ArtifactStore.isPackageRootValid) + return; - String flxPath = argResults['flx']; + dynamic manifest = _loadYamlFile(_kFlutterManifestPath); + if (manifest['services'] == null) + return; - if (!flxPath.isEmpty) { - if (!FileSystemEntity.isFileSync(flxPath)) { - printError('FLX does not exist: $flxPath'); - printError('(Omit the --flx option to build the FLX automatically)'); - return 1; - } - return _buildApk(components, flxPath); - } else { - await downloadToolchain(); - - // Find the path to the main Dart file. - String mainPath = findMainDartFile(argResults['target']); - - // Build the FLX. - flx.DirectoryResult buildResult = await flx.buildInTempDir(toolchain, mainPath: mainPath); - - try { - return _buildApk(components, buildResult.localBundlePath); - } finally { - buildResult.dispose(); + for (String service in manifest['services']) { + String serviceRoot = '${ArtifactStore.packageRoot}/$service/apk'; + dynamic serviceConfig = _loadYamlFile('$serviceRoot/config.yaml'); + if (serviceConfig == null || serviceConfig['jars'] == null) + continue; + components.services.addAll(serviceConfig['services']); + for (String jar in serviceConfig['jars']) { + if (jar.startsWith("android-sdk:")) { + // Jar is something shipped in the standard android SDK. + jar = jar.replaceAll('android-sdk:', '${components.androidSdk.path}/'); + components.jars.add(new File(jar)); + } else if (jar.startsWith("http")) { + // Jar is a URL to download. + String cachePath = await ArtifactStore.getThirdPartyFile(jar, service); + components.jars.add(new File(cachePath)); + } else { + // Assume jar is a path relative to the service's root dir. + components.jars.add(new File(path.join(serviceRoot, jar))); } } } } + +Future<_ApkComponents> _findApkComponents( + BuildConfiguration config, String enginePath, String manifest, String resources +) async { + String androidSdkPath; + List artifactPaths; + if (enginePath != null) { + androidSdkPath = '$enginePath/third_party/android_tools/sdk'; + artifactPaths = [ + '$enginePath/third_party/icu/android/icudtl.dat', + '${config.buildDir}/gen/sky/shell/shell/classes.dex.jar', + '${config.buildDir}/gen/sky/shell/shell/shell/libs/armeabi-v7a/libsky_shell.so', + '$enginePath/build/android/ant/chromium-debug.keystore', + ]; + } else { + androidSdkPath = AndroidDevice.getAndroidSdkPath(); + if (androidSdkPath == null) + return null; + List artifactTypes = [ + ArtifactType.androidIcuData, + ArtifactType.androidClassesJar, + ArtifactType.androidLibSkyShell, + ArtifactType.androidKeystore, + ]; + Iterable> pathFutures = artifactTypes.map( + (ArtifactType type) => ArtifactStore.getPath(ArtifactStore.getArtifact( + type: type, targetPlatform: TargetPlatform.android))); + artifactPaths = await Future.wait(pathFutures); + } + + _ApkComponents components = new _ApkComponents(); + components.androidSdk = new Directory(androidSdkPath); + components.manifest = new File(manifest); + components.icuData = new File(artifactPaths[0]); + components.jars = [new File(artifactPaths[1])]; + components.libSkyShell = new File(artifactPaths[2]); + components.debugKeystore = new File(artifactPaths[3]); + components.resources = new Directory(resources); + + await _findServices(components); + + if (!components.resources.existsSync()) { + // TODO(eseidel): This level should be higher when path is manually set. + printStatus('Can not locate Resources: ${components.resources}, ignoring.'); + components.resources = null; + } + + if (!components.androidSdk.existsSync()) { + printError('Can not locate Android SDK: $androidSdkPath'); + return null; + } + if (!(new _ApkBuilder(components.androidSdk.path).checkSdkPath())) { + printError('Can not locate expected Android SDK tools at $androidSdkPath'); + printError('You must install version $_kAndroidPlatformVersion of the SDK platform'); + printError('and version $_kBuildToolsVersion of the build tools.'); + return null; + } + for (File f in [components.manifest, components.icuData, + components.libSkyShell, components.debugKeystore] + ..addAll(components.jars)) { + if (!f.existsSync()) { + printError('Can not locate file: ${f.path}'); + return null; + } + } + + return components; +} + +// Outputs a services.json file for the flutter engine to read. Format: +// { +// services: [ +// { name: string, class: string }, +// ... +// ] +// } +void _generateServicesConfig(File servicesConfig, List> servicesIn) { + List> services = + servicesIn.map((Map service) => { + 'name': service['name'], + 'class': service['registration-class'] + }).toList(); + + Map json = { 'services': services }; + servicesConfig.writeAsStringSync(JSON.encode(json), mode: FileMode.WRITE, flush: true); +} + +int _buildApk( + _ApkComponents components, String flxPath, ApkKeystoreInfo keystore, String outputFile +) { + Directory tempDir = Directory.systemTemp.createTempSync('flutter_tools'); + try { + _ApkBuilder builder = new _ApkBuilder(components.androidSdk.path); + + File classesDex = new File('${tempDir.path}/classes.dex'); + builder.compileClassesDex(classesDex, components.jars); + + File servicesConfig = new File('${tempDir.path}/services.json'); + _generateServicesConfig(servicesConfig, components.services); + + _AssetBuilder assetBuilder = new _AssetBuilder(tempDir, 'assets'); + assetBuilder.add(components.icuData, 'icudtl.dat'); + assetBuilder.add(new File(flxPath), 'app.flx'); + assetBuilder.add(servicesConfig, 'services.json'); + + _AssetBuilder artifactBuilder = new _AssetBuilder(tempDir, 'artifacts'); + artifactBuilder.add(classesDex, 'classes.dex'); + artifactBuilder.add(components.libSkyShell, 'lib/armeabi-v7a/libsky_shell.so'); + + File unalignedApk = new File('${tempDir.path}/app.apk.unaligned'); + builder.package(unalignedApk, components.manifest, assetBuilder.directory, + artifactBuilder.directory, components.resources); + + int signResult = _signApk(builder, components, unalignedApk, keystore); + if (signResult != 0) + return signResult; + + File finalApk = new File(outputFile); + ensureDirectoryExists(finalApk.path); + builder.align(unalignedApk, finalApk); + + printStatus('APK generated: ${finalApk.path}'); + + return 0; + } finally { + tempDir.deleteSync(recursive: true); + } +} + +int _signApk( + _ApkBuilder builder, _ApkComponents components, File apk, ApkKeystoreInfo keystoreInfo +) { + File keystore; + String keystorePassword; + String keyAlias; + String keyPassword; + + if (keystoreInfo == null) { + printError('Signing the APK using the debug keystore.'); + keystore = components.debugKeystore; + keystorePassword = _kDebugKeystorePassword; + keyAlias = _kDebugKeystoreKeyAlias; + keyPassword = _kDebugKeystorePassword; + } else { + keystore = new File(keystoreInfo.keystore); + keystorePassword = keystoreInfo.password; + keyAlias = keystoreInfo.keyAlias; + if (keystorePassword.isEmpty || keyAlias.isEmpty) { + printError('Must provide a keystore password and a key alias.'); + return 1; + } + keyPassword = keystoreInfo.keyPassword; + if (keyPassword.isEmpty) + keyPassword = keystorePassword; + } + + builder.sign(keystore, keystorePassword, keyAlias, keyPassword, apk); + + return 0; +} + +// Creates a new ApplicationPackage from the Android manifest. +AndroidApk _getApplicationPackage(String apkPath, String manifest) { + if (!FileSystemEntity.isFileSync(manifest)) + return null; + String manifestString = new File(manifest).readAsStringSync(); + xml.XmlDocument document = xml.parse(manifestString); + + Iterable manifests = document.findElements('manifest'); + if (manifests.isEmpty) + return null; + String id = manifests.toList()[0].getAttribute('package'); + + String launchActivity; + for (xml.XmlElement category in document.findAllElements('category')) { + if (category.getAttribute('android:name') == 'android.intent.category.LAUNCHER') { + xml.XmlElement activity = category.parent.parent as xml.XmlElement; + String activityName = activity.getAttribute('android:name'); + launchActivity = "$id/$activityName"; + break; + } + } + if (id == null || launchActivity == null) + return null; + + return new AndroidApk(localPath: apkPath, id: id, launchActivity: launchActivity); +} + +// Returns true if the apk is out of date and needs to be rebuilt. +bool _needsRebuild(String apkPath, String manifest) { + FileStat apkStat = FileStat.statSync(apkPath); + // Note: This list of dependencies is imperfect, but will do for now. We + // purposely don't include the .dart files, because we can load those + // over the network without needing to rebuild (at least on Android). + List dependenciesStat = [ + manifest, + _kFlutterManifestPath, + _kPackagesStatusPath + ].map((String path) => FileStat.statSync(path)); + + if (apkStat.type == FileSystemEntityType.NOT_FOUND) + return true; + for (FileStat dep in dependenciesStat) { + if (dep.modified.isAfter(apkStat.modified)) + return true; + } + return false; +} + +Future buildAndroid({ + Toolchain toolchain, + List configs, + String enginePath, + bool force: false, + String manifest: _kDefaultAndroidManifestPath, + String resources: _kDefaultResourcesPath, + String outputFile: _kDefaultOutputPath, + String target: '', + String flxPath: '', + ApkKeystoreInfo keystore +}) async { + if (!_needsRebuild(outputFile, manifest)) { + printTrace('APK up to date. Skipping build step.'); + return 0; + } + + BuildConfiguration config = configs.firstWhere( + (BuildConfiguration bc) => bc.targetPlatform == TargetPlatform.android + ); + _ApkComponents components = await _findApkComponents(config, enginePath, manifest, resources); + if (components == null) { + printError('Failure building APK. Unable to find components.'); + return 1; + } + + printStatus('Building APK...'); + + if (!flxPath.isEmpty) { + if (!FileSystemEntity.isFileSync(flxPath)) { + printError('FLX does not exist: $flxPath'); + printError('(Omit the --flx option to build the FLX automatically)'); + return 1; + } + return _buildApk(components, flxPath, keystore, outputFile); + } else { + // Find the path to the main Dart file. + String mainPath = findMainDartFile(target); + + // Build the FLX. + flx.DirectoryResult buildResult = await flx.buildInTempDir(toolchain, mainPath: mainPath); + + try { + return _buildApk(components, buildResult.localBundlePath, keystore, outputFile); + } finally { + buildResult.dispose(); + } + } +} + +Future buildAll( + DeviceStore devices, + ApplicationPackageStore applicationPackages, + Toolchain toolchain, + List configs, { + String enginePath, + String target: '' +}) async { + for (Device device in devices.all) { + ApplicationPackage package = applicationPackages.getPackageForPlatform(device.platform); + if (package == null || !device.isConnected()) + continue; + + // TODO(mpcomplete): Temporary hack. We only support the apk builder atm. + if (package == applicationPackages.android) { + if (!FileSystemEntity.isFileSync(_kDefaultAndroidManifestPath)) { + printStatus('Using pre-built SkyShell.apk'); + continue; + } + + await buildAndroid( + toolchain: toolchain, + configs: configs, + enginePath: enginePath, + force: false, + target: target + ); + // Replace our pre-built AndroidApk with this custom-built one. + applicationPackages = new ApplicationPackageStore( + android: _getApplicationPackage(_kDefaultOutputPath, _kDefaultAndroidManifestPath), + iOS: applicationPackages.iOS, + iOSSimulator: applicationPackages.iOSSimulator + ); + } + } + return applicationPackages; +} diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index b8a3d94a1ba..947fbb94ca1 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -253,6 +253,7 @@ class AppDomain extends Domain { command.devices, command.applicationPackages, command.toolchain, + command.buildConfigurations, target: args['target'], route: args['route'], checked: args['checked'] ?? true diff --git a/packages/flutter_tools/lib/src/commands/listen.dart b/packages/flutter_tools/lib/src/commands/listen.dart index 4421d70fb9c..b139355cdf2 100644 --- a/packages/flutter_tools/lib/src/commands/listen.dart +++ b/packages/flutter_tools/lib/src/commands/listen.dart @@ -41,6 +41,7 @@ class ListenCommand extends StartCommandBase { devices, applicationPackages, toolchain, + buildConfigurations, target: argResults['target'], install: firstTime, stop: true, diff --git a/packages/flutter_tools/lib/src/commands/start.dart b/packages/flutter_tools/lib/src/commands/start.dart index d5d636be136..aa4fe6456b4 100644 --- a/packages/flutter_tools/lib/src/commands/start.dart +++ b/packages/flutter_tools/lib/src/commands/start.dart @@ -10,9 +10,11 @@ import 'package:path/path.dart' as path; import '../application_package.dart'; import '../base/common.dart'; import '../base/context.dart'; +import '../build_configuration.dart'; import '../device.dart'; import '../runner/flutter_command.dart'; import '../toolchain.dart'; +import 'apk.dart'; import 'install.dart'; import 'stop.dart'; @@ -92,7 +94,9 @@ class StartCommand extends StartCommandBase { devices, applicationPackages, toolchain, + buildConfigurations, target: argResults['target'], + enginePath: runner.enginePath, install: true, stop: argResults['full-restart'], checked: argResults['checked'], @@ -111,8 +115,10 @@ class StartCommand extends StartCommandBase { Future startApp( DeviceStore devices, ApplicationPackageStore applicationPackages, - Toolchain toolchain, { + Toolchain toolchain, + List configs, { String target, + String enginePath, bool stop: true, bool install: true, bool checked: true, @@ -132,6 +138,14 @@ Future startApp( return 1; } + if (install) { + printTrace('Running build command.'); + applicationPackages = await buildAll( + devices, applicationPackages, toolchain, configs, + enginePath: enginePath, + target: target); + } + if (stop) { printTrace('Running stop command.'); stopAll(devices, applicationPackages); diff --git a/packages/flutter_tools/lib/src/ios/device_ios.dart b/packages/flutter_tools/lib/src/ios/device_ios.dart index bef42bb9ee2..9e5cef17fbf 100644 --- a/packages/flutter_tools/lib/src/ios/device_ios.dart +++ b/packages/flutter_tools/lib/src/ios/device_ios.dart @@ -614,7 +614,7 @@ class _IOSSimulatorLogReader extends DeviceLogReader { } ); - return result; + return await result; } int get hashCode => device.logFilePath.hashCode; diff --git a/packages/flutter_tools/pubspec.yaml b/packages/flutter_tools/pubspec.yaml index b55efcdc57b..5b8898fe070 100644 --- a/packages/flutter_tools/pubspec.yaml +++ b/packages/flutter_tools/pubspec.yaml @@ -18,6 +18,7 @@ dependencies: stack_trace: ^1.4.0 test: 0.12.6+1 # see note below yaml: ^2.1.3 + xml: ^2.4.1 flx: path: ../flx diff --git a/packages/flutter_tools/test/listen_test.dart b/packages/flutter_tools/test/listen_test.dart index 898c585dc78..18170bb2c82 100644 --- a/packages/flutter_tools/test/listen_test.dart +++ b/packages/flutter_tools/test/listen_test.dart @@ -4,6 +4,7 @@ import 'package:args/command_runner.dart'; import 'package:flutter_tools/src/commands/listen.dart'; +import 'package:flutter_tools/src/runner/flutter_command_runner.dart'; import 'package:mockito/mockito.dart'; import 'package:test/test.dart'; @@ -22,8 +23,7 @@ defineTests() { when(mockDevices.iOS.isConnected()).thenReturn(false); when(mockDevices.iOSSimulator.isConnected()).thenReturn(false); - CommandRunner runner = new CommandRunner('test_flutter', '') - ..addCommand(command); + CommandRunner runner = new FlutterCommandRunner()..addCommand(command); runner.run(['listen']).then((int code) => expect(code, equals(0))); }); });