apk.dart 14.1 KB
Newer Older
1 2 3 4 5 6 7 8 9
// 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;

10
import '../android/android_sdk.dart';
11
import '../application_package.dart';
12
import '../artifacts.dart';
yjbanov's avatar
yjbanov committed
13
import '../base/file_system.dart' show ensureDirectoryExists;
14
import '../base/os.dart';
15
import '../base/process.dart';
Devon Carew's avatar
Devon Carew committed
16
import '../base/utils.dart';
17
import '../build_configuration.dart';
18
import '../device.dart';
19
import '../flx.dart' as flx;
20
import '../globals.dart';
21
import '../runner/flutter_command.dart';
22
import '../services.dart';
23
import '../toolchain.dart';
24
import 'run.dart';
25

26
const String _kDefaultAndroidManifestPath = 'android/AndroidManifest.xml';
27
const String _kDefaultOutputPath = 'build/app.apk';
28
const String _kDefaultResourcesPath = 'android/res';
29

30
const String _kFlutterManifestPath = 'flutter.yaml';
31
const String _kPackagesStatusPath = '.packages';
32

33 34 35 36 37
// Alias of the key provided in the Chromium debug keystore
const String _kDebugKeystoreKeyAlias = "chromiumdebugkey";

// Password for the Chromium debug keystore
const String _kDebugKeystorePassword = "chromium";
38 39 40 41 42 43 44 45 46 47 48 49 50 51

/// 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);
52
    ensureDirectoryExists(destPath);
53 54 55 56 57 58 59 60
    asset.copySync(destPath);
  }

  Directory get directory => _assetDir;
}

/// Builds an APK package using Android SDK tools.
class _ApkBuilder {
61
  final AndroidSdkVersion sdk;
62 63 64

  File _androidJar;
  File _aapt;
65
  File _dx;
66
  File _zipalign;
67 68 69 70 71 72 73 74
  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');
75 76 77
  }

  void compileClassesDex(File classesDex, List<File> jars) {
78
    List<String> packageArgs = <String>[_dx.path,
79 80 81 82 83 84 85 86
      '--dex',
      '--force-jumbo',
      '--output', classesDex.path
    ];

    packageArgs.addAll(jars.map((File f) => f.path));

    runCheckedSync(packageArgs);
87 88
  }

89
  void package(File outputApk, File androidManifest, Directory assets, Directory artifacts, Directory resources) {
90
    List<String> packageArgs = <String>[_aapt.path,
91 92 93 94 95
      'package',
      '-M', androidManifest.path,
      '-A', assets.path,
      '-I', _androidJar.path,
      '-F', outputApk.path,
96
    ];
97 98
    if (resources != null) {
      packageArgs.addAll(['-S', resources.absolute.path]);
99
    }
100
    packageArgs.add(artifacts.path);
101
    runCheckedSync(packageArgs);
102 103
  }

104
  void sign(File keystore, String keystorePassword, String keyAlias, String keyPassword, File outputApk) {
105
    runCheckedSync(<String>[_jarsigner.path,
106 107
      '-keystore', keystore.path,
      '-storepass', keystorePassword,
108
      '-keypass', keyPassword,
109
      outputApk.path,
110
      keyAlias,
111 112 113 114
    ]);
  }

  void align(File unalignedApk, File outputApk) {
115
    runCheckedSync(<String>[_zipalign.path, '-f', '4', unalignedApk.path, outputApk.path]);
116 117 118
  }
}

119 120 121
class _ApkComponents {
  File manifest;
  File icuData;
122 123
  List<File> jars;
  List<Map<String, String>> services = [];
124
  File libSkyShell;
125
  File debugKeystore;
126
  Directory resources;
127 128
}

129
class ApkKeystoreInfo {
130 131
  ApkKeystoreInfo({ this.keystore, this.password, this.keyAlias, this.keyPassword });

132 133 134 135 136 137
  String keystore;
  String password;
  String keyAlias;
  String keyPassword;
}

138 139 140 141 142 143 144 145 146
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.');
147 148 149 150
    argParser.addOption('resources',
        abbr: 'r',
        defaultsTo: _kDefaultResourcesPath,
        help: 'Resources directory path.');
151 152 153 154 155 156 157 158
    argParser.addOption('output-file',
        abbr: 'o',
        defaultsTo: _kDefaultOutputPath,
        help: 'Output APK file.');
    argParser.addOption('flx',
        abbr: 'f',
        defaultsTo: '',
        help: 'Path to the FLX file. If this is not provided, an FLX will be built.');
159 160 161 162 163 164 165 166 167 168 169 170
    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.');
171
    addTargetOption();
172 173
  }

174 175
  @override
  Future<int> runInProject() async {
176 177
    // Validate that we can find an android sdk.
    if (androidSdk == null) {
178
      printError('No Android SDK found. Try setting the ANDROID_HOME environment variable.');
179 180 181 182 183 184 185 186
      return 1;
    }

    if (!androidSdk.validateSdkWellFormed(complain: true)) {
      printError('Try re-installing or updating your Android SDK.');
      return 1;
    }

187
    await downloadToolchain();
188

189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207
    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']
      )
    );
  }
}
208

209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231
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);
  }
232

233 234 235 236 237 238 239 240
  _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);

241
  await parseServiceConfigs(components.services, jars: components.jars);
242 243 244 245 246 247

  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;
  }
248

249 250 251
  for (File f in [
    components.manifest, components.icuData, components.libSkyShell, components.debugKeystore
  ]..addAll(components.jars)) {
252 253
    if (!f.existsSync()) {
      printError('Can not locate file: ${f.path}');
254 255 256
      return null;
    }
  }
257

258 259
  return components;
}
260

261 262 263 264 265
int _buildApk(
  _ApkComponents components, String flxPath, ApkKeystoreInfo keystore, String outputFile
) {
  Directory tempDir = Directory.systemTemp.createTempSync('flutter_tools');
  try {
266
    _ApkBuilder builder = new _ApkBuilder(androidSdk.latestVersion);
267

268 269
    File classesDex = new File('${tempDir.path}/classes.dex');
    builder.compileClassesDex(classesDex, components.jars);
270

271
    File servicesConfig =
272
        generateServiceDefinitions(tempDir.path, components.services);
273

274 275 276 277
    _AssetBuilder assetBuilder = new _AssetBuilder(tempDir, 'assets');
    assetBuilder.add(components.icuData, 'icudtl.dat');
    assetBuilder.add(new File(flxPath), 'app.flx');
    assetBuilder.add(servicesConfig, 'services.json');
278

279 280 281
    _AssetBuilder artifactBuilder = new _AssetBuilder(tempDir, 'artifacts');
    artifactBuilder.add(classesDex, 'classes.dex');
    artifactBuilder.add(components.libSkyShell, 'lib/armeabi-v7a/libsky_shell.so');
282

283
    File unalignedApk = new File('${tempDir.path}/app.apk.unaligned');
284 285 286 287
    builder.package(
      unalignedApk, components.manifest, assetBuilder.directory,
      artifactBuilder.directory, components.resources
    );
288

289 290 291
    int signResult = _signApk(builder, components, unalignedApk, keystore);
    if (signResult != 0)
      return signResult;
292

293 294 295
    File finalApk = new File(outputFile);
    ensureDirectoryExists(finalApk.path);
    builder.align(unalignedApk, finalApk);
296

Devon Carew's avatar
Devon Carew committed
297 298 299
    File apkShaFile = new File('$outputFile.sha1');
    apkShaFile.writeAsStringSync(calculateSha(finalApk));

300
    printStatus('Generated APK to ${finalApk.path}.');
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316

    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) {
Devon Carew's avatar
Devon Carew committed
317
    printStatus('Warning: signing the APK using the debug keystore.');
318 319 320 321 322 323 324 325 326 327 328
    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;
329
    }
330 331 332
    keyPassword = keystoreInfo.keyPassword;
    if (keyPassword.isEmpty)
      keyPassword = keystorePassword;
333 334
  }

335 336 337 338 339 340 341 342 343 344 345
  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).
346
  Iterable<FileStat> dependenciesStat = [
347 348
    manifest,
    _kFlutterManifestPath,
349
    _kPackagesStatusPath
350 351 352 353
  ].map((String path) => FileStat.statSync(path));

  if (apkStat.type == FileSystemEntityType.NOT_FOUND)
    return true;
354

355
  for (FileStat dep in dependenciesStat) {
Devon Carew's avatar
Devon Carew committed
356
    if (dep.modified == null || dep.modified.isAfter(apkStat.modified))
357 358
      return true;
  }
359 360 361 362

  if (!FileSystemEntity.isFileSync('$apkPath.sha1'))
    return true;

363 364 365 366 367 368 369 370 371 372 373 374 375 376 377
  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 {
378 379
  // Validate that we can find an android sdk.
  if (androidSdk == null) {
380
    printError('No Android SDK found. Try setting the ANDROID_HOME environment variable.');
381 382 383 384 385 386 387 388
    return 1;
  }

  if (!androidSdk.validateSdkWellFormed(complain: true)) {
    printError('Try re-installing or updating your Android SDK.');
    return 1;
  }

389
  if (!force && !_needsRebuild(outputFile, manifest)) {
Devon Carew's avatar
Devon Carew committed
390
    printTrace('APK up to date; skipping build step.');
391 392 393
    return 0;
  }

394
  BuildConfiguration config = configs.firstWhere(
395
    (BuildConfiguration bc) => bc.targetPlatform == TargetPlatform.android
396 397 398 399 400 401 402 403
  );
  _ApkComponents components = await _findApkComponents(config, enginePath, manifest, resources);
  if (components == null) {
    printError('Failure building APK. Unable to find components.');
    return 1;
  }

  printStatus('Building APK...');
404

Ian Hickson's avatar
Ian Hickson committed
405
  if (flxPath.isNotEmpty) {
406 407 408
    if (!FileSystemEntity.isFileSync(flxPath)) {
      printError('FLX does not exist: $flxPath');
      printError('(Omit the --flx option to build the FLX automatically)');
409 410
      return 1;
    }
411 412 413 414
    return _buildApk(components, flxPath, keystore, outputFile);
  } else {
    // Find the path to the main Dart file.
    String mainPath = findMainDartFile(target);
415

416
    // Build the FLX.
Devon Carew's avatar
Devon Carew committed
417 418
    String localBundlePath = await flx.buildFlx(toolchain, mainPath: mainPath);
    return _buildApk(components, localBundlePath, keystore, outputFile);
419 420
  }
}
421

422
// TODO(mpcomplete): move this to Device?
423
/// This is currently Android specific.
424
Future<int> buildAll(
425
  List<Device> devices,
426 427 428 429 430 431
  ApplicationPackageStore applicationPackages,
  Toolchain toolchain,
  List<BuildConfiguration> configs, {
  String enginePath,
  String target: ''
}) async {
432
  for (Device device in devices) {
433
    ApplicationPackage package = applicationPackages.getPackageForPlatform(device.platform);
434
    if (package == null)
435 436 437
      continue;

    // TODO(mpcomplete): Temporary hack. We only support the apk builder atm.
438 439 440
    if (package != applicationPackages.android)
      continue;

441
    int result = await build(toolchain, configs, enginePath: enginePath, target: target);
442 443
    if (result != 0)
      return result;
444
  }
445 446

  return 0;
447
}
448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469

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;
}