From 07556429f25753b66f9fac26d849a11b163b9985 Mon Sep 17 00:00:00 2001 From: Jason Simmons Date: Mon, 16 Nov 2015 17:24:54 -0800 Subject: [PATCH] Add a Flutter command that builds an APK using a local build of the engine Example: cd flutter/examples/stocks flutter --engine-src-path /path/to/engine/src apk -o Stocks.apk -m apk/AndroidManifest.xml --- packages/flutter_tools/lib/executable.dart | 2 + .../flutter_tools/lib/src/commands/apk.dart | 215 ++++++++++++++++++ .../src/commands/flutter_command_runner.dart | 41 +++- .../flutter_tools/lib/src/commands/start.dart | 16 +- 4 files changed, 258 insertions(+), 16 deletions(-) create mode 100644 packages/flutter_tools/lib/src/commands/apk.dart diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart index 44cabd491bd..0beb7e43292 100644 --- a/packages/flutter_tools/lib/executable.dart +++ b/packages/flutter_tools/lib/executable.dart @@ -10,6 +10,7 @@ import 'package:logging/logging.dart'; import 'package:stack_trace/stack_trace.dart'; import 'src/commands/analyze.dart'; +import 'src/commands/apk.dart'; import 'src/commands/build.dart'; import 'src/commands/cache.dart'; import 'src/commands/daemon.dart'; @@ -48,6 +49,7 @@ Future main(List args) async { FlutterCommandRunner runner = new FlutterCommandRunner() ..addCommand(new AnalyzeCommand()) + ..addCommand(new ApkCommand()) ..addCommand(new BuildCommand()) ..addCommand(new CacheCommand()) ..addCommand(new DaemonCommand()) diff --git a/packages/flutter_tools/lib/src/commands/apk.dart b/packages/flutter_tools/lib/src/commands/apk.dart new file mode 100644 index 00000000000..3480d8decb5 --- /dev/null +++ b/packages/flutter_tools/lib/src/commands/apk.dart @@ -0,0 +1,215 @@ +// 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:logging/logging.dart'; +import 'package:path/path.dart' as path; + +import '../build_configuration.dart'; +import 'build.dart'; +import 'flutter_command.dart'; +import 'start.dart'; + +const String _kDefaultAndroidManifestPath = 'apk/AndroidManifest.xml'; +const String _kDefaultOutputPath = 'build/app.apk'; +const String _kKeystoreKeyName = "chromiumdebugkey"; +const String _kKeystorePassword = "chromium"; + +final Logger _logging = new Logger('flutter_tools.apk'); + +/// Create the ancestor directories of a file path if they do not already exist. +void _ensureDirectoryExists(String filePath) { + Directory dir = new Directory(path.dirname(filePath)); + if (!dir.existsSync()) + dir.createSync(recursive: true); +} + +/// 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 { + static const String _kAndroidPlatformVersion = '22'; + static const String _kBuildToolsVersion = '22.0.1'; + + final String androidSdk; + + File _androidJar; + File _aapt; + 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'); + _zipalign = new File('$buildTools/zipalign'); + _jarsigner = 'jarsigner'; + } + + void package(File outputApk, File androidManifest, Directory assets, Directory artifacts) { + _run(_aapt.path, [ + 'package', + '-M', androidManifest.path, + '-A', assets.path, + '-I', _androidJar.path, + '-F', outputApk.path, + artifacts.path + ]); + } + + void sign(File keystore, String keystorePassword, String keyName, File outputApk) { + _run(_jarsigner, [ + '-keystore', keystore.path, + '-storepass', keystorePassword, + outputApk.path, + keyName, + ]); + } + + void align(File unalignedApk, File outputApk) { + _run(_zipalign.path, ['-f', '4', unalignedApk.path, outputApk.path]); + } + + void _run(String command, List args, { String workingDirectory }) { + ProcessResult result = Process.runSync( + command, args, workingDirectory: workingDirectory + ); + if (result.exitCode == 0) + return; + stdout.write(result.stdout); + stderr.write(result.stderr); + } +} + +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('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.'); + } + + int _buildApk(BuildConfiguration config, String flxPath) { + File androidManifest = new File(argResults['manifest']); + File icuData = new File('${runner.enginePath}/third_party/icu/android/icudtl.dat'); + File classesDex = new File('${config.buildDir}/gen/sky/shell/shell/classes.dex'); + File libSkyShell = new File('${config.buildDir}/gen/sky/shell/shell/shell/libs/armeabi-v7a/libsky_shell.so'); + File keystore = new File('${runner.enginePath}/build/android/ant/chromium-debug.keystore'); + + for (File f in [androidManifest, icuData, classesDex, libSkyShell, keystore]) { + if (!f.existsSync()) { + _logging.severe('Can not locate file: ${f.path}'); + return 1; + } + } + + Directory androidSdk = new Directory('${runner.enginePath}/third_party/android_tools/sdk'); + if (!androidSdk.existsSync()) { + _logging.severe('Can not locate Android SDK: ${androidSdk.path}'); + return 1; + } + + Directory tempDir = Directory.systemTemp.createTempSync('flutter_tools'); + try { + _AssetBuilder assetBuilder = new _AssetBuilder(tempDir, 'assets'); + assetBuilder.add(icuData, 'icudtl.dat'); + assetBuilder.add(new File(flxPath), 'app.flx'); + + _AssetBuilder artifactBuilder = new _AssetBuilder(tempDir, 'artifacts'); + artifactBuilder.add(classesDex, 'classes.dex'); + artifactBuilder.add(libSkyShell, 'lib/armeabi-v7a/libsky_shell.so'); + + _ApkBuilder builder = new _ApkBuilder(androidSdk.path); + File unalignedApk = new File('${tempDir.path}/app.apk.unaligned'); + builder.package(unalignedApk, androidManifest, assetBuilder.directory, + artifactBuilder.directory); + builder.sign(keystore, _kKeystorePassword, _kKeystoreKeyName, unalignedApk); + + File finalApk = new File(argResults['output-file']); + _ensureDirectoryExists(finalApk.path); + builder.align(unalignedApk, finalApk); + + return 0; + } finally { + tempDir.deleteSync(recursive: true); + } + } + + @override + Future runInProject() async { + if (runner.enginePath == null) { + _logging.severe('Unable to locate the Flutter engine. Use the --engine-src-path option.'); + return 1; + } + + BuildConfiguration config = buildConfigurations.firstWhere( + (BuildConfiguration bc) => bc.targetPlatform == TargetPlatform.android + ); + + String flxPath = argResults['flx']; + + if (!flxPath.isEmpty) { + if (!FileSystemEntity.isFileSync(flxPath)) { + _logging.severe('FLX does not exist: $flxPath'); + _logging.severe('(Omit the --flx option to build the FLX automatically)'); + return 1; + } + return _buildApk(config, flxPath); + } else { + await downloadToolchain(); + + // Find the path to the main Dart file. + String mainPath = StartCommand.findMainDartFile(argResults['target']); + + // Build the FLX. + BuildCommand builder = new BuildCommand(); + builder.inheritFromParent(this); + int result; + await builder.buildInTempDir( + mainPath: mainPath, + onBundleAvailable: (String localBundlePath) { + result = _buildApk(config, localBundlePath); + } + ); + + return result; + } + } +} diff --git a/packages/flutter_tools/lib/src/commands/flutter_command_runner.dart b/packages/flutter_tools/lib/src/commands/flutter_command_runner.dart index 295e52cf397..595bfdf1ed9 100644 --- a/packages/flutter_tools/lib/src/commands/flutter_command_runner.dart +++ b/packages/flutter_tools/lib/src/commands/flutter_command_runner.dart @@ -113,6 +113,16 @@ class FlutterCommandRunner extends CommandRunner { } List _buildConfigurations; + String get enginePath { + if (!_enginePathSet) { + _enginePath = _findEnginePath(_globalResults); + _enginePathSet = true; + } + return _enginePath; + } + String _enginePath; + bool _enginePathSet = false; + ArgResults _globalResults; String get defaultFlutterRoot { @@ -141,33 +151,31 @@ class FlutterCommandRunner extends CommandRunner { return super.runCommand(globalResults); } - List _createBuildConfigurations(ArgResults globalResults) { - String enginePath = globalResults['engine-src-path'] ?? Platform.environment[kFlutterEngineEnvironmentVariableName]; + String _findEnginePath(ArgResults globalResults) { + String engineSourcePath = globalResults['engine-src-path'] ?? Platform.environment[kFlutterEngineEnvironmentVariableName]; bool isDebug = globalResults['debug']; bool isRelease = globalResults['release']; - HostPlatform hostPlatform = getCurrentHostPlatform(); - TargetPlatform hostPlatformAsTarget = getCurrentHostPlatformAsTarget(); - if (enginePath == null && (isDebug || isRelease)) { + if (engineSourcePath == null && (isDebug || isRelease)) { if (ArtifactStore.isPackageRootValid) { Directory engineDir = new Directory(path.join(ArtifactStore.packageRoot, kFlutterEnginePackageName)); try { String realEnginePath = engineDir.resolveSymbolicLinksSync(); - enginePath = path.dirname(path.dirname(path.dirname(path.dirname(realEnginePath)))); - bool dirExists = FileSystemEntity.isDirectorySync(path.join(enginePath, 'out')); - if (enginePath == '/' || enginePath.isEmpty || !dirExists) - enginePath = null; + engineSourcePath = path.dirname(path.dirname(path.dirname(path.dirname(realEnginePath)))); + bool dirExists = FileSystemEntity.isDirectorySync(path.join(engineSourcePath, 'out')); + if (engineSourcePath == '/' || engineSourcePath.isEmpty || !dirExists) + engineSourcePath = null; } on FileSystemException { } } - if (enginePath == null) { + if (engineSourcePath == null) { String tryEnginePath(String enginePath) { if (FileSystemEntity.isDirectorySync(path.join(enginePath, 'out'))) return enginePath; return null; } - enginePath = tryEnginePath(path.join(ArtifactStore.flutterRoot, '../engine/src')); + engineSourcePath = tryEnginePath(path.join(ArtifactStore.flutterRoot, '../engine/src')); } - if (enginePath == null) { + if (engineSourcePath == null) { stderr.writeln('Unable to detect local Flutter engine build directory.\n' 'Either specify a dependency_override for the $kFlutterEnginePackageName package in your pubspec.yaml and\n' 'ensure --package-root is set if necessary, or set the \$$kFlutterEngineEnvironmentVariableName environment variable, or\n' @@ -176,6 +184,15 @@ class FlutterCommandRunner extends CommandRunner { } } + return engineSourcePath; + } + + List _createBuildConfigurations(ArgResults globalResults) { + bool isDebug = globalResults['debug']; + bool isRelease = globalResults['release']; + HostPlatform hostPlatform = getCurrentHostPlatform(); + TargetPlatform hostPlatformAsTarget = getCurrentHostPlatformAsTarget(); + List configs = []; if (enginePath == null) { diff --git a/packages/flutter_tools/lib/src/commands/start.dart b/packages/flutter_tools/lib/src/commands/start.dart index 842d909b390..f70bcae1ed8 100644 --- a/packages/flutter_tools/lib/src/commands/start.dart +++ b/packages/flutter_tools/lib/src/commands/start.dart @@ -37,6 +37,17 @@ class StartCommand extends FlutterCommand { help: 'Boot the iOS Simulator if it isn\'t already running.'); } + /// Given the value of the --target option, return the path of the Dart file + /// where the app's main function should be. + static String findMainDartFile(String target) { + String targetPath = path.absolute(target); + if (FileSystemEntity.isDirectorySync(targetPath)) { + return path.join(targetPath, 'lib', 'main.dart'); + } else { + return targetPath; + } + } + @override Future runInProject() async { await Future.wait([ @@ -63,10 +74,7 @@ class StartCommand extends FlutterCommand { if (package == null || !device.isConnected()) continue; if (device is AndroidDevice) { - String target = path.absolute(argResults['target']); - String mainPath = target; - if (FileSystemEntity.isDirectorySync(target)) - mainPath = path.join(target, 'lib', 'main.dart'); + String mainPath = findMainDartFile(argResults['target']); if (!FileSystemEntity.isFileSync(mainPath)) { String message = 'Tried to run $mainPath, but that file does not exist.'; if (!argResults.wasParsed('target'))