| // 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 'dart:typed_data'; | |
| import 'package:convert/convert.dart'; | |
| 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 '../base/version.dart'; | |
| import '../build_info.dart'; | |
| import '../cache.dart'; | |
| import '../context/android_context.dart'; | |
| import '../context/tool_context.dart'; | |
| import '../convert.dart'; | |
| import '../flutter_manifest.dart'; | |
| import '../globals.dart' as globals; | |
| import '../project.dart'; | |
| import 'android_builder.dart'; | |
| import 'android_sdk.dart'; | |
| import 'android_studio.dart'; | |
| import 'gradle_errors.dart'; | |
| import 'gradle_utils.dart' as gradle; | |
| import 'gradle_utils.dart'; | |
| import 'java.dart'; | |
| import 'migrations/android_studio_java_gradle_conflict_migration.dart'; | |
| import 'migrations/cmake_android_16k_pages_migration.dart'; | |
| import 'migrations/disable_built_in_kotlin_migration.dart'; | |
| import 'migrations/disable_new_dsl_migration.dart'; | |
| import 'migrations/min_sdk_version_migration.dart'; | |
| import 'migrations/multidex_removal_migration.dart'; | |
| import 'migrations/top_level_gradle_build_file_migration.dart'; | |
| /// The regex to grab variant names from printBuildVariants gradle task | |
| /// | |
| /// The task is defined in flutter/packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt | |
| /// | |
| /// The expected output from the task should be similar to: | |
| /// | |
| /// BuildVariant: debug | |
| /// BuildVariant: release | |
| /// BuildVariant: profile | |
| final _kBuildVariantRegex = RegExp('^BuildVariant: (?<$_kBuildVariantRegexGroupName>.*)\$'); | |
| const _kBuildVariantRegexGroupName = 'variant'; | |
| const _kBuildVariantTaskName = 'printBuildVariants'; | |
| const _kSdkManagerPathProperty = 'flutter.sdkManagerPath'; | |
| const _kAndroidSdkRootProperty = 'flutter.androidSdkRoot'; | |
| const _kInstalledNdkVersionsProperty = 'flutter.installedNdkVersions'; | |
| @visibleForTesting | |
| const failedToStripDebugSymbolsErrorMessage = r''' | |
| Release app bundle failed to strip debug symbols from native libraries. | |
| Please run flutter doctor and ensure that the Android toolchain does not | |
| report any issues. | |
| Otherwise, file an issue at https://github.com/flutter/flutter/issues.'''; | |
| typedef _OutputParser = void Function(String line); | |
| 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'); | |
| } | |
| @visibleForTesting | |
| final apkAnalyzerBinaryName = globals.platform.isWindows ? 'apkanalyzer.bat' : 'apkanalyzer'; | |
| /// 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 [AndroidBuildInfo.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 flavorString = productFlavor.isEmpty ? '' : '-$productFlavor'; | |
| if (androidBuildInfo.splitPerAbi) { | |
| return androidBuildInfo.targetArchs.map<String>((CpuArch arch) { | |
| final String abi = arch.androidArchName; | |
| 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 kMaxRetryTime = Duration(seconds: 10); | |
| /// An implementation of the [AndroidBuilder] that delegates to gradle. | |
| class AndroidGradleBuilder implements AndroidBuilder { | |
| AndroidGradleBuilder({ | |
| required this._analytics, | |
| required this._androidStudio, | |
| required this._artifacts, | |
| required FileSystem fileSystem, | |
| required this._gradleUtils, | |
| required this._java, | |
| required Logger logger, | |
| required Platform platform, | |
| required ProcessManager processManager, | |
| this._androidSdk, | |
| }) : _fileSystem = fileSystem, | |
| _logger = logger, | |
| _fileSystemUtils = FileSystemUtils(fileSystem: fileSystem, platform: platform), | |
| _processUtils = ProcessUtils(logger: logger, processManager: processManager); | |
| AndroidGradleBuilder.fromContexts({ | |
| required this._analytics, | |
| required AndroidContext androidContext, | |
| required ToolContext toolContext, | |
| }) : _artifacts = toolContext.artifacts, | |
| _fileSystem = toolContext.fs, | |
| _gradleUtils = androidContext.gradleUtils, | |
| _java = androidContext.java, | |
| _logger = toolContext.logger, | |
| _androidStudio = androidContext.androidStudio, | |
| _androidSdk = androidContext.androidSdk, | |
| _fileSystemUtils = toolContext.fileSystemUtils, | |
| _processUtils = toolContext.processUtils; | |
| final Analytics _analytics; | |
| final Java? _java; | |
| final Logger _logger; | |
| final FileSystem _fileSystem; | |
| final Artifacts _artifacts; | |
| final GradleUtils _gradleUtils; | |
| final AndroidStudio? _androidStudio; | |
| final AndroidSdk? _androidSdk; | |
| final FileSystemUtils _fileSystemUtils; | |
| final ProcessUtils _processUtils; | |
| /// 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, | |
| required Future<void> Function(FlutterProject, {required bool releaseMode}) generateTooling, | |
| 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 in androidBuildInfo) { | |
| await generateTooling(project, releaseMode: androidBuildInfo.buildInfo.isRelease); | |
| 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<int> _runGradleTask( | |
| String taskName, { | |
| List<String> options = const <String>[], | |
| required FlutterProject project, | |
| required List<GradleHandledError> localGradleErrors, | |
| required String gradleExecutablePath, | |
| bool printOutput = true, | |
| int retry = 0, | |
| VoidCallback? preRunTask, | |
| VoidCallback? postRunTask, | |
| int? maxRetries, | |
| _OutputParser? outputParser, | |
| }) async { | |
| final bool usesAndroidX = isAppUsingAndroidX(project.android.hostAppGradleRoot); | |
| final String? agpVersion = gradle.getAgpVersion( | |
| project.android.hostAppGradleRoot, | |
| globals.logger, | |
| ); | |
| if (usesAndroidX) { | |
| _analytics.send( | |
| Event.flutterBuildInfo( | |
| label: 'app-using-android-x', | |
| buildType: 'gradle', | |
| settings: 'androidGradlePluginVersion: $agpVersion', | |
| ), | |
| ); | |
| } else if (!usesAndroidX) { | |
| _analytics.send( | |
| Event.flutterBuildInfo( | |
| label: 'app-not-using-android-x', | |
| buildType: 'gradle', | |
| settings: 'androidGradlePluginVersion: $agpVersion', | |
| ), | |
| ); | |
| } | |
| GradleHandledError? detectedGradleError; | |
| String? detectedGradleErrorLine; | |
| String? consumeLog(String line) { | |
| if (outputParser != null) { | |
| outputParser(line); | |
| } | |
| // The log lines that trigger incompatibleKotlinVersionHandler don't | |
| // always indicate an error, and there are times that that handler | |
| // covers up a more important error handler. Uniquely set it to be | |
| // the lowest priority handler by allowing it to be overridden. | |
| if (detectedGradleError != null && detectedGradleError != incompatibleKotlinVersionHandler) { | |
| // Pipe stdout/stderr from Gradle. | |
| return line; | |
| } | |
| for (final gradleError in localGradleErrors) { | |
| if (gradleError.test(line)) { | |
| detectedGradleErrorLine = line; | |
| detectedGradleError = gradleError; | |
| // The first error match wins. | |
| break; | |
| } | |
| } | |
| // Pipe stdout/stderr from Gradle. | |
| return printOutput ? line : null; | |
| } | |
| final Status status = _logger.startProgress("Running Gradle task '$taskName'..."); | |
| final command = <String>[ | |
| gradleExecutablePath, | |
| ...options, // suppresses gradle output. | |
| taskName, | |
| ]; | |
| preRunTask?.call(); | |
| var 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(); | |
| } | |
| postRunTask?.call(); | |
| if (exitCode != 0) { | |
| if (detectedGradleError == null) { | |
| _analytics.send( | |
| Event.flutterBuildInfo(label: 'gradle-unknown-failure', buildType: 'gradle'), | |
| ); | |
| return exitCode; | |
| } | |
| final GradleBuildStatus status = await detectedGradleError!.handler( | |
| line: detectedGradleErrorLine!, | |
| project: project, | |
| usesAndroidX: usesAndroidX, | |
| ); | |
| 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, min(retry, 7)).toInt() * 100, | |
| kMaxRetryTime.inMilliseconds, | |
| ); | |
| retry += 1; | |
| _logger.printStatus('Retrying Gradle Build: #$retry, wait time: ${waitTime}ms'); | |
| await Future<void>.delayed(Duration(milliseconds: waitTime)); | |
| exitCode = await _runGradleTask( | |
| taskName, | |
| options: options, | |
| preRunTask: preRunTask, | |
| postRunTask: postRunTask, | |
| localGradleErrors: localGradleErrors, | |
| gradleExecutablePath: gradleExecutablePath, | |
| printOutput: printOutput, | |
| retry: retry, | |
| project: project, | |
| maxRetries: maxRetries, | |
| ); | |
| if (exitCode == 0) { | |
| final successEventLabel = 'gradle-${detectedGradleError!.eventLabel}-success'; | |
| _analytics.send( | |
| Event.flutterBuildInfo( | |
| label: successEventLabel, | |
| buildType: 'gradle', | |
| settings: 'androidGradlePluginVersion: $agpVersion', | |
| ), | |
| ); | |
| return exitCode; | |
| } | |
| case GradleBuildStatus.exit: | |
| // Continue and throw tool exit. | |
| } | |
| } | |
| final usageLabel = 'gradle-${detectedGradleError!.eventLabel}-failure'; | |
| _analytics.send( | |
| Event.flutterBuildInfo( | |
| label: usageLabel, | |
| buildType: 'gradle', | |
| settings: 'androidGradlePluginVersion: $agpVersion', | |
| ), | |
| ); | |
| } | |
| return exitCode; | |
| } | |
| // Validate Java and Gradle compatibility after Gradle fails. | |
| // This check is done in Dart after a Gradle crash because: | |
| // 1. If Java and Gradle are incompatible, Gradle can crash during build script | |
| // compilation (e.g. Kotlin DSL compilation failing on newer JDKs) before | |
| // the Flutter Gradle Plugin (DependencyVersionChecker) is even applied. | |
| // See https://github.com/flutter/flutter/issues/189780 for context on | |
| // how JDK upgrades lead to cryptic Gradle compilation crashes. | |
| // 2. Checking only after Gradle crashes avoids blocking builds that currently | |
| // succeed despite an unsupported Java/Gradle version pair. | |
| // 3. This also helps address https://github.com/flutter/flutter/issues/167931 | |
| // by providing actionable version recommendations directly in the error. | |
| Future<void> _checkJavaAndGradleCompatibility(FlutterProject project, BuildInfo buildInfo) async { | |
| if (!buildInfo.androidSkipBuildDependencyValidation) { | |
| final Version? javaVersionObj = _java?.version; | |
| final String? javaVersion = javaVersionObj != null | |
| ? '${javaVersionObj.major}.${javaVersionObj.minor}.${javaVersionObj.patch}' | |
| : null; | |
| final String? gradleVersion = await getGradleVersionFromFile( | |
| project.android.hostAppGradleRoot, | |
| _logger, | |
| ); | |
| if (javaVersion != null && gradleVersion != null) { | |
| if (!gradle.validateJavaAndGradle( | |
| _logger, | |
| javaVersion: javaVersion, | |
| gradleVersion: gradleVersion, | |
| )) { | |
| final JavaGradleCompat? compat = gradle.getValidGradleVersionRangeForJavaVersion( | |
| _logger, | |
| javaV: javaVersion, | |
| ); | |
| final gradleRangeMax = compat != null && compat.gradleMax != null | |
| ? ' to ${compat.gradleMax}' | |
| : ''; | |
| final gradleRangeCompatSuggestion = compat != null | |
| ? '${compat.gradleMin}$gradleRangeMax or newer' | |
| : 'unknown'; | |
| final gradleRangeInfo = | |
| 'compatible Gradle versions for Java $javaVersion are $gradleRangeCompatSuggestion'; | |
| throwToolExit(""" | |
| Gradle build failed due to Java/Gradle incompatibility. | |
| The Java version used for the build is $javaVersion, which is incompatible with Gradle $gradleVersion. | |
| To fix this, you can either: | |
| 1. Upgrade your project's Gradle version (typically in gradle-wrapper.properties to a version matching the range: $gradleRangeInfo). | |
| 2. Use a different Java version for Flutter by running `flutter config --jdk-dir=<path>`."""); | |
| } | |
| } | |
| } | |
| } | |
| /// 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(_logger.terminal, _analytics); | |
| } | |
| final migrators = <ProjectMigrator>[ | |
| TopLevelGradleBuildFileMigration(project.android, _logger), | |
| AndroidStudioJavaGradleConflictMigration( | |
| _logger, | |
| project: project.android, | |
| androidStudio: _androidStudio, | |
| java: globals.java, | |
| ), | |
| MinSdkVersionMigration(project.android, _logger), | |
| MultidexRemovalMigration(project.android, _logger), | |
| CmakeAndroid16kPagesMigration(project.android, _logger), | |
| DisableBuiltInKotlinMigration(project.android, _logger), | |
| DisableNewDslMigration(project.android, _logger), | |
| ]; | |
| final migration = ProjectMigration(migrators); | |
| await migration.run(); | |
| // The default Gradle script reads the version name and number | |
| // from the local.properties file. | |
| updateLocalProperties( | |
| project: project, | |
| analytics: _analytics, | |
| androidSdk: _androidSdk, | |
| buildInfo: androidBuildInfo.buildInfo, | |
| fileSystemUtils: _fileSystemUtils, | |
| logger: _logger, | |
| ); | |
| final options = <String>[]; | |
| final String gradleExecutablePath = _gradleUtils.getExecutable(project); | |
| // All automatically created files should exist. | |
| if (configOnly) { | |
| return; | |
| } | |
| // Assembly work starts here. | |
| final BuildInfo buildInfo = androidBuildInfo.buildInfo; | |
| final String assembleTask = isBuildingBundle | |
| ? getBundleTaskFor(buildInfo) | |
| : getAssembleTaskFor(buildInfo); | |
| if (_logger.isVerbose) { | |
| options.add('--full-stacktrace'); | |
| options.add('--info'); | |
| options.add('-Pverbose=true'); | |
| } else { | |
| options.add('-q'); | |
| } | |
| if (!buildInfo.androidGradleDaemon) { | |
| options.add('--no-daemon'); | |
| } | |
| if (buildInfo.androidSkipBuildDependencyValidation) { | |
| options.add('-PskipDependencyChecks=true'); | |
| } | |
| 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}', | |
| ); | |
| options.add('-Plocal-engine-repo=${localEngineRepo.path}'); | |
| options.add('-Plocal-engine-build-mode=${buildInfo.modeName}'); | |
| options.add('-Plocal-engine-out=${localEngineInfo.targetOutPath}'); | |
| options.add('-Plocal-engine-host-out=${localEngineInfo.hostOutPath}'); | |
| options.add( | |
| '-Ptarget-platform=${_getTargetPlatformByLocalEnginePath(localEngineInfo.targetOutPath)}', | |
| ); | |
| } else if (androidBuildInfo.targetArchs.isNotEmpty) { | |
| final String targetPlatforms = androidBuildInfo.targetArchs | |
| .map((CpuArch e) => e.androidPlatformName) | |
| .join(','); | |
| options.add('-Ptarget-platform=$targetPlatforms'); | |
| } | |
| if (androidBuildInfo.releaseManifestEngineShellArgs != null) { | |
| // Base64-encode the JSON string to prevent shell or Gradle argument parser | |
| // from splitting or stripping double quotes and spaces across operating systems. | |
| final String base64JsonArgs = base64Encode( | |
| utf8.encode(jsonEncode(androidBuildInfo.releaseManifestEngineShellArgs)), | |
| ); | |
| options.add('-Pflutter.engineShellArgs=$base64JsonArgs'); | |
| } | |
| options.add('-Ptarget=$target'); | |
| // If using v1 embedding, we want to use FlutterApplication as the base app. | |
| final baseApplicationName = project.android.getEmbeddingVersion() == AndroidEmbeddingVersion.v2 | |
| ? 'android.app.Application' | |
| : 'io.flutter.app.FlutterApplication'; | |
| options.add('-Pbase-application-name=$baseApplicationName'); | |
| final List<DeferredComponent>? deferredComponents = project.manifest.deferredComponents; | |
| if (deferredComponents != null) { | |
| if (deferredComponentsEnabled) { | |
| options.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 componentNames = <String>[]; | |
| for (final DeferredComponent component in deferredComponents) { | |
| componentNames.add(component.name); | |
| } | |
| if (componentNames.isNotEmpty) { | |
| options.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, | |
| ); | |
| options.add('-Pshrink=false'); | |
| } | |
| } | |
| options.addAll(androidBuildInfo.buildInfo.toGradleConfig()); | |
| if (buildInfo.fileSystemRoots.isNotEmpty) { | |
| options.add('-Pfilesystem-roots=${buildInfo.fileSystemRoots.join('|')}'); | |
| } | |
| if (buildInfo.fileSystemScheme != null) { | |
| options.add('-Pfilesystem-scheme=${buildInfo.fileSystemScheme}'); | |
| } | |
| if (androidBuildInfo.splitPerAbi) { | |
| options.add('-Psplit-per-abi=true'); | |
| } | |
| options.addAll(_getAndroidNdkProvisioningProperties()); | |
| late Stopwatch sw; | |
| final int exitCode = await _runGradleTask( | |
| assembleTask, | |
| preRunTask: () { | |
| sw = Stopwatch()..start(); | |
| }, | |
| postRunTask: () { | |
| final Duration elapsedDuration = sw.elapsed; | |
| _analytics.send( | |
| Event.timing( | |
| workflow: 'build', | |
| variableName: 'gradle', | |
| elapsedMilliseconds: elapsedDuration.inMilliseconds, | |
| ), | |
| ); | |
| }, | |
| options: options, | |
| project: project, | |
| maxRetries: maxRetries, | |
| localGradleErrors: localGradleErrors, | |
| gradleExecutablePath: gradleExecutablePath, | |
| ); | |
| if (exitCode != 0) { | |
| await _checkJavaAndGradleCompatibility(project, androidBuildInfo.buildInfo); | |
| throwToolExit( | |
| 'Gradle task $assembleTask failed with exit code $exitCode', | |
| exitCode: exitCode, | |
| ); | |
| } | |
| if (isBuildingBundle) { | |
| final File bundleFile = findBundleFile(project, buildInfo, _logger, _analytics); | |
| if ((buildInfo.mode == BuildMode.release) && | |
| !(await _isAabStrippedOfDebugSymbols( | |
| project, | |
| bundleFile.path, | |
| androidBuildInfo.targetArchs, | |
| ))) { | |
| throwToolExit(failedToStripDebugSymbolsErrorMessage); | |
| } | |
| final appSize = (buildInfo.mode == BuildMode.debug) | |
| ? '' // Don't display the size when building a debug variant. | |
| : ' (${getSizeAsPlatformMB(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, _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, | |
| 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 appSize = (buildInfo.mode == BuildMode.debug) | |
| ? '' // Don't display the size when building a debug variant. | |
| : ' (${getSizeAsPlatformMB(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); | |
| } | |
| } | |
| } | |
| // Checks whether AGP has successfully stripped debug symbols from native libraries | |
| // - libflutter.so, aka the engine | |
| // - lib_app.so, aka the framework dart code | |
| // and moved them to the BUNDLE-METADATA directory. Block the build if this | |
| // isn't successful, as it means that debug symbols are getting included in | |
| // the final app that would be delivered to users. | |
| Future<bool> _isAabStrippedOfDebugSymbols( | |
| FlutterProject project, | |
| String aabPath, | |
| Iterable<CpuArch> targetArchs, | |
| ) async { | |
| if (_androidSdk == null) { | |
| _logger.printTrace( | |
| 'Failed to find android sdk when checking final appbundle for debug symbols.', | |
| ); | |
| return false; | |
| } | |
| final AndroidSdk androidSdk = _androidSdk; | |
| if (!androidSdk.cmdlineToolsAvailable) { | |
| _logger.printTrace( | |
| 'Failed to find cmdline-tools when checking final appbundle for debug symbols.', | |
| ); | |
| return false; | |
| } | |
| final String? apkAnalyzerPath = androidSdk.getCmdlineToolsPath(apkAnalyzerBinaryName); | |
| if (apkAnalyzerPath == null) { | |
| _logger.printTrace( | |
| 'Failed to find apkanalyzer when checking final appbundle for debug symbols.', | |
| ); | |
| return false; | |
| } | |
| final RunResult result = await _processUtils.run( | |
| <String>[apkAnalyzerPath, 'files', 'list', aabPath], | |
| workingDirectory: project.android.hostAppGradleRoot.path, | |
| environment: _java?.environment, | |
| ); | |
| if (result.exitCode != 0) { | |
| _logger.printTrace( | |
| 'apkanalyzer finished with exit code 0 when checking final appbundle for debug symbols.\n' | |
| 'stderr was: ${result.stderr}\n' | |
| 'and stdout was: ${result.stdout}', | |
| ); | |
| return false; | |
| } | |
| // As long as libflutter.so.sym/dbg and libapp.so.sym/dbg are present for at least | |
| // one architecture, assume AGP succeeded in stripping. | |
| if (!(result.stdout.contains('libflutter.so.sym') || | |
| result.stdout.contains('libflutter.so.dbg'))) { | |
| _logger.printTrace( | |
| 'libflutter.so.sym or libflutter.so.dbg not present when checking final appbundle for debug symbols.', | |
| ); | |
| return false; | |
| } | |
| if (!(result.stdout.contains('libapp.so.sym') || result.stdout.contains('libapp.so.dbg'))) { | |
| _logger.printTrace( | |
| 'libapp.so.sym or libapp.so.dbg not present when checking final appbundle for debug symbols.', | |
| ); | |
| return false; | |
| } | |
| return true; | |
| } | |
| Future<void> _performCodeSizeAnalysis( | |
| String kind, | |
| File zipFile, | |
| AndroidBuildInfo androidBuildInfo, | |
| ) async { | |
| final sizeAnalyzer = SizeAnalyzer( | |
| fileSystem: _fileSystem, | |
| logger: _logger, | |
| analytics: _analytics, | |
| ); | |
| final String archName = androidBuildInfo.targetArchs.single.androidArchName; | |
| 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}', | |
| ); | |
| _logger.printStatus( | |
| '\nTo analyze your app size in Dart DevTools, run the following command:\n' | |
| 'dart devtools --appSizeBase=${outputFile.path}', | |
| ); | |
| } | |
| /// Builds AAR and POM files. | |
| /// | |
| /// * [project] is typically [FlutterProject.current]. | |
| /// * [androidBuildInfo] is the build configuration. | |
| /// * [outputDirectory] 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 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()); | |
| command.addAll(_getAndroidNdkProvisioningProperties()); | |
| if (buildInfo.dartObfuscation && buildInfo.mode != BuildMode.release) { | |
| _logger.printStatus( | |
| 'Dart obfuscation is not supported in ${buildInfo.mode.uppercaseFriendlyName}' | |
| ' 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((CpuArch e) => e.androidPlatformName) | |
| .join(','); | |
| command.add('-Ptarget-platform=$targetPlatforms'); | |
| } | |
| command.add(aarTask); | |
| final 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; | |
| _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); | |
| await _checkJavaAndGradleCompatibility(project, androidBuildInfo.buildInfo); | |
| 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 { | |
| late Stopwatch sw; | |
| var exitCode = 1; | |
| final results = <String>[]; | |
| try { | |
| exitCode = await _runGradleTask( | |
| _kBuildVariantTaskName, | |
| preRunTask: () { | |
| sw = Stopwatch()..start(); | |
| }, | |
| postRunTask: () { | |
| final Duration elapsedDuration = sw.elapsed; | |
| _analytics.send( | |
| Event.timing( | |
| workflow: 'print', | |
| variableName: 'android build variants', | |
| elapsedMilliseconds: elapsedDuration.inMilliseconds, | |
| ), | |
| ); | |
| }, | |
| options: const <String>['-q'], | |
| project: project, | |
| localGradleErrors: gradleErrors, | |
| gradleExecutablePath: _gradleUtils.getExecutable(project), | |
| outputParser: (String line) { | |
| if (_kBuildVariantRegex.firstMatch(line) case final RegExpMatch match) { | |
| results.add(match.namedGroup(_kBuildVariantRegexGroupName)!); | |
| } | |
| }, | |
| ); | |
| } on Error catch (error) { | |
| _logger.printError(error.toString()); | |
| } | |
| if (exitCode != 0) { | |
| return const <String>[]; | |
| } | |
| return results; | |
| } | |
| @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', | |
| ); | |
| late Stopwatch sw; | |
| var exitCode = 1; | |
| try { | |
| exitCode = await _runGradleTask( | |
| taskName, | |
| preRunTask: () { | |
| sw = Stopwatch()..start(); | |
| }, | |
| postRunTask: () { | |
| final Duration elapsedDuration = sw.elapsed; | |
| _analytics.send( | |
| Event.timing( | |
| workflow: 'outputs', | |
| variableName: 'app link settings', | |
| elapsedMilliseconds: elapsedDuration.inMilliseconds, | |
| ), | |
| ); | |
| }, | |
| options: <String>['-q', '-PoutputPath=$outputPath'], | |
| project: project, | |
| localGradleErrors: gradleErrors, | |
| gradleExecutablePath: _gradleUtils.getExecutable(project), | |
| ); | |
| } on Error catch (error) { | |
| _logger.printError(error.toString()); | |
| } | |
| if (exitCode != 0) { | |
| throwToolExit('Gradle task $taskName failed with exit code $exitCode'); | |
| } | |
| return outputPath; | |
| } | |
| List<String> _getAndroidNdkProvisioningProperties() { | |
| final AndroidSdk? androidSdk = _androidSdk; | |
| if (androidSdk == null || !androidSdk.directory.existsSync()) { | |
| return const <String>[]; | |
| } | |
| final properties = <String>[ | |
| '-P$_kAndroidSdkRootProperty=${androidSdk.directory.path}', | |
| '-P$_kInstalledNdkVersionsProperty=${_getInstalledNdkVersionsForGradle(androidSdk).join(',')}', | |
| ]; | |
| final String? sdkManagerPath = androidSdk.sdkManagerPath; | |
| if (sdkManagerPath != null && | |
| androidSdk.cmdlineToolsAvailable && | |
| androidSdk.licensesAvailable) { | |
| properties.add('-P$_kSdkManagerPathProperty=$sdkManagerPath'); | |
| } | |
| return properties; | |
| } | |
| } | |
| /// 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 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/to/integrate-android-archive'); | |
| } | |
| /// Calculates the SHA-1 hash of the given [file] using chunked reading. | |
| @visibleForTesting | |
| String calculateSha(File file) { | |
| final RandomAccessFile openedFile = file.openSync(); | |
| try { | |
| final sink = AccumulatorSink<Digest>(); | |
| final ByteConversionSink sha1Sink = sha1.startChunkedConversion(sink); | |
| final buffer = Uint8List(64 * 1024); | |
| while (true) { | |
| final int bytesRead = openedFile.readIntoSync(buffer); | |
| if (bytesRead == 0) { | |
| break; | |
| } | |
| sha1Sink.add(Uint8List.sublistView(buffer, 0, bytesRead)); | |
| } | |
| sha1Sink.close(); | |
| return sink.events.single.toString(); | |
| } finally { | |
| openedFile.closeSync(); | |
| } | |
| } | |
| void _exitWithUnsupportedProjectMessage(Terminal terminal, Analytics analytics) { | |
| 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; | |
| } | |
| bool? usesAndroidX; | |
| final androidXRegExp = RegExp(r'^android\.useAndroidX(?:\s*[=:]\s*|\s+)(\S+)'); | |
| for (final String rawLine in properties.readAsLinesSync()) { | |
| final String line = rawLine.trimLeft(); | |
| if (line.isEmpty || line.startsWith('#') || line.startsWith('!')) { | |
| continue; | |
| } | |
| final RegExpMatch? match = androidXRegExp.firstMatch(line); | |
| if (match == null) { | |
| continue; | |
| } | |
| final String value = match.group(1)!.toLowerCase(); | |
| if (value == 'true') { | |
| usesAndroidX = true; | |
| } else if (value == 'false') { | |
| usesAndroidX = false; | |
| } | |
| } | |
| return usesAndroidX ?? false; | |
| } | |
| /// Returns the APK files for a given [FlutterProject] and [AndroidBuildInfo]. | |
| @visibleForTesting | |
| Iterable<String> findApkFilesModule( | |
| FlutterProject project, | |
| AndroidBuildInfo androidBuildInfo, | |
| Logger logger, | |
| 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, | |
| 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 apkPartialName = <String>[ | |
| if (androidBuildInfo.buildInfo.flavor?.isNotEmpty ?? false) | |
| androidBuildInfo.buildInfo.lowerCasedFlavor!, | |
| '$buildType.apk', | |
| ]; | |
| if (androidBuildInfo.splitPerAbi) { | |
| return <String>[ | |
| for (final CpuArch cpuArch in androidBuildInfo.targetArchs) | |
| <String>['app', cpuArch.androidArchName, ...apkPartialName].join('-'), | |
| ]; | |
| } | |
| return <String>[ | |
| <String>['app', ...apkPartialName].join('-'), | |
| ]; | |
| } | |
| @visibleForTesting | |
| File findBundleFile( | |
| FlutterProject project, | |
| BuildInfo buildInfo, | |
| Logger logger, | |
| Analytics analytics, | |
| ) { | |
| final Directory bundleDir = getBundleDirectory(project); | |
| if (!bundleDir.existsSync()) { | |
| _exitWithExpectedFileNotFound( | |
| project: project, | |
| fileExtension: '.aab', | |
| logger: logger, | |
| analytics: analytics, | |
| ); | |
| } | |
| final Iterable<File> allBundleFiles = bundleDir.listSync(recursive: true).whereType<File>().where( | |
| (File file) { | |
| return file.path.endsWith('.aab'); | |
| }, | |
| ); | |
| for (final bundleFile in allBundleFiles) { | |
| // Use lowercase bundle parent directory name to handle varying cases from Android Gradle Plugin | |
| final String bundleParentDir = bundleFile.parent.basename.toLowerCase(); | |
| if (buildInfo.flavor != null) { | |
| // Handle flavor builds (e.g., 'build/app/outputs/bundle/foo_barRelease/app-foo_bar-release.aab') | |
| if (bundleParentDir.contains( | |
| '${buildInfo.lowerCasedFlavor}${buildInfo.modeName.toLowerCase()}', | |
| ) && | |
| bundleFile.basename.endsWith('${buildInfo.modeName}.aab')) { | |
| return bundleFile; | |
| } | |
| // Support legacy Android Gradle Plugin versions that don't include flavor in AAB filename | |
| if (bundleParentDir.contains('${buildInfo.lowerCasedFlavor}${buildInfo.modeName}')) { | |
| return bundleFile; | |
| } | |
| } | |
| // Handle non-flavor builds (e.g., 'build/app/outputs/bundle/release/app-release.aab') | |
| if (bundleParentDir == buildInfo.modeName && | |
| bundleFile.basename.endsWith('${buildInfo.modeName}.aab')) { | |
| return bundleFile; | |
| } | |
| // Support legacy Android Gradle Plugin versions without build mode in AAB filename | |
| if (bundleParentDir == buildInfo.modeName && bundleFile.basename.endsWith('aab')) { | |
| return bundleFile; | |
| } | |
| } | |
| _exitWithExpectedFileNotFound( | |
| project: project, | |
| fileExtension: '.aab', | |
| logger: logger, | |
| analytics: analytics, | |
| ); | |
| } | |
| /// Throws a [ToolExit] exception and logs the event. | |
| Never _exitWithExpectedFileNotFound({ | |
| required FlutterProject project, | |
| required String fileExtension, | |
| required Logger logger, | |
| required Analytics analytics, | |
| }) { | |
| final String androidGradlePluginVersion = getGradleVersionForAndroidPlugin( | |
| project.android.hostAppGradleRoot, | |
| logger, | |
| ); | |
| final gradleBuildSettings = | |
| 'androidGradlePluginVersion: $androidGradlePluginVersion, ' | |
| 'fileExtension: $fileExtension'; | |
| 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 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 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) { | |
| var result = 'armeabi_v7a'; | |
| if (engineOutPath.contains('x64')) { | |
| result = 'x86_64'; | |
| } else if (engineOutPath.contains('arm64')) { | |
| result = 'arm64_v8a'; | |
| } | |
| return result; | |
| } | |
| String _getTargetPlatformByLocalEnginePath(String engineOutPath) { | |
| var result = 'android-arm'; | |
| if (engineOutPath.contains('x64')) { | |
| result = 'android-x64'; | |
| } else if (engineOutPath.contains('arm64')) { | |
| result = 'android-arm64'; | |
| } | |
| return result; | |
| } | |
| List<String> _getInstalledNdkVersionsForGradle(AndroidSdk androidSdk) { | |
| final Directory ndkDir = androidSdk.directory.childDirectory('ndk'); | |
| if (!ndkDir.existsSync()) { | |
| return const <String>[]; | |
| } | |
| final List<String> installedNdkVersions = | |
| ndkDir | |
| .listSync() | |
| .whereType<Directory>() | |
| .map((Directory dir) => dir.basename) | |
| .where( | |
| (String version) => | |
| ndkDir.childDirectory(version).childFile('source.properties').existsSync(), | |
| ) | |
| .toList() | |
| ..sort(); | |
| return installedNdkVersions; | |
| } |