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,11 +185,14 @@ abstract class FlutterCommand extends Command { ...@@ -185,11 +185,14 @@ abstract class FlutterCommand extends Command {
Validator commandValidator; Validator commandValidator;
bool _commandValidator() { bool _commandValidator() {
if (!FileSystemEntity.isFileSync('pubspec.yaml')) { if (!PackageMap.isUsingCustomPackagesPath) {
printError('Error: No pubspec.yaml file found.\n' // Don't expect a pubspec.yaml file if the user passed in an explicit .packages file path.
'This command should be run from the root of your Flutter project.\n' if (!FileSystemEntity.isFileSync('pubspec.yaml')) {
'Do not run this command from the root of your git clone of Flutter.'); printError('Error: No pubspec.yaml file found.\n'
return false; '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) { if (_usesTargetOption) {
......
...@@ -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