Commit 5d2281b6 authored by Matt Perry's avatar Matt Perry

'flutter start' calls 'flutter apk' if necessary.

flutter start no longer depends on a pre-built SkyShell.apk. It builds a
new one, as long as an AndroidManifest.xml exists.

We rebuild the .apk every time either AndroidManifest.xml or
flutter.yaml changes.
parent 4b7d601e
...@@ -8,15 +8,19 @@ import 'dart:io'; ...@@ -8,15 +8,19 @@ import 'dart:io';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:yaml/yaml.dart'; import 'package:yaml/yaml.dart';
import 'package:xml/xml.dart' as xml;
import '../android/device_android.dart'; import '../android/device_android.dart';
import '../application_package.dart';
import '../artifacts.dart'; import '../artifacts.dart';
import '../base/context.dart'; import '../base/context.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../base/process.dart'; import '../base/process.dart';
import '../build_configuration.dart'; import '../build_configuration.dart';
import '../device.dart';
import '../flx.dart' as flx; import '../flx.dart' as flx;
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
import '../toolchain.dart';
import 'start.dart'; import 'start.dart';
const String _kDefaultAndroidManifestPath = 'apk/AndroidManifest.xml'; const String _kDefaultAndroidManifestPath = 'apk/AndroidManifest.xml';
...@@ -25,6 +29,7 @@ const String _kDefaultResourcesPath = 'apk/res'; ...@@ -25,6 +29,7 @@ const String _kDefaultResourcesPath = 'apk/res';
const String _kFlutterManifestPath = 'flutter.yaml'; const String _kFlutterManifestPath = 'flutter.yaml';
const String _kPubspecYamlPath = 'pubspec.yaml'; const String _kPubspecYamlPath = 'pubspec.yaml';
const String _kPackagesStatusPath = '.packages';
// Alias of the key provided in the Chromium debug keystore // Alias of the key provided in the Chromium debug keystore
const String _kDebugKeystoreKeyAlias = "chromiumdebugkey"; const String _kDebugKeystoreKeyAlias = "chromiumdebugkey";
...@@ -132,6 +137,14 @@ class _ApkComponents { ...@@ -132,6 +137,14 @@ class _ApkComponents {
Directory resources; 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. // TODO(mpcomplete): find a better home for this.
dynamic _loadYamlFile(String path) { dynamic _loadYamlFile(String path) {
if (!FileSystemEntity.isFileSync(path)) if (!FileSystemEntity.isFileSync(path))
...@@ -179,227 +192,352 @@ class ApkCommand extends FlutterCommand { ...@@ -179,227 +192,352 @@ class ApkCommand extends FlutterCommand {
help: 'Password for the entry within the keystore.'); help: 'Password for the entry within the keystore.');
} }
Future _findServices(_ApkComponents components) async { @override
if (!ArtifactStore.isPackageRootValid) Future<int> runInProject() async {
return; await downloadToolchain();
return await buildAndroid(
dynamic manifest = _loadYamlFile(_kFlutterManifestPath); toolchain: toolchain,
if (manifest['services'] == null) configs: buildConfigurations,
return; 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']
)
);
}
}
for (String service in manifest['services']) { Future _findServices(_ApkComponents components) async {
String serviceRoot = '${ArtifactStore.packageRoot}/$service/apk'; if (!ArtifactStore.isPackageRootValid)
dynamic serviceConfig = _loadYamlFile('$serviceRoot/config.yaml'); return;
if (serviceConfig == null || serviceConfig['jars'] == null)
continue; dynamic manifest = _loadYamlFile(_kFlutterManifestPath);
components.services.addAll(serviceConfig['services']); if (manifest['services'] == null)
for (String jar in serviceConfig['jars']) { return;
if (jar.startsWith("android-sdk:")) {
// Jar is something shipped in the standard android SDK. for (String service in manifest['services']) {
jar = jar.replaceAll('android-sdk:', '${components.androidSdk.path}/'); String serviceRoot = '${ArtifactStore.packageRoot}/$service/apk';
components.jars.add(new File(jar)); dynamic serviceConfig = _loadYamlFile('$serviceRoot/config.yaml');
} else if (jar.startsWith("http")) { if (serviceConfig == null || serviceConfig['jars'] == null)
// Jar is a URL to download. continue;
String cachePath = await ArtifactStore.getThirdPartyFile(jar, service); components.services.addAll(serviceConfig['services']);
components.jars.add(new File(cachePath)); for (String jar in serviceConfig['jars']) {
} else { if (jar.startsWith("android-sdk:")) {
// Assume jar is a path relative to the service's root dir. // Jar is something shipped in the standard android SDK.
components.jars.add(new File(path.join(serviceRoot, jar))); 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 { Future<_ApkComponents> _findApkComponents(
String androidSdkPath; BuildConfiguration config, String enginePath, String manifest, String resources
List<String> artifactPaths; ) async {
if (runner.enginePath != null) { String androidSdkPath;
androidSdkPath = '${runner.enginePath}/third_party/android_tools/sdk'; List<String> artifactPaths;
artifactPaths = [ if (enginePath != null) {
'${runner.enginePath}/third_party/icu/android/icudtl.dat', androidSdkPath = '$enginePath/third_party/android_tools/sdk';
'${config.buildDir}/gen/sky/shell/shell/classes.dex.jar', artifactPaths = [
'${config.buildDir}/gen/sky/shell/shell/shell/libs/armeabi-v7a/libsky_shell.so', '$enginePath/third_party/icu/android/icudtl.dat',
'${runner.enginePath}/build/android/ant/chromium-debug.keystore', '${config.buildDir}/gen/sky/shell/shell/classes.dex.jar',
]; '${config.buildDir}/gen/sky/shell/shell/shell/libs/armeabi-v7a/libsky_shell.so',
} else { '$enginePath/build/android/ant/chromium-debug.keystore',
androidSdkPath = AndroidDevice.getAndroidSdkPath(); ];
if (androidSdkPath == null) { } else {
return null; androidSdkPath = AndroidDevice.getAndroidSdkPath();
} if (androidSdkPath == null)
List<ArtifactType> artifactTypes = <ArtifactType>[ return null;
ArtifactType.androidIcuData, List<ArtifactType> artifactTypes = <ArtifactType>[
ArtifactType.androidClassesJar, ArtifactType.androidIcuData,
ArtifactType.androidLibSkyShell, ArtifactType.androidClassesJar,
ArtifactType.androidKeystore, ArtifactType.androidLibSkyShell,
]; ArtifactType.androidKeystore,
Iterable<Future<String>> pathFutures = artifactTypes.map( ];
(ArtifactType type) => ArtifactStore.getPath(ArtifactStore.getArtifact( Iterable<Future<String>> pathFutures = artifactTypes.map(
type: type, targetPlatform: TargetPlatform.android))); (ArtifactType type) => ArtifactStore.getPath(ArtifactStore.getArtifact(
artifactPaths = await Future.wait(pathFutures); type: type, targetPlatform: TargetPlatform.android)));
} artifactPaths = await Future.wait(pathFutures);
}
_ApkComponents components = new _ApkComponents(); _ApkComponents components = new _ApkComponents();
components.androidSdk = new Directory(androidSdkPath); components.androidSdk = new Directory(androidSdkPath);
components.manifest = new File(argResults['manifest']); components.manifest = new File(manifest);
components.icuData = new File(artifactPaths[0]); components.icuData = new File(artifactPaths[0]);
components.jars = [new File(artifactPaths[1])]; components.jars = [new File(artifactPaths[1])];
components.libSkyShell = new File(artifactPaths[2]); components.libSkyShell = new File(artifactPaths[2]);
components.debugKeystore = new File(artifactPaths[3]); components.debugKeystore = new File(artifactPaths[3]);
components.resources = new Directory(argResults['resources']); components.resources = new Directory(resources);
await _findServices(components); await _findServices(components);
if (!components.resources.existsSync()) { if (!components.resources.existsSync()) {
// TODO(eseidel): This level should be higher when path is manually set. // TODO(eseidel): This level should be higher when path is manually set.
printStatus('Can not locate Resources: ${components.resources}, ignoring.'); printStatus('Can not locate Resources: ${components.resources}, ignoring.');
components.resources = null; components.resources = null;
} }
if (!components.androidSdk.existsSync()) { if (!components.androidSdk.existsSync()) {
printError('Can not locate Android SDK: $androidSdkPath'); printError('Can not locate Android SDK: $androidSdkPath');
return null; return null;
} }
if (!(new _ApkBuilder(components.androidSdk.path).checkSdkPath())) { if (!(new _ApkBuilder(components.androidSdk.path).checkSdkPath())) {
printError('Can not locate expected Android SDK tools at $androidSdkPath'); printError('Can not locate expected Android SDK tools at $androidSdkPath');
printError('You must install version $_kAndroidPlatformVersion of the SDK platform'); printError('You must install version $_kAndroidPlatformVersion of the SDK platform');
printError('and version $_kBuildToolsVersion of the build tools.'); 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 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: return components;
// { }
// services: [
// { name: string, class: string },
// ...
// ]
// }
void _generateServicesConfig(File servicesConfig, List<Map<String, String>> servicesIn) {
List<Map<String, String>> services =
servicesIn.map((Map<String, String> service) => {
'name': service['name'],
'class': service['registration-class']
}).toList();
Map<String, dynamic> json = { 'services': services };
servicesConfig.writeAsStringSync(JSON.encode(json), mode: FileMode.WRITE, flush: true);
}
int _buildApk(_ApkComponents components, String flxPath) { // Outputs a services.json file for the flutter engine to read. Format:
Directory tempDir = Directory.systemTemp.createTempSync('flutter_tools'); // {
try { // services: [
_ApkBuilder builder = new _ApkBuilder(components.androidSdk.path); // { name: string, class: string },
// ...
// ]
// }
void _generateServicesConfig(File servicesConfig, List<Map<String, String>> servicesIn) {
List<Map<String, String>> services =
servicesIn.map((Map<String, String> service) => {
'name': service['name'],
'class': service['registration-class']
}).toList();
Map<String, dynamic> json = { 'services': services };
servicesConfig.writeAsStringSync(JSON.encode(json), mode: FileMode.WRITE, flush: true);
}
File classesDex = new File('${tempDir.path}/classes.dex'); int _buildApk(
builder.compileClassesDex(classesDex, components.jars); _ApkComponents components, String flxPath, ApkKeystoreInfo keystore, String outputFile
) {
Directory tempDir = Directory.systemTemp.createTempSync('flutter_tools');
try {
_ApkBuilder builder = new _ApkBuilder(components.androidSdk.path);
File servicesConfig = new File('${tempDir.path}/services.json'); File classesDex = new File('${tempDir.path}/classes.dex');
_generateServicesConfig(servicesConfig, components.services); builder.compileClassesDex(classesDex, components.jars);
_AssetBuilder assetBuilder = new _AssetBuilder(tempDir, 'assets'); File servicesConfig = new File('${tempDir.path}/services.json');
assetBuilder.add(components.icuData, 'icudtl.dat'); _generateServicesConfig(servicesConfig, components.services);
assetBuilder.add(new File(flxPath), 'app.flx');
assetBuilder.add(servicesConfig, 'services.json');
_AssetBuilder artifactBuilder = new _AssetBuilder(tempDir, 'artifacts'); _AssetBuilder assetBuilder = new _AssetBuilder(tempDir, 'assets');
artifactBuilder.add(classesDex, 'classes.dex'); assetBuilder.add(components.icuData, 'icudtl.dat');
artifactBuilder.add(components.libSkyShell, 'lib/armeabi-v7a/libsky_shell.so'); assetBuilder.add(new File(flxPath), 'app.flx');
assetBuilder.add(servicesConfig, 'services.json');
File unalignedApk = new File('${tempDir.path}/app.apk.unaligned'); _AssetBuilder artifactBuilder = new _AssetBuilder(tempDir, 'artifacts');
builder.package(unalignedApk, components.manifest, assetBuilder.directory, artifactBuilder.add(classesDex, 'classes.dex');
artifactBuilder.directory, components.resources); artifactBuilder.add(components.libSkyShell, 'lib/armeabi-v7a/libsky_shell.so');
int signResult = _signApk(builder, components, unalignedApk); File unalignedApk = new File('${tempDir.path}/app.apk.unaligned');
if (signResult != 0) builder.package(unalignedApk, components.manifest, assetBuilder.directory,
return signResult; artifactBuilder.directory, components.resources);
File finalApk = new File(argResults['output-file']); int signResult = _signApk(builder, components, unalignedApk, keystore);
ensureDirectoryExists(finalApk.path); if (signResult != 0)
builder.align(unalignedApk, finalApk); return signResult;
printStatus('APK generated: ${finalApk.path}'); File finalApk = new File(outputFile);
ensureDirectoryExists(finalApk.path);
builder.align(unalignedApk, finalApk);
return 0; printStatus('APK generated: ${finalApk.path}');
} finally {
tempDir.deleteSync(recursive: true); 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;
} }
int _signApk(_ApkBuilder builder, _ApkComponents components, File apk) { builder.sign(keystore, keystorePassword, keyAlias, keyPassword, apk);
File keystore;
String keystorePassword; return 0;
String keyAlias; }
String keyPassword;
// Creates a new ApplicationPackage from the Android manifest.
if (argResults['keystore'].isEmpty) { AndroidApk _getApplicationPackage(String apkPath, String manifest) {
printError('Signing the APK using the debug keystore'); if (!FileSystemEntity.isFileSync(manifest))
keystore = components.debugKeystore; return null;
keystorePassword = _kDebugKeystorePassword; String manifestString = new File(manifest).readAsStringSync();
keyAlias = _kDebugKeystoreKeyAlias; xml.XmlDocument document = xml.parse(manifestString);
keyPassword = _kDebugKeystorePassword;
} else { Iterable<xml.XmlElement> manifests = document.findElements('manifest');
keystore = new File(argResults['keystore']); if (manifests.isEmpty)
keystorePassword = argResults['keystore-password']; return null;
keyAlias = argResults['keystore-key-alias']; String id = manifests.toList()[0].getAttribute('package');
if (keystorePassword.isEmpty || keyAlias.isEmpty) {
printError('Must provide a keystore password and a key alias'); String launchActivity;
return 1; for (xml.XmlElement category in document.findAllElements('category')) {
} if (category.getAttribute('android:name') == 'android.intent.category.LAUNCHER') {
keyPassword = argResults['keystore-key-password']; xml.XmlElement activity = category.parent.parent as xml.XmlElement;
if (keyPassword.isEmpty) String activityName = activity.getAttribute('android:name');
keyPassword = keystorePassword; launchActivity = "$id/$activityName";
break;
} }
}
if (id == null || launchActivity == null)
return null;
builder.sign(keystore, keystorePassword, keyAlias, keyPassword, apk); 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<FileStat> 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<int> buildAndroid({
Toolchain toolchain,
List<BuildConfiguration> 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; return 0;
} }
@override BuildConfiguration config = configs.firstWhere(
Future<int> runInProject() async { (BuildConfiguration bc) => bc.targetPlatform == TargetPlatform.android
BuildConfiguration config = buildConfigurations.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...');
_ApkComponents components = await _findApkComponents(config); if (!flxPath.isEmpty) {
if (components == null) { if (!FileSystemEntity.isFileSync(flxPath)) {
printError('Unable to build APK.'); printError('FLX does not exist: $flxPath');
printError('(Omit the --flx option to build the FLX automatically)');
return 1; return 1;
} }
return _buildApk(components, flxPath, keystore, outputFile);
} else {
// Find the path to the main Dart file.
String mainPath = findMainDartFile(target);
String flxPath = argResults['flx']; // Build the FLX.
flx.DirectoryResult buildResult = await flx.buildInTempDir(toolchain, mainPath: mainPath);
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. try {
flx.DirectoryResult buildResult = await flx.buildInTempDir(toolchain, mainPath: mainPath); return _buildApk(components, buildResult.localBundlePath, keystore, outputFile);
} finally {
buildResult.dispose();
}
}
}
try { Future<ApplicationPackageStore> buildAll(
return _buildApk(components, buildResult.localBundlePath); DeviceStore devices,
} finally { ApplicationPackageStore applicationPackages,
buildResult.dispose(); Toolchain toolchain,
List<BuildConfiguration> 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;
} }
...@@ -253,6 +253,7 @@ class AppDomain extends Domain { ...@@ -253,6 +253,7 @@ class AppDomain extends Domain {
command.devices, command.devices,
command.applicationPackages, command.applicationPackages,
command.toolchain, command.toolchain,
command.buildConfigurations,
target: args['target'], target: args['target'],
route: args['route'], route: args['route'],
checked: args['checked'] ?? true checked: args['checked'] ?? true
......
...@@ -41,6 +41,7 @@ class ListenCommand extends StartCommandBase { ...@@ -41,6 +41,7 @@ class ListenCommand extends StartCommandBase {
devices, devices,
applicationPackages, applicationPackages,
toolchain, toolchain,
buildConfigurations,
target: argResults['target'], target: argResults['target'],
install: firstTime, install: firstTime,
stop: true, stop: true,
......
...@@ -10,9 +10,11 @@ import 'package:path/path.dart' as path; ...@@ -10,9 +10,11 @@ import 'package:path/path.dart' as path;
import '../application_package.dart'; import '../application_package.dart';
import '../base/common.dart'; import '../base/common.dart';
import '../base/context.dart'; import '../base/context.dart';
import '../build_configuration.dart';
import '../device.dart'; import '../device.dart';
import '../runner/flutter_command.dart'; import '../runner/flutter_command.dart';
import '../toolchain.dart'; import '../toolchain.dart';
import 'apk.dart';
import 'install.dart'; import 'install.dart';
import 'stop.dart'; import 'stop.dart';
...@@ -92,7 +94,9 @@ class StartCommand extends StartCommandBase { ...@@ -92,7 +94,9 @@ class StartCommand extends StartCommandBase {
devices, devices,
applicationPackages, applicationPackages,
toolchain, toolchain,
buildConfigurations,
target: argResults['target'], target: argResults['target'],
enginePath: runner.enginePath,
install: true, install: true,
stop: argResults['full-restart'], stop: argResults['full-restart'],
checked: argResults['checked'], checked: argResults['checked'],
...@@ -111,8 +115,10 @@ class StartCommand extends StartCommandBase { ...@@ -111,8 +115,10 @@ class StartCommand extends StartCommandBase {
Future<int> startApp( Future<int> startApp(
DeviceStore devices, DeviceStore devices,
ApplicationPackageStore applicationPackages, ApplicationPackageStore applicationPackages,
Toolchain toolchain, { Toolchain toolchain,
List<BuildConfiguration> configs, {
String target, String target,
String enginePath,
bool stop: true, bool stop: true,
bool install: true, bool install: true,
bool checked: true, bool checked: true,
...@@ -132,6 +138,14 @@ Future<int> startApp( ...@@ -132,6 +138,14 @@ Future<int> startApp(
return 1; return 1;
} }
if (install) {
printTrace('Running build command.');
applicationPackages = await buildAll(
devices, applicationPackages, toolchain, configs,
enginePath: enginePath,
target: target);
}
if (stop) { if (stop) {
printTrace('Running stop command.'); printTrace('Running stop command.');
stopAll(devices, applicationPackages); stopAll(devices, applicationPackages);
......
...@@ -614,7 +614,7 @@ class _IOSSimulatorLogReader extends DeviceLogReader { ...@@ -614,7 +614,7 @@ class _IOSSimulatorLogReader extends DeviceLogReader {
} }
); );
return result; return await result;
} }
int get hashCode => device.logFilePath.hashCode; int get hashCode => device.logFilePath.hashCode;
......
...@@ -18,6 +18,7 @@ dependencies: ...@@ -18,6 +18,7 @@ dependencies:
stack_trace: ^1.4.0 stack_trace: ^1.4.0
test: 0.12.6+1 # see note below test: 0.12.6+1 # see note below
yaml: ^2.1.3 yaml: ^2.1.3
xml: ^2.4.1
flx: flx:
path: ../flx path: ../flx
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'package:args/command_runner.dart'; import 'package:args/command_runner.dart';
import 'package:flutter_tools/src/commands/listen.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:mockito/mockito.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
...@@ -22,8 +23,7 @@ defineTests() { ...@@ -22,8 +23,7 @@ defineTests() {
when(mockDevices.iOS.isConnected()).thenReturn(false); when(mockDevices.iOS.isConnected()).thenReturn(false);
when(mockDevices.iOSSimulator.isConnected()).thenReturn(false); when(mockDevices.iOSSimulator.isConnected()).thenReturn(false);
CommandRunner runner = new CommandRunner('test_flutter', '') CommandRunner runner = new FlutterCommandRunner()..addCommand(command);
..addCommand(command);
runner.run(['listen']).then((int code) => expect(code, equals(0))); runner.run(['listen']).then((int code) => expect(code, equals(0)));
}); });
}); });
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment