make flutter run work with a pre-built apk (#5307) * make flutter run work with a pre-built apk * refactor to remove the buildDir param
diff --git a/packages/flutter_tools/lib/src/application_package.dart b/packages/flutter_tools/lib/src/application_package.dart index b15dde6..47e32c3 100644 --- a/packages/flutter_tools/lib/src/application_package.dart +++ b/packages/flutter_tools/lib/src/application_package.dart
@@ -8,18 +8,16 @@ import 'package:xml/xml.dart' as xml; import 'android/gradle.dart'; +import 'base/process.dart'; import 'build_info.dart'; +import 'globals.dart'; import 'ios/plist_utils.dart'; abstract class ApplicationPackage { - /// Path to the package's root folder. - final String rootPath; - /// Package ID from the Android Manifest or equivalent. final String id; - ApplicationPackage({this.rootPath, this.id}) { - assert(rootPath != null); + ApplicationPackage({ this.id }) { assert(id != null); } @@ -39,15 +37,42 @@ final String launchActivity; AndroidApk({ - String buildDir, String id, this.apkPath, this.launchActivity - }) : super(rootPath: buildDir, id: id) { + }) : super(id: id) { assert(apkPath != null); assert(launchActivity != null); } + /// Creates a new AndroidApk from an existing APK. + factory AndroidApk.fromApk(String applicationBinary) { + String aaptPath = androidSdk?.latestVersion?.aaptPath; + if (aaptPath == null) { + printError('Unable to locate the Android SDK; please run \'flutter doctor\'.'); + return null; + } + + List<String> aaptArgs = <String>[aaptPath, 'dump', 'badging', applicationBinary]; + ApkManifestData data = ApkManifestData.parseFromAaptBadging(runCheckedSync(aaptArgs)); + + if (data == null) { + printError('Unable to read manifest info from $applicationBinary.'); + return null; + } + + if (data.packageName == null || data.launchableActivityName == null) { + printError('Unable to read manifest info from $applicationBinary.'); + return null; + } + + return new AndroidApk( + id: data.packageName, + apkPath: applicationBinary, + launchActivity: '${data.packageName}/${data.launchableActivityName}' + ); + } + /// Creates a new AndroidApk based on the information in the Android manifest. factory AndroidApk.fromCurrentDirectory() { String manifestPath; @@ -70,23 +95,23 @@ Iterable<xml.XmlElement> manifests = document.findElements('manifest'); if (manifests.isEmpty) return null; - String id = manifests.first.getAttribute('package'); + String packageId = manifests.first.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; String activityName = activity.getAttribute('android:name'); - launchActivity = "$id/$activityName"; + launchActivity = "$packageId/$activityName"; break; } } - if (id == null || launchActivity == null) + + if (packageId == null || launchActivity == null) return null; return new AndroidApk( - buildDir: 'build', - id: id, + id: packageId, apkPath: apkPath, launchActivity: launchActivity ); @@ -100,9 +125,9 @@ static final String kBundleName = 'Runner.app'; IOSApp({ - String projectDir, + this.appDirectory, String projectBundleId - }) : super(rootPath: projectDir, id: projectBundleId); + }) : super(id: projectBundleId); factory IOSApp.fromCurrentDirectory() { if (getCurrentHostPlatform() != HostPlatform.darwin_x64) @@ -114,7 +139,7 @@ return null; return new IOSApp( - projectDir: path.join('ios'), + appDirectory: path.join('ios'), projectBundleId: value ); } @@ -125,20 +150,26 @@ @override String get displayName => id; + final String appDirectory; + String get simulatorBundlePath => _buildAppPath('iphonesimulator'); String get deviceBundlePath => _buildAppPath('iphoneos'); String _buildAppPath(String type) { - return path.join(rootPath, 'build', 'Release-$type', kBundleName); + return path.join(appDirectory, 'build', 'Release-$type', kBundleName); } } -ApplicationPackage getApplicationPackageForPlatform(TargetPlatform platform) { +ApplicationPackage getApplicationPackageForPlatform(TargetPlatform platform, { + String applicationBinary +}) { switch (platform) { case TargetPlatform.android_arm: case TargetPlatform.android_x64: case TargetPlatform.android_x86: + if (applicationBinary != null) + return new AndroidApk.fromApk(applicationBinary); return new AndroidApk.fromCurrentDirectory(); case TargetPlatform.ios: return new IOSApp.fromCurrentDirectory(); @@ -173,3 +204,52 @@ return null; } } + +class ApkManifestData { + ApkManifestData._(this._data); + + static ApkManifestData parseFromAaptBadging(String data) { + if (data == null || data.trim().isEmpty) + return null; + + // package: name='io.flutter.gallery' versionCode='1' versionName='0.0.1' platformBuildVersionName='NMR1' + // launchable-activity: name='org.domokit.sky.shell.SkyActivity' label='' icon='' + Map<String, Map<String, String>> map = <String, Map<String, String>>{}; + + for (String line in data.split('\n')) { + int index = line.indexOf(':'); + if (index != -1) { + String name = line.substring(0, index); + line = line.substring(index + 1).trim(); + + Map<String, String> entries = <String, String>{}; + map[name] = entries; + + for (String entry in line.split(' ')) { + entry = entry.trim(); + if (entry.isNotEmpty && entry.contains('=')) { + int split = entry.indexOf('='); + String key = entry.substring(0, split); + String value = entry.substring(split + 1); + if (value.startsWith("'") && value.endsWith("'")) + value = value.substring(1, value.length - 1); + entries[key] = value; + } + } + } + } + + return new ApkManifestData._(map); + } + + final Map<String, Map<String, String>> _data; + + String get packageName => _data['package'] == null ? null : _data['package']['name']; + + String get launchableActivityName { + return _data['launchable-activity'] == null ? null : _data['launchable-activity']['name']; + } + + @override + String toString() => _data.toString(); +}
diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index c31c861..944b10c 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart
@@ -25,7 +25,6 @@ abstract class RunCommandBase extends FlutterCommand { RunCommandBase() { addBuildModeFlags(defaultToRelease: false); - argParser.addFlag('trace-startup', negatable: true, defaultsTo: false, @@ -59,6 +58,9 @@ argParser.addFlag('build', defaultsTo: true, help: 'If necessary, build the app before running.'); + argParser.addOption('use-application-binary', + hide: true, + help: 'Specify a pre-built application binary to use when running.'); usesPubOption(); // Option to enable hot reloading. @@ -172,7 +174,8 @@ target: targetFile, debuggingOptions: options, traceStartup: traceStartup, - benchmark: argResults['benchmark'] + benchmark: argResults['benchmark'], + applicationBinary: argResults['use-application-binary'] ); }
diff --git a/packages/flutter_tools/lib/src/dart/package_map.dart b/packages/flutter_tools/lib/src/dart/package_map.dart index 13152c4..c0652c8 100644 --- a/packages/flutter_tools/lib/src/dart/package_map.dart +++ b/packages/flutter_tools/lib/src/dart/package_map.dart
@@ -23,6 +23,8 @@ _globalPackagesPath = value; } + static bool get isUsingCustomPackagesPath => _globalPackagesPath != null; + static String _globalPackagesPath; final String packagesPath;
diff --git a/packages/flutter_tools/lib/src/ios/mac.dart b/packages/flutter_tools/lib/src/ios/mac.dart index 8e72015..e6e8ebe 100644 --- a/packages/flutter_tools/lib/src/ios/mac.dart +++ b/packages/flutter_tools/lib/src/ios/mac.dart
@@ -98,7 +98,7 @@ } Future<XcodeBuildResult> buildXcodeProject({ - ApplicationPackage app, + IOSApp app, BuildMode mode, String target: flx.defaultMainPath, bool buildForDevice, @@ -113,7 +113,7 @@ // Before the build, all service definitions must be updated and the dylibs // copied over to a location that is suitable for Xcodebuild to find them. - await _addServicesToBundle(new Directory(app.rootPath)); + await _addServicesToBundle(new Directory(app.appDirectory)); List<String> commands = <String>[ '/usr/bin/env', @@ -125,13 +125,13 @@ 'ONLY_ACTIVE_ARCH=YES', ]; - List<FileSystemEntity> contents = new Directory(app.rootPath).listSync(); + List<FileSystemEntity> contents = new Directory(app.appDirectory).listSync(); for (FileSystemEntity entity in contents) { if (path.extension(entity.path) == '.xcworkspace') { commands.addAll(<String>[ '-workspace', path.basename(entity.path), '-scheme', path.basenameWithoutExtension(entity.path), - "BUILD_DIR=${path.absolute(app.rootPath, 'build')}", + "BUILD_DIR=${path.absolute(app.appDirectory, 'build')}", ]); break; } @@ -154,7 +154,7 @@ RunResult result = await runAsync( commands, - workingDirectory: app.rootPath, + workingDirectory: app.appDirectory, allowReentrantFlutter: true ); @@ -170,7 +170,7 @@ Match match = regexp.firstMatch(result.stdout); String outputDir; if (match != null) - outputDir = path.join(app.rootPath, match.group(1)); + outputDir = path.join(app.appDirectory, match.group(1)); return new XcodeBuildResult(true, outputDir); } }
diff --git a/packages/flutter_tools/lib/src/run.dart b/packages/flutter_tools/lib/src/run.dart index ad1d3bf..7108ef1 100644 --- a/packages/flutter_tools/lib/src/run.dart +++ b/packages/flutter_tools/lib/src/run.dart
@@ -23,7 +23,8 @@ DebuggingOptions debuggingOptions, bool usesTerminalUI: true, this.traceStartup: false, - this.benchmark: false + this.benchmark: false, + this.applicationBinary }) : super(device, target: target, debuggingOptions: debuggingOptions, @@ -32,8 +33,9 @@ ApplicationPackage _package; String _mainPath; LaunchResult _result; - bool traceStartup; - bool benchmark; + final bool traceStartup; + final bool benchmark; + final String applicationBinary; @override Future<int> run({ @@ -105,7 +107,7 @@ return 1; } - _package = getApplicationPackageForPlatform(device.platform); + _package = getApplicationPackageForPlatform(device.platform, applicationBinary: applicationBinary); if (_package == null) { String message = 'No application found for ${device.platform}.';
diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart index 1e63160..b2f14f6 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart
@@ -185,11 +185,14 @@ Validator commandValidator; bool _commandValidator() { - if (!FileSystemEntity.isFileSync('pubspec.yaml')) { - printError('Error: No pubspec.yaml file found.\n' - 'This command should be run from the root of your Flutter project.\n' - 'Do not run this command from the root of your git clone of Flutter.'); - return false; + if (!PackageMap.isUsingCustomPackagesPath) { + // Don't expect a pubspec.yaml file if the user passed in an explicit .packages file path. + if (!FileSystemEntity.isFileSync('pubspec.yaml')) { + printError('Error: No pubspec.yaml file found.\n' + 'This command should be run from the root of your Flutter project.\n' + 'Do not run this command from the root of your git clone of Flutter.'); + return false; + } } if (_usesTargetOption) {
diff --git a/packages/flutter_tools/test/all.dart b/packages/flutter_tools/test/all.dart index 6d5ba70..93867c5 100644 --- a/packages/flutter_tools/test/all.dart +++ b/packages/flutter_tools/test/all.dart
@@ -15,6 +15,7 @@ import 'analyze_test.dart' as analyze_test; import 'android_device_test.dart' as android_device_test; import 'android_sdk_test.dart' as android_sdk_test; +import 'application_package_test.dart' as application_package_test; import 'base_utils_test.dart' as base_utils_test; import 'config_test.dart' as config_test; import 'context_test.dart' as context_test; @@ -44,6 +45,7 @@ analyze_test.main(); android_device_test.main(); android_sdk_test.main(); + application_package_test.main(); base_utils_test.main(); config_test.main(); context_test.main();
diff --git a/packages/flutter_tools/test/application_package_test.dart b/packages/flutter_tools/test/application_package_test.dart new file mode 100644 index 0000000..65987ba --- /dev/null +++ b/packages/flutter_tools/test/application_package_test.dart
@@ -0,0 +1,46 @@ +// Copyright 2016 The Chromium 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 'package:flutter_tools/src/application_package.dart'; +import 'package:test/test.dart'; + +import 'src/context.dart'; + +void main() { + group('ApkManifestData', () { + testUsingContext('parse sdk', () { + ApkManifestData data = ApkManifestData.parseFromAaptBadging(_aaptData); + expect(data, isNotNull); + expect(data.packageName, 'io.flutter.gallery'); + expect(data.launchableActivityName, 'org.domokit.sky.shell.SkyActivity'); + }); + }); +} + +final String _aaptData = ''' +package: name='io.flutter.gallery' versionCode='1' versionName='0.0.1' platformBuildVersionName='NMR1' +sdkVersion:'14' +targetSdkVersion:'21' +uses-permission: name='android.permission.INTERNET' +application-label:'Flutter Gallery' +application-icon-160:'res/mipmap-mdpi-v4/ic_launcher.png' +application-icon-240:'res/mipmap-hdpi-v4/ic_launcher.png' +application-icon-320:'res/mipmap-xhdpi-v4/ic_launcher.png' +application-icon-480:'res/mipmap-xxhdpi-v4/ic_launcher.png' +application-icon-640:'res/mipmap-xxxhdpi-v4/ic_launcher.png' +application: label='Flutter Gallery' icon='res/mipmap-mdpi-v4/ic_launcher.png' +application-debuggable +launchable-activity: name='org.domokit.sky.shell.SkyActivity' label='' icon='' +feature-group: label='' + uses-feature: name='android.hardware.screen.portrait' + uses-implied-feature: name='android.hardware.screen.portrait' reason='one or more activities have specified a portrait orientation' + uses-feature: name='android.hardware.touchscreen' + uses-implied-feature: name='android.hardware.touchscreen' reason='default feature for all apps' +main +supports-screens: 'small' 'normal' 'large' 'xlarge' +supports-any-density: 'true' +locales: '--_--' +densities: '160' '240' '320' '480' '640' +native-code: 'armeabi-v7a' +''';
diff --git a/packages/flutter_tools/test/src/mocks.dart b/packages/flutter_tools/test/src/mocks.dart index a384ea1..9633a9a 100644 --- a/packages/flutter_tools/test/src/mocks.dart +++ b/packages/flutter_tools/test/src/mocks.dart
@@ -17,13 +17,12 @@ class MockApplicationPackageStore extends ApplicationPackageStore { MockApplicationPackageStore() : super( android: new AndroidApk( - buildDir: '/mock/path/to/android', id: 'io.flutter.android.mock', apkPath: '/mock/path/to/android/SkyShell.apk', launchActivity: 'io.flutter.android.mock.MockActivity' ), iOS: new IOSApp( - projectDir: '/mock/path/to/iOS/SkyShell.app', + appDirectory: '/mock/path/to/iOS/SkyShell.app', projectBundleId: 'io.flutter.ios.mock' ) );