blob: ac0f5dc41b9907da71c97e37669d2a4f54c5e72a [file] [log] [blame]
// 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:convert';
import 'dart:io';
import 'package:path/path.dart' as path;
import 'package:yaml/yaml.dart';
import '../android/device_android.dart';
import '../artifacts.dart';
import '../base/context.dart';
import '../base/file_system.dart';
import '../base/process.dart';
import '../build_configuration.dart';
import '../flx.dart' as flx;
import '../runner/flutter_command.dart';
import 'start.dart';
const String _kDefaultAndroidManifestPath = 'apk/AndroidManifest.xml';
const String _kDefaultOutputPath = 'build/app.apk';
const String _kDefaultResourcesPath = 'apk/res';
const String _kFlutterManifestPath = 'flutter.yaml';
const String _kPubspecYamlPath = 'pubspec.yaml';
// Alias of the key provided in the Chromium debug keystore
const String _kDebugKeystoreKeyAlias = "chromiumdebugkey";
// Password for the Chromium debug keystore
const String _kDebugKeystorePassword = "chromium";
const String _kAndroidPlatformVersion = '22';
const String _kBuildToolsVersion = '22.0.1';
/// 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 String androidSdk;
File _androidJar;
File _aapt;
File _dx;
File _zipalign;
String _jarsigner;
_ApkBuilder(this.androidSdk) {
_androidJar = new File('$androidSdk/platforms/android-$_kAndroidPlatformVersion/android.jar');
String buildTools = '$androidSdk/build-tools/$_kBuildToolsVersion';
_aapt = new File('$buildTools/aapt');
_dx = new File('$buildTools/dx');
_zipalign = new File('$buildTools/zipalign');
_jarsigner = 'jarsigner';
}
bool checkSdkPath() {
return (_androidJar.existsSync() && _aapt.existsSync() && _dx.existsSync() && _zipalign.existsSync());
}
void compileClassesDex(File classesDex, List<File> jars) {
List<String> packageArgs = [_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 = [_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([_jarsigner,
'-keystore', keystore.path,
'-storepass', keystorePassword,
'-keypass', keyPassword,
outputApk.path,
keyAlias,
]);
}
void align(File unalignedApk, File outputApk) {
runCheckedSync([_zipalign.path, '-f', '4', unalignedApk.path, outputApk.path]);
}
}
class _ApkComponents {
Directory androidSdk;
File manifest;
File icuData;
List<File> jars;
List<Map<String, String>> services = [];
File libSkyShell;
File debugKeystore;
Directory resources;
}
// TODO(mpcomplete): find a better home for this.
dynamic _loadYamlFile(String path) {
if (!FileSystemEntity.isFileSync(path))
return null;
String manifestString = new File(path).readAsStringSync();
return loadYaml(manifestString);
}
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: '',
help: 'Target app path or filename used to build the FLX.');
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.');
}
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
Future<int> runInProject() async {
BuildConfiguration config = buildConfigurations.firstWhere(
(BuildConfiguration bc) => bc.targetPlatform == TargetPlatform.android
);
_ApkComponents components = await _findApkComponents(config);
if (components == null) {
printError('Unable to build APK.');
return 1;
}
String flxPath = argResults['flx'];
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);
} else {
await downloadToolchain();
// Find the path to the main Dart file.
String mainPath = findMainDartFile(argResults['target']);
// Build the FLX.
flx.DirectoryResult buildResult = await flx.buildInTempDir(toolchain, mainPath: mainPath);
try {
return _buildApk(components, buildResult.localBundlePath);
} finally {
buildResult.dispose();
}
}
}
}