gradle.dart 37.1 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

7
import 'package:crypto/crypto.dart';
8
import 'package:meta/meta.dart';
9
import 'package:xml/xml.dart' as xml;
10

11
import '../artifacts.dart';
12
import '../base/common.dart';
13
import '../base/file_system.dart';
14
import '../base/io.dart';
15 16
import '../base/logger.dart';
import '../base/process.dart';
17
import '../base/terminal.dart';
18 19 20
import '../base/utils.dart';
import '../build_info.dart';
import '../cache.dart';
21
import '../flutter_manifest.dart';
22
import '../globals.dart' as globals;
23
import '../project.dart';
24
import '../reporting/reporting.dart';
25 26
import 'gradle_errors.dart';
import 'gradle_utils.dart';
27

28 29 30 31 32 33 34 35 36 37 38
/// The directory where the APK artifact is generated.
@visibleForTesting
Directory getApkDirectory(FlutterProject project) {
  return project.isModule
    ? project.android.buildDirectory
        .childDirectory('host')
        .childDirectory('outputs')
        .childDirectory('apk')
    : project.android.buildDirectory
        .childDirectory('app')
        .childDirectory('outputs')
39
        .childDirectory('flutter-apk');
40
}
41

42 43 44 45 46 47 48 49 50 51 52 53 54
/// The directory where the app bundle artifact is generated.
@visibleForTesting
Directory getBundleDirectory(FlutterProject project) {
  return project.isModule
    ? project.android.buildDirectory
        .childDirectory('host')
        .childDirectory('outputs')
        .childDirectory('bundle')
    : project.android.buildDirectory
        .childDirectory('app')
        .childDirectory('outputs')
        .childDirectory('bundle');
}
55

56 57 58 59 60 61
/// The directory where the repo is generated.
/// Only applicable to AARs.
Directory getRepoDirectory(Directory buildDirectory) {
  return buildDirectory
    .childDirectory('outputs')
    .childDirectory('repo');
62 63
}

64 65 66 67 68 69
/// Returns the name of Gradle task that starts with [prefix].
String _taskFor(String prefix, BuildInfo buildInfo) {
  final String buildType = camelCase(buildInfo.modeName);
  final String productFlavor = buildInfo.flavor ?? '';
  return '$prefix${toTitleCase(productFlavor)}${toTitleCase(buildType)}';
}
70

71 72 73 74
/// Returns the task to build an APK.
@visibleForTesting
String getAssembleTaskFor(BuildInfo buildInfo) {
  return _taskFor('assemble', buildInfo);
75
}
76

77 78 79 80 81
/// Returns the task to build an AAB.
@visibleForTesting
String getBundleTaskFor(BuildInfo buildInfo) {
  return _taskFor('bundle', buildInfo);
}
82

83 84 85 86 87
/// Returns the task to build an AAR.
@visibleForTesting
String getAarTaskFor(BuildInfo buildInfo) {
  return _taskFor('assembleAar', buildInfo);
}
88

89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
/// Returns the output APK file names for a given [AndroidBuildInfo].
///
/// For example, when [splitPerAbi] is true, multiple APKs are created.
Iterable<String> _apkFilesFor(AndroidBuildInfo androidBuildInfo) {
  final String buildType = camelCase(androidBuildInfo.buildInfo.modeName);
  final String productFlavor = androidBuildInfo.buildInfo.flavor ?? '';
  final String flavorString = productFlavor.isEmpty ? '' : '-$productFlavor';
  if (androidBuildInfo.splitPerAbi) {
    return androidBuildInfo.targetArchs.map<String>((AndroidArch arch) {
      final String abi = getNameForAndroidArch(arch);
      return 'app$flavorString-$abi-$buildType.apk';
    });
  }
  return <String>['app$flavorString-$buildType.apk'];
}
104

105 106
/// Returns true if the current version of the Gradle plugin is supported.
bool _isSupportedVersion(AndroidProject project) {
107
  final File plugin = project.hostAppGradleRoot.childFile(
108
      globals.fs.path.join('buildSrc', 'src', 'main', 'groovy', 'FlutterPlugin.groovy'));
109
  if (plugin.existsSync()) {
110
    return false;
111
  }
112
  final File appGradle = project.hostAppGradleRoot.childFile(
113
      globals.fs.path.join('app', 'build.gradle'));
114 115 116
  if (!appGradle.existsSync()) {
    return false;
  }
117
  for (final String line in appGradle.readAsLinesSync()) {
118 119 120
    if (line.contains(RegExp(r'apply from: .*/flutter.gradle')) ||
        line.contains("def flutterPluginVersion = 'managed'")) {
      return true;
121
    }
122
  }
123
  return false;
124 125
}

126 127
/// Returns the apk file created by [buildGradleProject]
Future<File> getGradleAppOut(AndroidProject androidProject) async {
128 129
  if (!_isSupportedVersion(androidProject)) {
    _exitWithUnsupportedProjectMessage();
130
  }
131
  return getApkDirectory(androidProject.parent).childFile('app.apk');
132 133
}

134 135 136
/// Runs `gradlew dependencies`, ensuring that dependencies are resolved and
/// potentially downloaded.
Future<void> checkGradleDependencies() async {
137
  final Status progress = globals.logger.startProgress(
138 139 140
    'Ensuring gradle dependencies are up to date...',
    timeout: timeoutConfiguration.slowOperation,
  );
141
  final FlutterProject flutterProject = FlutterProject.current();
142
  await processUtils.run(<String>[
143 144 145
      gradleUtils.getExecutable(flutterProject),
      'dependencies',
    ],
146
    throwOnError: true,
147
    workingDirectory: flutterProject.android.hostAppGradleRoot.path,
148
    environment: gradleEnvironment,
149
  );
150
  globals.androidSdk?.reinitialize();
151 152 153
  progress.stop();
}

154 155 156
/// Tries to create `settings_aar.gradle` in an app project by removing the subprojects
/// from the existing `settings.gradle` file. This operation will fail if the existing
/// `settings.gradle` file has local edits.
157
@visibleForTesting
158 159 160 161 162 163 164 165 166 167 168
void createSettingsAarGradle(Directory androidDirectory) {
  final File newSettingsFile = androidDirectory.childFile('settings_aar.gradle');
  if (newSettingsFile.existsSync()) {
    return;
  }
  final File currentSettingsFile = androidDirectory.childFile('settings.gradle');
  if (!currentSettingsFile.existsSync()) {
    return;
  }
  final String currentFileContent = currentSettingsFile.readAsStringSync();

169 170
  final String newSettingsRelativeFile = globals.fs.path.relative(newSettingsFile.path);
  final Status status = globals.logger.startProgress('✏️  Creating `$newSettingsRelativeFile`...',
171 172
      timeout: timeoutConfiguration.fastOperation);

173
  final String flutterRoot = globals.fs.path.absolute(Cache.flutterRoot);
174 175 176
  final File legacySettingsDotGradleFiles = globals.fs.file(globals.fs.path.join(flutterRoot, 'packages','flutter_tools',
      'gradle', 'settings.gradle.legacy_versions'));
  assert(legacySettingsDotGradleFiles.existsSync());
177
  final String settingsAarContent = globals.fs.file(globals.fs.path.join(flutterRoot, 'packages','flutter_tools',
178 179 180
      'gradle', 'settings_aar.gradle.tmpl')).readAsStringSync();

  // Get the `settings.gradle` content variants that should be patched.
181
  final List<String> existingVariants = legacySettingsDotGradleFiles.readAsStringSync().split(';EOF');
182 183 184
  existingVariants.add(settingsAarContent);

  bool exactMatch = false;
185
  for (final String fileContentVariant in existingVariants) {
186 187 188 189 190 191 192
    if (currentFileContent.trim() == fileContentVariant.trim()) {
      exactMatch = true;
      break;
    }
  }
  if (!exactMatch) {
    status.cancel();
193
    globals.printStatus('$warningMark Flutter tried to create the file `$newSettingsRelativeFile`, but failed.');
194
    // Print how to manually update the file.
195
    globals.printStatus(globals.fs.file(globals.fs.path.join(flutterRoot, 'packages','flutter_tools',
196 197 198 199 200 201
        'gradle', 'manual_migration_settings.gradle.md')).readAsStringSync());
    throwToolExit('Please create the file and run this command again.');
  }
  // Copy the new file.
  newSettingsFile.writeAsStringSync(settingsAarContent);
  status.stop();
202
  globals.printStatus('$successMark `$newSettingsRelativeFile` created successfully.');
203 204
}

205 206 207 208
/// Builds an app.
///
/// * [project] is typically [FlutterProject.current()].
/// * [androidBuildInfo] is the build configuration.
209
/// * [target] is the target dart entry point. Typically, `lib/main.dart`.
210 211 212 213 214 215 216 217 218 219 220 221 222 223
/// * If [isBuildingBundle] is `true`, then the output artifact is an `*.aab`,
///   otherwise the output artifact is an `*.apk`.
/// * The plugins are built as AARs if [shouldBuildPluginAsAar] is `true`. This isn't set by default
///   because it makes the build slower proportional to the number of plugins.
/// * [retries] is the max number of build retries in case one of the [GradleHandledError] handler
///   returns [GradleBuildStatus.retry] or [GradleBuildStatus.retryWithAarPlugins].
Future<void> buildGradleApp({
  @required FlutterProject project,
  @required AndroidBuildInfo androidBuildInfo,
  @required String target,
  @required bool isBuildingBundle,
  @required List<GradleHandledError> localGradleErrors,
  bool shouldBuildPluginAsAar = false,
  int retries = 1,
224
}) async {
225 226 227 228 229
  assert(project != null);
  assert(androidBuildInfo != null);
  assert(target != null);
  assert(isBuildingBundle != null);
  assert(localGradleErrors != null);
230
  assert(globals.androidSdk != null);
231

232 233
  if (!project.android.isUsingGradle) {
    _exitWithProjectNotUsingGradleMessage();
234
  }
235 236
  if (!_isSupportedVersion(project.android)) {
    _exitWithUnsupportedProjectMessage();
237
  }
238
  final Directory buildDirectory = project.android.buildDirectory;
239

240 241
  final bool usesAndroidX = isAppUsingAndroidX(project.android.hostAppGradleRoot);
  if (usesAndroidX) {
242
    BuildEvent('app-using-android-x', flutterUsage: globals.flutterUsage).send();
243
  } else if (!usesAndroidX) {
244
    BuildEvent('app-not-using-android-x', flutterUsage: globals.flutterUsage).send();
245
    globals.printStatus("$warningMark Your app isn't using AndroidX.", emphasis: true);
246
    globals.printStatus(
247
      'To avoid potential build failures, you can quickly migrate your app '
248
      'by following the steps on https://goo.gl/CP92wY .',
249
      indent: 4,
Emmanuel Garcia's avatar
Emmanuel Garcia committed
250 251
    );
  }
252 253 254
  // The default Gradle script reads the version name and number
  // from the local.properties file.
  updateLocalProperties(project: project, buildInfo: androidBuildInfo.buildInfo);
Emmanuel Garcia's avatar
Emmanuel Garcia committed
255

256 257 258 259 260 261
  if (shouldBuildPluginAsAar) {
    // Create a settings.gradle that doesn't import the plugins as subprojects.
    createSettingsAarGradle(project.android.hostAppGradleRoot);
    await buildPluginsAsAar(
      project,
      androidBuildInfo,
262
      buildDirectory: buildDirectory.childDirectory('app'),
263 264 265
    );
  }

266 267 268 269
  final BuildInfo buildInfo = androidBuildInfo.buildInfo;
  final String assembleTask = isBuildingBundle
    ? getBundleTaskFor(buildInfo)
    : getAssembleTaskFor(buildInfo);
270

271
  final Status status = globals.logger.startProgress(
272
    "Running Gradle task '$assembleTask'...",
273 274 275
    timeout: timeoutConfiguration.slowOperation,
    multilineOutput: true,
  );
276

277 278 279
  final List<String> command = <String>[
    gradleUtils.getExecutable(project),
  ];
280
  if (globals.logger.isVerbose) {
281 282 283
    command.add('-Pverbose=true');
  } else {
    command.add('-q');
284
  }
285 286
  if (globals.artifacts is LocalEngineArtifacts) {
    final LocalEngineArtifacts localEngineArtifacts = globals.artifacts as LocalEngineArtifacts;
287 288 289 290
    final Directory localEngineRepo = _getLocalEngineRepo(
      engineOutPath: localEngineArtifacts.engineOutPath,
      androidBuildInfo: androidBuildInfo,
    );
291
    globals.printTrace(
292 293 294 295 296 297
      'Using local engine: ${localEngineArtifacts.engineOutPath}\n'
      'Local Maven repo: ${localEngineRepo.path}'
    );
    command.add('-Plocal-engine-repo=${localEngineRepo.path}');
    command.add('-Plocal-engine-build-mode=${buildInfo.modeName}');
    command.add('-Plocal-engine-out=${localEngineArtifacts.engineOutPath}');
298 299 300 301 302 303 304
    command.add('-Ptarget-platform=${_getTargetPlatformByLocalEnginePath(
        localEngineArtifacts.engineOutPath)}');
  } else if (androidBuildInfo.targetArchs.isNotEmpty) {
    final String targetPlatforms = androidBuildInfo
        .targetArchs
        .map(getPlatformNameForAndroidArch).join(',');
    command.add('-Ptarget-platform=$targetPlatforms');
305
  }
306 307
  if (target != null) {
    command.add('-Ptarget=$target');
308
  }
309 310 311 312
  assert(buildInfo.trackWidgetCreation != null);
  command.add('-Ptrack-widget-creation=${buildInfo.trackWidgetCreation}');

  if (buildInfo.extraFrontEndOptions != null) {
313
    command.add('-Pextra-front-end-options=${encodeDartDefines(buildInfo.extraFrontEndOptions)}');
314
  }
315
  if (buildInfo.extraGenSnapshotOptions != null) {
316
    command.add('-Pextra-gen-snapshot-options=${encodeDartDefines(buildInfo.extraGenSnapshotOptions)}');
317
  }
318 319
  if (buildInfo.fileSystemRoots != null && buildInfo.fileSystemRoots.isNotEmpty) {
    command.add('-Pfilesystem-roots=${buildInfo.fileSystemRoots.join('|')}');
320
  }
321 322
  if (buildInfo.fileSystemScheme != null) {
    command.add('-Pfilesystem-scheme=${buildInfo.fileSystemScheme}');
323
  }
324 325
  if (androidBuildInfo.splitPerAbi) {
    command.add('-Psplit-per-abi=true');
326
  }
327 328
  if (androidBuildInfo.shrink) {
    command.add('-Pshrink=true');
329
  }
330
  if (androidBuildInfo.buildInfo.dartDefines?.isNotEmpty ?? false) {
331
    command.add('-Pdart-defines=${encodeDartDefines(androidBuildInfo.buildInfo.dartDefines)}');
332
  }
333 334 335 336 337 338
  if (shouldBuildPluginAsAar) {
    // Pass a system flag instead of a project flag, so this flag can be
    // read from include_flutter.groovy.
    command.add('-Dbuild-plugins-as-aars=true');
    // Don't use settings.gradle from the current project since it includes the plugins as subprojects.
    command.add('--settings-file=settings_aar.gradle');
339
  }
340 341 342
  if (androidBuildInfo.fastStart) {
    command.add('-Pfast-start=true');
  }
343 344 345
  if (androidBuildInfo.buildInfo.splitDebugInfoPath != null) {
    command.add('-Psplit-debug-info=${androidBuildInfo.buildInfo.splitDebugInfoPath}');
  }
346 347 348
  if (androidBuildInfo.buildInfo.treeShakeIcons) {
    command.add('-Ptree-shake-icons=true');
  }
349 350 351
  if (androidBuildInfo.buildInfo.dartObfuscation) {
    command.add('-Pdart-obfuscation=true');
  }
352 353 354
  if (androidBuildInfo.buildInfo.bundleSkSLPath != null) {
    command.add('-Pbundle-sksl-path=${androidBuildInfo.buildInfo.bundleSkSLPath}');
  }
355 356 357
  if (androidBuildInfo.buildInfo.performanceMeasurementFile != null) {
    command.add('-Pperformance-measurement-file=${androidBuildInfo.buildInfo.performanceMeasurementFile}');
  }
358
  command.add(assembleTask);
359

360 361
  GradleHandledError detectedGradleError;
  String detectedGradleErrorLine;
362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386
  String consumeLog(String line) {
    // This message was removed from first-party plugins,
    // but older plugin versions still display this message.
    if (androidXPluginWarningRegex.hasMatch(line)) {
      // Don't pipe.
      return null;
    }
    if (detectedGradleError != null) {
      // Pipe stdout/stderr from Gradle.
      return line;
    }
    for (final GradleHandledError gradleError in localGradleErrors) {
      if (gradleError.test(line)) {
        detectedGradleErrorLine = line;
        detectedGradleError = gradleError;
        // The first error match wins.
        break;
      }
    }
    // Pipe stdout/stderr from Gradle.
    return line;
  }

  final Stopwatch sw = Stopwatch()..start();
  int exitCode = 1;
387
  try {
388
    exitCode = await processUtils.stream(
389 390 391 392
      command,
      workingDirectory: project.android.hostAppGradleRoot.path,
      allowReentrantFlutter: true,
      environment: gradleEnvironment,
393
      mapFunction: consumeLog,
394
    );
395 396 397 398 399 400 401
  } on ProcessException catch(exception) {
    consumeLog(exception.toString());
    // Rethrow the exception if the error isn't handled by any of the
    // `localGradleErrors`.
    if (detectedGradleError == null) {
      rethrow;
    }
402 403
  } finally {
    status.stop();
404 405
  }

406
  globals.flutterUsage.sendTiming('build', 'gradle', sw.elapsed);
407

408 409
  if (exitCode != 0) {
    if (detectedGradleError == null) {
410
      BuildEvent('gradle-unkown-failure', flutterUsage: globals.flutterUsage).send();
411 412 413 414 415 416 417 418 419 420 421
      throwToolExit(
        'Gradle task $assembleTask failed with exit code $exitCode',
        exitCode: exitCode,
      );
    } else {
      final GradleBuildStatus status = await detectedGradleError.handler(
        line: detectedGradleErrorLine,
        project: project,
        usesAndroidX: usesAndroidX,
        shouldBuildPluginAsAar: shouldBuildPluginAsAar,
      );
422

423
      if (retries >= 1) {
424
        final String successEventLabel = 'gradle-${detectedGradleError.eventLabel}-success';
425 426 427 428 429 430 431 432 433 434 435
        switch (status) {
          case GradleBuildStatus.retry:
            await buildGradleApp(
              project: project,
              androidBuildInfo: androidBuildInfo,
              target: target,
              isBuildingBundle: isBuildingBundle,
              localGradleErrors: localGradleErrors,
              shouldBuildPluginAsAar: shouldBuildPluginAsAar,
              retries: retries - 1,
            );
436
            BuildEvent(successEventLabel, flutterUsage: globals.flutterUsage).send();
437 438 439 440 441 442 443 444 445 446 447
            return;
          case GradleBuildStatus.retryWithAarPlugins:
            await buildGradleApp(
              project: project,
              androidBuildInfo: androidBuildInfo,
              target: target,
              isBuildingBundle: isBuildingBundle,
              localGradleErrors: localGradleErrors,
              shouldBuildPluginAsAar: true,
              retries: retries - 1,
            );
448
            BuildEvent(successEventLabel, flutterUsage: globals.flutterUsage).send();
449 450 451 452
            return;
          case GradleBuildStatus.exit:
            // noop.
        }
453
      }
454
      BuildEvent('gradle-${detectedGradleError.eventLabel}-failure', flutterUsage: globals.flutterUsage).send();
455 456 457 458
      throwToolExit(
        'Gradle task $assembleTask failed with exit code $exitCode',
        exitCode: exitCode,
      );
459
    }
460 461
  }

462 463 464 465 466
  if (isBuildingBundle) {
    final File bundleFile = findBundleFile(project, buildInfo);
    final String appSize = (buildInfo.mode == BuildMode.debug)
      ? '' // Don't display the size when building a debug variant.
      : ' (${getSizeAsMB(bundleFile.lengthSync())})';
467

468 469
    globals.printStatus(
      '$successMark Built ${globals.fs.path.relative(bundleFile.path)}$appSize.',
470 471 472
      color: TerminalColor.green,
    );
    return;
473
  }
474
  // Gradle produced an APK.
475 476 477
  final Iterable<String> apkFilesPaths = project.isModule
    ? findApkFilesModule(project, androidBuildInfo)
    : listApkPaths(androidBuildInfo);
478
  final Directory apkDirectory = getApkDirectory(project);
479 480 481 482 483 484 485 486
  final File apkFile = apkDirectory.childFile(apkFilesPaths.first);
  if (!apkFile.existsSync()) {
    _exitWithExpectedFileNotFound(
      project: project,
      fileExtension: '.apk',
    );
  }

487 488
  // Copy the first APK to app.apk, so `flutter run` can find it.
  // TODO(egarciad): Handle multiple APKs.
489
  apkFile.copySync(apkDirectory.childFile('app.apk').path);
490
  globals.printTrace('calculateSha: $apkDirectory/app.apk');
491

492
  final File apkShaFile = apkDirectory.childFile('app.apk.sha1');
493
  apkShaFile.writeAsStringSync(_calculateSha(apkFile));
494

495 496 497 498 499 500 501
  final String appSize = (buildInfo.mode == BuildMode.debug)
    ? '' // Don't display the size when building a debug variant.
    : ' (${getSizeAsMB(apkFile.lengthSync())})';
  globals.printStatus(
    '$successMark Built ${globals.fs.path.relative(apkFile.path)}$appSize.',
    color: TerminalColor.green,
  );
502 503
}

504 505 506 507 508
/// Builds AAR and POM files.
///
/// * [project] is typically [FlutterProject.current()].
/// * [androidBuildInfo] is the build configuration.
/// * [outputDir] is the destination of the artifacts,
509
/// * [buildNumber] is the build number of the output aar,
510 511 512 513
Future<void> buildGradleAar({
  @required FlutterProject project,
  @required AndroidBuildInfo androidBuildInfo,
  @required String target,
514
  @required Directory outputDirectory,
515
  @required String buildNumber,
516
}) async {
517 518
  assert(project != null);
  assert(target != null);
519 520
  assert(androidBuildInfo != null);
  assert(outputDirectory != null);
521
  assert(globals.androidSdk != null);
522

523
  final FlutterManifest manifest = project.manifest;
524
  if (!manifest.isModule && !manifest.isPlugin) {
525 526 527
    throwToolExit('AARs can only be built for plugin or module projects.');
  }

528 529
  final BuildInfo buildInfo = androidBuildInfo.buildInfo;
  final String aarTask = getAarTaskFor(buildInfo);
530
  final Status status = globals.logger.startProgress(
531
    "Running Gradle task '$aarTask'...",
532 533 534 535
    timeout: timeoutConfiguration.slowOperation,
    multilineOutput: true,
  );

536 537
  final String flutterRoot = globals.fs.path.absolute(Cache.flutterRoot);
  final String initScript = globals.fs.path.join(
538 539 540 541 542 543
    flutterRoot,
    'packages',
    'flutter_tools',
    'gradle',
    'aar_init_script.gradle',
  );
544
  final List<String> command = <String>[
545
    gradleUtils.getExecutable(project),
546 547
    '-I=$initScript',
    '-Pflutter-root=$flutterRoot',
548
    '-Poutput-dir=${outputDirectory.path}',
549
    '-Pis-plugin=${manifest.isPlugin}',
550
    '-PbuildNumber=$buildNumber'
551
  ];
552 553 554 555 556
  if (globals.logger.isVerbose) {
    command.add('-Pverbose=true');
  } else {
    command.add('-q');
  }
557 558 559 560

  if (target != null && target.isNotEmpty) {
    command.add('-Ptarget=$target');
  }
561 562 563 564 565 566 567 568 569 570 571 572 573
  if (buildInfo.splitDebugInfoPath != null) {
    command.add('-Psplit-debug-info=${buildInfo.splitDebugInfoPath}');
  }
  if (buildInfo.treeShakeIcons) {
    command.add('-Pfont-subset=true');
  }
  if (buildInfo.dartObfuscation) {
    if (buildInfo.mode == BuildMode.debug || buildInfo.mode == BuildMode.profile) {
      globals.printStatus('Dart obfuscation is not supported in ${toTitleCase(buildInfo.friendlyModeName)} mode, building as unobfuscated.');
    } else {
      command.add('-Pdart-obfuscation=true');
    }
  }
574

575 576
  if (globals.artifacts is LocalEngineArtifacts) {
    final LocalEngineArtifacts localEngineArtifacts = globals.artifacts as LocalEngineArtifacts;
577 578 579 580
    final Directory localEngineRepo = _getLocalEngineRepo(
      engineOutPath: localEngineArtifacts.engineOutPath,
      androidBuildInfo: androidBuildInfo,
    );
581
    globals.printTrace(
582 583 584 585
      'Using local engine: ${localEngineArtifacts.engineOutPath}\n'
      'Local Maven repo: ${localEngineRepo.path}'
    );
    command.add('-Plocal-engine-repo=${localEngineRepo.path}');
586
    command.add('-Plocal-engine-build-mode=${buildInfo.modeName}');
587
    command.add('-Plocal-engine-out=${localEngineArtifacts.engineOutPath}');
588 589 590

    // Copy the local engine repo in the output directory.
    try {
591
      globals.fsUtils.copyDirectorySync(
592 593 594 595 596 597 598 599 600
        localEngineRepo,
        getRepoDirectory(outputDirectory),
      );
    } on FileSystemException catch(_) {
      throwToolExit(
        'Failed to copy the local engine ${localEngineRepo.path} repo '
        'in ${outputDirectory.path}'
      );
    }
601 602 603 604 605 606
    command.add('-Ptarget-platform=${_getTargetPlatformByLocalEnginePath(
        localEngineArtifacts.engineOutPath)}');
  } else if (androidBuildInfo.targetArchs.isNotEmpty) {
    final String targetPlatforms = androidBuildInfo.targetArchs
        .map(getPlatformNameForAndroidArch).join(',');
    command.add('-Ptarget-platform=$targetPlatforms');
607 608
  }

609 610 611
  command.add(aarTask);

  final Stopwatch sw = Stopwatch()..start();
612
  RunResult result;
613
  try {
614
    result = await processUtils.run(
615 616 617
      command,
      workingDirectory: project.android.hostAppGradleRoot.path,
      allowReentrantFlutter: true,
618
      environment: gradleEnvironment,
619 620 621 622
    );
  } finally {
    status.stop();
  }
623
  globals.flutterUsage.sendTiming('build', 'gradle-aar', sw.elapsed);
624

625
  if (result.exitCode != 0) {
626 627
    globals.printStatus(result.stdout, wrap: false);
    globals.printError(result.stderr, wrap: false);
628 629 630 631
    throwToolExit(
      'Gradle task $aarTask failed with exit code $exitCode.',
      exitCode: exitCode,
    );
632
  }
633
  final Directory repoDirectory = getRepoDirectory(outputDirectory);
634
  if (!repoDirectory.existsSync()) {
635 636
    globals.printStatus(result.stdout, wrap: false);
    globals.printError(result.stderr, wrap: false);
637 638 639 640
    throwToolExit(
      'Gradle task $aarTask failed to produce $repoDirectory.',
      exitCode: exitCode,
    );
641
  }
642 643
  globals.printStatus(
    '$successMark Built ${globals.fs.path.relative(repoDirectory.path)}.',
644
    color: TerminalColor.green,
645
  );
646 647 648
}

/// Prints how to consume the AAR from a host app.
649 650
void printHowToConsumeAar({
  @required Set<String> buildModes,
651
  @required String androidPackage,
652
  @required Directory repoDirectory,
653 654
  @required Logger logger,
  @required FileSystem fileSystem,
655
  String buildNumber,
656
}) {
657
  assert(buildModes != null && buildModes.isNotEmpty);
658
  assert(androidPackage != null);
659
  assert(repoDirectory != null);
660
  buildNumber ??= '1.0';
661

662 663 664
  logger.printStatus('\nConsuming the Module', emphasis: true);
  logger.printStatus('''
  1. Open ${fileSystem.path.join('<host>', 'app', 'build.gradle')}
665 666
  2. Ensure you have the repositories configured, otherwise add them:

667
      String storageUrl = System.env.FLUTTER_STORAGE_BASE_URL ?: "https://storage.googleapis.com"
668 669
      repositories {
        maven {
670
            url '${repoDirectory.path}'
671 672
        }
        maven {
673
            url '\$storageUrl/download.flutter.io'
674 675 676
        }
      }

677
  3. Make the host app depend on the Flutter module:
678

679 680
    dependencies {''');

681
  for (final String buildMode in buildModes) {
682
    logger.printStatus("""
683
      ${buildMode}Implementation '$androidPackage:flutter_$buildMode:$buildNumber'""");
684 685
  }

686
  logger.printStatus('''
687 688 689 690
    }
''');

  if (buildModes.contains('profile')) {
691
    logger.printStatus('''
692 693 694 695 696 697 698 699

  4. Add the `profile` build type:

    android {
      buildTypes {
        profile {
          initWith debug
        }
700
      }
701 702 703
    }
''');
  }
704

705
  logger.printStatus('To learn more, visit https://flutter.dev/go/build-aar');
706 707
}

708 709
String _hex(List<int> bytes) {
  final StringBuffer result = StringBuffer();
710
  for (final int part in bytes) {
711
    result.write('${part < 16 ? '0' : ''}${part.toRadixString(16)}');
712
  }
713 714 715 716 717 718
  return result.toString();
}

String _calculateSha(File file) {
  final Stopwatch sw = Stopwatch()..start();
  final List<int> bytes = file.readAsBytesSync();
719
  globals.printTrace('calculateSha: reading file took ${sw.elapsedMilliseconds}us');
720
  globals.flutterUsage.sendTiming('build', 'apk-sha-read', sw.elapsed);
721 722
  sw.reset();
  final String sha = _hex(sha1.convert(bytes).bytes);
723
  globals.printTrace('calculateSha: computing sha took ${sw.elapsedMilliseconds}us');
724
  globals.flutterUsage.sendTiming('build', 'apk-sha-calc', sw.elapsed);
725 726 727
  return sha;
}

728
void _exitWithUnsupportedProjectMessage() {
729
  BuildEvent('unsupported-project', eventError: 'gradle-plugin', flutterUsage: globals.flutterUsage).send();
730 731 732 733 734
  throwToolExit(
    '$warningMark Your app is using an unsupported Gradle project. '
    'To fix this problem, create a new project by running `flutter create -t app <app-directory>` '
    'and then move the dart code, assets and pubspec.yaml to the new project.',
  );
735 736
}

737
void _exitWithProjectNotUsingGradleMessage() {
738
  BuildEvent('unsupported-project', eventError: 'app-not-using-gradle', flutterUsage: globals.flutterUsage).send();
739 740 741 742 743
  throwToolExit(
    '$warningMark The build process for Android has changed, and the '
    'current project configuration is no longer valid. Please consult\n\n'
    'https://github.com/flutter/flutter/wiki/Upgrading-Flutter-projects-to-build-with-gradle\n\n'
    'for details on how to upgrade the project.'
744
  );
745 746
}

747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762
/// Returns [true] if the current app uses AndroidX.
// TODO(egarciad): https://github.com/flutter/flutter/issues/40800
// Remove `FlutterManifest.usesAndroidX` and provide a unified `AndroidProject.usesAndroidX`.
bool isAppUsingAndroidX(Directory androidDirectory) {
  final File properties = androidDirectory.childFile('gradle.properties');
  if (!properties.existsSync()) {
    return false;
  }
  return properties.readAsStringSync().contains('android.useAndroidX=true');
}

/// Builds the plugins as AARs.
@visibleForTesting
Future<void> buildPluginsAsAar(
  FlutterProject flutterProject,
  AndroidBuildInfo androidBuildInfo, {
763
  Directory buildDirectory,
764 765 766 767 768 769
}) async {
  final File flutterPluginFile = flutterProject.flutterPluginsFile;
  if (!flutterPluginFile.existsSync()) {
    return;
  }
  final List<String> plugins = flutterPluginFile.readAsStringSync().split('\n');
770
  for (final String plugin in plugins) {
771 772 773 774
    final List<String> pluginParts = plugin.split('=');
    if (pluginParts.length != 2) {
      continue;
    }
775
    final Directory pluginDirectory = globals.fs.directory(pluginParts.last);
776 777 778
    assert(pluginDirectory.existsSync());

    final String pluginName = pluginParts.first;
779 780
    final File buildGradleFile = pluginDirectory.childDirectory('android').childFile('build.gradle');
    if (!buildGradleFile.existsSync()) {
781
      globals.printTrace("Skipping plugin $pluginName since it doesn't have a android/build.gradle file");
782 783
      continue;
    }
784
    globals.logger.printStatus('Building plugin $pluginName...');
785 786 787
    try {
      await buildGradleAar(
        project: FlutterProject.fromDirectory(pluginDirectory),
788
        androidBuildInfo: AndroidBuildInfo(
789 790 791
          BuildInfo(
            BuildMode.release, // Plugins are built as release.
            null, // Plugins don't define flavors.
792
            treeShakeIcons: androidBuildInfo.buildInfo.treeShakeIcons,
793 794 795
          ),
        ),
        target: '',
796
        outputDirectory: buildDirectory,
797
        buildNumber: '1.0'
798 799 800 801
      );
    } on ToolExit {
      // Log the entire plugin entry in `.flutter-plugins` since it
      // includes the plugin name and the version.
802
      BuildEvent('gradle-plugin-aar-failure', eventError: plugin, flutterUsage: globals.flutterUsage).send();
803
      throwToolExit('The plugin $pluginName could not be built due to the issue above.');
804 805 806 807
    }
  }
}

808
/// Returns the APK files for a given [FlutterProject] and [AndroidBuildInfo].
809
@visibleForTesting
810
Iterable<String> findApkFilesModule(
811
  FlutterProject project,
812 813
  AndroidBuildInfo androidBuildInfo,
) {
814 815
  final Iterable<String> apkFileNames = _apkFilesFor(androidBuildInfo);
  final Directory apkDirectory = getApkDirectory(project);
816
  final Iterable<File> apks = apkFileNames.expand<File>((String apkFileName) {
817
    File apkFile = apkDirectory.childFile(apkFileName);
818
    if (apkFile.existsSync()) {
819
      return <File>[apkFile];
820
    }
821 822
    final BuildInfo buildInfo = androidBuildInfo.buildInfo;
    final String modeName = camelCase(buildInfo.modeName);
823 824 825
    apkFile = apkDirectory
      .childDirectory(modeName)
      .childFile(apkFileName);
826
    if (apkFile.existsSync()) {
827
      return <File>[apkFile];
828
    }
829 830
    if (buildInfo.flavor != null) {
      // Android Studio Gradle plugin v3 adds flavor to path.
831 832 833 834
      apkFile = apkDirectory
        .childDirectory(buildInfo.flavor)
        .childDirectory(modeName)
        .childFile(apkFileName);
835
      if (apkFile.existsSync()) {
836
        return <File>[apkFile];
837
      }
838
    }
839
    return const <File>[];
840
  });
841 842 843 844 845 846
  if (apks.isEmpty) {
    _exitWithExpectedFileNotFound(
      project: project,
      fileExtension: '.apk',
    );
  }
847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879
  return apks.map((File file) => file.path);
}

/// Returns the APK files for a given [FlutterProject] and [AndroidBuildInfo].
///
/// The flutter.gradle plugin will copy APK outputs into:
/// $buildDir/app/outputs/flutter-apk/app-<abi>-<flavor-flag>-<build-mode-flag>.apk
@visibleForTesting
Iterable<String> listApkPaths(
  AndroidBuildInfo androidBuildInfo,
) {
  final String buildType = camelCase(androidBuildInfo.buildInfo.modeName);
  final List<String> apkPartialName = <String>[
    if (androidBuildInfo.buildInfo.flavor?.isNotEmpty ?? false)
      androidBuildInfo.buildInfo.flavor,
    '$buildType.apk',
  ];
  if (androidBuildInfo.splitPerAbi) {
    return <String>[
      for (AndroidArch androidArch in androidBuildInfo.targetArchs)
        <String>[
          'app',
          getNameForAndroidArch(androidArch),
          ...apkPartialName
        ].join('-')
    ];
  }
  return <String>[
    <String>[
      'app',
      ...apkPartialName,
    ].join('-')
  ];
880
}
881

882
@visibleForTesting
883
File findBundleFile(FlutterProject project, BuildInfo buildInfo) {
884
  final List<File> fileCandidates = <File>[
885
    getBundleDirectory(project)
886 887
      .childDirectory(camelCase(buildInfo.modeName))
      .childFile('app.aab'),
888
    getBundleDirectory(project)
889 890 891 892 893 894 895 896
      .childDirectory(camelCase(buildInfo.modeName))
      .childFile('app-${buildInfo.modeName}.aab'),
  ];
  if (buildInfo.flavor != null) {
    // The Android Gradle plugin 3.0.0 adds the flavor name to the path.
    // For example: In release mode, if the flavor name is `foo_bar`, then
    // the directory name is `foo_barRelease`.
    fileCandidates.add(
897
      getBundleDirectory(project)
898 899 900 901 902 903 904
        .childDirectory('${buildInfo.flavor}${camelCase('_' + buildInfo.modeName)}')
        .childFile('app.aab'));

    // The Android Gradle plugin 3.5.0 adds the flavor name to file name.
    // For example: In release mode, if the flavor name is `foo_bar`, then
    // the file name name is `app-foo_bar-release.aab`.
    fileCandidates.add(
905
      getBundleDirectory(project)
906 907 908 909 910 911 912
        .childDirectory('${buildInfo.flavor}${camelCase('_' + buildInfo.modeName)}')
        .childFile('app-${buildInfo.flavor}-${buildInfo.modeName}.aab'));
  }
  for (final File bundleFile in fileCandidates) {
    if (bundleFile.existsSync()) {
      return bundleFile;
    }
913
  }
914 915 916 917
  _exitWithExpectedFileNotFound(
    project: project,
    fileExtension: '.aab',
  );
918 919
  return null;
}
920 921 922 923 924 925 926 927 928 929

/// Throws a [ToolExit] exception and logs the event.
void _exitWithExpectedFileNotFound({
  @required FlutterProject project,
  @required String fileExtension,
}) {
  assert(project != null);
  assert(fileExtension != null);

  final String androidGradlePluginVersion =
930
  getGradleVersionForAndroidPlugin(project.android.hostAppGradleRoot);
931 932
  BuildEvent('gradle-expected-file-not-found',
    settings:
933 934 935 936
    'androidGradlePluginVersion: $androidGradlePluginVersion, '
      'fileExtension: $fileExtension',
    flutterUsage: globals.flutterUsage,
  ).send();
937 938
  throwToolExit(
    'Gradle build failed to produce an $fileExtension file. '
939 940
    "It's likely that this file was generated under ${project.android.buildDirectory.path}, "
    "but the tool couldn't find it."
941 942
  );
}
943 944

void _createSymlink(String targetPath, String linkPath) {
945
  final File targetFile = globals.fs.file(targetPath);
946
  if (!targetFile.existsSync()) {
947
    throwToolExit("The file $targetPath wasn't found in the local engine out directory.");
948
  }
949
  final File linkFile = globals.fs.file(linkPath);
950 951 952 953 954 955 956 957 958 959 960
  final Link symlink = linkFile.parent.childLink(linkFile.basename);
  try {
    symlink.createSync(targetPath, recursive: true);
  } on FileSystemException catch (exception) {
    throwToolExit(
      'Failed to create the symlink $linkPath->$targetPath: $exception'
    );
  }
}

String _getLocalArtifactVersion(String pomPath) {
961
  final File pomFile = globals.fs.file(pomPath);
962
  if (!pomFile.existsSync()) {
963
    throwToolExit("The file $pomPath wasn't found in the local engine out directory.");
964 965 966 967 968 969 970 971 972 973
  }
  xml.XmlDocument document;
  try {
    document = xml.parse(pomFile.readAsStringSync());
  } on xml.XmlParserException {
    throwToolExit(
      'Error parsing $pomPath. Please ensure that this is a valid XML document.'
    );
  } on FileSystemException {
    throwToolExit(
974
      'Error reading $pomPath. Please ensure that you have read permission to this '
975 976 977 978
      'file and try again.');
  }
  final Iterable<xml.XmlElement> project = document.findElements('project');
  assert(project.isNotEmpty);
979
  for (final xml.XmlElement versionElement in document.findAllElements('version')) {
980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998
    if (versionElement.parent == project.first) {
      return versionElement.text;
    }
  }
  throwToolExit('Error while parsing the <version> element from $pomPath');
  return null;
}

/// Returns the local Maven repository for a local engine build.
/// For example, if the engine is built locally at <home>/engine/src/out/android_release_unopt
/// This method generates symlinks in the temp directory to the engine artifacts
/// following the convention specified on https://maven.apache.org/pom.html#Repositories
Directory _getLocalEngineRepo({
  @required String engineOutPath,
  @required AndroidBuildInfo androidBuildInfo,
}) {
  assert(engineOutPath != null);
  assert(androidBuildInfo != null);

999
  final String abi = _getAbiByLocalEnginePath(engineOutPath);
1000
  final Directory localEngineRepo = globals.fs.systemTempDirectory
1001 1002 1003
    .createTempSync('flutter_tool_local_engine_repo.');

  // Remove the local engine repo before the tool exits.
1004 1005 1006 1007 1008
  shutdownHooks.addShutdownHook(() {
      if (localEngineRepo.existsSync()) {
        localEngineRepo.deleteSync(recursive: true);
      }
    },
1009 1010 1011 1012 1013
    ShutdownStage.CLEANUP,
  );

  final String buildMode = androidBuildInfo.buildInfo.modeName;
  final String artifactVersion = _getLocalArtifactVersion(
1014
    globals.fs.path.join(
1015 1016 1017 1018
      engineOutPath,
      'flutter_embedding_$buildMode.pom',
    )
  );
1019
  for (final String artifact in const <String>['pom', 'jar']) {
1020 1021
    // The Android embedding artifacts.
    _createSymlink(
1022
      globals.fs.path.join(
1023 1024 1025
        engineOutPath,
        'flutter_embedding_$buildMode.$artifact',
      ),
1026
      globals.fs.path.join(
1027 1028 1029 1030 1031 1032 1033 1034 1035 1036
        localEngineRepo.path,
        'io',
        'flutter',
        'flutter_embedding_$buildMode',
        artifactVersion,
        'flutter_embedding_$buildMode-$artifactVersion.$artifact',
      ),
    );
    // The engine artifacts (libflutter.so).
    _createSymlink(
1037
      globals.fs.path.join(
1038 1039 1040
        engineOutPath,
        '${abi}_$buildMode.$artifact',
      ),
1041
      globals.fs.path.join(
1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052
        localEngineRepo.path,
        'io',
        'flutter',
        '${abi}_$buildMode',
        artifactVersion,
        '${abi}_$buildMode-$artifactVersion.$artifact',
      ),
    );
  }
  return localEngineRepo;
}
1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076

String _getAbiByLocalEnginePath(String engineOutPath) {
  String result = 'armeabi_v7a';
  if (engineOutPath.contains('x86')) {
    result = 'x86';
  } else if (engineOutPath.contains('x64')) {
    result = 'x86_64';
  } else if (engineOutPath.contains('arm64')) {
    result = 'arm64_v8a';
  }
  return result;
}

String _getTargetPlatformByLocalEnginePath(String engineOutPath) {
  String result = 'android-arm';
  if (engineOutPath.contains('x86')) {
    result = 'android-x86';
  } else if (engineOutPath.contains('x64')) {
    result = 'android-x64';
  } else if (engineOutPath.contains('arm64')) {
    result = 'android-arm64';
  }
  return result;
}