gradle.dart 41.4 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/net.dart';
20
import '../base/platform.dart';
21
import '../base/process.dart';
22
import '../base/project_migrator.dart';
23
import '../base/terminal.dart';
24 25 26
import '../base/utils.dart';
import '../build_info.dart';
import '../cache.dart';
27
import '../convert.dart';
28
import '../flutter_manifest.dart';
29
import '../globals.dart' as globals;
30
import '../project.dart';
31
import '../reporting/reporting.dart';
32
import 'android_builder.dart';
33
import 'android_studio.dart';
34 35
import 'gradle_errors.dart';
import 'gradle_utils.dart';
36
import 'java.dart';
37
import 'migrations/android_studio_java_gradle_conflict_migration.dart';
38
import 'migrations/min_sdk_version_migration.dart';
39
import 'migrations/top_level_gradle_build_file_migration.dart';
40
import 'multidex.dart';
41

42
/// The regex to grab variant names from printBuildVariants gradle task
43
///
44
/// The task is defined in flutter/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy
45 46 47 48 49 50 51 52
///
/// The expected output from the task should be similar to:
///
/// BuildVariant: debug
/// BuildVariant: release
/// BuildVariant: profile
final RegExp _kBuildVariantRegex = RegExp('^BuildVariant: (?<$_kBuildVariantRegexGroupName>.*)\$');
const String _kBuildVariantRegexGroupName = 'variant';
53 54
const String _kBuildVariantTaskName = 'printBuildVariants';

55 56
String _getOutputAppLinkSettingsTaskFor(String buildVariant) {
  return _taskForBuildVariant('output', buildVariant, 'AppLinkSettings');
57
}
58

59 60 61 62 63 64 65 66 67 68
/// 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')
69
        .childDirectory('flutter-apk');
70
}
71

72 73 74 75 76 77 78 79 80 81 82 83 84
/// 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');
}
85

86 87 88 89 90 91
/// The directory where the repo is generated.
/// Only applicable to AARs.
Directory getRepoDirectory(Directory buildDirectory) {
  return buildDirectory
    .childDirectory('outputs')
    .childDirectory('repo');
92 93
}

94 95 96 97
/// 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 ?? '';
98
  return _taskForBuildVariant(prefix, '$productFlavor${sentenceCase(buildType)}');
99
}
100

101 102 103 104 105
String _taskForBuildVariant(String prefix, String buildVariant, [String suffix = '']) {
  return '$prefix${sentenceCase(buildVariant)}$suffix';
}


106 107 108 109
/// Returns the task to build an APK.
@visibleForTesting
String getAssembleTaskFor(BuildInfo buildInfo) {
  return _taskFor('assemble', buildInfo);
110
}
111

112 113 114 115 116
/// Returns the task to build an AAB.
@visibleForTesting
String getBundleTaskFor(BuildInfo buildInfo) {
  return _taskFor('bundle', buildInfo);
}
117

118 119 120 121 122
/// Returns the task to build an AAR.
@visibleForTesting
String getAarTaskFor(BuildInfo buildInfo) {
  return _taskFor('assembleAar', buildInfo);
}
123

124 125 126 127 128
/// 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);
129
  final String productFlavor = androidBuildInfo.buildInfo.lowerCasedFlavor ?? '';
130 131 132
  final String flavorString = productFlavor.isEmpty ? '' : '-$productFlavor';
  if (androidBuildInfo.splitPerAbi) {
    return androidBuildInfo.targetArchs.map<String>((AndroidArch arch) {
133
      final String abi = arch.archName;
134 135 136 137 138
      return 'app$flavorString-$abi-$buildType.apk';
    });
  }
  return <String>['app$flavorString-$buildType.apk'];
}
139

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

143 144
/// An implementation of the [AndroidBuilder] that delegates to gradle.
class AndroidGradleBuilder implements AndroidBuilder {
145
  AndroidGradleBuilder({
146
    required Java? java,
147 148 149 150 151 152 153
    required Logger logger,
    required ProcessManager processManager,
    required FileSystem fileSystem,
    required Artifacts artifacts,
    required Usage usage,
    required GradleUtils gradleUtils,
    required Platform platform,
154
    required AndroidStudio? androidStudio,
155 156
  }) : _java = java,
       _logger = logger,
157
       _fileSystem = fileSystem,
158
       _artifacts = artifacts,
159
       _usage = usage,
160
       _gradleUtils = gradleUtils,
161
       _androidStudio = androidStudio,
162
       _fileSystemUtils = FileSystemUtils(fileSystem: fileSystem, platform: platform),
163
       _processUtils = ProcessUtils(logger: logger, processManager: processManager);
164

165
  final Java? _java;
166
  final Logger _logger;
167 168
  final ProcessUtils _processUtils;
  final FileSystem _fileSystem;
169
  final Artifacts _artifacts;
170
  final Usage _usage;
171 172
  final GradleUtils _gradleUtils;
  final FileSystemUtils _fileSystemUtils;
173
  final AndroidStudio? _androidStudio;
174

175 176 177
  /// Builds the AAR and POM files for the current Flutter module or plugin.
  @override
  Future<void> buildAar({
178 179 180 181 182
    required FlutterProject project,
    required Set<AndroidBuildInfo> androidBuildInfo,
    required String target,
    String? outputDirectoryPath,
    required String buildNumber,
183
  }) async {
184 185 186 187 188 189 190 191 192 193 194 195
    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,
196 197 198
        buildNumber: buildNumber,
      );
    }
199 200 201 202 203 204 205 206 207 208 209
    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,
    );
210
  }
211 212 213 214

  /// Builds the APK.
  @override
  Future<void> buildApk({
215 216 217
    required FlutterProject project,
    required AndroidBuildInfo androidBuildInfo,
    required String target,
218
    bool configOnly = false,
219
  }) async {
220 221 222 223 224 225
    await buildGradleApp(
      project: project,
      androidBuildInfo: androidBuildInfo,
      target: target,
      isBuildingBundle: false,
      localGradleErrors: gradleErrors,
226
      configOnly: configOnly,
227
      maxRetries: 1,
228
    );
Emmanuel Garcia's avatar
Emmanuel Garcia committed
229
  }
230 231 232 233

  /// Builds the App Bundle.
  @override
  Future<void> buildAab({
234 235 236
    required FlutterProject project,
    required AndroidBuildInfo androidBuildInfo,
    required String target,
237 238
    bool validateDeferredComponents = true,
    bool deferredComponentsEnabled = false,
239
    bool configOnly = false,
240
  }) async {
241 242 243 244 245 246
    await buildGradleApp(
      project: project,
      androidBuildInfo: androidBuildInfo,
      target: target,
      isBuildingBundle: true,
      localGradleErrors: gradleErrors,
247 248
      validateDeferredComponents: validateDeferredComponents,
      deferredComponentsEnabled: deferredComponentsEnabled,
249
      configOnly: configOnly,
250
      maxRetries: 1,
251
    );
252 253
  }

254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
  Future<RunResult> _runGradleTask(
    String taskName, {
    List<String> options = const <String>[],
    required FlutterProject project
  }) async {
    final Status status = _logger.startProgress(
      "Running Gradle task '$taskName'...",
    );
    final List<String> command = <String>[
      _gradleUtils.getExecutable(project),
      ...options, // suppresses gradle output.
      taskName,
    ];

    RunResult result;
    try {
      result = await _processUtils.run(
        command,
        workingDirectory: project.android.hostAppGradleRoot.path,
        allowReentrantFlutter: true,
        environment: _java?.environment,
      );
    } finally {
      status.stop();
    }
    return result;
  }

282 283 284 285 286 287 288
  /// 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`.
289
  /// * [maxRetries] If not `null`, this is the max number of build retries in case a retry is triggered.
290
  Future<void> buildGradleApp({
291 292 293 294 295
    required FlutterProject project,
    required AndroidBuildInfo androidBuildInfo,
    required String target,
    required bool isBuildingBundle,
    required List<GradleHandledError> localGradleErrors,
296
    required bool configOnly,
297 298
    bool validateDeferredComponents = true,
    bool deferredComponentsEnabled = false,
299 300
    int retry = 0,
    @visibleForTesting int? maxRetries,
301
  }) async {
302
    if (!project.android.isSupportedVersion) {
303
      _exitWithUnsupportedProjectMessage(_usage, _logger.terminal);
304 305
    }

306 307
    final List<ProjectMigrator> migrators = <ProjectMigrator>[
      TopLevelGradleBuildFileMigration(project.android, _logger),
308 309 310
      AndroidStudioJavaGradleConflictMigration(_logger,
          project: project.android,
          androidStudio: _androidStudio,
311 312
          java: globals.java),
      MinSdkVersionMigration(project.android, _logger),
313 314 315 316 317
    ];

    final ProjectMigration migration = ProjectMigration(migrators);
    migration.run();

318 319
    final bool usesAndroidX = isAppUsingAndroidX(project.android.hostAppGradleRoot);
    if (usesAndroidX) {
320
      BuildEvent('app-using-android-x', type: 'gradle', flutterUsage: _usage).send();
321
    } else if (!usesAndroidX) {
322
      BuildEvent('app-not-using-android-x', type: 'gradle', flutterUsage: _usage).send();
323
      _logger.printStatus("${_logger.terminal.warningMark} Your app isn't using AndroidX.", emphasis: true);
324
      _logger.printStatus(
325 326 327 328 329 330 331
        '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.
332 333
    updateLocalProperties(
        project: project, buildInfo: androidBuildInfo.buildInfo);
334

335 336 337 338 339 340 341 342 343 344 345 346 347
    final List<String> command = <String>[
      // This does more than get gradlewrapper. It creates the file, ensures it
      // exists and verifies the file is executable.
      _gradleUtils.getExecutable(project),
    ];

    // All automatically created files should exist.
    if (configOnly) {
      _logger.printStatus('Config complete.');
      return;
    }

    // Assembly work starts here.
348 349 350 351
    final BuildInfo buildInfo = androidBuildInfo.buildInfo;
    final String assembleTask = isBuildingBundle
        ? getBundleTaskFor(buildInfo)
        : getAssembleTaskFor(buildInfo);
352

353
    final Status status = _logger.startProgress(
354
      "Running Gradle task '$assembleTask'...",
355
    );
356

357
    if (_logger.isVerbose) {
358
      command.add('--full-stacktrace');
359
      command.add('--info');
360 361 362 363 364 365 366
      command.add('-Pverbose=true');
    } else {
      command.add('-q');
    }
    if (!buildInfo.androidGradleDaemon) {
      command.add('--no-daemon');
    }
367 368
    final LocalEngineInfo? localEngineInfo = _artifacts.localEngineInfo;
    if (localEngineInfo != null) {
369
      final Directory localEngineRepo = _getLocalEngineRepo(
370
        engineOutPath: localEngineInfo.targetOutPath,
371
        androidBuildInfo: androidBuildInfo,
372
        fileSystem: _fileSystem,
373
      );
374
      _logger.printTrace(
375
          'Using local engine: ${localEngineInfo.targetOutPath}\n'
376 377 378 379
              'Local Maven repo: ${localEngineRepo.path}'
      );
      command.add('-Plocal-engine-repo=${localEngineRepo.path}');
      command.add('-Plocal-engine-build-mode=${buildInfo.modeName}');
380 381
      command.add('-Plocal-engine-out=${localEngineInfo.targetOutPath}');
      command.add('-Plocal-engine-host-out=${localEngineInfo.hostOutPath}');
382
      command.add('-Ptarget-platform=${_getTargetPlatformByLocalEnginePath(
383
          localEngineInfo.targetOutPath)}');
384 385 386
    } else if (androidBuildInfo.targetArchs.isNotEmpty) {
      final String targetPlatforms = androidBuildInfo
          .targetArchs
387
          .map((AndroidArch e) => e.platformName).join(',');
388 389
      command.add('-Ptarget-platform=$targetPlatforms');
    }
390
    command.add('-Ptarget=$target');
391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406
    // 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');
407
    final List<DeferredComponent>? deferredComponents = project.manifest.deferredComponents;
408
    if (deferredComponents != null) {
409 410 411 412 413 414 415
      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>[];
416
      for (final DeferredComponent component in deferredComponents) {
417 418 419 420 421 422 423 424 425 426 427 428 429
        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');
      }
    }
430
    command.addAll(androidBuildInfo.buildInfo.toGradleConfig());
431
    if (buildInfo.fileSystemRoots.isNotEmpty) {
432 433 434 435 436 437 438 439 440 441 442 443 444
      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);

445 446 447
    GradleHandledError? detectedGradleError;
    String? detectedGradleErrorLine;
    String? consumeLog(String line) {
448 449 450 451 452 453 454 455 456 457 458 459
      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;
        }
      }
460 461 462
      // Pipe stdout/stderr from Gradle.
      return line;
    }
463 464 465 466 467

    final Stopwatch sw = Stopwatch()
      ..start();
    int exitCode = 1;
    try {
468
      exitCode = await _processUtils.stream(
469 470 471
        command,
        workingDirectory: project.android.hostAppGradleRoot.path,
        allowReentrantFlutter: true,
472
        environment: _java?.environment,
473 474 475 476 477 478 479 480
        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;
481
      }
482 483
    } finally {
      status.stop();
484 485
    }

486
    _usage.sendTiming('build', 'gradle', sw.elapsed);
487 488 489

    if (exitCode != 0) {
      if (detectedGradleError == null) {
490
        BuildEvent('gradle-unknown-failure', type: 'gradle', flutterUsage: _usage).send();
491 492 493 494
        throwToolExit(
          'Gradle task $assembleTask failed with exit code $exitCode',
          exitCode: exitCode,
        );
495 496 497 498 499 500 501
      }
      final GradleBuildStatus status = await detectedGradleError!.handler(
        line: detectedGradleErrorLine!,
        project: project,
        usesAndroidX: usesAndroidX,
        multidexEnabled: androidBuildInfo.multidexEnabled,
      );
502

503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519
      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,
520
              configOnly: configOnly,
521 522 523 524 525 526
            );
            final String successEventLabel = 'gradle-${detectedGradleError!.eventLabel}-success';
            BuildEvent(successEventLabel, type: 'gradle', flutterUsage: _usage).send();
            return;
          case GradleBuildStatus.exit:
            // Continue and throw tool exit.
527 528
        }
      }
529 530 531 532 533
      BuildEvent('gradle-${detectedGradleError?.eventLabel}-failure', type: 'gradle', flutterUsage: _usage).send();
      throwToolExit(
        'Gradle task $assembleTask failed with exit code $exitCode',
        exitCode: exitCode,
      );
534
    }
535

536
    if (isBuildingBundle) {
537
      final File bundleFile = findBundleFile(project, buildInfo, _logger, _usage);
538 539 540
      final String appSize = (buildInfo.mode == BuildMode.debug)
          ? '' // Don't display the size when building a debug variant.
          : ' (${getSizeAsMB(bundleFile.lengthSync())})';
541

542 543 544 545
      if (buildInfo.codeSizeDirectory != null) {
        await _performCodeSizeAnalysis('aab', bundleFile, androidBuildInfo);
      }

546
      _logger.printStatus(
547
        '${_logger.terminal.successMark} Built ${_fileSystem.path.relative(bundleFile.path)}$appSize.',
548
        color: TerminalColor.green,
549
      );
550 551
      return;
    }
552
    // Gradle produced APKs.
553
    final Iterable<String> apkFilesPaths = project.isModule
554
        ? findApkFilesModule(project, androidBuildInfo, _logger, _usage)
555 556
        : listApkPaths(androidBuildInfo);
    final Directory apkDirectory = getApkDirectory(project);
557

558 559 560 561 562 563 564 565 566 567
    // Generate sha1 for every generated APKs.
    for (final File apkFile in apkFilesPaths.map(apkDirectory.childFile)) {
      if (!apkFile.existsSync()) {
        _exitWithExpectedFileNotFound(
          project: project,
          fileExtension: '.apk',
          logger: _logger,
          usage: _usage,
        );
      }
568

569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584
      final String filename = apkFile.basename;
      _logger.printTrace('Calculate SHA1: $apkDirectory/$filename');
      final File apkShaFile = apkDirectory.childFile('$filename.sha1');
      apkShaFile.writeAsStringSync(_calculateSha(apkFile));

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

      if (buildInfo.codeSizeDirectory != null) {
        await _performCodeSizeAnalysis('apk', apkFile, androidBuildInfo);
      }
585
    }
586 587
  }

588 589 590 591
  Future<void> _performCodeSizeAnalysis(String kind,
      File zipFile,
      AndroidBuildInfo androidBuildInfo,) async {
    final SizeAnalyzer sizeAnalyzer = SizeAnalyzer(
592
      fileSystem: _fileSystem,
593
      logger: _logger,
594
      flutterUsage: _usage,
595
    );
596
    final String archName = androidBuildInfo.targetArchs.single.archName;
597
    final BuildInfo buildInfo = androidBuildInfo.buildInfo;
598
    final File aotSnapshot = _fileSystem.directory(buildInfo.codeSizeDirectory)
599
        .childFile('snapshot.$archName.json');
600
    final File precompilerTrace = _fileSystem.directory(buildInfo.codeSizeDirectory)
601
        .childFile('trace.$archName.json');
602
    final Map<String, Object?> output = await sizeAnalyzer.analyzeZipSizeAndAotSnapshot(
603 604 605 606 607
      zipFile: zipFile,
      aotSnapshot: aotSnapshot,
      precompilerTrace: precompilerTrace,
      kind: kind,
    );
608
    final File outputFile = _fileSystemUtils.getUniqueFile(
609
      _fileSystem
610
        .directory(_fileSystemUtils.homeDirPath)
611
        .childDirectory('.flutter-devtools'), '$kind-code-size-analysis', 'json',
612 613 614
    )
      ..writeAsStringSync(jsonEncode(output));
    // This message is used as a sentinel in analyze_apk_size_test.dart
615
    _logger.printStatus(
616 617
      'A summary of your ${kind.toUpperCase()} analysis can be found at: ${outputFile.path}',
    );
618

619 620 621 622 623
    // DevTools expects a file path relative to the .flutter-devtools/ dir.
    final String relativeAppSizePath = outputFile.path
        .split('.flutter-devtools/')
        .last
        .trim();
624
    _logger.printStatus(
625
        '\nTo analyze your app size in Dart DevTools, run the following command:\n'
626
            'dart devtools --appSizeBase=$relativeAppSizePath'
627
    );
628
  }
629

630 631 632 633 634 635 636
  /// 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({
637 638 639 640 641
    required FlutterProject project,
    required AndroidBuildInfo androidBuildInfo,
    required String target,
    required Directory outputDirectory,
    required String buildNumber,
642 643 644 645 646 647 648 649 650
  }) async {

    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);
651
    final Status status = _logger.startProgress(
652
      "Running Gradle task '$aarTask'...",
653
    );
654

655
    final String flutterRoot = _fileSystem.path.absolute(Cache.flutterRoot!);
656
    final String initScript = _fileSystem.path.join(
657 658 659 660 661
      flutterRoot,
      'packages',
      'flutter_tools',
      'gradle',
      'aar_init_script.gradle',
662
    );
663
    final List<String> command = <String>[
664
      _gradleUtils.getExecutable(project),
665 666 667 668
      '-I=$initScript',
      '-Pflutter-root=$flutterRoot',
      '-Poutput-dir=${outputDirectory.path}',
      '-Pis-plugin=${manifest.isPlugin}',
669
      '-PbuildNumber=$buildNumber',
670
    ];
671
    if (_logger.isVerbose) {
672
      command.add('--full-stacktrace');
673
      command.add('--info');
674 675 676 677 678 679 680
      command.add('-Pverbose=true');
    } else {
      command.add('-q');
    }
    if (!buildInfo.androidGradleDaemon) {
      command.add('--no-daemon');
    }
681

682
    if (target.isNotEmpty) {
683 684 685 686
      command.add('-Ptarget=$target');
    }
    command.addAll(androidBuildInfo.buildInfo.toGradleConfig());
    if (buildInfo.dartObfuscation && buildInfo.mode != BuildMode.release) {
687
      _logger.printStatus(
688
        'Dart obfuscation is not supported in ${sentenceCase(buildInfo.friendlyModeName)}'
689
            ' mode, building as un-obfuscated.',
690
      );
691 692
    }

693 694
    final LocalEngineInfo? localEngineInfo = _artifacts.localEngineInfo;
    if (localEngineInfo != null) {
695
      final Directory localEngineRepo = _getLocalEngineRepo(
696
        engineOutPath: localEngineInfo.targetOutPath,
697
        androidBuildInfo: androidBuildInfo,
698
        fileSystem: _fileSystem,
699
      );
700
      _logger.printTrace(
701
        'Using local engine: ${localEngineInfo.targetOutPath}\n'
702
        'Local Maven repo: ${localEngineRepo.path}'
703
      );
704 705
      command.add('-Plocal-engine-repo=${localEngineRepo.path}');
      command.add('-Plocal-engine-build-mode=${buildInfo.modeName}');
706 707
      command.add('-Plocal-engine-out=${localEngineInfo.targetOutPath}');
      command.add('-Plocal-engine-host-out=${localEngineInfo.hostOutPath}');
708 709 710

      // Copy the local engine repo in the output directory.
      try {
711
        copyDirectory(
712 713 714
          localEngineRepo,
          getRepoDirectory(outputDirectory),
        );
715
      } on FileSystemException catch (error, st) {
716 717
        throwToolExit(
            'Failed to copy the local engine ${localEngineRepo.path} repo '
718
                'in ${outputDirectory.path}: $error, $st'
719 720 721
        );
      }
      command.add('-Ptarget-platform=${_getTargetPlatformByLocalEnginePath(
722
          localEngineInfo.targetOutPath)}');
723 724
    } else if (androidBuildInfo.targetArchs.isNotEmpty) {
      final String targetPlatforms = androidBuildInfo.targetArchs
725
          .map((AndroidArch e) => e.platformName).join(',');
726
      command.add('-Ptarget-platform=$targetPlatforms');
727
    }
728

729
    command.add(aarTask);
730

731 732 733 734
    final Stopwatch sw = Stopwatch()
      ..start();
    RunResult result;
    try {
735
      result = await _processUtils.run(
736 737 738
        command,
        workingDirectory: project.android.hostAppGradleRoot.path,
        allowReentrantFlutter: true,
739
        environment: _java?.environment,
740 741 742 743
      );
    } finally {
      status.stop();
    }
744
    _usage.sendTiming('build', 'gradle-aar', sw.elapsed);
745 746

    if (result.exitCode != 0) {
747 748
      _logger.printStatus(result.stdout, wrap: false);
      _logger.printError(result.stderr, wrap: false);
749 750 751 752 753 754 755
      throwToolExit(
        'Gradle task $aarTask failed with exit code ${result.exitCode}.',
        exitCode: result.exitCode,
      );
    }
    final Directory repoDirectory = getRepoDirectory(outputDirectory);
    if (!repoDirectory.existsSync()) {
756 757
      _logger.printStatus(result.stdout, wrap: false);
      _logger.printError(result.stderr, wrap: false);
758 759 760 761 762
      throwToolExit(
        'Gradle task $aarTask failed to produce $repoDirectory.',
        exitCode: exitCode,
      );
    }
763
    _logger.printStatus(
764
      '${_logger.terminal.successMark} Built ${_fileSystem.path.relative(repoDirectory.path)}.',
765
      color: TerminalColor.green,
766
    );
767
  }
768 769 770 771 772

  @override
  Future<List<String>> getBuildVariants({required FlutterProject project}) async {
    final Stopwatch sw = Stopwatch()
      ..start();
773 774 775 776 777 778
    final RunResult result = await _runGradleTask(
      _kBuildVariantTaskName,
      options: const <String>['-q'],
      project: project,
    );

779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794
    _usage.sendTiming('print', 'android build variants', sw.elapsed);

    if (result.exitCode != 0) {
      _logger.printStatus(result.stdout, wrap: false);
      _logger.printError(result.stderr, wrap: false);
      return const <String>[];
    }
    final List<String> options = <String>[];
    for (final String line in LineSplitter.split(result.stdout)) {
      final RegExpMatch? match = _kBuildVariantRegex.firstMatch(line);
      if (match != null) {
        options.add(match.namedGroup(_kBuildVariantRegexGroupName)!);
      }
    }
    return options;
  }
795 796

  @override
797
  Future<void> outputsAppLinkSettings(
798 799 800
    String buildVariant, {
    required FlutterProject project,
  }) async {
801
    final String taskName = _getOutputAppLinkSettingsTaskFor(buildVariant);
802 803 804 805 806 807 808
    final Stopwatch sw = Stopwatch()
      ..start();
    final RunResult result = await _runGradleTask(
      taskName,
      options: const <String>['-q'],
      project: project,
    );
809
    _usage.sendTiming('outputs', 'app link settings', sw.elapsed);
810 811 812 813 814 815

    if (result.exitCode != 0) {
      _logger.printStatus(result.stdout, wrap: false);
      _logger.printError(result.stderr, wrap: false);
    }
  }
816 817 818
}

/// Prints how to consume the AAR from a host app.
819
void printHowToConsumeAar({
820 821 822 823 824 825
  required Set<String> buildModes,
  String? androidPackage = 'unknown',
  required Directory repoDirectory,
  required Logger logger,
  required FileSystem fileSystem,
  String? buildNumber,
826
}) {
827
  assert(buildModes.isNotEmpty);
828
  buildNumber ??= '1.0';
829

830 831 832
  logger.printStatus('\nConsuming the Module', emphasis: true);
  logger.printStatus('''
  1. Open ${fileSystem.path.join('<host>', 'app', 'build.gradle')}
833 834
  2. Ensure you have the repositories configured, otherwise add them:

835
      String storageUrl = System.env.$kFlutterStorageBaseUrl ?: "https://storage.googleapis.com"
836 837
      repositories {
        maven {
838
            url '${repoDirectory.path}'
839 840
        }
        maven {
841
            url "\$storageUrl/download.flutter.io"
842 843 844
        }
      }

845
  3. Make the host app depend on the Flutter module:
846

847 848
    dependencies {''');

849
  for (final String buildMode in buildModes) {
850
    logger.printStatus("""
851
      ${buildMode}Implementation '$androidPackage:flutter_$buildMode:$buildNumber'""");
852 853
  }

854
  logger.printStatus('''
855 856 857 858
    }
''');

  if (buildModes.contains('profile')) {
859
    logger.printStatus('''
860 861 862 863 864 865 866 867

  4. Add the `profile` build type:

    android {
      buildTypes {
        profile {
          initWith debug
        }
868
      }
869 870 871
    }
''');
  }
872

873
  logger.printStatus('To learn more, visit https://flutter.dev/go/build-aar');
874 875
}

876 877
String _hex(List<int> bytes) {
  final StringBuffer result = StringBuffer();
878
  for (final int part in bytes) {
879
    result.write('${part < 16 ? '0' : ''}${part.toRadixString(16)}');
880
  }
881 882 883 884 885
  return result.toString();
}

String _calculateSha(File file) {
  final List<int> bytes = file.readAsBytesSync();
886
  return _hex(sha1.convert(bytes).bytes);
887 888
}

889
void _exitWithUnsupportedProjectMessage(Usage usage, Terminal terminal) {
890
  BuildEvent('unsupported-project', type: 'gradle', eventError: 'gradle-plugin', flutterUsage: usage).send();
891
  throwToolExit(
892
    '${terminal.warningMark} Your app is using an unsupported Gradle project. '
893 894 895
    '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.',
  );
896 897
}

898 899 900 901 902 903 904 905 906 907 908
/// 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');
}

909
/// Returns the APK files for a given [FlutterProject] and [AndroidBuildInfo].
910
@visibleForTesting
911
Iterable<String> findApkFilesModule(
912
  FlutterProject project,
913
  AndroidBuildInfo androidBuildInfo,
914
  Logger logger,
915
  Usage usage,
916
) {
917 918
  final Iterable<String> apkFileNames = _apkFilesFor(androidBuildInfo);
  final Directory apkDirectory = getApkDirectory(project);
919
  final Iterable<File> apks = apkFileNames.expand<File>((String apkFileName) {
920
    File apkFile = apkDirectory.childFile(apkFileName);
921
    if (apkFile.existsSync()) {
922
      return <File>[apkFile];
923
    }
924 925
    final BuildInfo buildInfo = androidBuildInfo.buildInfo;
    final String modeName = camelCase(buildInfo.modeName);
926 927 928
    apkFile = apkDirectory
      .childDirectory(modeName)
      .childFile(apkFileName);
929
    if (apkFile.existsSync()) {
930
      return <File>[apkFile];
931
    }
932
    final String? flavor = buildInfo.flavor;
933
    if (flavor != null) {
934
      // Android Studio Gradle plugin v3 adds flavor to path.
935
      apkFile = apkDirectory
936
        .childDirectory(flavor)
937 938
        .childDirectory(modeName)
        .childFile(apkFileName);
939
      if (apkFile.existsSync()) {
940
        return <File>[apkFile];
941
      }
942
    }
943
    return const <File>[];
944
  });
945 946 947 948
  if (apks.isEmpty) {
    _exitWithExpectedFileNotFound(
      project: project,
      fileExtension: '.apk',
949
      logger: logger,
950
      usage: usage,
951 952
    );
  }
953 954 955 956 957 958
  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:
959
/// `$buildDir/app/outputs/flutter-apk/app-<abi>-<flavor-flag>-<build-mode-flag>.apk`
960 961 962 963 964 965 966
@visibleForTesting
Iterable<String> listApkPaths(
  AndroidBuildInfo androidBuildInfo,
) {
  final String buildType = camelCase(androidBuildInfo.buildInfo.modeName);
  final List<String> apkPartialName = <String>[
    if (androidBuildInfo.buildInfo.flavor?.isNotEmpty ?? false)
967
      androidBuildInfo.buildInfo.lowerCasedFlavor!,
968 969 970 971
    '$buildType.apk',
  ];
  if (androidBuildInfo.splitPerAbi) {
    return <String>[
972
      for (final AndroidArch androidArch in androidBuildInfo.targetArchs)
973 974
        <String>[
          'app',
975
          androidArch.archName,
976 977
          ...apkPartialName,
        ].join('-'),
978 979 980 981 982 983
    ];
  }
  return <String>[
    <String>[
      'app',
      ...apkPartialName,
984
    ].join('-'),
985
  ];
986
}
987

988
@visibleForTesting
989
File findBundleFile(FlutterProject project, BuildInfo buildInfo, Logger logger, Usage usage) {
990
  final List<File> fileCandidates = <File>[
991
    getBundleDirectory(project)
992 993
      .childDirectory(camelCase(buildInfo.modeName))
      .childFile('app.aab'),
994
    getBundleDirectory(project)
995 996 997 998 999 1000 1001 1002
      .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(
1003
      getBundleDirectory(project)
1004
        .childDirectory('${buildInfo.lowerCasedFlavor}${camelCase('_${buildInfo.modeName}')}')
1005 1006 1007 1008
        .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
1009
    // the file name is `app-foo_bar-release.aab`.
1010
    fileCandidates.add(
1011
      getBundleDirectory(project)
1012
        .childDirectory('${buildInfo.lowerCasedFlavor}${camelCase('_${buildInfo.modeName}')}')
1013
        .childFile('app-${buildInfo.lowerCasedFlavor}-${buildInfo.modeName}.aab'));
1014 1015 1016 1017 1018

    // 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'));
1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034

    // The Android Gradle plugin uses kebab-case and lowercases the first character of the flavor name
    // when multiple flavor dimensions are used:
    // e.g.
    // flavorDimensions "dimension1","dimension2"
    // productFlavors {
    //   foo {
    //     dimension "dimension1"
    //   }
    //   bar {
    //     dimension "dimension2"
    //   }
    // }
    fileCandidates.add(getBundleDirectory(project)
        .childDirectory('${buildInfo.uncapitalizedFlavor}${camelCase('_${buildInfo.modeName}')}')
        .childFile('app-${kebabCase(buildInfo.uncapitalizedFlavor!)}-${buildInfo.modeName}.aab'));
1035 1036 1037 1038 1039
  }
  for (final File bundleFile in fileCandidates) {
    if (bundleFile.existsSync()) {
      return bundleFile;
    }
1040
  }
1041 1042 1043
  _exitWithExpectedFileNotFound(
    project: project,
    fileExtension: '.aab',
1044
    logger: logger,
1045
    usage: usage,
1046
  );
1047
}
1048 1049

/// Throws a [ToolExit] exception and logs the event.
1050 1051 1052 1053 1054
Never _exitWithExpectedFileNotFound({
  required FlutterProject project,
  required String fileExtension,
  required Logger logger,
  required Usage usage,
1055 1056 1057
}) {

  final String androidGradlePluginVersion =
1058
  getGradleVersionForAndroidPlugin(project.android.hostAppGradleRoot, logger);
1059
  BuildEvent('gradle-expected-file-not-found',
1060
    type: 'gradle',
1061
    settings:
1062 1063
    'androidGradlePluginVersion: $androidGradlePluginVersion, '
      'fileExtension: $fileExtension',
1064
    flutterUsage: usage,
1065
  ).send();
1066 1067
  throwToolExit(
    'Gradle build failed to produce an $fileExtension file. '
1068 1069
    "It's likely that this file was generated under ${project.android.buildDirectory.path}, "
    "but the tool couldn't find it."
1070 1071
  );
}
1072

1073 1074
void _createSymlink(String targetPath, String linkPath, FileSystem fileSystem) {
  final File targetFile = fileSystem.file(targetPath);
1075
  if (!targetFile.existsSync()) {
1076
    throwToolExit("The file $targetPath wasn't found in the local engine out directory.");
1077
  }
1078
  final File linkFile = fileSystem.file(linkPath);
1079 1080 1081 1082 1083 1084 1085 1086 1087 1088
  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'
    );
  }
}

1089 1090
String _getLocalArtifactVersion(String pomPath, FileSystem fileSystem) {
  final File pomFile = fileSystem.file(pomPath);
1091
  if (!pomFile.existsSync()) {
1092
    throwToolExit("The file $pomPath wasn't found in the local engine out directory.");
1093
  }
Dan Field's avatar
Dan Field committed
1094
  XmlDocument document;
1095
  try {
Dan Field's avatar
Dan Field committed
1096
    document = XmlDocument.parse(pomFile.readAsStringSync());
1097
  } on XmlException {
1098 1099 1100 1101 1102
    throwToolExit(
      'Error parsing $pomPath. Please ensure that this is a valid XML document.'
    );
  } on FileSystemException {
    throwToolExit(
1103
      'Error reading $pomPath. Please ensure that you have read permission to this '
1104 1105
      'file and try again.');
  }
Dan Field's avatar
Dan Field committed
1106
  final Iterable<XmlElement> project = document.findElements('project');
1107
  assert(project.isNotEmpty);
Dan Field's avatar
Dan Field committed
1108
  for (final XmlElement versionElement in document.findAllElements('version')) {
1109
    if (versionElement.parent == project.first) {
1110
      return versionElement.innerText;
1111 1112 1113 1114 1115 1116 1117 1118 1119 1120
    }
  }
  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({
1121 1122 1123
  required String engineOutPath,
  required AndroidBuildInfo androidBuildInfo,
  required FileSystem fileSystem,
1124 1125
}) {

1126
  final String abi = _getAbiByLocalEnginePath(engineOutPath);
1127
  final Directory localEngineRepo = fileSystem.systemTempDirectory
1128 1129 1130
    .createTempSync('flutter_tool_local_engine_repo.');
  final String buildMode = androidBuildInfo.buildInfo.modeName;
  final String artifactVersion = _getLocalArtifactVersion(
1131
    fileSystem.path.join(
1132 1133
      engineOutPath,
      'flutter_embedding_$buildMode.pom',
1134 1135
    ),
    fileSystem,
1136
  );
1137
  for (final String artifact in const <String>['pom', 'jar']) {
1138 1139
    // The Android embedding artifacts.
    _createSymlink(
1140
      fileSystem.path.join(
1141 1142 1143
        engineOutPath,
        'flutter_embedding_$buildMode.$artifact',
      ),
1144
      fileSystem.path.join(
1145 1146 1147 1148 1149 1150 1151
        localEngineRepo.path,
        'io',
        'flutter',
        'flutter_embedding_$buildMode',
        artifactVersion,
        'flutter_embedding_$buildMode-$artifactVersion.$artifact',
      ),
1152
      fileSystem,
1153 1154 1155
    );
    // The engine artifacts (libflutter.so).
    _createSymlink(
1156
      fileSystem.path.join(
1157 1158 1159
        engineOutPath,
        '${abi}_$buildMode.$artifact',
      ),
1160
      fileSystem.path.join(
1161 1162 1163 1164 1165 1166 1167
        localEngineRepo.path,
        'io',
        'flutter',
        '${abi}_$buildMode',
        artifactVersion,
        '${abi}_$buildMode-$artifactVersion.$artifact',
      ),
1168
      fileSystem,
1169 1170
    );
  }
1171 1172
  for (final String artifact in <String>['flutter_embedding_$buildMode', '${abi}_$buildMode']) {
    _createSymlink(
1173
      fileSystem.path.join(
1174 1175 1176
        engineOutPath,
        '$artifact.maven-metadata.xml',
      ),
1177
      fileSystem.path.join(
1178 1179 1180 1181 1182 1183
        localEngineRepo.path,
        'io',
        'flutter',
        artifact,
        'maven-metadata.xml',
      ),
1184
      fileSystem,
1185 1186
    );
  }
1187 1188
  return localEngineRepo;
}
1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212

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