mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Merge pull request #437 from jason-simmons/apk_package
Add a Flutter command that builds an APK using a local build of the e…
This commit is contained in:
commit
ba10546889
@ -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<String> args) async {
|
||||
|
||||
FlutterCommandRunner runner = new FlutterCommandRunner()
|
||||
..addCommand(new AnalyzeCommand())
|
||||
..addCommand(new ApkCommand())
|
||||
..addCommand(new BuildCommand())
|
||||
..addCommand(new CacheCommand())
|
||||
..addCommand(new DaemonCommand())
|
||||
|
||||
215
packages/flutter_tools/lib/src/commands/apk.dart
Normal file
215
packages/flutter_tools/lib/src/commands/apk.dart
Normal file
@ -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<String> 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<int> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -116,6 +116,16 @@ class FlutterCommandRunner extends CommandRunner {
|
||||
}
|
||||
List<BuildConfiguration> _buildConfigurations;
|
||||
|
||||
String get enginePath {
|
||||
if (!_enginePathSet) {
|
||||
_enginePath = _findEnginePath(_globalResults);
|
||||
_enginePathSet = true;
|
||||
}
|
||||
return _enginePath;
|
||||
}
|
||||
String _enginePath;
|
||||
bool _enginePathSet = false;
|
||||
|
||||
ArgResults _globalResults;
|
||||
|
||||
String get defaultFlutterRoot {
|
||||
@ -152,33 +162,31 @@ class FlutterCommandRunner extends CommandRunner {
|
||||
return super.runCommand(globalResults);
|
||||
}
|
||||
|
||||
List<BuildConfiguration> _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'
|
||||
@ -187,6 +195,15 @@ class FlutterCommandRunner extends CommandRunner {
|
||||
}
|
||||
}
|
||||
|
||||
return engineSourcePath;
|
||||
}
|
||||
|
||||
List<BuildConfiguration> _createBuildConfigurations(ArgResults globalResults) {
|
||||
bool isDebug = globalResults['debug'];
|
||||
bool isRelease = globalResults['release'];
|
||||
HostPlatform hostPlatform = getCurrentHostPlatform();
|
||||
TargetPlatform hostPlatformAsTarget = getCurrentHostPlatformAsTarget();
|
||||
|
||||
List<BuildConfiguration> configs = <BuildConfiguration>[];
|
||||
|
||||
if (enginePath == null) {
|
||||
|
||||
@ -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<int> 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'))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user