mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
'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.
This commit is contained in:
parent
4b7d601e03
commit
5d2281b69b
@ -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';
|
|||||||
|
|
||||||
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 {
|
|||||||
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 {
|
|||||||
help: 'Password for the entry within the keystore.');
|
help: 'Password for the entry within the keystore.');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future _findServices(_ApkComponents components) async {
|
|
||||||
if (!ArtifactStore.isPackageRootValid)
|
|
||||||
return;
|
|
||||||
|
|
||||||
dynamic manifest = _loadYamlFile(_kFlutterManifestPath);
|
|
||||||
if (manifest['services'] == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
for (String service in manifest['services']) {
|
|
||||||
String serviceRoot = '${ArtifactStore.packageRoot}/$service/apk';
|
|
||||||
dynamic serviceConfig = _loadYamlFile('$serviceRoot/config.yaml');
|
|
||||||
if (serviceConfig == null || serviceConfig['jars'] == null)
|
|
||||||
continue;
|
|
||||||
components.services.addAll(serviceConfig['services']);
|
|
||||||
for (String jar in serviceConfig['jars']) {
|
|
||||||
if (jar.startsWith("android-sdk:")) {
|
|
||||||
// Jar is something shipped in the standard android SDK.
|
|
||||||
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 {
|
|
||||||
String androidSdkPath;
|
|
||||||
List<String> artifactPaths;
|
|
||||||
if (runner.enginePath != null) {
|
|
||||||
androidSdkPath = '${runner.enginePath}/third_party/android_tools/sdk';
|
|
||||||
artifactPaths = [
|
|
||||||
'${runner.enginePath}/third_party/icu/android/icudtl.dat',
|
|
||||||
'${config.buildDir}/gen/sky/shell/shell/classes.dex.jar',
|
|
||||||
'${config.buildDir}/gen/sky/shell/shell/shell/libs/armeabi-v7a/libsky_shell.so',
|
|
||||||
'${runner.enginePath}/build/android/ant/chromium-debug.keystore',
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
androidSdkPath = AndroidDevice.getAndroidSdkPath();
|
|
||||||
if (androidSdkPath == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
List<ArtifactType> artifactTypes = <ArtifactType>[
|
|
||||||
ArtifactType.androidIcuData,
|
|
||||||
ArtifactType.androidClassesJar,
|
|
||||||
ArtifactType.androidLibSkyShell,
|
|
||||||
ArtifactType.androidKeystore,
|
|
||||||
];
|
|
||||||
Iterable<Future<String>> pathFutures = artifactTypes.map(
|
|
||||||
(ArtifactType type) => ArtifactStore.getPath(ArtifactStore.getArtifact(
|
|
||||||
type: type, targetPlatform: TargetPlatform.android)));
|
|
||||||
artifactPaths = await Future.wait(pathFutures);
|
|
||||||
}
|
|
||||||
|
|
||||||
_ApkComponents components = new _ApkComponents();
|
|
||||||
components.androidSdk = new Directory(androidSdkPath);
|
|
||||||
components.manifest = new File(argResults['manifest']);
|
|
||||||
components.icuData = new File(artifactPaths[0]);
|
|
||||||
components.jars = [new File(artifactPaths[1])];
|
|
||||||
components.libSkyShell = new File(artifactPaths[2]);
|
|
||||||
components.debugKeystore = new File(artifactPaths[3]);
|
|
||||||
components.resources = new Directory(argResults['resources']);
|
|
||||||
|
|
||||||
await _findServices(components);
|
|
||||||
|
|
||||||
if (!components.resources.existsSync()) {
|
|
||||||
// TODO(eseidel): This level should be higher when path is manually set.
|
|
||||||
printStatus('Can not locate Resources: ${components.resources}, ignoring.');
|
|
||||||
components.resources = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!components.androidSdk.existsSync()) {
|
|
||||||
printError('Can not locate Android SDK: $androidSdkPath');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!(new _ApkBuilder(components.androidSdk.path).checkSdkPath())) {
|
|
||||||
printError('Can not locate expected Android SDK tools at $androidSdkPath');
|
|
||||||
printError('You must install version $_kAndroidPlatformVersion of the SDK platform');
|
|
||||||
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 components;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Outputs a services.json file for the flutter engine to read. Format:
|
|
||||||
// {
|
|
||||||
// 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) {
|
|
||||||
Directory tempDir = Directory.systemTemp.createTempSync('flutter_tools');
|
|
||||||
try {
|
|
||||||
_ApkBuilder builder = new _ApkBuilder(components.androidSdk.path);
|
|
||||||
|
|
||||||
File classesDex = new File('${tempDir.path}/classes.dex');
|
|
||||||
builder.compileClassesDex(classesDex, components.jars);
|
|
||||||
|
|
||||||
File servicesConfig = new File('${tempDir.path}/services.json');
|
|
||||||
_generateServicesConfig(servicesConfig, components.services);
|
|
||||||
|
|
||||||
_AssetBuilder assetBuilder = new _AssetBuilder(tempDir, 'assets');
|
|
||||||
assetBuilder.add(components.icuData, 'icudtl.dat');
|
|
||||||
assetBuilder.add(new File(flxPath), 'app.flx');
|
|
||||||
assetBuilder.add(servicesConfig, 'services.json');
|
|
||||||
|
|
||||||
_AssetBuilder artifactBuilder = new _AssetBuilder(tempDir, 'artifacts');
|
|
||||||
artifactBuilder.add(classesDex, 'classes.dex');
|
|
||||||
artifactBuilder.add(components.libSkyShell, 'lib/armeabi-v7a/libsky_shell.so');
|
|
||||||
|
|
||||||
File unalignedApk = new File('${tempDir.path}/app.apk.unaligned');
|
|
||||||
builder.package(unalignedApk, components.manifest, assetBuilder.directory,
|
|
||||||
artifactBuilder.directory, components.resources);
|
|
||||||
|
|
||||||
int signResult = _signApk(builder, components, unalignedApk);
|
|
||||||
if (signResult != 0)
|
|
||||||
return signResult;
|
|
||||||
|
|
||||||
File finalApk = new File(argResults['output-file']);
|
|
||||||
ensureDirectoryExists(finalApk.path);
|
|
||||||
builder.align(unalignedApk, finalApk);
|
|
||||||
|
|
||||||
printStatus('APK generated: ${finalApk.path}');
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
} finally {
|
|
||||||
tempDir.deleteSync(recursive: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int _signApk(_ApkBuilder builder, _ApkComponents components, File apk) {
|
|
||||||
File keystore;
|
|
||||||
String keystorePassword;
|
|
||||||
String keyAlias;
|
|
||||||
String keyPassword;
|
|
||||||
|
|
||||||
if (argResults['keystore'].isEmpty) {
|
|
||||||
printError('Signing the APK using the debug keystore');
|
|
||||||
keystore = components.debugKeystore;
|
|
||||||
keystorePassword = _kDebugKeystorePassword;
|
|
||||||
keyAlias = _kDebugKeystoreKeyAlias;
|
|
||||||
keyPassword = _kDebugKeystorePassword;
|
|
||||||
} else {
|
|
||||||
keystore = new File(argResults['keystore']);
|
|
||||||
keystorePassword = argResults['keystore-password'];
|
|
||||||
keyAlias = argResults['keystore-key-alias'];
|
|
||||||
if (keystorePassword.isEmpty || keyAlias.isEmpty) {
|
|
||||||
printError('Must provide a keystore password and a key alias');
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
keyPassword = argResults['keystore-key-password'];
|
|
||||||
if (keyPassword.isEmpty)
|
|
||||||
keyPassword = keystorePassword;
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.sign(keystore, keystorePassword, keyAlias, keyPassword, apk);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<int> runInProject() async {
|
Future<int> runInProject() async {
|
||||||
BuildConfiguration config = buildConfigurations.firstWhere(
|
await downloadToolchain();
|
||||||
(BuildConfiguration bc) => bc.targetPlatform == TargetPlatform.android
|
return await buildAndroid(
|
||||||
|
toolchain: toolchain,
|
||||||
|
configs: buildConfigurations,
|
||||||
|
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']
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_ApkComponents components = await _findApkComponents(config);
|
Future _findServices(_ApkComponents components) async {
|
||||||
if (components == null) {
|
if (!ArtifactStore.isPackageRootValid)
|
||||||
printError('Unable to build APK.');
|
return;
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
String flxPath = argResults['flx'];
|
dynamic manifest = _loadYamlFile(_kFlutterManifestPath);
|
||||||
|
if (manifest['services'] == null)
|
||||||
|
return;
|
||||||
|
|
||||||
if (!flxPath.isEmpty) {
|
for (String service in manifest['services']) {
|
||||||
if (!FileSystemEntity.isFileSync(flxPath)) {
|
String serviceRoot = '${ArtifactStore.packageRoot}/$service/apk';
|
||||||
printError('FLX does not exist: $flxPath');
|
dynamic serviceConfig = _loadYamlFile('$serviceRoot/config.yaml');
|
||||||
printError('(Omit the --flx option to build the FLX automatically)');
|
if (serviceConfig == null || serviceConfig['jars'] == null)
|
||||||
return 1;
|
continue;
|
||||||
}
|
components.services.addAll(serviceConfig['services']);
|
||||||
return _buildApk(components, flxPath);
|
for (String jar in serviceConfig['jars']) {
|
||||||
} else {
|
if (jar.startsWith("android-sdk:")) {
|
||||||
await downloadToolchain();
|
// Jar is something shipped in the standard android SDK.
|
||||||
|
jar = jar.replaceAll('android-sdk:', '${components.androidSdk.path}/');
|
||||||
// Find the path to the main Dart file.
|
components.jars.add(new File(jar));
|
||||||
String mainPath = findMainDartFile(argResults['target']);
|
} else if (jar.startsWith("http")) {
|
||||||
|
// Jar is a URL to download.
|
||||||
// Build the FLX.
|
String cachePath = await ArtifactStore.getThirdPartyFile(jar, service);
|
||||||
flx.DirectoryResult buildResult = await flx.buildInTempDir(toolchain, mainPath: mainPath);
|
components.jars.add(new File(cachePath));
|
||||||
|
} else {
|
||||||
try {
|
// Assume jar is a path relative to the service's root dir.
|
||||||
return _buildApk(components, buildResult.localBundlePath);
|
components.jars.add(new File(path.join(serviceRoot, jar)));
|
||||||
} finally {
|
|
||||||
buildResult.dispose();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<_ApkComponents> _findApkComponents(
|
||||||
|
BuildConfiguration config, String enginePath, String manifest, String resources
|
||||||
|
) async {
|
||||||
|
String androidSdkPath;
|
||||||
|
List<String> artifactPaths;
|
||||||
|
if (enginePath != null) {
|
||||||
|
androidSdkPath = '$enginePath/third_party/android_tools/sdk';
|
||||||
|
artifactPaths = [
|
||||||
|
'$enginePath/third_party/icu/android/icudtl.dat',
|
||||||
|
'${config.buildDir}/gen/sky/shell/shell/classes.dex.jar',
|
||||||
|
'${config.buildDir}/gen/sky/shell/shell/shell/libs/armeabi-v7a/libsky_shell.so',
|
||||||
|
'$enginePath/build/android/ant/chromium-debug.keystore',
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
androidSdkPath = AndroidDevice.getAndroidSdkPath();
|
||||||
|
if (androidSdkPath == null)
|
||||||
|
return null;
|
||||||
|
List<ArtifactType> artifactTypes = <ArtifactType>[
|
||||||
|
ArtifactType.androidIcuData,
|
||||||
|
ArtifactType.androidClassesJar,
|
||||||
|
ArtifactType.androidLibSkyShell,
|
||||||
|
ArtifactType.androidKeystore,
|
||||||
|
];
|
||||||
|
Iterable<Future<String>> pathFutures = artifactTypes.map(
|
||||||
|
(ArtifactType type) => ArtifactStore.getPath(ArtifactStore.getArtifact(
|
||||||
|
type: type, targetPlatform: TargetPlatform.android)));
|
||||||
|
artifactPaths = await Future.wait(pathFutures);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ApkComponents components = new _ApkComponents();
|
||||||
|
components.androidSdk = new Directory(androidSdkPath);
|
||||||
|
components.manifest = new File(manifest);
|
||||||
|
components.icuData = new File(artifactPaths[0]);
|
||||||
|
components.jars = [new File(artifactPaths[1])];
|
||||||
|
components.libSkyShell = new File(artifactPaths[2]);
|
||||||
|
components.debugKeystore = new File(artifactPaths[3]);
|
||||||
|
components.resources = new Directory(resources);
|
||||||
|
|
||||||
|
await _findServices(components);
|
||||||
|
|
||||||
|
if (!components.resources.existsSync()) {
|
||||||
|
// TODO(eseidel): This level should be higher when path is manually set.
|
||||||
|
printStatus('Can not locate Resources: ${components.resources}, ignoring.');
|
||||||
|
components.resources = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!components.androidSdk.existsSync()) {
|
||||||
|
printError('Can not locate Android SDK: $androidSdkPath');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!(new _ApkBuilder(components.androidSdk.path).checkSdkPath())) {
|
||||||
|
printError('Can not locate expected Android SDK tools at $androidSdkPath');
|
||||||
|
printError('You must install version $_kAndroidPlatformVersion of the SDK platform');
|
||||||
|
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 components;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Outputs a services.json file for the flutter engine to read. Format:
|
||||||
|
// {
|
||||||
|
// 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, ApkKeystoreInfo keystore, String outputFile
|
||||||
|
) {
|
||||||
|
Directory tempDir = Directory.systemTemp.createTempSync('flutter_tools');
|
||||||
|
try {
|
||||||
|
_ApkBuilder builder = new _ApkBuilder(components.androidSdk.path);
|
||||||
|
|
||||||
|
File classesDex = new File('${tempDir.path}/classes.dex');
|
||||||
|
builder.compileClassesDex(classesDex, components.jars);
|
||||||
|
|
||||||
|
File servicesConfig = new File('${tempDir.path}/services.json');
|
||||||
|
_generateServicesConfig(servicesConfig, components.services);
|
||||||
|
|
||||||
|
_AssetBuilder assetBuilder = new _AssetBuilder(tempDir, 'assets');
|
||||||
|
assetBuilder.add(components.icuData, 'icudtl.dat');
|
||||||
|
assetBuilder.add(new File(flxPath), 'app.flx');
|
||||||
|
assetBuilder.add(servicesConfig, 'services.json');
|
||||||
|
|
||||||
|
_AssetBuilder artifactBuilder = new _AssetBuilder(tempDir, 'artifacts');
|
||||||
|
artifactBuilder.add(classesDex, 'classes.dex');
|
||||||
|
artifactBuilder.add(components.libSkyShell, 'lib/armeabi-v7a/libsky_shell.so');
|
||||||
|
|
||||||
|
File unalignedApk = new File('${tempDir.path}/app.apk.unaligned');
|
||||||
|
builder.package(unalignedApk, components.manifest, assetBuilder.directory,
|
||||||
|
artifactBuilder.directory, components.resources);
|
||||||
|
|
||||||
|
int signResult = _signApk(builder, components, unalignedApk, keystore);
|
||||||
|
if (signResult != 0)
|
||||||
|
return signResult;
|
||||||
|
|
||||||
|
File finalApk = new File(outputFile);
|
||||||
|
ensureDirectoryExists(finalApk.path);
|
||||||
|
builder.align(unalignedApk, finalApk);
|
||||||
|
|
||||||
|
printStatus('APK generated: ${finalApk.path}');
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.sign(keystore, keystorePassword, keyAlias, keyPassword, apk);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a new ApplicationPackage from the Android manifest.
|
||||||
|
AndroidApk _getApplicationPackage(String apkPath, String manifest) {
|
||||||
|
if (!FileSystemEntity.isFileSync(manifest))
|
||||||
|
return null;
|
||||||
|
String manifestString = new File(manifest).readAsStringSync();
|
||||||
|
xml.XmlDocument document = xml.parse(manifestString);
|
||||||
|
|
||||||
|
Iterable<xml.XmlElement> manifests = document.findElements('manifest');
|
||||||
|
if (manifests.isEmpty)
|
||||||
|
return null;
|
||||||
|
String id = manifests.toList()[0].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 as xml.XmlElement;
|
||||||
|
String activityName = activity.getAttribute('android:name');
|
||||||
|
launchActivity = "$id/$activityName";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (id == null || launchActivity == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildConfiguration config = configs.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...');
|
||||||
|
|
||||||
|
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, keystore, outputFile);
|
||||||
|
} else {
|
||||||
|
// Find the path to the main Dart file.
|
||||||
|
String mainPath = findMainDartFile(target);
|
||||||
|
|
||||||
|
// Build the FLX.
|
||||||
|
flx.DirectoryResult buildResult = await flx.buildInTempDir(toolchain, mainPath: mainPath);
|
||||||
|
|
||||||
|
try {
|
||||||
|
return _buildApk(components, buildResult.localBundlePath, keystore, outputFile);
|
||||||
|
} finally {
|
||||||
|
buildResult.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ApplicationPackageStore> buildAll(
|
||||||
|
DeviceStore devices,
|
||||||
|
ApplicationPackageStore applicationPackages,
|
||||||
|
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 {
|
|||||||
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 {
|
|||||||
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;
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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(
|
|||||||
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 {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return result;
|
return await result;
|
||||||
}
|
}
|
||||||
|
|
||||||
int get hashCode => device.logFilePath.hashCode;
|
int get hashCode => device.logFilePath.hashCode;
|
||||||
|
@ -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 @@
|
|||||||
|
|
||||||
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() {
|
|||||||
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)));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user