Commit 14483586 authored by Devon Carew's avatar Devon Carew Committed by GitHub

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
parent a0aa0edf
...@@ -8,18 +8,16 @@ import 'package:path/path.dart' as path; ...@@ -8,18 +8,16 @@ import 'package:path/path.dart' as path;
import 'package:xml/xml.dart' as xml; import 'package:xml/xml.dart' as xml;
import 'android/gradle.dart'; import 'android/gradle.dart';
import 'base/process.dart';
import 'build_info.dart'; import 'build_info.dart';
import 'globals.dart';
import 'ios/plist_utils.dart'; import 'ios/plist_utils.dart';
abstract class ApplicationPackage { abstract class ApplicationPackage {
/// Path to the package's root folder.
final String rootPath;
/// Package ID from the Android Manifest or equivalent. /// Package ID from the Android Manifest or equivalent.
final String id; final String id;
ApplicationPackage({this.rootPath, this.id}) { ApplicationPackage({ this.id }) {
assert(rootPath != null);
assert(id != null); assert(id != null);
} }
...@@ -39,15 +37,42 @@ class AndroidApk extends ApplicationPackage { ...@@ -39,15 +37,42 @@ class AndroidApk extends ApplicationPackage {
final String launchActivity; final String launchActivity;
AndroidApk({ AndroidApk({
String buildDir,
String id, String id,
this.apkPath, this.apkPath,
this.launchActivity this.launchActivity
}) : super(rootPath: buildDir, id: id) { }) : super(id: id) {
assert(apkPath != null); assert(apkPath != null);
assert(launchActivity != 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. /// Creates a new AndroidApk based on the information in the Android manifest.
factory AndroidApk.fromCurrentDirectory() { factory AndroidApk.fromCurrentDirectory() {
String manifestPath; String manifestPath;
...@@ -70,23 +95,23 @@ class AndroidApk extends ApplicationPackage { ...@@ -70,23 +95,23 @@ class AndroidApk extends ApplicationPackage {
Iterable<xml.XmlElement> manifests = document.findElements('manifest'); Iterable<xml.XmlElement> manifests = document.findElements('manifest');
if (manifests.isEmpty) if (manifests.isEmpty)
return null; return null;
String id = manifests.first.getAttribute('package'); String packageId = manifests.first.getAttribute('package');
String launchActivity; String launchActivity;
for (xml.XmlElement category in document.findAllElements('category')) { for (xml.XmlElement category in document.findAllElements('category')) {
if (category.getAttribute('android:name') == 'android.intent.category.LAUNCHER') { if (category.getAttribute('android:name') == 'android.intent.category.LAUNCHER') {
xml.XmlElement activity = category.parent.parent; xml.XmlElement activity = category.parent.parent;
String activityName = activity.getAttribute('android:name'); String activityName = activity.getAttribute('android:name');
launchActivity = "$id/$activityName"; launchActivity = "$packageId/$activityName";
break; break;
} }
} }
if (id == null || launchActivity == null)
if (packageId == null || launchActivity == null)
return null; return null;
return new AndroidApk( return new AndroidApk(
buildDir: 'build', id: packageId,
id: id,
apkPath: apkPath, apkPath: apkPath,
launchActivity: launchActivity launchActivity: launchActivity
); );
...@@ -100,9 +125,9 @@ class IOSApp extends ApplicationPackage { ...@@ -100,9 +125,9 @@ class IOSApp extends ApplicationPackage {
static final String kBundleName = 'Runner.app'; static final String kBundleName = 'Runner.app';
IOSApp({ IOSApp({
String projectDir, this.appDirectory,
String projectBundleId String projectBundleId
}) : super(rootPath: projectDir, id: projectBundleId); }) : super(id: projectBundleId);
factory IOSApp.fromCurrentDirectory() { factory IOSApp.fromCurrentDirectory() {
if (getCurrentHostPlatform() != HostPlatform.darwin_x64) if (getCurrentHostPlatform() != HostPlatform.darwin_x64)
...@@ -114,7 +139,7 @@ class IOSApp extends ApplicationPackage { ...@@ -114,7 +139,7 @@ class IOSApp extends ApplicationPackage {
return null; return null;
return new IOSApp( return new IOSApp(
projectDir: path.join('ios'), appDirectory: path.join('ios'),
projectBundleId: value projectBundleId: value
); );
} }
...@@ -125,20 +150,26 @@ class IOSApp extends ApplicationPackage { ...@@ -125,20 +150,26 @@ class IOSApp extends ApplicationPackage {
@override @override
String get displayName => id; String get displayName => id;
final String appDirectory;
String get simulatorBundlePath => _buildAppPath('iphonesimulator'); String get simulatorBundlePath => _buildAppPath('iphonesimulator');
String get deviceBundlePath => _buildAppPath('iphoneos'); String get deviceBundlePath => _buildAppPath('iphoneos');
String _buildAppPath(String type) { 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) { switch (platform) {
case TargetPlatform.android_arm: case TargetPlatform.android_arm:
case TargetPlatform.android_x64: case TargetPlatform.android_x64:
case TargetPlatform.android_x86: case TargetPlatform.android_x86:
if (applicationBinary != null)
return new AndroidApk.fromApk(applicationBinary);
return new AndroidApk.fromCurrentDirectory(); return new AndroidApk.fromCurrentDirectory();
case TargetPlatform.ios: case TargetPlatform.ios:
return new IOSApp.fromCurrentDirectory(); return new IOSApp.fromCurrentDirectory();
...@@ -173,3 +204,52 @@ class ApplicationPackageStore { ...@@ -173,3 +204,52 @@ class ApplicationPackageStore {
return null; 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();
}
...@@ -25,7 +25,6 @@ import '../base/os.dart'; ...@@ -25,7 +25,6 @@ import '../base/os.dart';
abstract class RunCommandBase extends FlutterCommand { abstract class RunCommandBase extends FlutterCommand {
RunCommandBase() { RunCommandBase() {
addBuildModeFlags(defaultToRelease: false); addBuildModeFlags(defaultToRelease: false);
argParser.addFlag('trace-startup', argParser.addFlag('trace-startup',
negatable: true, negatable: true,
defaultsTo: false, defaultsTo: false,
...@@ -59,6 +58,9 @@ class RunCommand extends RunCommandBase { ...@@ -59,6 +58,9 @@ class RunCommand extends RunCommandBase {
argParser.addFlag('build', argParser.addFlag('build',
defaultsTo: true, defaultsTo: true,
help: 'If necessary, build the app before running.'); 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(); usesPubOption();
// Option to enable hot reloading. // Option to enable hot reloading.
...@@ -172,7 +174,8 @@ class RunCommand extends RunCommandBase { ...@@ -172,7 +174,8 @@ class RunCommand extends RunCommandBase {
target: targetFile, target: targetFile,
debuggingOptions: options, debuggingOptions: options,
traceStartup: traceStartup, traceStartup: traceStartup,
benchmark: argResults['benchmark'] benchmark: argResults['benchmark'],
applicationBinary: argResults['use-application-binary']
); );
} }
......
...@@ -23,6 +23,8 @@ class PackageMap { ...@@ -23,6 +23,8 @@ class PackageMap {
_globalPackagesPath = value; _globalPackagesPath = value;
} }
static bool get isUsingCustomPackagesPath => _globalPackagesPath != null;
static String _globalPackagesPath; static String _globalPackagesPath;
final String packagesPath; final String packagesPath;
......
...@@ -98,7 +98,7 @@ bool _xcodeVersionCheckValid(int major, int minor) { ...@@ -98,7 +98,7 @@ bool _xcodeVersionCheckValid(int major, int minor) {
} }
Future<XcodeBuildResult> buildXcodeProject({ Future<XcodeBuildResult> buildXcodeProject({
ApplicationPackage app, IOSApp app,
BuildMode mode, BuildMode mode,
String target: flx.defaultMainPath, String target: flx.defaultMainPath,
bool buildForDevice, bool buildForDevice,
...@@ -113,7 +113,7 @@ Future<XcodeBuildResult> buildXcodeProject({ ...@@ -113,7 +113,7 @@ Future<XcodeBuildResult> buildXcodeProject({
// Before the build, all service definitions must be updated and the dylibs // 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. // 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>[ List<String> commands = <String>[
'/usr/bin/env', '/usr/bin/env',
...@@ -125,13 +125,13 @@ Future<XcodeBuildResult> buildXcodeProject({ ...@@ -125,13 +125,13 @@ Future<XcodeBuildResult> buildXcodeProject({
'ONLY_ACTIVE_ARCH=YES', '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) { for (FileSystemEntity entity in contents) {
if (path.extension(entity.path) == '.xcworkspace') { if (path.extension(entity.path) == '.xcworkspace') {
commands.addAll(<String>[ commands.addAll(<String>[
'-workspace', path.basename(entity.path), '-workspace', path.basename(entity.path),
'-scheme', path.basenameWithoutExtension(entity.path), '-scheme', path.basenameWithoutExtension(entity.path),
"BUILD_DIR=${path.absolute(app.rootPath, 'build')}", "BUILD_DIR=${path.absolute(app.appDirectory, 'build')}",
]); ]);
break; break;
} }
...@@ -154,7 +154,7 @@ Future<XcodeBuildResult> buildXcodeProject({ ...@@ -154,7 +154,7 @@ Future<XcodeBuildResult> buildXcodeProject({
RunResult result = await runAsync( RunResult result = await runAsync(
commands, commands,
workingDirectory: app.rootPath, workingDirectory: app.appDirectory,
allowReentrantFlutter: true allowReentrantFlutter: true
); );
...@@ -170,7 +170,7 @@ Future<XcodeBuildResult> buildXcodeProject({ ...@@ -170,7 +170,7 @@ Future<XcodeBuildResult> buildXcodeProject({
Match match = regexp.firstMatch(result.stdout); Match match = regexp.firstMatch(result.stdout);
String outputDir; String outputDir;
if (match != null) if (match != null)
outputDir = path.join(app.rootPath, match.group(1)); outputDir = path.join(app.appDirectory, match.group(1));
return new XcodeBuildResult(true, outputDir); return new XcodeBuildResult(true, outputDir);
} }
} }
......
...@@ -23,7 +23,8 @@ class RunAndStayResident extends ResidentRunner { ...@@ -23,7 +23,8 @@ class RunAndStayResident extends ResidentRunner {
DebuggingOptions debuggingOptions, DebuggingOptions debuggingOptions,
bool usesTerminalUI: true, bool usesTerminalUI: true,
this.traceStartup: false, this.traceStartup: false,
this.benchmark: false this.benchmark: false,
this.applicationBinary
}) : super(device, }) : super(device,
target: target, target: target,
debuggingOptions: debuggingOptions, debuggingOptions: debuggingOptions,
...@@ -32,8 +33,9 @@ class RunAndStayResident extends ResidentRunner { ...@@ -32,8 +33,9 @@ class RunAndStayResident extends ResidentRunner {
ApplicationPackage _package; ApplicationPackage _package;
String _mainPath; String _mainPath;
LaunchResult _result; LaunchResult _result;
bool traceStartup; final bool traceStartup;
bool benchmark; final bool benchmark;
final String applicationBinary;
@override @override
Future<int> run({ Future<int> run({
...@@ -105,7 +107,7 @@ class RunAndStayResident extends ResidentRunner { ...@@ -105,7 +107,7 @@ class RunAndStayResident extends ResidentRunner {
return 1; return 1;
} }
_package = getApplicationPackageForPlatform(device.platform); _package = getApplicationPackageForPlatform(device.platform, applicationBinary: applicationBinary);
if (_package == null) { if (_package == null) {
String message = 'No application found for ${device.platform}.'; String message = 'No application found for ${device.platform}.';
......
...@@ -185,12 +185,15 @@ abstract class FlutterCommand extends Command { ...@@ -185,12 +185,15 @@ abstract class FlutterCommand extends Command {
Validator commandValidator; Validator commandValidator;
bool _commandValidator() { bool _commandValidator() {
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')) { if (!FileSystemEntity.isFileSync('pubspec.yaml')) {
printError('Error: No pubspec.yaml file found.\n' printError('Error: No pubspec.yaml file found.\n'
'This command should be run from the root of your Flutter project.\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.'); 'Do not run this command from the root of your git clone of Flutter.');
return false; return false;
} }
}
if (_usesTargetOption) { if (_usesTargetOption) {
String targetPath = targetFile; String targetPath = targetFile;
......
...@@ -15,6 +15,7 @@ import 'analyze_duplicate_names_test.dart' as analyze_duplicate_names_test; ...@@ -15,6 +15,7 @@ import 'analyze_duplicate_names_test.dart' as analyze_duplicate_names_test;
import 'analyze_test.dart' as analyze_test; import 'analyze_test.dart' as analyze_test;
import 'android_device_test.dart' as android_device_test; import 'android_device_test.dart' as android_device_test;
import 'android_sdk_test.dart' as android_sdk_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 'base_utils_test.dart' as base_utils_test;
import 'config_test.dart' as config_test; import 'config_test.dart' as config_test;
import 'context_test.dart' as context_test; import 'context_test.dart' as context_test;
...@@ -44,6 +45,7 @@ void main() { ...@@ -44,6 +45,7 @@ void main() {
analyze_test.main(); analyze_test.main();
android_device_test.main(); android_device_test.main();
android_sdk_test.main(); android_sdk_test.main();
application_package_test.main();
base_utils_test.main(); base_utils_test.main();
config_test.main(); config_test.main();
context_test.main(); context_test.main();
......
// 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'
''';
...@@ -17,13 +17,12 @@ import 'package:mockito/mockito.dart'; ...@@ -17,13 +17,12 @@ import 'package:mockito/mockito.dart';
class MockApplicationPackageStore extends ApplicationPackageStore { class MockApplicationPackageStore extends ApplicationPackageStore {
MockApplicationPackageStore() : super( MockApplicationPackageStore() : super(
android: new AndroidApk( android: new AndroidApk(
buildDir: '/mock/path/to/android',
id: 'io.flutter.android.mock', id: 'io.flutter.android.mock',
apkPath: '/mock/path/to/android/SkyShell.apk', apkPath: '/mock/path/to/android/SkyShell.apk',
launchActivity: 'io.flutter.android.mock.MockActivity' launchActivity: 'io.flutter.android.mock.MockActivity'
), ),
iOS: new IOSApp( iOS: new IOSApp(
projectDir: '/mock/path/to/iOS/SkyShell.app', appDirectory: '/mock/path/to/iOS/SkyShell.app',
projectBundleId: 'io.flutter.ios.mock' projectBundleId: 'io.flutter.ios.mock'
) )
); );
......
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