gradle.dart 37 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 6
import 'dart:math';

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

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

35 36 37 38 39 40 41 42 43 44
/// 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')
45
        .childDirectory('flutter-apk');
46
}
47

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

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

70 71 72 73
/// 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 ?? '';
74
  return '$prefix${sentenceCase(productFlavor)}${sentenceCase(buildType)}';
75
}
76

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

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

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

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

111 112 113
// The maximum time to wait before the tool retries a Gradle build.
const Duration kMaxRetryTime = Duration(seconds: 10);

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

  final Logger _logger;
133 134
  final ProcessUtils _processUtils;
  final FileSystem _fileSystem;
135
  final Artifacts _artifacts;
136
  final Usage _usage;
137 138
  final GradleUtils _gradleUtils;
  final FileSystemUtils _fileSystemUtils;
139

140 141 142
  /// Builds the AAR and POM files for the current Flutter module or plugin.
  @override
  Future<void> buildAar({
143 144 145 146 147
    required FlutterProject project,
    required Set<AndroidBuildInfo> androidBuildInfo,
    required String target,
    String? outputDirectoryPath,
    required String buildNumber,
148
  }) async {
149 150 151 152 153 154 155 156 157 158 159 160
    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,
161 162 163
        buildNumber: buildNumber,
      );
    }
164 165 166 167 168 169 170 171 172 173 174
    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,
    );
175
  }
176 177 178 179

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

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

213 214 215 216 217 218 219
  /// 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`.
220
  /// * [maxRetries] If not `null`, this is the max number of build retries in case a retry is triggered.
221
  Future<void> buildGradleApp({
222 223 224 225 226
    required FlutterProject project,
    required AndroidBuildInfo androidBuildInfo,
    required String target,
    required bool isBuildingBundle,
    required List<GradleHandledError> localGradleErrors,
227 228
    bool validateDeferredComponents = true,
    bool deferredComponentsEnabled = false,
229 230
    int retry = 0,
    @visibleForTesting int? maxRetries,
231 232 233 234 235 236 237
  }) async {
    assert(project != null);
    assert(androidBuildInfo != null);
    assert(target != null);
    assert(isBuildingBundle != null);
    assert(localGradleErrors != null);

238
    if (!project.android.isSupportedVersion) {
239
      _exitWithUnsupportedProjectMessage(_usage, _logger.terminal);
240 241 242 243
    }

    final bool usesAndroidX = isAppUsingAndroidX(project.android.hostAppGradleRoot);
    if (usesAndroidX) {
244
      BuildEvent('app-using-android-x', type: 'gradle', flutterUsage: _usage).send();
245
    } else if (!usesAndroidX) {
246
      BuildEvent('app-not-using-android-x', type: 'gradle', flutterUsage: _usage).send();
247
      _logger.printStatus("${_logger.terminal.warningMark} Your app isn't using AndroidX.", emphasis: true);
248
      _logger.printStatus(
249 250 251 252 253 254 255 256 257 258 259 260 261
        '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);
262

263
    final Status status = _logger.startProgress(
264
      "Running Gradle task '$assembleTask'...",
265
    );
266

267
    final List<String> command = <String>[
268
      _gradleUtils.getExecutable(project),
269
    ];
270
    if (_logger.isVerbose) {
271 272 273 274 275 276 277
      command.add('-Pverbose=true');
    } else {
      command.add('-q');
    }
    if (!buildInfo.androidGradleDaemon) {
      command.add('--no-daemon');
    }
278 279
    if (_artifacts is LocalEngineArtifacts) {
      final LocalEngineArtifacts localEngineArtifacts = _artifacts as LocalEngineArtifacts;
280 281 282
      final Directory localEngineRepo = _getLocalEngineRepo(
        engineOutPath: localEngineArtifacts.engineOutPath,
        androidBuildInfo: androidBuildInfo,
283
        fileSystem: _fileSystem,
284
      );
285
      _logger.printTrace(
286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302
          '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');
    }
303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318
    // Only attempt adding multidex support if all the flutter generated files exist.
    // If the files do not exist and it was unintentional, the app will fail to build
    // and prompt the developer if they wish Flutter to add the files again via gradle_error.dart.
    if (androidBuildInfo.multidexEnabled &&
        multiDexApplicationExists(project.directory) &&
        androidManifestHasNameVariable(project.directory)) {
      command.add('-Pmultidex-enabled=true');
      ensureMultiDexApplicationExists(project.directory);
      _logger.printStatus('Building with Flutter multidex support enabled.');
    }
    // If using v1 embedding, we want to use FlutterApplication as the base app.
    final String baseApplicationName =
        project.android.getEmbeddingVersion() == AndroidEmbeddingVersion.v2 ?
          'android.app.Application' :
          'io.flutter.app.FlutterApplication';
    command.add('-Pbase-application-name=$baseApplicationName');
319
    final List<DeferredComponent>? deferredComponents = project.manifest.deferredComponents;
320
    if (deferredComponents != null) {
321 322 323 324 325 326 327
      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>[];
328
      for (final DeferredComponent component in deferredComponents) {
329 330 331 332 333 334 335 336 337 338 339 340 341
        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');
      }
    }
342 343 344 345 346 347 348 349 350 351 352 353 354 355 356
    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);

357 358 359
    GradleHandledError? detectedGradleError;
    String? detectedGradleErrorLine;
    String? consumeLog(String line) {
360 361 362 363 364 365 366 367 368 369 370 371
      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;
        }
      }
372 373 374
      // Pipe stdout/stderr from Gradle.
      return line;
    }
375 376 377 378 379

    final Stopwatch sw = Stopwatch()
      ..start();
    int exitCode = 1;
    try {
380
      exitCode = await _processUtils.stream(
381 382 383
        command,
        workingDirectory: project.android.hostAppGradleRoot.path,
        allowReentrantFlutter: true,
384 385
        environment: <String, String>{
          if (javaPath != null)
386
            'JAVA_HOME': javaPath!,
387
        },
388 389 390 391 392 393 394 395
        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;
396
      }
397 398
    } finally {
      status.stop();
399 400
    }

401
    _usage.sendTiming('build', 'gradle', sw.elapsed);
402 403 404

    if (exitCode != 0) {
      if (detectedGradleError == null) {
405
        BuildEvent('gradle-unknown-failure', type: 'gradle', flutterUsage: _usage).send();
406 407 408 409
        throwToolExit(
          'Gradle task $assembleTask failed with exit code $exitCode',
          exitCode: exitCode,
        );
410 411 412 413 414 415 416
      }
      final GradleBuildStatus status = await detectedGradleError!.handler(
        line: detectedGradleErrorLine!,
        project: project,
        usesAndroidX: usesAndroidX,
        multidexEnabled: androidBuildInfo.multidexEnabled,
      );
417

418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440
      if (maxRetries == null || retry < maxRetries) {
        switch (status) {
          case GradleBuildStatus.retry:
            // Use binary exponential backoff before retriggering the build.
            // The expected wait times are: 100ms, 200ms, 400ms, and so on...
            final int waitTime = min(pow(2, retry).toInt() * 100, kMaxRetryTime.inMicroseconds);
            retry += 1;
            _logger.printStatus('Retrying Gradle Build: #$retry, wait time: ${waitTime}ms');
            await Future<void>.delayed(Duration(milliseconds: waitTime));
            await buildGradleApp(
              project: project,
              androidBuildInfo: androidBuildInfo,
              target: target,
              isBuildingBundle: isBuildingBundle,
              localGradleErrors: localGradleErrors,
              retry: retry,
              maxRetries: maxRetries,
            );
            final String successEventLabel = 'gradle-${detectedGradleError!.eventLabel}-success';
            BuildEvent(successEventLabel, type: 'gradle', flutterUsage: _usage).send();
            return;
          case GradleBuildStatus.exit:
            // Continue and throw tool exit.
441 442
        }
      }
443 444 445 446 447
      BuildEvent('gradle-${detectedGradleError?.eventLabel}-failure', type: 'gradle', flutterUsage: _usage).send();
      throwToolExit(
        'Gradle task $assembleTask failed with exit code $exitCode',
        exitCode: exitCode,
      );
448
    }
449

450
    if (isBuildingBundle) {
451
      final File bundleFile = findBundleFile(project, buildInfo, _logger, _usage);
452 453 454
      final String appSize = (buildInfo.mode == BuildMode.debug)
          ? '' // Don't display the size when building a debug variant.
          : ' (${getSizeAsMB(bundleFile.lengthSync())})';
455

456 457 458 459
      if (buildInfo.codeSizeDirectory != null) {
        await _performCodeSizeAnalysis('aab', bundleFile, androidBuildInfo);
      }

460
      _logger.printStatus(
461
        '${_logger.terminal.successMark} Built ${_fileSystem.path.relative(bundleFile.path)}$appSize.',
462
        color: TerminalColor.green,
463
      );
464 465 466 467
      return;
    }
    // Gradle produced an APK.
    final Iterable<String> apkFilesPaths = project.isModule
468
        ? findApkFilesModule(project, androidBuildInfo, _logger, _usage)
469 470 471 472 473
        : listApkPaths(androidBuildInfo);
    final Directory apkDirectory = getApkDirectory(project);
    final File apkFile = apkDirectory.childFile(apkFilesPaths.first);
    if (!apkFile.existsSync()) {
      _exitWithExpectedFileNotFound(
474
        project: project,
475
        fileExtension: '.apk',
476
        logger: _logger,
477
        usage: _usage,
478
      );
479
    }
480

481 482 483 484 485
    // 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);
486
    _logger.printTrace('calculateSha: $apkDirectory/app.apk');
487

488 489
    final File apkShaFile = apkDirectory.childFile('app.apk.sha1');
    apkShaFile.writeAsStringSync(_calculateSha(apkFile));
490

491 492 493
    final String appSize = (buildInfo.mode == BuildMode.debug)
        ? '' // Don't display the size when building a debug variant.
        : ' (${getSizeAsMB(apkFile.lengthSync())})';
494
    _logger.printStatus(
495
      '${_logger.terminal.successMark}  Built ${_fileSystem.path.relative(apkFile.path)}$appSize.',
496 497
      color: TerminalColor.green,
    );
498

499 500 501
    if (buildInfo.codeSizeDirectory != null) {
      await _performCodeSizeAnalysis('apk', apkFile, androidBuildInfo);
    }
502 503
  }

504 505 506 507
  Future<void> _performCodeSizeAnalysis(String kind,
      File zipFile,
      AndroidBuildInfo androidBuildInfo,) async {
    final SizeAnalyzer sizeAnalyzer = SizeAnalyzer(
508
      fileSystem: _fileSystem,
509
      logger: _logger,
510
      flutterUsage: _usage,
511 512 513
    );
    final String archName = getNameForAndroidArch(androidBuildInfo.targetArchs.single);
    final BuildInfo buildInfo = androidBuildInfo.buildInfo;
514
    final File aotSnapshot = _fileSystem.directory(buildInfo.codeSizeDirectory)
515
        .childFile('snapshot.$archName.json');
516
    final File precompilerTrace = _fileSystem.directory(buildInfo.codeSizeDirectory)
517
        .childFile('trace.$archName.json');
518
    final Map<String, Object?> output = await sizeAnalyzer.analyzeZipSizeAndAotSnapshot(
519 520 521 522 523
      zipFile: zipFile,
      aotSnapshot: aotSnapshot,
      precompilerTrace: precompilerTrace,
      kind: kind,
    );
524
    final File outputFile = _fileSystemUtils.getUniqueFile(
525
      _fileSystem
526
        .directory(_fileSystemUtils.homeDirPath)
527
        .childDirectory('.flutter-devtools'), '$kind-code-size-analysis', 'json',
528 529 530
    )
      ..writeAsStringSync(jsonEncode(output));
    // This message is used as a sentinel in analyze_apk_size_test.dart
531
    _logger.printStatus(
532 533
      'A summary of your ${kind.toUpperCase()} analysis can be found at: ${outputFile.path}',
    );
534

535 536 537 538 539
    // DevTools expects a file path relative to the .flutter-devtools/ dir.
    final String relativeAppSizePath = outputFile.path
        .split('.flutter-devtools/')
        .last
        .trim();
540
    _logger.printStatus(
541 542 543
        '\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'
544
    );
545
  }
546

547 548 549 550 551 552 553
  /// 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({
554 555 556 557 558
    required FlutterProject project,
    required AndroidBuildInfo androidBuildInfo,
    required String target,
    required Directory outputDirectory,
    required String buildNumber,
559 560 561 562 563 564 565 566 567 568 569 570 571
  }) 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);
572
    final Status status = _logger.startProgress(
573
      "Running Gradle task '$aarTask'...",
574
    );
575

576
    final String flutterRoot = _fileSystem.path.absolute(Cache.flutterRoot!);
577
    final String initScript = _fileSystem.path.join(
578 579 580 581 582
      flutterRoot,
      'packages',
      'flutter_tools',
      'gradle',
      'aar_init_script.gradle',
583
    );
584
    final List<String> command = <String>[
585
      _gradleUtils.getExecutable(project),
586 587 588 589 590 591
      '-I=$initScript',
      '-Pflutter-root=$flutterRoot',
      '-Poutput-dir=${outputDirectory.path}',
      '-Pis-plugin=${manifest.isPlugin}',
      '-PbuildNumber=$buildNumber'
    ];
592
    if (_logger.isVerbose) {
593 594 595 596 597 598 599
      command.add('-Pverbose=true');
    } else {
      command.add('-q');
    }
    if (!buildInfo.androidGradleDaemon) {
      command.add('--no-daemon');
    }
600

601 602 603 604 605
    if (target != null && target.isNotEmpty) {
      command.add('-Ptarget=$target');
    }
    command.addAll(androidBuildInfo.buildInfo.toGradleConfig());
    if (buildInfo.dartObfuscation && buildInfo.mode != BuildMode.release) {
606
      _logger.printStatus(
607
        'Dart obfuscation is not supported in ${sentenceCase(buildInfo.friendlyModeName)}'
608
            ' mode, building as un-obfuscated.',
609
      );
610 611
    }

612 613
    if (_artifacts is LocalEngineArtifacts) {
      final LocalEngineArtifacts localEngineArtifacts = _artifacts as LocalEngineArtifacts;
614 615 616
      final Directory localEngineRepo = _getLocalEngineRepo(
        engineOutPath: localEngineArtifacts.engineOutPath,
        androidBuildInfo: androidBuildInfo,
617
        fileSystem: _fileSystem,
618
      );
619 620 621
      _logger.printTrace(
        'Using local engine: ${localEngineArtifacts.engineOutPath}\n'
        'Local Maven repo: ${localEngineRepo.path}'
622
      );
623 624 625 626 627 628
      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 {
629
        copyDirectory(
630 631 632
          localEngineRepo,
          getRepoDirectory(outputDirectory),
        );
633
      } on FileSystemException catch (error, st) {
634 635
        throwToolExit(
            'Failed to copy the local engine ${localEngineRepo.path} repo '
636
                'in ${outputDirectory.path}: $error, $st'
637 638 639 640 641 642 643 644
        );
      }
      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');
645
    }
646

647
    command.add(aarTask);
648

649 650 651 652
    final Stopwatch sw = Stopwatch()
      ..start();
    RunResult result;
    try {
653
      result = await _processUtils.run(
654 655 656
        command,
        workingDirectory: project.android.hostAppGradleRoot.path,
        allowReentrantFlutter: true,
657 658
        environment: <String, String>{
          if (javaPath != null)
659
            'JAVA_HOME': javaPath!,
660
        },
661 662 663 664
      );
    } finally {
      status.stop();
    }
665
    _usage.sendTiming('build', 'gradle-aar', sw.elapsed);
666 667

    if (result.exitCode != 0) {
668 669
      _logger.printStatus(result.stdout, wrap: false);
      _logger.printError(result.stderr, wrap: false);
670 671 672 673 674 675 676
      throwToolExit(
        'Gradle task $aarTask failed with exit code ${result.exitCode}.',
        exitCode: result.exitCode,
      );
    }
    final Directory repoDirectory = getRepoDirectory(outputDirectory);
    if (!repoDirectory.existsSync()) {
677 678
      _logger.printStatus(result.stdout, wrap: false);
      _logger.printError(result.stderr, wrap: false);
679 680 681 682 683
      throwToolExit(
        'Gradle task $aarTask failed to produce $repoDirectory.',
        exitCode: exitCode,
      );
    }
684
    _logger.printStatus(
685
      '${_logger.terminal.successMark} Built ${_fileSystem.path.relative(repoDirectory.path)}.',
686
      color: TerminalColor.green,
687
    );
688
  }
689 690 691
}

/// Prints how to consume the AAR from a host app.
692
void printHowToConsumeAar({
693 694 695 696 697 698
  required Set<String> buildModes,
  String? androidPackage = 'unknown',
  required Directory repoDirectory,
  required Logger logger,
  required FileSystem fileSystem,
  String? buildNumber,
699
}) {
700 701
  assert(buildModes != null && buildModes.isNotEmpty);
  assert(repoDirectory != null);
702
  buildNumber ??= '1.0';
703

704 705 706
  logger.printStatus('\nConsuming the Module', emphasis: true);
  logger.printStatus('''
  1. Open ${fileSystem.path.join('<host>', 'app', 'build.gradle')}
707 708
  2. Ensure you have the repositories configured, otherwise add them:

709
      String storageUrl = System.env.FLUTTER_STORAGE_BASE_URL ?: "https://storage.googleapis.com"
710 711
      repositories {
        maven {
712
            url '${repoDirectory.path}'
713 714
        }
        maven {
715
            url "\$storageUrl/download.flutter.io"
716 717 718
        }
      }

719
  3. Make the host app depend on the Flutter module:
720

721 722
    dependencies {''');

723
  for (final String buildMode in buildModes) {
724
    logger.printStatus("""
725
      ${buildMode}Implementation '$androidPackage:flutter_$buildMode:$buildNumber'""");
726 727
  }

728
  logger.printStatus('''
729 730 731 732
    }
''');

  if (buildModes.contains('profile')) {
733
    logger.printStatus('''
734 735 736 737 738 739 740 741

  4. Add the `profile` build type:

    android {
      buildTypes {
        profile {
          initWith debug
        }
742
      }
743 744 745
    }
''');
  }
746

747
  logger.printStatus('To learn more, visit https://flutter.dev/go/build-aar');
748 749
}

750 751
String _hex(List<int> bytes) {
  final StringBuffer result = StringBuffer();
752
  for (final int part in bytes) {
753
    result.write('${part < 16 ? '0' : ''}${part.toRadixString(16)}');
754
  }
755 756 757 758 759
  return result.toString();
}

String _calculateSha(File file) {
  final List<int> bytes = file.readAsBytesSync();
760
  return _hex(sha1.convert(bytes).bytes);
761 762
}

763
void _exitWithUnsupportedProjectMessage(Usage usage, Terminal terminal) {
764
  BuildEvent('unsupported-project', type: 'gradle', eventError: 'gradle-plugin', flutterUsage: usage).send();
765
  throwToolExit(
766
    '${terminal.warningMark} Your app is using an unsupported Gradle project. '
767 768 769
    '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.',
  );
770 771
}

772 773 774 775 776 777 778 779 780 781 782
/// 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');
}

783
/// Returns the APK files for a given [FlutterProject] and [AndroidBuildInfo].
784
@visibleForTesting
785
Iterable<String> findApkFilesModule(
786
  FlutterProject project,
787
  AndroidBuildInfo androidBuildInfo,
788
  Logger logger,
789
  Usage usage,
790
) {
791 792
  final Iterable<String> apkFileNames = _apkFilesFor(androidBuildInfo);
  final Directory apkDirectory = getApkDirectory(project);
793
  final Iterable<File> apks = apkFileNames.expand<File>((String apkFileName) {
794
    File apkFile = apkDirectory.childFile(apkFileName);
795
    if (apkFile.existsSync()) {
796
      return <File>[apkFile];
797
    }
798 799
    final BuildInfo buildInfo = androidBuildInfo.buildInfo;
    final String modeName = camelCase(buildInfo.modeName);
800 801 802
    apkFile = apkDirectory
      .childDirectory(modeName)
      .childFile(apkFileName);
803
    if (apkFile.existsSync()) {
804
      return <File>[apkFile];
805
    }
806
    final String? flavor = buildInfo.flavor;
807
    if (flavor != null) {
808
      // Android Studio Gradle plugin v3 adds flavor to path.
809
      apkFile = apkDirectory
810
        .childDirectory(flavor)
811 812
        .childDirectory(modeName)
        .childFile(apkFileName);
813
      if (apkFile.existsSync()) {
814
        return <File>[apkFile];
815
      }
816
    }
817
    return const <File>[];
818
  });
819 820 821 822
  if (apks.isEmpty) {
    _exitWithExpectedFileNotFound(
      project: project,
      fileExtension: '.apk',
823
      logger: logger,
824
      usage: usage,
825 826
    );
  }
827 828 829 830 831 832
  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:
833
/// `$buildDir/app/outputs/flutter-apk/app-<abi>-<flavor-flag>-<build-mode-flag>.apk`
834 835 836 837 838 839 840
@visibleForTesting
Iterable<String> listApkPaths(
  AndroidBuildInfo androidBuildInfo,
) {
  final String buildType = camelCase(androidBuildInfo.buildInfo.modeName);
  final List<String> apkPartialName = <String>[
    if (androidBuildInfo.buildInfo.flavor?.isNotEmpty ?? false)
841
      androidBuildInfo.buildInfo.lowerCasedFlavor!,
842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859
    '$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('-')
  ];
860
}
861

862
@visibleForTesting
863
File findBundleFile(FlutterProject project, BuildInfo buildInfo, Logger logger, Usage usage) {
864
  final List<File> fileCandidates = <File>[
865
    getBundleDirectory(project)
866 867
      .childDirectory(camelCase(buildInfo.modeName))
      .childFile('app.aab'),
868
    getBundleDirectory(project)
869 870 871 872 873 874 875 876
      .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(
877
      getBundleDirectory(project)
878
        .childDirectory('${buildInfo.lowerCasedFlavor}${camelCase('_${buildInfo.modeName}')}')
879 880 881 882 883 884
        .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(
885
      getBundleDirectory(project)
886
        .childDirectory('${buildInfo.lowerCasedFlavor}${camelCase('_${buildInfo.modeName}')}')
887
        .childFile('app-${buildInfo.lowerCasedFlavor}-${buildInfo.modeName}.aab'));
888 889 890 891 892

    // The Android Gradle plugin 4.1.0 does only lowercase the first character of flavor name.
    fileCandidates.add(getBundleDirectory(project)
        .childDirectory('${buildInfo.uncapitalizedFlavor}${camelCase('_${buildInfo.modeName}')}')
        .childFile('app-${buildInfo.uncapitalizedFlavor}-${buildInfo.modeName}.aab'));
893 894 895 896 897
  }
  for (final File bundleFile in fileCandidates) {
    if (bundleFile.existsSync()) {
      return bundleFile;
    }
898
  }
899 900 901
  _exitWithExpectedFileNotFound(
    project: project,
    fileExtension: '.aab',
902
    logger: logger,
903
    usage: usage,
904
  );
905
}
906 907

/// Throws a [ToolExit] exception and logs the event.
908 909 910 911 912
Never _exitWithExpectedFileNotFound({
  required FlutterProject project,
  required String fileExtension,
  required Logger logger,
  required Usage usage,
913 914 915 916 917
}) {
  assert(project != null);
  assert(fileExtension != null);

  final String androidGradlePluginVersion =
918
  getGradleVersionForAndroidPlugin(project.android.hostAppGradleRoot, logger);
919
  BuildEvent('gradle-expected-file-not-found',
920
    type: 'gradle',
921
    settings:
922 923
    'androidGradlePluginVersion: $androidGradlePluginVersion, '
      'fileExtension: $fileExtension',
924
    flutterUsage: usage,
925
  ).send();
926 927
  throwToolExit(
    'Gradle build failed to produce an $fileExtension file. '
928 929
    "It's likely that this file was generated under ${project.android.buildDirectory.path}, "
    "but the tool couldn't find it."
930 931
  );
}
932

933 934
void _createSymlink(String targetPath, String linkPath, FileSystem fileSystem) {
  final File targetFile = fileSystem.file(targetPath);
935
  if (!targetFile.existsSync()) {
936
    throwToolExit("The file $targetPath wasn't found in the local engine out directory.");
937
  }
938
  final File linkFile = fileSystem.file(linkPath);
939 940 941 942 943 944 945 946 947 948
  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'
    );
  }
}

949 950
String _getLocalArtifactVersion(String pomPath, FileSystem fileSystem) {
  final File pomFile = fileSystem.file(pomPath);
951
  if (!pomFile.existsSync()) {
952
    throwToolExit("The file $pomPath wasn't found in the local engine out directory.");
953
  }
Dan Field's avatar
Dan Field committed
954
  XmlDocument document;
955
  try {
Dan Field's avatar
Dan Field committed
956 957
    document = XmlDocument.parse(pomFile.readAsStringSync());
  } on XmlParserException {
958 959 960 961 962
    throwToolExit(
      'Error parsing $pomPath. Please ensure that this is a valid XML document.'
    );
  } on FileSystemException {
    throwToolExit(
963
      'Error reading $pomPath. Please ensure that you have read permission to this '
964 965
      'file and try again.');
  }
Dan Field's avatar
Dan Field committed
966
  final Iterable<XmlElement> project = document.findElements('project');
967
  assert(project.isNotEmpty);
Dan Field's avatar
Dan Field committed
968
  for (final XmlElement versionElement in document.findAllElements('version')) {
969 970 971 972 973 974 975 976 977 978 979 980
    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({
981 982 983
  required String engineOutPath,
  required AndroidBuildInfo androidBuildInfo,
  required FileSystem fileSystem,
984 985 986 987
}) {
  assert(engineOutPath != null);
  assert(androidBuildInfo != null);

988
  final String abi = _getAbiByLocalEnginePath(engineOutPath);
989
  final Directory localEngineRepo = fileSystem.systemTempDirectory
990 991 992
    .createTempSync('flutter_tool_local_engine_repo.');
  final String buildMode = androidBuildInfo.buildInfo.modeName;
  final String artifactVersion = _getLocalArtifactVersion(
993
    fileSystem.path.join(
994 995
      engineOutPath,
      'flutter_embedding_$buildMode.pom',
996 997
    ),
    fileSystem,
998
  );
999
  for (final String artifact in const <String>['pom', 'jar']) {
1000 1001
    // The Android embedding artifacts.
    _createSymlink(
1002
      fileSystem.path.join(
1003 1004 1005
        engineOutPath,
        'flutter_embedding_$buildMode.$artifact',
      ),
1006
      fileSystem.path.join(
1007 1008 1009 1010 1011 1012 1013
        localEngineRepo.path,
        'io',
        'flutter',
        'flutter_embedding_$buildMode',
        artifactVersion,
        'flutter_embedding_$buildMode-$artifactVersion.$artifact',
      ),
1014
      fileSystem,
1015 1016 1017
    );
    // The engine artifacts (libflutter.so).
    _createSymlink(
1018
      fileSystem.path.join(
1019 1020 1021
        engineOutPath,
        '${abi}_$buildMode.$artifact',
      ),
1022
      fileSystem.path.join(
1023 1024 1025 1026 1027 1028 1029
        localEngineRepo.path,
        'io',
        'flutter',
        '${abi}_$buildMode',
        artifactVersion,
        '${abi}_$buildMode-$artifactVersion.$artifact',
      ),
1030
      fileSystem,
1031 1032
    );
  }
1033 1034
  for (final String artifact in <String>['flutter_embedding_$buildMode', '${abi}_$buildMode']) {
    _createSymlink(
1035
      fileSystem.path.join(
1036 1037 1038
        engineOutPath,
        '$artifact.maven-metadata.xml',
      ),
1039
      fileSystem.path.join(
1040 1041 1042 1043 1044 1045
        localEngineRepo.path,
        'io',
        'flutter',
        artifact,
        'maven-metadata.xml',
      ),
1046
      fileSystem,
1047 1048
    );
  }
1049 1050
  return localEngineRepo;
}
1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074

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