| // 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: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/platform.dart'; |
| import '../base/process.dart'; |
| import '../base/terminal.dart'; |
| import '../base/utils.dart'; |
| import '../build_info.dart'; |
| import '../cache.dart'; |
| import '../convert.dart'; |
| import '../flutter_manifest.dart'; |
| import '../project.dart'; |
| import '../reporting/reporting.dart'; |
| import 'android_builder.dart'; |
| import 'android_studio.dart'; |
| import 'gradle_errors.dart'; |
| import 'gradle_utils.dart'; |
| import 'multidex.dart'; |
| |
| /// 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 '$prefix${sentenceCase(productFlavor)}${sentenceCase(buildType)}'; |
| } |
| |
| /// 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 = getNameForAndroidArch(arch); |
| 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 Logger logger, |
| required ProcessManager processManager, |
| required FileSystem fileSystem, |
| required Artifacts artifacts, |
| required Usage usage, |
| required GradleUtils gradleUtils, |
| required Platform platform, |
| }) : _logger = logger, |
| _fileSystem = fileSystem, |
| _artifacts = artifacts, |
| _usage = usage, |
| _gradleUtils = gradleUtils, |
| _fileSystemUtils = FileSystemUtils(fileSystem: fileSystem, platform: platform), |
| _processUtils = ProcessUtils(logger: logger, processManager: processManager); |
| |
| final Logger _logger; |
| final ProcessUtils _processUtils; |
| final FileSystem _fileSystem; |
| final Artifacts _artifacts; |
| final Usage _usage; |
| final GradleUtils _gradleUtils; |
| final FileSystemUtils _fileSystemUtils; |
| |
| /// 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, |
| }) async { |
| await buildGradleApp( |
| project: project, |
| androidBuildInfo: androidBuildInfo, |
| target: target, |
| isBuildingBundle: false, |
| localGradleErrors: gradleErrors, |
| ); |
| } |
| |
| /// Builds the App Bundle. |
| @override |
| Future<void> buildAab({ |
| required FlutterProject project, |
| required AndroidBuildInfo androidBuildInfo, |
| required String target, |
| bool validateDeferredComponents = true, |
| bool deferredComponentsEnabled = false, |
| }) async { |
| await buildGradleApp( |
| project: project, |
| androidBuildInfo: androidBuildInfo, |
| target: target, |
| isBuildingBundle: true, |
| localGradleErrors: gradleErrors, |
| validateDeferredComponents: validateDeferredComponents, |
| deferredComponentsEnabled: deferredComponentsEnabled, |
| ); |
| } |
| |
| /// 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, |
| bool validateDeferredComponents = true, |
| bool deferredComponentsEnabled = false, |
| int retry = 0, |
| @visibleForTesting int? maxRetries, |
| }) async { |
| |
| if (!project.android.isSupportedVersion) { |
| _exitWithUnsupportedProjectMessage(_usage, _logger.terminal); |
| } |
| |
| final bool usesAndroidX = isAppUsingAndroidX(project.android.hostAppGradleRoot); |
| if (usesAndroidX) { |
| BuildEvent('app-using-android-x', type: 'gradle', flutterUsage: _usage).send(); |
| } else if (!usesAndroidX) { |
| BuildEvent('app-not-using-android-x', type: 'gradle', flutterUsage: _usage).send(); |
| _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 BuildInfo buildInfo = androidBuildInfo.buildInfo; |
| final String assembleTask = isBuildingBundle |
| ? getBundleTaskFor(buildInfo) |
| : getAssembleTaskFor(buildInfo); |
| |
| final Status status = _logger.startProgress( |
| "Running Gradle task '$assembleTask'...", |
| ); |
| |
| final List<String> command = <String>[ |
| _gradleUtils.getExecutable(project), |
| ]; |
| 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.engineOutPath, |
| androidBuildInfo: androidBuildInfo, |
| fileSystem: _fileSystem, |
| ); |
| _logger.printTrace( |
| 'Using local engine: ${localEngineInfo.engineOutPath}\n' |
| 'Local Maven repo: ${localEngineRepo.path}' |
| ); |
| command.add('-Plocal-engine-repo=${localEngineRepo.path}'); |
| command.add('-Plocal-engine-build-mode=${buildInfo.modeName}'); |
| command.add('-Plocal-engine-out=${localEngineInfo.engineOutPath}'); |
| command.add('-Ptarget-platform=${_getTargetPlatformByLocalEnginePath( |
| localEngineInfo.engineOutPath)}'); |
| } else if (androidBuildInfo.targetArchs.isNotEmpty) { |
| final String targetPlatforms = androidBuildInfo |
| .targetArchs |
| .map(getPlatformNameForAndroidArch).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: <String, String>{ |
| if (javaPath != null) |
| 'JAVA_HOME': javaPath!, |
| }, |
| 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(); |
| } |
| |
| _usage.sendTiming('build', 'gradle', sw.elapsed); |
| |
| if (exitCode != 0) { |
| if (detectedGradleError == null) { |
| BuildEvent('gradle-unknown-failure', type: 'gradle', flutterUsage: _usage).send(); |
| 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, |
| ); |
| final String successEventLabel = 'gradle-${detectedGradleError!.eventLabel}-success'; |
| BuildEvent(successEventLabel, type: 'gradle', flutterUsage: _usage).send(); |
| return; |
| case GradleBuildStatus.exit: |
| // Continue and throw tool exit. |
| } |
| } |
| BuildEvent('gradle-${detectedGradleError?.eventLabel}-failure', type: 'gradle', flutterUsage: _usage).send(); |
| throwToolExit( |
| 'Gradle task $assembleTask failed with exit code $exitCode', |
| exitCode: exitCode, |
| ); |
| } |
| |
| if (isBuildingBundle) { |
| final File bundleFile = findBundleFile(project, buildInfo, _logger, _usage); |
| 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) |
| : 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, |
| ); |
| } |
| |
| 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, |
| ); |
| final String archName = getNameForAndroidArch(androidBuildInfo.targetArchs.single); |
| 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' |
| 'flutter pub global activate devtools; flutter pub global run 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 && !manifest.isPlugin) { |
| throwToolExit('AARs can only be built for plugin or 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.engineOutPath, |
| androidBuildInfo: androidBuildInfo, |
| fileSystem: _fileSystem, |
| ); |
| _logger.printTrace( |
| 'Using local engine: ${localEngineInfo.engineOutPath}\n' |
| 'Local Maven repo: ${localEngineRepo.path}' |
| ); |
| command.add('-Plocal-engine-repo=${localEngineRepo.path}'); |
| command.add('-Plocal-engine-build-mode=${buildInfo.modeName}'); |
| command.add('-Plocal-engine-out=${localEngineInfo.engineOutPath}'); |
| |
| // 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.engineOutPath)}'); |
| } else if (androidBuildInfo.targetArchs.isNotEmpty) { |
| final String targetPlatforms = androidBuildInfo.targetArchs |
| .map(getPlatformNameForAndroidArch).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: <String, String>{ |
| if (javaPath != null) |
| 'JAVA_HOME': javaPath!, |
| }, |
| ); |
| } finally { |
| status.stop(); |
| } |
| _usage.sendTiming('build', 'gradle-aar', sw.elapsed); |
| |
| 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, |
| ); |
| } |
| } |
| |
| /// 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.FLUTTER_STORAGE_BASE_URL ?: "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) { |
| BuildEvent('unsupported-project', type: 'gradle', eventError: 'gradle-plugin', flutterUsage: usage).send(); |
| 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, |
| ) { |
| 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, |
| ); |
| } |
| 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 (AndroidArch androidArch in androidBuildInfo.targetArchs) |
| <String>[ |
| 'app', |
| getNameForAndroidArch(androidArch), |
| ...apkPartialName, |
| ].join('-'), |
| ]; |
| } |
| return <String>[ |
| <String>[ |
| 'app', |
| ...apkPartialName, |
| ].join('-'), |
| ]; |
| } |
| |
| @visibleForTesting |
| File findBundleFile(FlutterProject project, BuildInfo buildInfo, Logger logger, Usage usage) { |
| 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')); |
| } |
| for (final File bundleFile in fileCandidates) { |
| if (bundleFile.existsSync()) { |
| return bundleFile; |
| } |
| } |
| _exitWithExpectedFileNotFound( |
| project: project, |
| fileExtension: '.aab', |
| logger: logger, |
| usage: usage, |
| ); |
| } |
| |
| /// Throws a [ToolExit] exception and logs the event. |
| Never _exitWithExpectedFileNotFound({ |
| required FlutterProject project, |
| required String fileExtension, |
| required Logger logger, |
| required Usage usage, |
| }) { |
| |
| final String androidGradlePluginVersion = |
| getGradleVersionForAndroidPlugin(project.android.hostAppGradleRoot, logger); |
| BuildEvent('gradle-expected-file-not-found', |
| type: 'gradle', |
| settings: |
| 'androidGradlePluginVersion: $androidGradlePluginVersion, ' |
| 'fileExtension: $fileExtension', |
| flutterUsage: usage, |
| ).send(); |
| 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.text; |
| } |
| } |
| throwToolExit('Error while parsing the <version> element from $pomPath'); |
| } |
| |
| /// Returns the local Maven repository for a local engine build. |
| /// For example, if the engine is built locally at <home>/engine/src/out/android_release_unopt |
| /// This method generates symlinks in the temp directory to the engine artifacts |
| /// following the convention specified on https://maven.apache.org/pom.html#Repositories |
| Directory _getLocalEngineRepo({ |
| 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; |
| } |