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

5
import 'package:crypto/crypto.dart';
6
import 'package:meta/meta.dart';
7
import 'package:process/process.dart';
Dan Field's avatar
Dan Field committed
8
import 'package:xml/xml.dart';
9

10
import '../artifacts.dart';
11
import '../base/analyze_size.dart';
12
import '../base/common.dart';
13
import '../base/deferred_component.dart';
14
import '../base/file_system.dart';
15
import '../base/io.dart';
16
import '../base/logger.dart';
17
import '../base/platform.dart';
18
import '../base/process.dart';
19
import '../base/terminal.dart';
20 21 22
import '../base/utils.dart';
import '../build_info.dart';
import '../cache.dart';
23
import '../convert.dart';
24
import '../flutter_manifest.dart';
25
import '../project.dart';
26
import '../reporting/reporting.dart';
27
import 'android_builder.dart';
28
import 'android_studio.dart';
29 30
import 'gradle_errors.dart';
import 'gradle_utils.dart';
31

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

45 46 47 48 49 50 51 52 53 54 55 56 57
/// 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');
}
58

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

67 68 69 70 71 72
/// 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)}';
}
73

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

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

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

92 93 94 95 96
/// 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);
97
  final String productFlavor = androidBuildInfo.buildInfo.lowerCasedFlavor ?? '';
98 99 100 101 102 103 104 105 106
  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'];
}
107

108 109
/// An implementation of the [AndroidBuilder] that delegates to gradle.
class AndroidGradleBuilder implements AndroidBuilder {
110
  AndroidGradleBuilder({
111 112 113 114 115 116 117
    required Logger logger,
    required ProcessManager processManager,
    required FileSystem fileSystem,
    required Artifacts artifacts,
    required Usage usage,
    required GradleUtils gradleUtils,
    required Platform platform,
118 119
  }) : _logger = logger,
       _fileSystem = fileSystem,
120
       _artifacts = artifacts,
121
       _usage = usage,
122 123
       _gradleUtils = gradleUtils,
       _fileSystemUtils = FileSystemUtils(fileSystem: fileSystem, platform: platform),
124
       _processUtils = ProcessUtils(logger: logger, processManager: processManager);
125 126

  final Logger _logger;
127 128
  final ProcessUtils _processUtils;
  final FileSystem _fileSystem;
129
  final Artifacts _artifacts;
130
  final Usage _usage;
131 132
  final GradleUtils _gradleUtils;
  final FileSystemUtils _fileSystemUtils;
133

134 135 136
  /// Builds the AAR and POM files for the current Flutter module or plugin.
  @override
  Future<void> buildAar({
137 138 139 140 141
    required FlutterProject project,
    required Set<AndroidBuildInfo> androidBuildInfo,
    required String target,
    String? outputDirectoryPath,
    required String buildNumber,
142
  }) async {
143 144 145 146 147 148 149 150 151 152 153 154
    Directory outputDirectory =
      _fileSystem.directory(outputDirectoryPath ?? project.android.buildDirectory);
    if (project.isModule) {
      // Module projects artifacts are located in `build/host`.
      outputDirectory = outputDirectory.childDirectory('host');
    }
    for (final AndroidBuildInfo androidBuildInfo in androidBuildInfo) {
      await buildGradleAar(
        project: project,
        androidBuildInfo: androidBuildInfo,
        target: target,
        outputDirectory: outputDirectory,
155 156 157
        buildNumber: buildNumber,
      );
    }
158 159 160 161 162 163 164 165 166 167 168
    printHowToConsumeAar(
      buildModes: androidBuildInfo
        .map<String>((AndroidBuildInfo androidBuildInfo) {
          return androidBuildInfo.buildInfo.modeName;
        }).toSet(),
      androidPackage: project.manifest.androidPackage,
      repoDirectory: getRepoDirectory(outputDirectory),
      buildNumber: buildNumber,
      logger: _logger,
      fileSystem: _fileSystem,
    );
169
  }
170 171 172 173

  /// Builds the APK.
  @override
  Future<void> buildApk({
174 175 176
    required FlutterProject project,
    required AndroidBuildInfo androidBuildInfo,
    required String target,
177
  }) async {
178 179 180 181 182 183 184
    await buildGradleApp(
      project: project,
      androidBuildInfo: androidBuildInfo,
      target: target,
      isBuildingBundle: false,
      localGradleErrors: gradleErrors,
    );
Emmanuel Garcia's avatar
Emmanuel Garcia committed
185
  }
186 187 188 189

  /// Builds the App Bundle.
  @override
  Future<void> buildAab({
190 191 192
    required FlutterProject project,
    required AndroidBuildInfo androidBuildInfo,
    required String target,
193 194
    bool validateDeferredComponents = true,
    bool deferredComponentsEnabled = false,
195
  }) async {
196 197 198 199 200 201
    await buildGradleApp(
      project: project,
      androidBuildInfo: androidBuildInfo,
      target: target,
      isBuildingBundle: true,
      localGradleErrors: gradleErrors,
202 203
      validateDeferredComponents: validateDeferredComponents,
      deferredComponentsEnabled: deferredComponentsEnabled,
204
    );
205 206
  }

207 208 209 210 211 212 213 214 215
  /// Builds an app.
  ///
  /// * [project] is typically [FlutterProject.current()].
  /// * [androidBuildInfo] is the build configuration.
  /// * [target] is the target dart entry point. Typically, `lib/main.dart`.
  /// * If [isBuildingBundle] is `true`, then the output artifact is an `*.aab`,
  ///   otherwise the output artifact is an `*.apk`.
  /// * [retries] is the max number of build retries in case one of the [GradleHandledError] handler
  Future<void> buildGradleApp({
216 217 218 219 220
    required FlutterProject project,
    required AndroidBuildInfo androidBuildInfo,
    required String target,
    required bool isBuildingBundle,
    required List<GradleHandledError> localGradleErrors,
221 222
    bool validateDeferredComponents = true,
    bool deferredComponentsEnabled = false,
223 224 225 226 227 228 229 230
    int retries = 1,
  }) async {
    assert(project != null);
    assert(androidBuildInfo != null);
    assert(target != null);
    assert(isBuildingBundle != null);
    assert(localGradleErrors != null);

231
    if (!project.android.isSupportedVersion) {
232
      _exitWithUnsupportedProjectMessage(_usage, _logger.terminal);
233 234 235 236
    }

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

    final BuildInfo buildInfo = androidBuildInfo.buildInfo;
    final String assembleTask = isBuildingBundle
        ? getBundleTaskFor(buildInfo)
        : getAssembleTaskFor(buildInfo);
255

256
    final Status status = _logger.startProgress(
257
      "Running Gradle task '$assembleTask'...",
258
    );
259

260
    final List<String> command = <String>[
261
      _gradleUtils.getExecutable(project),
262
    ];
263
    if (_logger.isVerbose) {
264 265 266 267 268 269 270
      command.add('-Pverbose=true');
    } else {
      command.add('-q');
    }
    if (!buildInfo.androidGradleDaemon) {
      command.add('--no-daemon');
    }
271 272
    if (_artifacts is LocalEngineArtifacts) {
      final LocalEngineArtifacts localEngineArtifacts = _artifacts as LocalEngineArtifacts;
273 274 275
      final Directory localEngineRepo = _getLocalEngineRepo(
        engineOutPath: localEngineArtifacts.engineOutPath,
        androidBuildInfo: androidBuildInfo,
276
        fileSystem: _fileSystem,
277
      );
278
      _logger.printTrace(
279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295
          '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}');
      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');
    }
    if (target != null) {
      command.add('-Ptarget=$target');
    }
296
    final List<DeferredComponent>? deferredComponents = project.manifest.deferredComponents;
297
    if (deferredComponents != null) {
298 299 300 301 302 303 304
      if (deferredComponentsEnabled) {
        command.add('-Pdeferred-components=true');
        androidBuildInfo.buildInfo.dartDefines.add('validate-deferred-components=$validateDeferredComponents');
      }
      // Pass in deferred components regardless of building split aot to satisfy
      // android dynamic features registry in build.gradle.
      final List<String> componentNames = <String>[];
305
      for (final DeferredComponent component in deferredComponents) {
306 307 308 309 310 311 312 313 314 315 316 317 318
        componentNames.add(component.name);
      }
      if (componentNames.isNotEmpty) {
        command.add('-Pdeferred-component-names=${componentNames.join(',')}');
        // Multi-apk applications cannot use shrinking. This is only relevant when using
        // android dynamic feature modules.
        _logger.printStatus(
          'Shrinking has been disabled for this build due to deferred components. Shrinking is '
          'not available for multi-apk applications. This limitation is expected to be removed '
          'when Gradle plugin 4.2+ is available in Flutter.', color: TerminalColor.yellow);
        command.add('-Pshrink=false');
      }
    }
319 320 321 322 323 324 325 326 327 328 329 330 331 332 333
    command.addAll(androidBuildInfo.buildInfo.toGradleConfig());
    if (buildInfo.fileSystemRoots != null && buildInfo.fileSystemRoots.isNotEmpty) {
      command.add('-Pfilesystem-roots=${buildInfo.fileSystemRoots.join('|')}');
    }
    if (buildInfo.fileSystemScheme != null) {
      command.add('-Pfilesystem-scheme=${buildInfo.fileSystemScheme}');
    }
    if (androidBuildInfo.splitPerAbi) {
      command.add('-Psplit-per-abi=true');
    }
    if (androidBuildInfo.fastStart) {
      command.add('-Pfast-start=true');
    }
    command.add(assembleTask);

334 335 336
    GradleHandledError? detectedGradleError;
    String? detectedGradleErrorLine;
    String? consumeLog(String line) {
337 338 339 340 341 342 343 344 345 346 347 348
      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;
        }
      }
349 350 351
      // Pipe stdout/stderr from Gradle.
      return line;
    }
352 353 354 355 356

    final Stopwatch sw = Stopwatch()
      ..start();
    int exitCode = 1;
    try {
357
      exitCode = await _processUtils.stream(
358 359 360
        command,
        workingDirectory: project.android.hostAppGradleRoot.path,
        allowReentrantFlutter: true,
361 362
        environment: <String, String>{
          if (javaPath != null)
363
            'JAVA_HOME': javaPath!,
364
        },
365 366 367 368 369 370 371 372
        mapFunction: consumeLog,
      );
    } 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;
373
      }
374 375
    } finally {
      status.stop();
376 377
    }

378
    _usage.sendTiming('build', 'gradle', sw.elapsed);
379 380 381

    if (exitCode != 0) {
      if (detectedGradleError == null) {
382
        BuildEvent('gradle-unknown-failure', type: 'gradle', flutterUsage: _usage).send();
383 384 385 386 387
        throwToolExit(
          'Gradle task $assembleTask failed with exit code $exitCode',
          exitCode: exitCode,
        );
      } else {
388 389
        final GradleBuildStatus status = await detectedGradleError!.handler(
          line: detectedGradleErrorLine!,
390 391 392 393 394
          project: project,
          usesAndroidX: usesAndroidX,
        );

        if (retries >= 1) {
395
          final String successEventLabel = 'gradle-${detectedGradleError!.eventLabel}-success';
396 397 398 399 400 401 402 403 404 405
          switch (status) {
            case GradleBuildStatus.retry:
              await buildGradleApp(
                project: project,
                androidBuildInfo: androidBuildInfo,
                target: target,
                isBuildingBundle: isBuildingBundle,
                localGradleErrors: localGradleErrors,
                retries: retries - 1,
              );
406
              BuildEvent(successEventLabel, type: 'gradle', flutterUsage: _usage).send();
407 408 409 410 411
              return;
            case GradleBuildStatus.exit:
            // noop.
          }
        }
412
        BuildEvent('gradle-${detectedGradleError?.eventLabel}-failure', type: 'gradle', flutterUsage: _usage).send();
413 414 415 416 417 418
        throwToolExit(
          'Gradle task $assembleTask failed with exit code $exitCode',
          exitCode: exitCode,
        );
      }
    }
419

420
    if (isBuildingBundle) {
421
      final File bundleFile = findBundleFile(project, buildInfo, _logger, _usage);
422 423 424
      final String appSize = (buildInfo.mode == BuildMode.debug)
          ? '' // Don't display the size when building a debug variant.
          : ' (${getSizeAsMB(bundleFile.lengthSync())})';
425

426 427 428 429
      if (buildInfo.codeSizeDirectory != null) {
        await _performCodeSizeAnalysis('aab', bundleFile, androidBuildInfo);
      }

430
      _logger.printStatus(
431
        '${_logger.terminal.successMark} Built ${_fileSystem.path.relative(bundleFile.path)}$appSize.',
432
        color: TerminalColor.green,
433
      );
434 435 436 437
      return;
    }
    // Gradle produced an APK.
    final Iterable<String> apkFilesPaths = project.isModule
438
        ? findApkFilesModule(project, androidBuildInfo, _logger, _usage)
439 440 441 442 443
        : listApkPaths(androidBuildInfo);
    final Directory apkDirectory = getApkDirectory(project);
    final File apkFile = apkDirectory.childFile(apkFilesPaths.first);
    if (!apkFile.existsSync()) {
      _exitWithExpectedFileNotFound(
444
        project: project,
445
        fileExtension: '.apk',
446
        logger: _logger,
447
        usage: _usage,
448
      );
449
    }
450

451 452 453 454 455
    // Copy the first APK to app.apk, so `flutter run` can find it.
    // TODO(egarciad): Handle multiple APKs.
    apkFile.copySync(apkDirectory
        .childFile('app.apk')
        .path);
456
    _logger.printTrace('calculateSha: $apkDirectory/app.apk');
457

458 459
    final File apkShaFile = apkDirectory.childFile('app.apk.sha1');
    apkShaFile.writeAsStringSync(_calculateSha(apkFile));
460

461 462 463
    final String appSize = (buildInfo.mode == BuildMode.debug)
        ? '' // Don't display the size when building a debug variant.
        : ' (${getSizeAsMB(apkFile.lengthSync())})';
464
    _logger.printStatus(
465
      '${_logger.terminal.successMark}  Built ${_fileSystem.path.relative(apkFile.path)}$appSize.',
466 467
      color: TerminalColor.green,
    );
468

469 470 471
    if (buildInfo.codeSizeDirectory != null) {
      await _performCodeSizeAnalysis('apk', apkFile, androidBuildInfo);
    }
472 473
  }

474 475 476 477
  Future<void> _performCodeSizeAnalysis(String kind,
      File zipFile,
      AndroidBuildInfo androidBuildInfo,) async {
    final SizeAnalyzer sizeAnalyzer = SizeAnalyzer(
478
      fileSystem: _fileSystem,
479
      logger: _logger,
480
      flutterUsage: _usage,
481 482 483
    );
    final String archName = getNameForAndroidArch(androidBuildInfo.targetArchs.single);
    final BuildInfo buildInfo = androidBuildInfo.buildInfo;
484
    final File aotSnapshot = _fileSystem.directory(buildInfo.codeSizeDirectory)
485
        .childFile('snapshot.$archName.json');
486
    final File precompilerTrace = _fileSystem.directory(buildInfo.codeSizeDirectory)
487
        .childFile('trace.$archName.json');
488
    final Map<String, Object?> output = await sizeAnalyzer.analyzeZipSizeAndAotSnapshot(
489 490 491 492 493
      zipFile: zipFile,
      aotSnapshot: aotSnapshot,
      precompilerTrace: precompilerTrace,
      kind: kind,
    );
494
    final File outputFile = _fileSystemUtils.getUniqueFile(
495
      _fileSystem
496
        .directory(_fileSystemUtils.homeDirPath)
497
        .childDirectory('.flutter-devtools'), '$kind-code-size-analysis', 'json',
498 499 500
    )
      ..writeAsStringSync(jsonEncode(output));
    // This message is used as a sentinel in analyze_apk_size_test.dart
501
    _logger.printStatus(
502 503
      'A summary of your ${kind.toUpperCase()} analysis can be found at: ${outputFile.path}',
    );
504

505 506 507 508 509
    // DevTools expects a file path relative to the .flutter-devtools/ dir.
    final String relativeAppSizePath = outputFile.path
        .split('.flutter-devtools/')
        .last
        .trim();
510
    _logger.printStatus(
511 512 513
        '\nTo analyze your app size in Dart DevTools, run the following command:\n'
            'flutter pub global activate devtools; flutter pub global run devtools '
            '--appSizeBase=$relativeAppSizePath'
514
    );
515
  }
516

517 518 519 520 521 522 523
  /// Builds AAR and POM files.
  ///
  /// * [project] is typically [FlutterProject.current()].
  /// * [androidBuildInfo] is the build configuration.
  /// * [outputDir] is the destination of the artifacts,
  /// * [buildNumber] is the build number of the output aar,
  Future<void> buildGradleAar({
524 525 526 527 528
    required FlutterProject project,
    required AndroidBuildInfo androidBuildInfo,
    required String target,
    required Directory outputDirectory,
    required String buildNumber,
529 530 531 532 533 534 535 536 537 538 539 540 541
  }) async {
    assert(project != null);
    assert(target != null);
    assert(androidBuildInfo != null);
    assert(outputDirectory != null);

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

    final BuildInfo buildInfo = androidBuildInfo.buildInfo;
    final String aarTask = getAarTaskFor(buildInfo);
542
    final Status status = _logger.startProgress(
543
      "Running Gradle task '$aarTask'...",
544
    );
545

546
    final String flutterRoot = _fileSystem.path.absolute(Cache.flutterRoot!);
547
    final String initScript = _fileSystem.path.join(
548 549 550 551 552
      flutterRoot,
      'packages',
      'flutter_tools',
      'gradle',
      'aar_init_script.gradle',
553
    );
554
    final List<String> command = <String>[
555
      _gradleUtils.getExecutable(project),
556 557 558 559 560 561
      '-I=$initScript',
      '-Pflutter-root=$flutterRoot',
      '-Poutput-dir=${outputDirectory.path}',
      '-Pis-plugin=${manifest.isPlugin}',
      '-PbuildNumber=$buildNumber'
    ];
562
    if (_logger.isVerbose) {
563 564 565 566 567 568 569
      command.add('-Pverbose=true');
    } else {
      command.add('-q');
    }
    if (!buildInfo.androidGradleDaemon) {
      command.add('--no-daemon');
    }
570

571 572 573 574 575
    if (target != null && target.isNotEmpty) {
      command.add('-Ptarget=$target');
    }
    command.addAll(androidBuildInfo.buildInfo.toGradleConfig());
    if (buildInfo.dartObfuscation && buildInfo.mode != BuildMode.release) {
576
      _logger.printStatus(
577 578
        'Dart obfuscation is not supported in ${toTitleCase(buildInfo.friendlyModeName)}'
            ' mode, building as un-obfuscated.',
579
      );
580 581
    }

582 583
    if (_artifacts is LocalEngineArtifacts) {
      final LocalEngineArtifacts localEngineArtifacts = _artifacts as LocalEngineArtifacts;
584 585 586
      final Directory localEngineRepo = _getLocalEngineRepo(
        engineOutPath: localEngineArtifacts.engineOutPath,
        androidBuildInfo: androidBuildInfo,
587
        fileSystem: _fileSystem,
588
      );
589 590 591
      _logger.printTrace(
        'Using local engine: ${localEngineArtifacts.engineOutPath}\n'
        'Local Maven repo: ${localEngineRepo.path}'
592
      );
593 594 595 596 597 598
      command.add('-Plocal-engine-repo=${localEngineRepo.path}');
      command.add('-Plocal-engine-build-mode=${buildInfo.modeName}');
      command.add('-Plocal-engine-out=${localEngineArtifacts.engineOutPath}');

      // Copy the local engine repo in the output directory.
      try {
599
        copyDirectory(
600 601 602
          localEngineRepo,
          getRepoDirectory(outputDirectory),
        );
603
      } on FileSystemException catch (error, st) {
604 605
        throwToolExit(
            'Failed to copy the local engine ${localEngineRepo.path} repo '
606
                'in ${outputDirectory.path}: $error, $st'
607 608 609 610 611 612 613 614
        );
      }
      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');
615
    }
616

617
    command.add(aarTask);
618

619 620 621 622
    final Stopwatch sw = Stopwatch()
      ..start();
    RunResult result;
    try {
623
      result = await _processUtils.run(
624 625 626
        command,
        workingDirectory: project.android.hostAppGradleRoot.path,
        allowReentrantFlutter: true,
627 628
        environment: <String, String>{
          if (javaPath != null)
629
            'JAVA_HOME': javaPath!,
630
        },
631 632 633 634
      );
    } finally {
      status.stop();
    }
635
    _usage.sendTiming('build', 'gradle-aar', sw.elapsed);
636 637

    if (result.exitCode != 0) {
638 639
      _logger.printStatus(result.stdout, wrap: false);
      _logger.printError(result.stderr, wrap: false);
640 641 642 643 644 645 646
      throwToolExit(
        'Gradle task $aarTask failed with exit code ${result.exitCode}.',
        exitCode: result.exitCode,
      );
    }
    final Directory repoDirectory = getRepoDirectory(outputDirectory);
    if (!repoDirectory.existsSync()) {
647 648
      _logger.printStatus(result.stdout, wrap: false);
      _logger.printError(result.stderr, wrap: false);
649 650 651 652 653
      throwToolExit(
        'Gradle task $aarTask failed to produce $repoDirectory.',
        exitCode: exitCode,
      );
    }
654
    _logger.printStatus(
655
      '${_logger.terminal.successMark} Built ${_fileSystem.path.relative(repoDirectory.path)}.',
656
      color: TerminalColor.green,
657
    );
658
  }
659 660 661
}

/// Prints how to consume the AAR from a host app.
662
void printHowToConsumeAar({
663 664 665 666 667 668
  required Set<String> buildModes,
  String? androidPackage = 'unknown',
  required Directory repoDirectory,
  required Logger logger,
  required FileSystem fileSystem,
  String? buildNumber,
669
}) {
670 671
  assert(buildModes != null && buildModes.isNotEmpty);
  assert(repoDirectory != null);
672
  buildNumber ??= '1.0';
673

674 675 676
  logger.printStatus('\nConsuming the Module', emphasis: true);
  logger.printStatus('''
  1. Open ${fileSystem.path.join('<host>', 'app', 'build.gradle')}
677 678
  2. Ensure you have the repositories configured, otherwise add them:

679
      String storageUrl = System.env.FLUTTER_STORAGE_BASE_URL ?: "https://storage.googleapis.com"
680 681
      repositories {
        maven {
682
            url '${repoDirectory.path}'
683 684
        }
        maven {
685
            url "\$storageUrl/download.flutter.io"
686 687 688
        }
      }

689
  3. Make the host app depend on the Flutter module:
690

691 692
    dependencies {''');

693
  for (final String buildMode in buildModes) {
694
    logger.printStatus("""
695
      ${buildMode}Implementation '$androidPackage:flutter_$buildMode:$buildNumber'""");
696 697
  }

698
  logger.printStatus('''
699 700 701 702
    }
''');

  if (buildModes.contains('profile')) {
703
    logger.printStatus('''
704 705 706 707 708 709 710 711

  4. Add the `profile` build type:

    android {
      buildTypes {
        profile {
          initWith debug
        }
712
      }
713 714 715
    }
''');
  }
716

717
  logger.printStatus('To learn more, visit https://flutter.dev/go/build-aar');
718 719
}

720 721
String _hex(List<int> bytes) {
  final StringBuffer result = StringBuffer();
722
  for (final int part in bytes) {
723
    result.write('${part < 16 ? '0' : ''}${part.toRadixString(16)}');
724
  }
725 726 727 728 729
  return result.toString();
}

String _calculateSha(File file) {
  final List<int> bytes = file.readAsBytesSync();
730
  return _hex(sha1.convert(bytes).bytes);
731 732
}

733
void _exitWithUnsupportedProjectMessage(Usage usage, Terminal terminal) {
734
  BuildEvent('unsupported-project', type: 'gradle', eventError: 'gradle-plugin', flutterUsage: usage).send();
735
  throwToolExit(
736
    '${terminal.warningMark} Your app is using an unsupported Gradle project. '
737 738 739
    '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.',
  );
740 741
}

742 743 744 745 746 747 748 749 750 751 752
/// 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');
}

753
/// Returns the APK files for a given [FlutterProject] and [AndroidBuildInfo].
754
@visibleForTesting
755
Iterable<String> findApkFilesModule(
756
  FlutterProject project,
757
  AndroidBuildInfo androidBuildInfo,
758
  Logger logger,
759
  Usage usage,
760
) {
761 762
  final Iterable<String> apkFileNames = _apkFilesFor(androidBuildInfo);
  final Directory apkDirectory = getApkDirectory(project);
763
  final Iterable<File> apks = apkFileNames.expand<File>((String apkFileName) {
764
    File apkFile = apkDirectory.childFile(apkFileName);
765
    if (apkFile.existsSync()) {
766
      return <File>[apkFile];
767
    }
768 769
    final BuildInfo buildInfo = androidBuildInfo.buildInfo;
    final String modeName = camelCase(buildInfo.modeName);
770 771 772
    apkFile = apkDirectory
      .childDirectory(modeName)
      .childFile(apkFileName);
773
    if (apkFile.existsSync()) {
774
      return <File>[apkFile];
775
    }
776
    final String? flavor = buildInfo.flavor;
777
    if (flavor != null) {
778
      // Android Studio Gradle plugin v3 adds flavor to path.
779
      apkFile = apkDirectory
780
        .childDirectory(flavor)
781 782
        .childDirectory(modeName)
        .childFile(apkFileName);
783
      if (apkFile.existsSync()) {
784
        return <File>[apkFile];
785
      }
786
    }
787
    return const <File>[];
788
  });
789 790 791 792
  if (apks.isEmpty) {
    _exitWithExpectedFileNotFound(
      project: project,
      fileExtension: '.apk',
793
      logger: logger,
794
      usage: usage,
795 796
    );
  }
797 798 799 800 801 802
  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:
803
/// `$buildDir/app/outputs/flutter-apk/app-<abi>-<flavor-flag>-<build-mode-flag>.apk`
804 805 806 807 808 809 810
@visibleForTesting
Iterable<String> listApkPaths(
  AndroidBuildInfo androidBuildInfo,
) {
  final String buildType = camelCase(androidBuildInfo.buildInfo.modeName);
  final List<String> apkPartialName = <String>[
    if (androidBuildInfo.buildInfo.flavor?.isNotEmpty ?? false)
811
      androidBuildInfo.buildInfo.lowerCasedFlavor!,
812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829
    '$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('-')
  ];
830
}
831

832
@visibleForTesting
833
File findBundleFile(FlutterProject project, BuildInfo buildInfo, Logger logger, Usage usage) {
834
  final List<File> fileCandidates = <File>[
835
    getBundleDirectory(project)
836 837
      .childDirectory(camelCase(buildInfo.modeName))
      .childFile('app.aab'),
838
    getBundleDirectory(project)
839 840 841 842 843 844 845 846
      .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(
847
      getBundleDirectory(project)
848
        .childDirectory('${buildInfo.lowerCasedFlavor}${camelCase('_${buildInfo.modeName}')}')
849 850 851 852 853 854
        .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(
855
      getBundleDirectory(project)
856
        .childDirectory('${buildInfo.lowerCasedFlavor}${camelCase('_${buildInfo.modeName}')}')
857
        .childFile('app-${buildInfo.lowerCasedFlavor}-${buildInfo.modeName}.aab'));
858 859 860 861 862
  }
  for (final File bundleFile in fileCandidates) {
    if (bundleFile.existsSync()) {
      return bundleFile;
    }
863
  }
864 865 866
  _exitWithExpectedFileNotFound(
    project: project,
    fileExtension: '.aab',
867
    logger: logger,
868
    usage: usage,
869
  );
870
}
871 872

/// Throws a [ToolExit] exception and logs the event.
873 874 875 876 877
Never _exitWithExpectedFileNotFound({
  required FlutterProject project,
  required String fileExtension,
  required Logger logger,
  required Usage usage,
878 879 880 881 882
}) {
  assert(project != null);
  assert(fileExtension != null);

  final String androidGradlePluginVersion =
883
  getGradleVersionForAndroidPlugin(project.android.hostAppGradleRoot, logger);
884
  BuildEvent('gradle-expected-file-not-found',
885
    type: 'gradle',
886
    settings:
887 888
    'androidGradlePluginVersion: $androidGradlePluginVersion, '
      'fileExtension: $fileExtension',
889
    flutterUsage: usage,
890
  ).send();
891 892
  throwToolExit(
    'Gradle build failed to produce an $fileExtension file. '
893 894
    "It's likely that this file was generated under ${project.android.buildDirectory.path}, "
    "but the tool couldn't find it."
895 896
  );
}
897

898 899
void _createSymlink(String targetPath, String linkPath, FileSystem fileSystem) {
  final File targetFile = fileSystem.file(targetPath);
900
  if (!targetFile.existsSync()) {
901
    throwToolExit("The file $targetPath wasn't found in the local engine out directory.");
902
  }
903
  final File linkFile = fileSystem.file(linkPath);
904 905 906 907 908 909 910 911 912 913
  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'
    );
  }
}

914 915
String _getLocalArtifactVersion(String pomPath, FileSystem fileSystem) {
  final File pomFile = fileSystem.file(pomPath);
916
  if (!pomFile.existsSync()) {
917
    throwToolExit("The file $pomPath wasn't found in the local engine out directory.");
918
  }
Dan Field's avatar
Dan Field committed
919
  XmlDocument document;
920
  try {
Dan Field's avatar
Dan Field committed
921 922
    document = XmlDocument.parse(pomFile.readAsStringSync());
  } on XmlParserException {
923 924 925 926 927
    throwToolExit(
      'Error parsing $pomPath. Please ensure that this is a valid XML document.'
    );
  } on FileSystemException {
    throwToolExit(
928
      'Error reading $pomPath. Please ensure that you have read permission to this '
929 930
      'file and try again.');
  }
Dan Field's avatar
Dan Field committed
931
  final Iterable<XmlElement> project = document.findElements('project');
932
  assert(project.isNotEmpty);
Dan Field's avatar
Dan Field committed
933
  for (final XmlElement versionElement in document.findAllElements('version')) {
934 935 936 937 938 939 940 941 942 943 944 945
    if (versionElement.parent == project.first) {
      return versionElement.text;
    }
  }
  throwToolExit('Error while parsing the <version> element from $pomPath');
}

/// 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({
946 947 948
  required String engineOutPath,
  required AndroidBuildInfo androidBuildInfo,
  required FileSystem fileSystem,
949 950 951 952
}) {
  assert(engineOutPath != null);
  assert(androidBuildInfo != null);

953
  final String abi = _getAbiByLocalEnginePath(engineOutPath);
954
  final Directory localEngineRepo = fileSystem.systemTempDirectory
955 956 957
    .createTempSync('flutter_tool_local_engine_repo.');
  final String buildMode = androidBuildInfo.buildInfo.modeName;
  final String artifactVersion = _getLocalArtifactVersion(
958
    fileSystem.path.join(
959 960
      engineOutPath,
      'flutter_embedding_$buildMode.pom',
961 962
    ),
    fileSystem,
963
  );
964
  for (final String artifact in const <String>['pom', 'jar']) {
965 966
    // The Android embedding artifacts.
    _createSymlink(
967
      fileSystem.path.join(
968 969 970
        engineOutPath,
        'flutter_embedding_$buildMode.$artifact',
      ),
971
      fileSystem.path.join(
972 973 974 975 976 977 978
        localEngineRepo.path,
        'io',
        'flutter',
        'flutter_embedding_$buildMode',
        artifactVersion,
        'flutter_embedding_$buildMode-$artifactVersion.$artifact',
      ),
979
      fileSystem,
980 981 982
    );
    // The engine artifacts (libflutter.so).
    _createSymlink(
983
      fileSystem.path.join(
984 985 986
        engineOutPath,
        '${abi}_$buildMode.$artifact',
      ),
987
      fileSystem.path.join(
988 989 990 991 992 993 994
        localEngineRepo.path,
        'io',
        'flutter',
        '${abi}_$buildMode',
        artifactVersion,
        '${abi}_$buildMode-$artifactVersion.$artifact',
      ),
995
      fileSystem,
996 997
    );
  }
998 999
  for (final String artifact in <String>['flutter_embedding_$buildMode', '${abi}_$buildMode']) {
    _createSymlink(
1000
      fileSystem.path.join(
1001 1002 1003
        engineOutPath,
        '$artifact.maven-metadata.xml',
      ),
1004
      fileSystem.path.join(
1005 1006 1007 1008 1009 1010
        localEngineRepo.path,
        'io',
        'flutter',
        artifact,
        'maven-metadata.xml',
      ),
1011
      fileSystem,
1012 1013
    );
  }
1014 1015
  return localEngineRepo;
}
1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039

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