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;
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 @@ class AndroidApk extends ApplicationPackage {
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 @@ class AndroidApk extends ApplicationPackage {
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 @@ class IOSApp extends ApplicationPackage {
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 @@ class IOSApp extends ApplicationPackage {
return null;
return new IOSApp(
projectDir: path.join('ios'),
appDirectory: path.join('ios'),
projectBundleId: value
);
}
......@@ -125,20 +150,26 @@ class IOSApp extends ApplicationPackage {
@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 @@ class ApplicationPackageStore {
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';
abstract class RunCommandBase extends FlutterCommand {
RunCommandBase() {
addBuildModeFlags(defaultToRelease: false);
argParser.addFlag('trace-startup',
negatable: true,
defaultsTo: false,
......@@ -59,6 +58,9 @@ class RunCommand extends RunCommandBase {
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 @@ class RunCommand extends RunCommandBase {
target: targetFile,
debuggingOptions: options,
traceStartup: traceStartup,
benchmark: argResults['benchmark']
benchmark: argResults['benchmark'],
applicationBinary: argResults['use-application-binary']
);
}
......
......@@ -23,6 +23,8 @@ class PackageMap {
_globalPackagesPath = value;
}
static bool get isUsingCustomPackagesPath => _globalPackagesPath != null;
static String _globalPackagesPath;
final String packagesPath;
......
......@@ -98,7 +98,7 @@ bool _xcodeVersionCheckValid(int major, int minor) {
}
Future<XcodeBuildResult> buildXcodeProject({
ApplicationPackage app,
IOSApp app,
BuildMode mode,
String target: flx.defaultMainPath,
bool buildForDevice,
......@@ -113,7 +113,7 @@ Future<XcodeBuildResult> buildXcodeProject({
// 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 @@ Future<XcodeBuildResult> buildXcodeProject({
'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 @@ Future<XcodeBuildResult> buildXcodeProject({
RunResult result = await runAsync(
commands,
workingDirectory: app.rootPath,
workingDirectory: app.appDirectory,
allowReentrantFlutter: true
);
......@@ -170,7 +170,7 @@ Future<XcodeBuildResult> buildXcodeProject({
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);
}
}
......
......@@ -23,7 +23,8 @@ class RunAndStayResident extends ResidentRunner {
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 @@ class RunAndStayResident extends ResidentRunner {
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 @@ class RunAndStayResident extends ResidentRunner {
return 1;
}
_package = getApplicationPackageForPlatform(device.platform);
_package = getApplicationPackageForPlatform(device.platform, applicationBinary: applicationBinary);
if (_package == null) {
String message = 'No application found for ${device.platform}.';
......
......@@ -185,11 +185,14 @@ abstract class FlutterCommand extends Command {
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) {
......
......@@ -15,6 +15,7 @@ import 'analyze_duplicate_names_test.dart' as analyze_duplicate_names_test;
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 @@ void main() {
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();
......
// 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';
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'
)
);
......
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