| // Copyright 2014 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:math'; |
| |
| import 'package:crypto/crypto.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:process/process.dart'; |
| import 'package:unified_analytics/unified_analytics.dart'; |
| import 'package:xml/xml.dart'; |
| |
| import '../artifacts.dart'; |
| import '../base/analyze_size.dart'; |
| import '../base/common.dart'; |
| import '../base/deferred_component.dart'; |
| import '../base/file_system.dart'; |
| import '../base/io.dart'; |
| import '../base/logger.dart'; |
| import '../base/net.dart'; |
| import '../base/platform.dart'; |
| import '../base/process.dart'; |
| import '../base/project_migrator.dart'; |
| import '../base/terminal.dart'; |
| import '../base/utils.dart'; |
| import '../build_info.dart'; |
| import '../cache.dart'; |
| import '../convert.dart'; |
| import '../flutter_manifest.dart'; |
| import '../globals.dart' as globals; |
| import '../project.dart'; |
| import '../reporting/reporting.dart'; |
| import 'android_builder.dart'; |
| import 'android_studio.dart'; |
| import 'gradle_errors.dart'; |
| import 'gradle_utils.dart'; |
| import 'java.dart'; |
| import 'migrations/android_studio_java_gradle_conflict_migration.dart'; |
| import 'migrations/min_sdk_version_migration.dart'; |
| import 'migrations/top_level_gradle_build_file_migration.dart'; |
| import 'multidex.dart'; |
| |
| /// The regex to grab variant names from printBuildVariants gradle task |
| /// |
| /// The task is defined in flutter/packages/flutter_tools/gradle/src/main/groovy/flutter.groovy |
| /// |
| /// 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'; |
| const String _kBuildVariantTaskName = 'printBuildVariants'; |
| |
| String _getOutputAppLinkSettingsTaskFor(String buildVariant) { |
| return _taskForBuildVariant('output', buildVariant, 'AppLinkSettings'); |
| } |
| |
| /// 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') |
| .childDirectory('flutter-apk'); |
| } |
| |
| /// 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'); |
| } |
| |
| /// The directory where the repo is generated. |
| /// Only applicable to AARs. |
| Directory getRepoDirectory(Directory buildDirectory) { |
| return buildDirectory |
| .childDirectory('outputs') |
| .childDirectory('repo'); |
| } |
| |
| /// 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 _taskForBuildVariant(prefix, '$productFlavor${sentenceCase(buildType)}'); |
| } |
| |
| String _taskForBuildVariant(String prefix, String buildVariant, [String suffix = '']) { |
| return '$prefix${sentenceCase(buildVariant)}$suffix'; |
| } |
| |
| |
| /// Returns the task to build an APK. |
| @visibleForTesting |
| String getAssembleTaskFor(BuildInfo buildInfo) { |
| return _taskFor('assemble', buildInfo); |
| } |
| |
| /// Returns the task to build an AAB. |
| @visibleForTesting |
| String getBundleTaskFor(BuildInfo buildInfo) { |
| return _taskFor('bundle', buildInfo); |
| } |
| |
| /// Returns the task to build an AAR. |
| @visibleForTesting |
| String getAarTaskFor(BuildInfo buildInfo) { |
| return _taskFor('assembleAar', buildInfo); |
| } |
| |
| /// Returns the output APK file names for a given [AndroidBuildInfo]. |
| /// |
| /// For example, when [splitPerAbi] is true, multiple APKs are created. |
| Iterable<String> _apkFilesFor(AndroidBuildInfo androidBuildInfo) { |
| final String buildType = camelCase(androidBuildInfo.buildInfo.modeName); |
| final String productFlavor = androidBuildInfo.buildInfo.lowerCasedFlavor ?? ''; |
| final String flavorString = productFlavor.isEmpty ? '' : '-$productFlavor'; |
| if (androidBuildInfo.splitPerAbi) { |
| return androidBuildInfo.targetArchs.map<String>((AndroidArch arch) { |
| final String abi = arch.archName; |
| return 'app$flavorString-$abi-$buildType.apk'; |
| }); |
| } |
| return <String>['app$flavorString-$buildType.apk']; |
| } |
| |
| // The maximum time to wait before the tool retries a Gradle build. |
| const Duration kMaxRetryTime = Duration(seconds: 10); |
| |
| /// An implementation of the [AndroidBuilder] that delegates to gradle. |
| class AndroidGradleBuilder implements AndroidBuilder { |
| AndroidGradleBuilder({ |
| required Java? java, |
| required Logger logger, |
| required ProcessManager processManager, |
| required FileSystem fileSystem, |
| required Artifacts artifacts, |
| required Usage usage, |
| required Analytics analytics, |
| required GradleUtils gradleUtils, |
| required Platform platform, |
| required AndroidStudio? androidStudio, |
| }) : _java = java, |
| _logger = logger, |
| _fileSystem = fileSystem, |
| _artifacts = artifacts, |
| _usage = usage, |
| _analytics = analytics, |
| _gradleUtils = gradleUtils, |
| _androidStudio = androidStudio, |
| _fileSystemUtils = FileSystemUtils(fileSystem: fileSystem, platform: platform), |
| _processUtils = ProcessUtils(logger: logger, processManager: processManager); |
| |
| final Java? _java; |
| final Logger _logger; |
| final ProcessUtils _processUtils; |
| final FileSystem _fileSystem; |
| final Artifacts _artifacts; |
| final Usage _usage; |
| final Analytics _analytics; |
| final GradleUtils _gradleUtils; |
| final FileSystemUtils _fileSystemUtils; |
| final AndroidStudio? _androidStudio; |
| |
| /// Builds the AAR and POM files for the current Flutter module or plugin. |
| @override |
| Future<void> buildAar({ |
| required FlutterProject project, |
| required Set<AndroidBuildInfo> androidBuildInfo, |
| required String target, |
| String? outputDirectoryPath, |
| required String buildNumber, |
| }) async { |
| 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, |
| buildNumber: buildNumber, |
| ); |
| } |
| 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, |
| ); |
| } |
| |
| /// Builds the APK. |
| @override |
| Future<void> buildApk({ |
| required FlutterProject project, |
| required AndroidBuildInfo androidBuildInfo, |
| required String target, |
| bool configOnly = false, |
| }) async { |
| await buildGradleApp( |
| project: project, |
| androidBuildInfo: androidBuildInfo, |
| target: target, |
| isBuildingBundle: false, |
| localGradleErrors: gradleErrors, |
| configOnly: configOnly, |
| maxRetries: 1, |
| ); |
| } |
| |
| /// Builds the App Bundle. |
| @override |
| Future<void> buildAab({ |
| required FlutterProject project, |
| required AndroidBuildInfo androidBuildInfo, |
| required String target, |
| bool validateDeferredComponents = true, |
| bool deferredComponentsEnabled = false, |
| bool configOnly = false, |
| }) async { |
| await buildGradleApp( |
| project: project, |
| androidBuildInfo: androidBuildInfo, |
| target: target, |
| isBuildingBundle: true, |
| localGradleErrors: gradleErrors, |
| validateDeferredComponents: validateDeferredComponents, |
| deferredComponentsEnabled: deferredComponentsEnabled, |
| configOnly: configOnly, |
| maxRetries: 1, |
| ); |
| } |
| |
| 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; |
| } |
| |
| /// 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`. |
| /// * [maxRetries] If not `null`, this is the max number of build retries in case a retry is triggered. |
| Future<void> buildGradleApp({ |
| required FlutterProject project, |
| required AndroidBuildInfo androidBuildInfo, |
| required String target, |
| required bool isBuildingBundle, |
| required List<GradleHandledError> localGradleErrors, |
| required bool configOnly, |
| bool validateDeferredComponents = true, |
| bool deferredComponentsEnabled = false, |
| int retry = 0, |
| @visibleForTesting int? maxRetries, |
| }) async { |
| if (!project.android.isSupportedVersion) { |
| _exitWithUnsupportedProjectMessage(_usage, _logger.terminal, analytics: _analytics); |
| } |
| |
| final List<ProjectMigrator> migrators = <ProjectMigrator>[ |
| TopLevelGradleBuildFileMigration(project.android, _logger), |
| AndroidStudioJavaGradleConflictMigration(_logger, |
| project: project.android, |
| androidStudio: _androidStudio, |
| java: globals.java), |
| MinSdkVersionMigration(project.android, _logger), |
| ]; |
| |
| final ProjectMigration migration = ProjectMigration(migrators); |
| migration.run(); |
| |
| final bool usesAndroidX = isAppUsingAndroidX(project.android.hostAppGradleRoot); |
| if (usesAndroidX) { |
| BuildEvent('app-using-android-x', type: 'gradle', flutterUsage: _usage).send(); |
| _analytics.send(Event.flutterBuildInfo(label: 'app-using-android-x', buildType: 'gradle')); |
| } else if (!usesAndroidX) { |
| BuildEvent('app-not-using-android-x', type: 'gradle', flutterUsage: _usage).send(); |
| _analytics.send(Event.flutterBuildInfo(label: 'app-not-using-android-x', buildType: 'gradle')); |
| |
| _logger.printStatus("${_logger.terminal.warningMark} Your app isn't using AndroidX.", emphasis: true); |
| _logger.printStatus( |
| '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 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. |
| final BuildInfo buildInfo = androidBuildInfo.buildInfo; |
| final String assembleTask = isBuildingBundle |
| ? getBundleTaskFor(buildInfo) |
| : getAssembleTaskFor(buildInfo); |
| |
| final Status status = _logger.startProgress( |
| "Running Gradle task '$assembleTask'...", |
| ); |
| |
| if (_logger.isVerbose) { |
| command.add('--full-stacktrace'); |
| command.add('--info'); |
| command.add('-Pverbose=true'); |
| } else { |
| command.add('-q'); |
| } |
| if (!buildInfo.androidGradleDaemon) { |
| command.add('--no-daemon'); |
| } |
| final LocalEngineInfo? localEngineInfo = _artifacts.localEngineInfo; |
| if (localEngineInfo != null) { |
| final Directory localEngineRepo = _getLocalEngineRepo( |
| engineOutPath: localEngineInfo.targetOutPath, |
| androidBuildInfo: androidBuildInfo, |
| fileSystem: _fileSystem, |
| ); |
| _logger.printTrace( |
| 'Using local engine: ${localEngineInfo.targetOutPath}\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=${localEngineInfo.targetOutPath}'); |
| command.add('-Plocal-engine-host-out=${localEngineInfo.hostOutPath}'); |
| command.add('-Ptarget-platform=${_getTargetPlatformByLocalEnginePath( |
| localEngineInfo.targetOutPath)}'); |
| } else if (androidBuildInfo.targetArchs.isNotEmpty) { |
| final String targetPlatforms = androidBuildInfo |
| .targetArchs |
| .map((AndroidArch e) => e.platformName).join(','); |
| command.add('-Ptarget-platform=$targetPlatforms'); |
| } |
| command.add('-Ptarget=$target'); |
| // 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'); |
| final List<DeferredComponent>? deferredComponents = project.manifest.deferredComponents; |
| if (deferredComponents != null) { |
| 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>[]; |
| for (final DeferredComponent component in deferredComponents) { |
| 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'); |
| } |
| } |
| command.addAll(androidBuildInfo.buildInfo.toGradleConfig()); |
| if (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); |
| |
| GradleHandledError? detectedGradleError; |
| String? detectedGradleErrorLine; |
| String? consumeLog(String line) { |
| if (detectedGradleError != null) { |
| // Pipe stdout/stderr from Gradle. |
| return line; |
| } |
| for (final GradleHandledError gradleError in localGradleErrors) { |
| if (gradleError.test(line)) { |
| detectedGradleErrorLine = line; |
| detectedGradleError = gradleError; |
| // The first error match wins. |
| break; |
| } |
| } |
| // Pipe stdout/stderr from Gradle. |
| return line; |
| } |
| |
| final Stopwatch sw = Stopwatch() |
| ..start(); |
| int exitCode = 1; |
| try { |
| exitCode = await _processUtils.stream( |
| command, |
| workingDirectory: project.android.hostAppGradleRoot.path, |
| allowReentrantFlutter: true, |
| environment: _java?.environment, |
| 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; |
| } |
| } finally { |
| status.stop(); |
| } |
| |
| final Duration elapsedDuration = sw.elapsed; |
| _usage.sendTiming('build', 'gradle', elapsedDuration); |
| _analytics.send(Event.timing( |
| workflow: 'build', |
| variableName: 'gradle', |
| elapsedMilliseconds: elapsedDuration.inMilliseconds, |
| )); |
| |
| if (exitCode != 0) { |
| if (detectedGradleError == null) { |
| BuildEvent('gradle-unknown-failure', type: 'gradle', flutterUsage: _usage).send(); |
| _analytics.send(Event.flutterBuildInfo(label: 'gradle-unknown-failure', buildType: 'gradle')); |
| |
| throwToolExit( |
| 'Gradle task $assembleTask failed with exit code $exitCode', |
| exitCode: exitCode, |
| ); |
| } |
| final GradleBuildStatus status = await detectedGradleError!.handler( |
| line: detectedGradleErrorLine!, |
| project: project, |
| usesAndroidX: usesAndroidX, |
| multidexEnabled: androidBuildInfo.multidexEnabled, |
| ); |
| |
| 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, |
| configOnly: configOnly, |
| ); |
| final String successEventLabel = 'gradle-${detectedGradleError!.eventLabel}-success'; |
| |
| BuildEvent(successEventLabel, type: 'gradle', flutterUsage: _usage).send(); |
| _analytics.send(Event.flutterBuildInfo(label: successEventLabel, buildType: 'gradle')); |
| |
| return; |
| case GradleBuildStatus.exit: |
| // Continue and throw tool exit. |
| } |
| } |
| final String usageLabel = 'gradle-${detectedGradleError?.eventLabel}-failure'; |
| BuildEvent(usageLabel, type: 'gradle', flutterUsage: _usage).send(); |
| _analytics.send(Event.flutterBuildInfo(label: usageLabel, buildType: 'gradle')); |
| |
| throwToolExit( |
| 'Gradle task $assembleTask failed with exit code $exitCode', |
| exitCode: exitCode, |
| ); |
| } |
| |
| if (isBuildingBundle) { |
| final File bundleFile = findBundleFile(project, buildInfo, _logger, _usage, _analytics); |
| final String appSize = (buildInfo.mode == BuildMode.debug) |
| ? '' // Don't display the size when building a debug variant. |
| : ' (${getSizeAsMB(bundleFile.lengthSync())})'; |
| |
| if (buildInfo.codeSizeDirectory != null) { |
| await _performCodeSizeAnalysis('aab', bundleFile, androidBuildInfo); |
| } |
| |
| _logger.printStatus( |
| '${_logger.terminal.successMark} Built ${_fileSystem.path.relative(bundleFile.path)}$appSize.', |
| color: TerminalColor.green, |
| ); |
| return; |
| } |
| // Gradle produced APKs. |
| final Iterable<String> apkFilesPaths = project.isModule |
| ? findApkFilesModule(project, androidBuildInfo, _logger, _usage, _analytics) |
| : listApkPaths(androidBuildInfo); |
| final Directory apkDirectory = getApkDirectory(project); |
| |
| // 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, |
| analytics: _analytics, |
| ); |
| } |
| |
| 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); |
| } |
| } |
| } |
| |
| Future<void> _performCodeSizeAnalysis(String kind, |
| File zipFile, |
| AndroidBuildInfo androidBuildInfo,) async { |
| final SizeAnalyzer sizeAnalyzer = SizeAnalyzer( |
| fileSystem: _fileSystem, |
| logger: _logger, |
| flutterUsage: _usage, |
| analytics: _analytics, |
| ); |
| final String archName = androidBuildInfo.targetArchs.single.archName; |
| final BuildInfo buildInfo = androidBuildInfo.buildInfo; |
| final File aotSnapshot = _fileSystem.directory(buildInfo.codeSizeDirectory) |
| .childFile('snapshot.$archName.json'); |
| final File precompilerTrace = _fileSystem.directory(buildInfo.codeSizeDirectory) |
| .childFile('trace.$archName.json'); |
| final Map<String, Object?> output = await sizeAnalyzer.analyzeZipSizeAndAotSnapshot( |
| zipFile: zipFile, |
| aotSnapshot: aotSnapshot, |
| precompilerTrace: precompilerTrace, |
| kind: kind, |
| ); |
| final File outputFile = _fileSystemUtils.getUniqueFile( |
| _fileSystem |
| .directory(_fileSystemUtils.homeDirPath) |
| .childDirectory('.flutter-devtools'), '$kind-code-size-analysis', 'json', |
| ) |
| ..writeAsStringSync(jsonEncode(output)); |
| // This message is used as a sentinel in analyze_apk_size_test.dart |
| _logger.printStatus( |
| 'A summary of your ${kind.toUpperCase()} analysis can be found at: ${outputFile.path}', |
| ); |
| |
| // DevTools expects a file path relative to the .flutter-devtools/ dir. |
| final String relativeAppSizePath = outputFile.path |
| .split('.flutter-devtools/') |
| .last |
| .trim(); |
| _logger.printStatus( |
| '\nTo analyze your app size in Dart DevTools, run the following command:\n' |
| 'dart devtools --appSizeBase=$relativeAppSizePath' |
| ); |
| } |
| |
| /// 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({ |
| required FlutterProject project, |
| required AndroidBuildInfo androidBuildInfo, |
| required String target, |
| required Directory outputDirectory, |
| required String buildNumber, |
| }) async { |
| final FlutterManifest manifest = project.manifest; |
| if (!manifest.isModule) { |
| throwToolExit('AARs can only be built for module projects.'); |
| } |
| |
| final BuildInfo buildInfo = androidBuildInfo.buildInfo; |
| final String aarTask = getAarTaskFor(buildInfo); |
| final Status status = _logger.startProgress( |
| "Running Gradle task '$aarTask'...", |
| ); |
| |
| final String flutterRoot = _fileSystem.path.absolute(Cache.flutterRoot!); |
| final String initScript = _fileSystem.path.join( |
| flutterRoot, |
| 'packages', |
| 'flutter_tools', |
| 'gradle', |
| 'aar_init_script.gradle', |
| ); |
| final List<String> command = <String>[ |
| _gradleUtils.getExecutable(project), |
| '-I=$initScript', |
| '-Pflutter-root=$flutterRoot', |
| '-Poutput-dir=${outputDirectory.path}', |
| '-Pis-plugin=${manifest.isPlugin}', |
| '-PbuildNumber=$buildNumber', |
| ]; |
| if (_logger.isVerbose) { |
| command.add('--full-stacktrace'); |
| command.add('--info'); |
| command.add('-Pverbose=true'); |
| } else { |
| command.add('-q'); |
| } |
| if (!buildInfo.androidGradleDaemon) { |
| command.add('--no-daemon'); |
| } |
| |
| if (target.isNotEmpty) { |
| command.add('-Ptarget=$target'); |
| } |
| command.addAll(androidBuildInfo.buildInfo.toGradleConfig()); |
| if (buildInfo.dartObfuscation && buildInfo.mode != BuildMode.release) { |
| _logger.printStatus( |
| 'Dart obfuscation is not supported in ${sentenceCase(buildInfo.friendlyModeName)}' |
| ' mode, building as un-obfuscated.', |
| ); |
| } |
| |
| final LocalEngineInfo? localEngineInfo = _artifacts.localEngineInfo; |
| if (localEngineInfo != null) { |
| final Directory localEngineRepo = _getLocalEngineRepo( |
| engineOutPath: localEngineInfo.targetOutPath, |
| androidBuildInfo: androidBuildInfo, |
| fileSystem: _fileSystem, |
| ); |
| _logger.printTrace( |
| 'Using local engine: ${localEngineInfo.targetOutPath}\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=${localEngineInfo.targetOutPath}'); |
| command.add('-Plocal-engine-host-out=${localEngineInfo.hostOutPath}'); |
| |
| // Copy the local engine repo in the output directory. |
| try { |
| copyDirectory( |
| localEngineRepo, |
| getRepoDirectory(outputDirectory), |
| ); |
| } on FileSystemException catch (error, st) { |
| throwToolExit( |
| 'Failed to copy the local engine ${localEngineRepo.path} repo ' |
| 'in ${outputDirectory.path}: $error, $st' |
| ); |
| } |
| command.add('-Ptarget-platform=${_getTargetPlatformByLocalEnginePath( |
| localEngineInfo.targetOutPath)}'); |
| } else if (androidBuildInfo.targetArchs.isNotEmpty) { |
| final String targetPlatforms = androidBuildInfo.targetArchs |
| .map((AndroidArch e) => e.platformName).join(','); |
| command.add('-Ptarget-platform=$targetPlatforms'); |
| } |
| |
| command.add(aarTask); |
| |
| final Stopwatch sw = Stopwatch() |
| ..start(); |
| RunResult result; |
| try { |
| result = await _processUtils.run( |
| command, |
| workingDirectory: project.android.hostAppGradleRoot.path, |
| allowReentrantFlutter: true, |
| environment: _java?.environment, |
| ); |
| } finally { |
| status.stop(); |
| } |
| final Duration elapsedDuration = sw.elapsed; |
| _usage.sendTiming('build', 'gradle-aar', elapsedDuration); |
| _analytics.send(Event.timing( |
| workflow: 'build', |
| variableName: 'gradle-aar', |
| elapsedMilliseconds: elapsedDuration.inMilliseconds, |
| )); |
| |
| if (result.exitCode != 0) { |
| _logger.printStatus(result.stdout, wrap: false); |
| _logger.printError(result.stderr, wrap: false); |
| throwToolExit( |
| 'Gradle task $aarTask failed with exit code ${result.exitCode}.', |
| exitCode: result.exitCode, |
| ); |
| } |
| final Directory repoDirectory = getRepoDirectory(outputDirectory); |
| if (!repoDirectory.existsSync()) { |
| _logger.printStatus(result.stdout, wrap: false); |
| _logger.printError(result.stderr, wrap: false); |
| throwToolExit( |
| 'Gradle task $aarTask failed to produce $repoDirectory.', |
| exitCode: exitCode, |
| ); |
| } |
| _logger.printStatus( |
| '${_logger.terminal.successMark} Built ${_fileSystem.path.relative(repoDirectory.path)}.', |
| color: TerminalColor.green, |
| ); |
| } |
| |
| @override |
| Future<List<String>> getBuildVariants({required FlutterProject project}) async { |
| final Stopwatch sw = Stopwatch() |
| ..start(); |
| final RunResult result = await _runGradleTask( |
| _kBuildVariantTaskName, |
| options: const <String>['-q'], |
| project: project, |
| ); |
| |
| final Duration elapsedDuration = sw.elapsed; |
| _usage.sendTiming('print', 'android build variants', elapsedDuration); |
| _analytics.send(Event.timing( |
| workflow: 'print', |
| variableName: 'android build variants', |
| elapsedMilliseconds: elapsedDuration.inMilliseconds, |
| )); |
| |
| 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; |
| } |
| |
| @override |
| Future<String> outputsAppLinkSettings( |
| String buildVariant, { |
| required FlutterProject project, |
| }) async { |
| final String taskName = _getOutputAppLinkSettingsTaskFor(buildVariant); |
| final Directory directory = await project.buildDirectory |
| .childDirectory('deeplink_data').create(recursive: true); |
| final String outputPath = globals.fs.path.join( |
| directory.absolute.path, |
| 'app-link-settings-$buildVariant.json', |
| ); |
| final Stopwatch sw = Stopwatch() |
| ..start(); |
| final RunResult result = await _runGradleTask( |
| taskName, |
| options: <String>['-q', '-PoutputPath=$outputPath'], |
| project: project, |
| ); |
| final Duration elapsedDuration = sw.elapsed; |
| _usage.sendTiming('outputs', 'app link settings', elapsedDuration); |
| _analytics.send(Event.timing( |
| workflow: 'outputs', |
| variableName: 'app link settings', |
| elapsedMilliseconds: elapsedDuration.inMilliseconds, |
| )); |
| |
| if (result.exitCode != 0) { |
| _logger.printStatus(result.stdout, wrap: false); |
| _logger.printError(result.stderr, wrap: false); |
| throwToolExit(result.stderr); |
| } |
| return outputPath; |
| } |
| } |
| |
| /// Prints how to consume the AAR from a host app. |
| void printHowToConsumeAar({ |
| required Set<String> buildModes, |
| String? androidPackage = 'unknown', |
| required Directory repoDirectory, |
| required Logger logger, |
| required FileSystem fileSystem, |
| String? buildNumber, |
| }) { |
| assert(buildModes.isNotEmpty); |
| buildNumber ??= '1.0'; |
| |
| logger.printStatus('\nConsuming the Module', emphasis: true); |
| logger.printStatus(''' |
| 1. Open ${fileSystem.path.join('<host>', 'app', 'build.gradle')} |
| 2. Ensure you have the repositories configured, otherwise add them: |
| |
| String storageUrl = System.env.$kFlutterStorageBaseUrl ?: "https://storage.googleapis.com" |
| repositories { |
| maven { |
| url '${repoDirectory.path}' |
| } |
| maven { |
| url "\$storageUrl/download.flutter.io" |
| } |
| } |
| |
| 3. Make the host app depend on the Flutter module: |
| |
| dependencies {'''); |
| |
| for (final String buildMode in buildModes) { |
| logger.printStatus(""" |
| ${buildMode}Implementation '$androidPackage:flutter_$buildMode:$buildNumber'"""); |
| } |
| |
| logger.printStatus(''' |
| } |
| '''); |
| |
| if (buildModes.contains('profile')) { |
| logger.printStatus(''' |
| |
| 4. Add the `profile` build type: |
| |
| android { |
| buildTypes { |
| profile { |
| initWith debug |
| } |
| } |
| } |
| '''); |
| } |
| |
| logger.printStatus('To learn more, visit https://flutter.dev/go/build-aar'); |
| } |
| |
| String _hex(List<int> bytes) { |
| final StringBuffer result = StringBuffer(); |
| for (final int part in bytes) { |
| result.write('${part < 16 ? '0' : ''}${part.toRadixString(16)}'); |
| } |
| return result.toString(); |
| } |
| |
| String _calculateSha(File file) { |
| final List<int> bytes = file.readAsBytesSync(); |
| return _hex(sha1.convert(bytes).bytes); |
| } |
| |
| void _exitWithUnsupportedProjectMessage(Usage usage, Terminal terminal, {required Analytics analytics}) { |
| BuildEvent('unsupported-project', type: 'gradle', eventError: 'gradle-plugin', flutterUsage: usage).send(); |
| analytics.send(Event.flutterBuildInfo( |
| label: 'unsupported-project', |
| buildType: 'gradle', |
| error: 'gradle-plugin', |
| )); |
| |
| throwToolExit( |
| '${terminal.warningMark} Your app is using an unsupported Gradle project. ' |
| 'To fix this problem, create a new project by running `flutter create -t app <app-directory>` ' |
| 'and then move the dart code, assets and pubspec.yaml to the new project.', |
| ); |
| } |
| |
| /// 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'); |
| } |
| |
| /// Returns the APK files for a given [FlutterProject] and [AndroidBuildInfo]. |
| @visibleForTesting |
| Iterable<String> findApkFilesModule( |
| FlutterProject project, |
| AndroidBuildInfo androidBuildInfo, |
| Logger logger, |
| Usage usage, |
| Analytics analytics, |
| ) { |
| final Iterable<String> apkFileNames = _apkFilesFor(androidBuildInfo); |
| final Directory apkDirectory = getApkDirectory(project); |
| final Iterable<File> apks = apkFileNames.expand<File>((String apkFileName) { |
| File apkFile = apkDirectory.childFile(apkFileName); |
| if (apkFile.existsSync()) { |
| return <File>[apkFile]; |
| } |
| final BuildInfo buildInfo = androidBuildInfo.buildInfo; |
| final String modeName = camelCase(buildInfo.modeName); |
| apkFile = apkDirectory |
| .childDirectory(modeName) |
| .childFile(apkFileName); |
| if (apkFile.existsSync()) { |
| return <File>[apkFile]; |
| } |
| final String? flavor = buildInfo.flavor; |
| if (flavor != null) { |
| // Android Studio Gradle plugin v3 adds flavor to path. |
| apkFile = apkDirectory |
| .childDirectory(flavor) |
| .childDirectory(modeName) |
| .childFile(apkFileName); |
| if (apkFile.existsSync()) { |
| return <File>[apkFile]; |
| } |
| } |
| return const <File>[]; |
| }); |
| if (apks.isEmpty) { |
| _exitWithExpectedFileNotFound( |
| project: project, |
| fileExtension: '.apk', |
| logger: logger, |
| usage: usage, |
| analytics: analytics, |
| ); |
| } |
| return apks.map((File file) => file.path); |
| } |
| |
| /// Returns the APK files for a given [FlutterProject] and [AndroidBuildInfo]. |
| /// |
| /// The flutter.gradle plugin will copy APK outputs into: |
| /// `$buildDir/app/outputs/flutter-apk/app-<abi>-<flavor-flag>-<build-mode-flag>.apk` |
| @visibleForTesting |
| Iterable<String> listApkPaths( |
| AndroidBuildInfo androidBuildInfo, |
| ) { |
| final String buildType = camelCase(androidBuildInfo.buildInfo.modeName); |
| final List<String> apkPartialName = <String>[ |
| if (androidBuildInfo.buildInfo.flavor?.isNotEmpty ?? false) |
| androidBuildInfo.buildInfo.lowerCasedFlavor!, |
| '$buildType.apk', |
| ]; |
| if (androidBuildInfo.splitPerAbi) { |
| return <String>[ |
| for (final AndroidArch androidArch in androidBuildInfo.targetArchs) |
| <String>[ |
| 'app', |
| androidArch.archName, |
| ...apkPartialName, |
| ].join('-'), |
| ]; |
| } |
| return <String>[ |
| <String>[ |
| 'app', |
| ...apkPartialName, |
| ].join('-'), |
| ]; |
| } |
| |
| @visibleForTesting |
| File findBundleFile(FlutterProject project, BuildInfo buildInfo, Logger logger, Usage usage, Analytics analytics) { |
| final List<File> fileCandidates = <File>[ |
| getBundleDirectory(project) |
| .childDirectory(camelCase(buildInfo.modeName)) |
| .childFile('app.aab'), |
| getBundleDirectory(project) |
| .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( |
| getBundleDirectory(project) |
| .childDirectory('${buildInfo.lowerCasedFlavor}${camelCase('_${buildInfo.modeName}')}') |
| .childFile('app.aab')); |
| |
| // The Android Gradle plugin 3.5.0 adds the flavor name to file name. |
| // For example: In release mode, if the flavor name is `foo_bar`, then |
| // the file name is `app-foo_bar-release.aab`. |
| fileCandidates.add( |
| getBundleDirectory(project) |
| .childDirectory('${buildInfo.lowerCasedFlavor}${camelCase('_${buildInfo.modeName}')}') |
| .childFile('app-${buildInfo.lowerCasedFlavor}-${buildInfo.modeName}.aab')); |
| |
| // 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')); |
| |
| // 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')); |
| } |
| for (final File bundleFile in fileCandidates) { |
| if (bundleFile.existsSync()) { |
| return bundleFile; |
| } |
| } |
| _exitWithExpectedFileNotFound( |
| project: project, |
| fileExtension: '.aab', |
| logger: logger, |
| usage: usage, |
| analytics: analytics, |
| ); |
| } |
| |
| /// Throws a [ToolExit] exception and logs the event. |
| Never _exitWithExpectedFileNotFound({ |
| required FlutterProject project, |
| required String fileExtension, |
| required Logger logger, |
| required Usage usage, |
| required Analytics analytics, |
| }) { |
| |
| final String androidGradlePluginVersion = |
| getGradleVersionForAndroidPlugin(project.android.hostAppGradleRoot, logger); |
| final String gradleBuildSettings = 'androidGradlePluginVersion: $androidGradlePluginVersion, ' |
| 'fileExtension: $fileExtension'; |
| |
| BuildEvent('gradle-expected-file-not-found', |
| type: 'gradle', |
| settings: gradleBuildSettings, |
| flutterUsage: usage, |
| ).send(); |
| analytics.send(Event.flutterBuildInfo( |
| label: 'gradle-expected-file-not-found', |
| buildType: 'gradle', |
| settings: gradleBuildSettings, |
| )); |
| |
| throwToolExit( |
| 'Gradle build failed to produce an $fileExtension file. ' |
| "It's likely that this file was generated under ${project.android.buildDirectory.path}, " |
| "but the tool couldn't find it." |
| ); |
| } |
| |
| void _createSymlink(String targetPath, String linkPath, FileSystem fileSystem) { |
| final File targetFile = fileSystem.file(targetPath); |
| if (!targetFile.existsSync()) { |
| throwToolExit("The file $targetPath wasn't found in the local engine out directory."); |
| } |
| final File linkFile = fileSystem.file(linkPath); |
| final Link symlink = linkFile.parent.childLink(linkFile.basename); |
| try { |
| symlink.createSync(targetPath, recursive: true); |
| } on FileSystemException catch (exception) { |
| throwToolExit( |
| 'Failed to create the symlink $linkPath->$targetPath: $exception' |
| ); |
| } |
| } |
| |
| String _getLocalArtifactVersion(String pomPath, FileSystem fileSystem) { |
| final File pomFile = fileSystem.file(pomPath); |
| if (!pomFile.existsSync()) { |
| throwToolExit("The file $pomPath wasn't found in the local engine out directory."); |
| } |
| XmlDocument document; |
| try { |
| document = XmlDocument.parse(pomFile.readAsStringSync()); |
| } on XmlException { |
| throwToolExit( |
| 'Error parsing $pomPath. Please ensure that this is a valid XML document.' |
| ); |
| } on FileSystemException { |
| throwToolExit( |
| 'Error reading $pomPath. Please ensure that you have read permission to this ' |
| 'file and try again.'); |
| } |
| final Iterable<XmlElement> project = document.findElements('project'); |
| assert(project.isNotEmpty); |
| for (final XmlElement versionElement in document.findAllElements('version')) { |
| if (versionElement.parent == project.first) { |
| return versionElement.innerText; |
| } |
| } |
| 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({ |
| required String engineOutPath, |
| required AndroidBuildInfo androidBuildInfo, |
| required FileSystem fileSystem, |
| }) { |
| |
| final String abi = _getAbiByLocalEnginePath(engineOutPath); |
| final Directory localEngineRepo = fileSystem.systemTempDirectory |
| .createTempSync('flutter_tool_local_engine_repo.'); |
| final String buildMode = androidBuildInfo.buildInfo.modeName; |
| final String artifactVersion = _getLocalArtifactVersion( |
| fileSystem.path.join( |
| engineOutPath, |
| 'flutter_embedding_$buildMode.pom', |
| ), |
| fileSystem, |
| ); |
| for (final String artifact in const <String>['pom', 'jar']) { |
| // The Android embedding artifacts. |
| _createSymlink( |
| fileSystem.path.join( |
| engineOutPath, |
| 'flutter_embedding_$buildMode.$artifact', |
| ), |
| fileSystem.path.join( |
| localEngineRepo.path, |
| 'io', |
| 'flutter', |
| 'flutter_embedding_$buildMode', |
| artifactVersion, |
| 'flutter_embedding_$buildMode-$artifactVersion.$artifact', |
| ), |
| fileSystem, |
| ); |
| // The engine artifacts (libflutter.so). |
| _createSymlink( |
| fileSystem.path.join( |
| engineOutPath, |
| '${abi}_$buildMode.$artifact', |
| ), |
| fileSystem.path.join( |
| localEngineRepo.path, |
| 'io', |
| 'flutter', |
| '${abi}_$buildMode', |
| artifactVersion, |
| '${abi}_$buildMode-$artifactVersion.$artifact', |
| ), |
| fileSystem, |
| ); |
| } |
| for (final String artifact in <String>['flutter_embedding_$buildMode', '${abi}_$buildMode']) { |
| _createSymlink( |
| fileSystem.path.join( |
| engineOutPath, |
| '$artifact.maven-metadata.xml', |
| ), |
| fileSystem.path.join( |
| localEngineRepo.path, |
| 'io', |
| 'flutter', |
| artifact, |
| 'maven-metadata.xml', |
| ), |
| fileSystem, |
| ); |
| } |
| return localEngineRepo; |
| } |
| |
| 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; |
| } |