| // 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(); |
| } |
| } |
| } |
| } |