flutter/packages/flutter_tools/lib/src/commands/apk.dart
Yegor Jbanov 677e63b79d decouple flutter drive from flutter start
flutter start's method of finding devices to run the app on is not suitable for flutter drive.

This commit also refactors several tool services to allow mocking in unit tests.
2016-02-26 10:18:24 -08:00

478 lines
14 KiB
Dart

// Copyright 2015 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 'dart:async';
import 'dart:io';
import 'package:path/path.dart' as path;
import '../android/android_sdk.dart';
import '../application_package.dart';
import '../artifacts.dart';
import '../base/file_system.dart' show ensureDirectoryExists;
import '../base/os.dart';
import '../base/process.dart';
import '../build_configuration.dart';
import '../device.dart';
import '../flx.dart' as flx;
import '../globals.dart';
import '../runner/flutter_command.dart';
import '../services.dart';
import '../toolchain.dart';
import 'run.dart';
const String _kDefaultAndroidManifestPath = 'android/AndroidManifest.xml';
const String _kDefaultOutputPath = 'build/app.apk';
const String _kDefaultResourcesPath = 'android/res';
const String _kFlutterManifestPath = 'flutter.yaml';
const String _kPackagesStatusPath = '.packages';
// Alias of the key provided in the Chromium debug keystore
const String _kDebugKeystoreKeyAlias = "chromiumdebugkey";
// Password for the Chromium debug keystore
const String _kDebugKeystorePassword = "chromium";
/// Copies files into a new directory structure.
class _AssetBuilder {
final Directory outDir;
Directory _assetDir;
_AssetBuilder(this.outDir, String assetDirName) {
_assetDir = new Directory('${outDir.path}/$assetDirName');
_assetDir.createSync(recursive: true);
}
void add(File asset, String relativePath) {
String destPath = path.join(_assetDir.path, relativePath);
ensureDirectoryExists(destPath);
asset.copySync(destPath);
}
Directory get directory => _assetDir;
}
/// Builds an APK package using Android SDK tools.
class _ApkBuilder {
final AndroidSdkVersion sdk;
File _androidJar;
File _aapt;
File _dx;
File _zipalign;
File _jarsigner;
_ApkBuilder(this.sdk) {
_androidJar = new File(sdk.androidJarPath);
_aapt = new File(sdk.aaptPath);
_dx = new File(sdk.dxPath);
_zipalign = new File(sdk.zipalignPath);
_jarsigner = os.which('jarsigner');
}
void compileClassesDex(File classesDex, List<File> jars) {
List<String> packageArgs = <String>[_dx.path,
'--dex',
'--force-jumbo',
'--output', classesDex.path
];
packageArgs.addAll(jars.map((File f) => f.path));
runCheckedSync(packageArgs);
}
void package(File outputApk, File androidManifest, Directory assets, Directory artifacts, Directory resources) {
List<String> packageArgs = <String>[_aapt.path,
'package',
'-M', androidManifest.path,
'-A', assets.path,
'-I', _androidJar.path,
'-F', outputApk.path,
];
if (resources != null) {
packageArgs.addAll(['-S', resources.absolute.path]);
}
packageArgs.add(artifacts.path);
runCheckedSync(packageArgs);
}
void sign(File keystore, String keystorePassword, String keyAlias, String keyPassword, File outputApk) {
runCheckedSync(<String>[_jarsigner.path,
'-keystore', keystore.path,
'-storepass', keystorePassword,
'-keypass', keyPassword,
outputApk.path,
keyAlias,
]);
}
void align(File unalignedApk, File outputApk) {
runCheckedSync(<String>[_zipalign.path, '-f', '4', unalignedApk.path, outputApk.path]);
}
}
class _ApkComponents {
File manifest;
File icuData;
List<File> jars;
List<Map<String, String>> services = [];
File libSkyShell;
File debugKeystore;
Directory resources;
}
class ApkKeystoreInfo {
ApkKeystoreInfo({ this.keystore, this.password, this.keyAlias, this.keyPassword });
String keystore;
String password;
String keyAlias;
String keyPassword;
}
class ApkCommand extends FlutterCommand {
final String name = 'apk';
final String description = 'Build an Android APK package.';
ApkCommand() {
argParser.addOption('manifest',
abbr: 'm',
defaultsTo: _kDefaultAndroidManifestPath,
help: 'Android manifest XML file.');
argParser.addOption('resources',
abbr: 'r',
defaultsTo: _kDefaultResourcesPath,
help: 'Resources directory path.');
argParser.addOption('output-file',
abbr: 'o',
defaultsTo: _kDefaultOutputPath,
help: 'Output APK file.');
argParser.addOption('target',
abbr: 't',
defaultsTo: flx.defaultMainPath,
help: 'Target app path / main entry-point file.');
argParser.addOption('flx',
abbr: 'f',
defaultsTo: '',
help: 'Path to the FLX file. If this is not provided, an FLX will be built.');
argParser.addOption('keystore',
defaultsTo: '',
help: 'Path to the keystore used to sign the app.');
argParser.addOption('keystore-password',
defaultsTo: '',
help: 'Password used to access the keystore.');
argParser.addOption('keystore-key-alias',
defaultsTo: '',
help: 'Alias of the entry within the keystore.');
argParser.addOption('keystore-key-password',
defaultsTo: '',
help: 'Password for the entry within the keystore.');
}
@override
Future<int> runInProject() async {
// Validate that we can find an android sdk.
if (androidSdk == null) {
printError('No Android SDK found. Try setting the ANDROID_HOME environment variable.');
return 1;
}
if (!androidSdk.validateSdkWellFormed(complain: true)) {
printError('Try re-installing or updating your Android SDK.');
return 1;
}
await downloadToolchain();
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']
)
);
}
}
Future<_ApkComponents> _findApkComponents(
BuildConfiguration config, String enginePath, String manifest, String resources
) async {
List<String> artifactPaths;
if (enginePath != null) {
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 {
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.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 parseServiceConfigs(components.services, jars: components.jars);
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;
}
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;
}
int _buildApk(
_ApkComponents components, String flxPath, ApkKeystoreInfo keystore, String outputFile
) {
Directory tempDir = Directory.systemTemp.createTempSync('flutter_tools');
try {
_ApkBuilder builder = new _ApkBuilder(androidSdk.latestVersion);
File classesDex = new File('${tempDir.path}/classes.dex');
builder.compileClassesDex(classesDex, components.jars);
File servicesConfig =
generateServiceDefinitions(tempDir.path, 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('Generated APK to ${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;
}
// 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).
Iterable<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 {
// Validate that we can find an android sdk.
if (androidSdk == null) {
printError('No Android SDK found. Try setting the ANDROID_HOME environment variable.');
return 1;
}
if (!androidSdk.validateSdkWellFormed(complain: true)) {
printError('Try re-installing or updating your Android SDK.');
return 1;
}
if (!force && !_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();
}
}
}
// TODO(mpcomplete): move this to Device?
/// This is currently Android specific.
Future<int> 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)
continue;
// TODO(devoncarew): Remove this warning after a few releases.
if (FileSystemEntity.isDirectorySync('apk') && !FileSystemEntity.isDirectorySync('android')) {
// Tell people the android directory location changed.
printStatus(
"Warning: Flutter now looks for Android resources in the android/ directory; "
"consider renaming your 'apk/' directory to 'android/'.");
}
int result = await build(toolchain, configs, enginePath: enginePath,
target: target);
if (result != 0)
return result;
}
return 0;
}
Future<int> build(
Toolchain toolchain,
List<BuildConfiguration> configs, {
String enginePath,
String target: ''
}) async {
if (!FileSystemEntity.isFileSync(_kDefaultAndroidManifestPath)) {
printStatus('Using pre-built SkyShell.apk.');
return 0;
}
int result = await buildAndroid(
toolchain: toolchain,
configs: configs,
enginePath: enginePath,
force: false,
target: target
);
return result;
}