| // 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:async'; |
| |
| import 'package:crypto/crypto.dart'; |
| import 'package:meta/meta.dart'; |
| import 'package:xml/xml.dart' as xml; |
| |
| import '../android/android_sdk.dart'; |
| import '../artifacts.dart'; |
| import '../base/common.dart'; |
| import '../base/file_system.dart'; |
| import '../base/io.dart'; |
| import '../base/logger.dart'; |
| import '../base/process.dart'; |
| import '../base/terminal.dart'; |
| import '../base/utils.dart'; |
| import '../build_info.dart'; |
| import '../cache.dart'; |
| import '../flutter_manifest.dart'; |
| import '../globals.dart' as globals; |
| import '../project.dart'; |
| import '../reporting/reporting.dart'; |
| import 'android_sdk.dart'; |
| import 'gradle_errors.dart'; |
| import 'gradle_utils.dart'; |
| |
| /// The directory where the APK artifact is generated. |
| @visibleForTesting |
| Directory getApkDirectory(FlutterProject project) { |
| return project.isModule |
| ? project.android.buildDirectory |
| .childDirectory('host') |
| .childDirectory('outputs') |
| .childDirectory('apk') |
| : project.android.buildDirectory |
| .childDirectory('app') |
| .childDirectory('outputs') |
| .childDirectory('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${toTitleCase(productFlavor)}${toTitleCase(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.flavor ?? ''; |
| 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']; |
| } |
| |
| /// Returns true if the current version of the Gradle plugin is supported. |
| bool _isSupportedVersion(AndroidProject project) { |
| final File plugin = project.hostAppGradleRoot.childFile( |
| globals.fs.path.join('buildSrc', 'src', 'main', 'groovy', 'FlutterPlugin.groovy')); |
| if (plugin.existsSync()) { |
| return false; |
| } |
| final File appGradle = project.hostAppGradleRoot.childFile( |
| globals.fs.path.join('app', 'build.gradle')); |
| if (!appGradle.existsSync()) { |
| return false; |
| } |
| for (final String line in appGradle.readAsLinesSync()) { |
| if (line.contains(RegExp(r'apply from: .*/flutter.gradle')) || |
| line.contains("def flutterPluginVersion = 'managed'")) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /// Returns the apk file created by [buildGradleProject] |
| Future<File> getGradleAppOut(AndroidProject androidProject) async { |
| if (!_isSupportedVersion(androidProject)) { |
| _exitWithUnsupportedProjectMessage(); |
| } |
| return getApkDirectory(androidProject.parent).childFile('app.apk'); |
| } |
| |
| /// Runs `gradlew dependencies`, ensuring that dependencies are resolved and |
| /// potentially downloaded. |
| Future<void> checkGradleDependencies() async { |
| final Status progress = globals.logger.startProgress( |
| 'Ensuring gradle dependencies are up to date...', |
| timeout: timeoutConfiguration.slowOperation, |
| ); |
| final FlutterProject flutterProject = FlutterProject.current(); |
| await processUtils.run(<String>[ |
| gradleUtils.getExecutable(flutterProject), |
| 'dependencies', |
| ], |
| throwOnError: true, |
| workingDirectory: flutterProject.android.hostAppGradleRoot.path, |
| environment: gradleEnvironment, |
| ); |
| androidSdk?.reinitialize(); |
| progress.stop(); |
| } |
| |
| /// Tries to create `settings_aar.gradle` in an app project by removing the subprojects |
| /// from the existing `settings.gradle` file. This operation will fail if the existing |
| /// `settings.gradle` file has local edits. |
| @visibleForTesting |
| void createSettingsAarGradle(Directory androidDirectory) { |
| final File newSettingsFile = androidDirectory.childFile('settings_aar.gradle'); |
| if (newSettingsFile.existsSync()) { |
| return; |
| } |
| final File currentSettingsFile = androidDirectory.childFile('settings.gradle'); |
| if (!currentSettingsFile.existsSync()) { |
| return; |
| } |
| final String currentFileContent = currentSettingsFile.readAsStringSync(); |
| |
| final String newSettingsRelativeFile = globals.fs.path.relative(newSettingsFile.path); |
| final Status status = globals.logger.startProgress('✏️ Creating `$newSettingsRelativeFile`...', |
| timeout: timeoutConfiguration.fastOperation); |
| |
| final String flutterRoot = globals.fs.path.absolute(Cache.flutterRoot); |
| final File deprecatedFile = globals.fs.file(globals.fs.path.join(flutterRoot, 'packages','flutter_tools', |
| 'gradle', 'deprecated_settings.gradle')); |
| assert(deprecatedFile.existsSync()); |
| final String settingsAarContent = globals.fs.file(globals.fs.path.join(flutterRoot, 'packages','flutter_tools', |
| 'gradle', 'settings_aar.gradle.tmpl')).readAsStringSync(); |
| |
| // Get the `settings.gradle` content variants that should be patched. |
| final List<String> existingVariants = deprecatedFile.readAsStringSync().split(';EOF'); |
| existingVariants.add(settingsAarContent); |
| |
| bool exactMatch = false; |
| for (final String fileContentVariant in existingVariants) { |
| if (currentFileContent.trim() == fileContentVariant.trim()) { |
| exactMatch = true; |
| break; |
| } |
| } |
| if (!exactMatch) { |
| status.cancel(); |
| globals.printStatus('$warningMark Flutter tried to create the file `$newSettingsRelativeFile`, but failed.'); |
| // Print how to manually update the file. |
| globals.printStatus(globals.fs.file(globals.fs.path.join(flutterRoot, 'packages','flutter_tools', |
| 'gradle', 'manual_migration_settings.gradle.md')).readAsStringSync()); |
| throwToolExit('Please create the file and run this command again.'); |
| } |
| // Copy the new file. |
| newSettingsFile.writeAsStringSync(settingsAarContent); |
| status.stop(); |
| globals.printStatus('$successMark `$newSettingsRelativeFile` created successfully.'); |
| } |
| |
| /// 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`. |
| /// * The plugins are built as AARs if [shouldBuildPluginAsAar] is `true`. This isn't set by default |
| /// because it makes the build slower proportional to the number of plugins. |
| /// * [retries] is the max number of build retries in case one of the [GradleHandledError] handler |
| /// returns [GradleBuildStatus.retry] or [GradleBuildStatus.retryWithAarPlugins]. |
| Future<void> buildGradleApp({ |
| @required FlutterProject project, |
| @required AndroidBuildInfo androidBuildInfo, |
| @required String target, |
| @required bool isBuildingBundle, |
| @required List<GradleHandledError> localGradleErrors, |
| bool shouldBuildPluginAsAar = false, |
| int retries = 1, |
| }) async { |
| assert(project != null); |
| assert(androidBuildInfo != null); |
| assert(target != null); |
| assert(isBuildingBundle != null); |
| assert(localGradleErrors != null); |
| assert(androidSdk != null); |
| |
| if (!project.android.isUsingGradle) { |
| _exitWithProjectNotUsingGradleMessage(); |
| } |
| if (!_isSupportedVersion(project.android)) { |
| _exitWithUnsupportedProjectMessage(); |
| } |
| final Directory buildDirectory = project.android.buildDirectory; |
| |
| final bool usesAndroidX = isAppUsingAndroidX(project.android.hostAppGradleRoot); |
| if (usesAndroidX) { |
| BuildEvent('app-using-android-x').send(); |
| } else if (!usesAndroidX) { |
| BuildEvent('app-not-using-android-x').send(); |
| globals.printStatus("$warningMark Your app isn't using AndroidX.", emphasis: true); |
| globals.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); |
| |
| if (shouldBuildPluginAsAar) { |
| // Create a settings.gradle that doesn't import the plugins as subprojects. |
| createSettingsAarGradle(project.android.hostAppGradleRoot); |
| await buildPluginsAsAar( |
| project, |
| androidBuildInfo, |
| buildDirectory: buildDirectory.childDirectory('app'), |
| ); |
| } |
| |
| final BuildInfo buildInfo = androidBuildInfo.buildInfo; |
| final String assembleTask = isBuildingBundle |
| ? getBundleTaskFor(buildInfo) |
| : getAssembleTaskFor(buildInfo); |
| |
| final Status status = globals.logger.startProgress( |
| "Running Gradle task '$assembleTask'...", |
| timeout: timeoutConfiguration.slowOperation, |
| multilineOutput: true, |
| ); |
| |
| final List<String> command = <String>[ |
| gradleUtils.getExecutable(project), |
| ]; |
| if (globals.logger.isVerbose) { |
| command.add('-Pverbose=true'); |
| } else { |
| command.add('-q'); |
| } |
| if (globals.artifacts is LocalEngineArtifacts) { |
| final LocalEngineArtifacts localEngineArtifacts = globals.artifacts as LocalEngineArtifacts; |
| final Directory localEngineRepo = _getLocalEngineRepo( |
| engineOutPath: localEngineArtifacts.engineOutPath, |
| androidBuildInfo: androidBuildInfo, |
| ); |
| globals.printTrace( |
| 'Using local engine: ${localEngineArtifacts.engineOutPath}\n' |
| 'Local Maven repo: ${localEngineRepo.path}' |
| ); |
| command.add('-Plocal-engine-repo=${localEngineRepo.path}'); |
| command.add('-Plocal-engine-build-mode=${buildInfo.modeName}'); |
| command.add('-Plocal-engine-out=${localEngineArtifacts.engineOutPath}'); |
| command.add('-Ptarget-platform=${_getTargetPlatformByLocalEnginePath( |
| localEngineArtifacts.engineOutPath)}'); |
| } else if (androidBuildInfo.targetArchs.isNotEmpty) { |
| final String targetPlatforms = androidBuildInfo |
| .targetArchs |
| .map(getPlatformNameForAndroidArch).join(','); |
| command.add('-Ptarget-platform=$targetPlatforms'); |
| } |
| if (target != null) { |
| command.add('-Ptarget=$target'); |
| } |
| assert(buildInfo.trackWidgetCreation != null); |
| command.add('-Ptrack-widget-creation=${buildInfo.trackWidgetCreation}'); |
| |
| if (buildInfo.extraFrontEndOptions != null) { |
| command.add('-Pextra-front-end-options=${buildInfo.extraFrontEndOptions.join(',')}'); |
| } |
| if (buildInfo.extraGenSnapshotOptions != null) { |
| command.add('-Pextra-gen-snapshot-options=${buildInfo.extraGenSnapshotOptions}'); |
| } |
| if (buildInfo.fileSystemRoots != null && buildInfo.fileSystemRoots.isNotEmpty) { |
| command.add('-Pfilesystem-roots=${buildInfo.fileSystemRoots.join('|')}'); |
| } |
| if (buildInfo.fileSystemScheme != null) { |
| command.add('-Pfilesystem-scheme=${buildInfo.fileSystemScheme}'); |
| } |
| if (androidBuildInfo.splitPerAbi) { |
| command.add('-Psplit-per-abi=true'); |
| } |
| if (androidBuildInfo.shrink) { |
| command.add('-Pshrink=true'); |
| } |
| if (androidBuildInfo.buildInfo.dartDefines?.isNotEmpty ?? false) { |
| command.add('-Pdart-defines=${androidBuildInfo.buildInfo.dartDefines.join(',')}'); |
| } |
| if (shouldBuildPluginAsAar) { |
| // Pass a system flag instead of a project flag, so this flag can be |
| // read from include_flutter.groovy. |
| command.add('-Dbuild-plugins-as-aars=true'); |
| // Don't use settings.gradle from the current project since it includes the plugins as subprojects. |
| command.add('--settings-file=settings_aar.gradle'); |
| } |
| if (androidBuildInfo.fastStart) { |
| command.add('-Pfast-start=true'); |
| } |
| if (androidBuildInfo.buildInfo.splitDebugInfoPath != null) { |
| command.add('-Psplit-debug-info=${androidBuildInfo.buildInfo.splitDebugInfoPath}'); |
| } |
| if (androidBuildInfo.buildInfo.treeShakeIcons) { |
| command.add('-Ptree-shake-icons=true'); |
| } |
| if (androidBuildInfo.buildInfo.dartObfuscation) { |
| command.add('-Pdart-obfuscation=true'); |
| } |
| command.add(assembleTask); |
| |
| GradleHandledError detectedGradleError; |
| String detectedGradleErrorLine; |
| String consumeLog(String line) { |
| // This message was removed from first-party plugins, |
| // but older plugin versions still display this message. |
| if (androidXPluginWarningRegex.hasMatch(line)) { |
| // Don't pipe. |
| return null; |
| } |
| 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: gradleEnvironment, |
| 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(); |
| } |
| |
| globals.flutterUsage.sendTiming('build', 'gradle', sw.elapsed); |
| |
| if (exitCode != 0) { |
| if (detectedGradleError == null) { |
| BuildEvent('gradle-unkown-failure').send(); |
| throwToolExit( |
| 'Gradle task $assembleTask failed with exit code $exitCode', |
| exitCode: exitCode, |
| ); |
| } else { |
| final GradleBuildStatus status = await detectedGradleError.handler( |
| line: detectedGradleErrorLine, |
| project: project, |
| usesAndroidX: usesAndroidX, |
| shouldBuildPluginAsAar: shouldBuildPluginAsAar, |
| ); |
| |
| if (retries >= 1) { |
| final String successEventLabel = 'gradle-${detectedGradleError.eventLabel}-success'; |
| switch (status) { |
| case GradleBuildStatus.retry: |
| await buildGradleApp( |
| project: project, |
| androidBuildInfo: androidBuildInfo, |
| target: target, |
| isBuildingBundle: isBuildingBundle, |
| localGradleErrors: localGradleErrors, |
| shouldBuildPluginAsAar: shouldBuildPluginAsAar, |
| retries: retries - 1, |
| ); |
| BuildEvent(successEventLabel).send(); |
| return; |
| case GradleBuildStatus.retryWithAarPlugins: |
| await buildGradleApp( |
| project: project, |
| androidBuildInfo: androidBuildInfo, |
| target: target, |
| isBuildingBundle: isBuildingBundle, |
| localGradleErrors: localGradleErrors, |
| shouldBuildPluginAsAar: true, |
| retries: retries - 1, |
| ); |
| BuildEvent(successEventLabel).send(); |
| return; |
| case GradleBuildStatus.exit: |
| // noop. |
| } |
| } |
| BuildEvent('gradle-${detectedGradleError.eventLabel}-failure').send(); |
| throwToolExit( |
| 'Gradle task $assembleTask failed with exit code $exitCode', |
| exitCode: exitCode, |
| ); |
| } |
| } |
| |
| if (isBuildingBundle) { |
| final File bundleFile = findBundleFile(project, buildInfo); |
| final String appSize = (buildInfo.mode == BuildMode.debug) |
| ? '' // Don't display the size when building a debug variant. |
| : ' (${getSizeAsMB(bundleFile.lengthSync())})'; |
| |
| globals.printStatus( |
| '$successMark Built ${globals.fs.path.relative(bundleFile.path)}$appSize.', |
| color: TerminalColor.green, |
| ); |
| return; |
| } |
| // Gradle produced an APK. |
| final Iterable<File> apkFiles = findApkFiles(project, androidBuildInfo); |
| final Directory apkDirectory = getApkDirectory(project); |
| // Copy the first APK to app.apk, so `flutter run` can find it. |
| // TODO(egarciad): Handle multiple APKs. |
| apkFiles.first.copySync(apkDirectory.childFile('app.apk').path); |
| globals.printTrace('calculateSha: $apkDirectory/app.apk'); |
| |
| final File apkShaFile = apkDirectory.childFile('app.apk.sha1'); |
| apkShaFile.writeAsStringSync(_calculateSha(apkFiles.first)); |
| |
| for (final File apkFile in apkFiles) { |
| final String appSize = (buildInfo.mode == BuildMode.debug) |
| ? '' // Don't display the size when building a debug variant. |
| : ' (${getSizeAsMB(apkFile.lengthSync())})'; |
| globals.printStatus( |
| '$successMark Built ${globals.fs.path.relative(apkFile.path)}$appSize.', |
| color: TerminalColor.green, |
| ); |
| } |
| } |
| |
| /// 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 { |
| assert(project != null); |
| assert(target != null); |
| assert(androidBuildInfo != null); |
| assert(outputDirectory != null); |
| assert(androidSdk != null); |
| |
| final FlutterManifest manifest = project.manifest; |
| if (!manifest.isModule && !manifest.isPlugin) { |
| throwToolExit('AARs can only be built for plugin or module projects.'); |
| } |
| |
| final String aarTask = getAarTaskFor(androidBuildInfo.buildInfo); |
| final Status status = globals.logger.startProgress( |
| "Running Gradle task '$aarTask'...", |
| timeout: timeoutConfiguration.slowOperation, |
| multilineOutput: true, |
| ); |
| |
| final String flutterRoot = globals.fs.path.absolute(Cache.flutterRoot); |
| final String initScript = globals.fs.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 (target != null && target.isNotEmpty) { |
| command.add('-Ptarget=$target'); |
| } |
| |
| if (globals.artifacts is LocalEngineArtifacts) { |
| final LocalEngineArtifacts localEngineArtifacts = globals.artifacts as LocalEngineArtifacts; |
| final Directory localEngineRepo = _getLocalEngineRepo( |
| engineOutPath: localEngineArtifacts.engineOutPath, |
| androidBuildInfo: androidBuildInfo, |
| ); |
| globals.printTrace( |
| 'Using local engine: ${localEngineArtifacts.engineOutPath}\n' |
| 'Local Maven repo: ${localEngineRepo.path}' |
| ); |
| command.add('-Plocal-engine-repo=${localEngineRepo.path}'); |
| command.add('-Plocal-engine-build-mode=${androidBuildInfo.buildInfo.modeName}'); |
| command.add('-Plocal-engine-out=${localEngineArtifacts.engineOutPath}'); |
| if (androidBuildInfo.buildInfo.treeShakeIcons) { |
| command.add('-Pfont-subset=true'); |
| } |
| |
| // Copy the local engine repo in the output directory. |
| try { |
| globals.fsUtils.copyDirectorySync( |
| localEngineRepo, |
| getRepoDirectory(outputDirectory), |
| ); |
| } on FileSystemException catch(_) { |
| throwToolExit( |
| 'Failed to copy the local engine ${localEngineRepo.path} repo ' |
| 'in ${outputDirectory.path}' |
| ); |
| } |
| command.add('-Ptarget-platform=${_getTargetPlatformByLocalEnginePath( |
| localEngineArtifacts.engineOutPath)}'); |
| } else if (androidBuildInfo.targetArchs.isNotEmpty) { |
| final String targetPlatforms = androidBuildInfo.targetArchs |
| .map(getPlatformNameForAndroidArch).join(','); |
| command.add('-Ptarget-platform=$targetPlatforms'); |
| } |
| |
| command.add(aarTask); |
| |
| final Stopwatch sw = Stopwatch()..start(); |
| RunResult result; |
| try { |
| result = await processUtils.run( |
| command, |
| workingDirectory: project.android.hostAppGradleRoot.path, |
| allowReentrantFlutter: true, |
| environment: gradleEnvironment, |
| ); |
| } finally { |
| status.stop(); |
| } |
| globals.flutterUsage.sendTiming('build', 'gradle-aar', sw.elapsed); |
| |
| if (result.exitCode != 0) { |
| globals.printStatus(result.stdout, wrap: false); |
| globals.printError(result.stderr, wrap: false); |
| throwToolExit( |
| 'Gradle task $aarTask failed with exit code $exitCode.', |
| exitCode: exitCode, |
| ); |
| } |
| final Directory repoDirectory = getRepoDirectory(outputDirectory); |
| if (!repoDirectory.existsSync()) { |
| globals.printStatus(result.stdout, wrap: false); |
| globals.printError(result.stderr, wrap: false); |
| throwToolExit( |
| 'Gradle task $aarTask failed to produce $repoDirectory.', |
| exitCode: exitCode, |
| ); |
| } |
| globals.printStatus( |
| '$successMark Built ${globals.fs.path.relative(repoDirectory.path)}.', |
| color: TerminalColor.green, |
| ); |
| } |
| |
| /// Prints how to consume the AAR from a host app. |
| void printHowToConsumeAar({ |
| @required Set<String> buildModes, |
| @required String androidPackage, |
| @required Directory repoDirectory, |
| String buildNumber, |
| }) { |
| assert(buildModes != null && buildModes.isNotEmpty); |
| assert(androidPackage != null); |
| assert(repoDirectory != null); |
| buildNumber ??= '1.0'; |
| |
| globals.printStatus(''' |
| |
| ${globals.terminal.bolden('Consuming the Module')} |
| 1. Open ${globals.fs.path.join('<host>', 'app', 'build.gradle')} |
| 2. Ensure you have the repositories configured, otherwise add them: |
| |
| repositories { |
| maven { |
| url '${repoDirectory.path}' |
| } |
| maven { |
| url 'https://storage.googleapis.com/download.flutter.io' |
| } |
| } |
| |
| 3. Make the host app depend on the Flutter module: |
| |
| dependencies {'''); |
| |
| for (final String buildMode in buildModes) { |
| globals.printStatus(""" |
| ${buildMode}Implementation '$androidPackage:flutter_$buildMode:$buildNumber'"""); |
| } |
| |
| globals.printStatus(''' |
| } |
| '''); |
| |
| if (buildModes.contains('profile')) { |
| globals.printStatus(''' |
| |
| 4. Add the `profile` build type: |
| |
| android { |
| buildTypes { |
| profile { |
| initWith debug |
| } |
| } |
| } |
| '''); |
| } |
| |
| globals.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 Stopwatch sw = Stopwatch()..start(); |
| final List<int> bytes = file.readAsBytesSync(); |
| globals.printTrace('calculateSha: reading file took ${sw.elapsedMilliseconds}us'); |
| globals.flutterUsage.sendTiming('build', 'apk-sha-read', sw.elapsed); |
| sw.reset(); |
| final String sha = _hex(sha1.convert(bytes).bytes); |
| globals.printTrace('calculateSha: computing sha took ${sw.elapsedMilliseconds}us'); |
| globals.flutterUsage.sendTiming('build', 'apk-sha-calc', sw.elapsed); |
| return sha; |
| } |
| |
| void _exitWithUnsupportedProjectMessage() { |
| BuildEvent('unsupported-project', eventError: 'gradle-plugin').send(); |
| throwToolExit( |
| '$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.', |
| ); |
| } |
| |
| void _exitWithProjectNotUsingGradleMessage() { |
| BuildEvent('unsupported-project', eventError: 'app-not-using-gradle').send(); |
| throwToolExit( |
| '$warningMark The build process for Android has changed, and the ' |
| 'current project configuration is no longer valid. Please consult\n\n' |
| 'https://github.com/flutter/flutter/wiki/Upgrading-Flutter-projects-to-build-with-gradle\n\n' |
| 'for details on how to upgrade the 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'); |
| } |
| |
| /// Builds the plugins as AARs. |
| @visibleForTesting |
| Future<void> buildPluginsAsAar( |
| FlutterProject flutterProject, |
| AndroidBuildInfo androidBuildInfo, { |
| Directory buildDirectory, |
| }) async { |
| final File flutterPluginFile = flutterProject.flutterPluginsFile; |
| if (!flutterPluginFile.existsSync()) { |
| return; |
| } |
| final List<String> plugins = flutterPluginFile.readAsStringSync().split('\n'); |
| for (final String plugin in plugins) { |
| final List<String> pluginParts = plugin.split('='); |
| if (pluginParts.length != 2) { |
| continue; |
| } |
| final Directory pluginDirectory = globals.fs.directory(pluginParts.last); |
| assert(pluginDirectory.existsSync()); |
| |
| final String pluginName = pluginParts.first; |
| final File buildGradleFile = pluginDirectory.childDirectory('android').childFile('build.gradle'); |
| if (!buildGradleFile.existsSync()) { |
| globals.printTrace("Skipping plugin $pluginName since it doesn't have a android/build.gradle file"); |
| continue; |
| } |
| globals.logger.printStatus('Building plugin $pluginName...'); |
| try { |
| await buildGradleAar( |
| project: FlutterProject.fromDirectory(pluginDirectory), |
| androidBuildInfo: AndroidBuildInfo( |
| BuildInfo( |
| BuildMode.release, // Plugins are built as release. |
| null, // Plugins don't define flavors. |
| treeShakeIcons: androidBuildInfo.buildInfo.treeShakeIcons, |
| ), |
| ), |
| target: '', |
| outputDirectory: buildDirectory, |
| buildNumber: '1.0' |
| ); |
| } on ToolExit { |
| // Log the entire plugin entry in `.flutter-plugins` since it |
| // includes the plugin name and the version. |
| BuildEvent('gradle-plugin-aar-failure', eventError: plugin).send(); |
| throwToolExit('The plugin $pluginName could not be built due to the issue above.'); |
| } |
| } |
| } |
| |
| /// Returns the APK files for a given [FlutterProject] and [AndroidBuildInfo]. |
| @visibleForTesting |
| Iterable<File> findApkFiles( |
| FlutterProject project, |
| AndroidBuildInfo androidBuildInfo, |
| ) { |
| 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]; |
| } |
| if (buildInfo.flavor != null) { |
| // Android Studio Gradle plugin v3 adds flavor to path. |
| apkFile = apkDirectory |
| .childDirectory(buildInfo.flavor) |
| .childDirectory(modeName) |
| .childFile(apkFileName); |
| if (apkFile.existsSync()) { |
| return <File>[apkFile]; |
| } |
| } |
| return const <File>[]; |
| }); |
| if (apks.isEmpty) { |
| _exitWithExpectedFileNotFound( |
| project: project, |
| fileExtension: '.apk', |
| ); |
| } |
| return apks; |
| } |
| |
| @visibleForTesting |
| File findBundleFile(FlutterProject project, BuildInfo buildInfo) { |
| 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.flavor}${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 name is `app-foo_bar-release.aab`. |
| fileCandidates.add( |
| getBundleDirectory(project) |
| .childDirectory('${buildInfo.flavor}${camelCase('_' + buildInfo.modeName)}') |
| .childFile('app-${buildInfo.flavor}-${buildInfo.modeName}.aab')); |
| } |
| for (final File bundleFile in fileCandidates) { |
| if (bundleFile.existsSync()) { |
| return bundleFile; |
| } |
| } |
| _exitWithExpectedFileNotFound( |
| project: project, |
| fileExtension: '.aab', |
| ); |
| return null; |
| } |
| |
| /// Throws a [ToolExit] exception and logs the event. |
| void _exitWithExpectedFileNotFound({ |
| @required FlutterProject project, |
| @required String fileExtension, |
| }) { |
| assert(project != null); |
| assert(fileExtension != null); |
| |
| final String androidGradlePluginVersion = |
| getGradleVersionForAndroidPlugin(project.android.hostAppGradleRoot); |
| BuildEvent('gradle-expected-file-not-found', |
| settings: |
| 'androidGradlePluginVersion: $androidGradlePluginVersion, ' |
| 'fileExtension: $fileExtension' |
| ).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) { |
| final File targetFile = globals.fs.file(targetPath); |
| if (!targetFile.existsSync()) { |
| throwToolExit("The file $targetPath wasn't found in the local engine out directory."); |
| } |
| final File linkFile = globals.fs.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) { |
| final File pomFile = globals.fs.file(pomPath); |
| if (!pomFile.existsSync()) { |
| throwToolExit("The file $pomPath wasn't found in the local engine out directory."); |
| } |
| xml.XmlDocument document; |
| try { |
| document = xml.parse(pomFile.readAsStringSync()); |
| } on xml.XmlParserException { |
| 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<xml.XmlElement> project = document.findElements('project'); |
| assert(project.isNotEmpty); |
| for (final xml.XmlElement versionElement in document.findAllElements('version')) { |
| if (versionElement.parent == project.first) { |
| return versionElement.text; |
| } |
| } |
| throwToolExit('Error while parsing the <version> element from $pomPath'); |
| return null; |
| } |
| |
| /// 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, |
| }) { |
| assert(engineOutPath != null); |
| assert(androidBuildInfo != null); |
| |
| final String abi = _getAbiByLocalEnginePath(engineOutPath); |
| final Directory localEngineRepo = globals.fs.systemTempDirectory |
| .createTempSync('flutter_tool_local_engine_repo.'); |
| |
| // Remove the local engine repo before the tool exits. |
| shutdownHooks.addShutdownHook( |
| () => localEngineRepo.deleteSync(recursive: true), |
| ShutdownStage.CLEANUP, |
| ); |
| |
| final String buildMode = androidBuildInfo.buildInfo.modeName; |
| final String artifactVersion = _getLocalArtifactVersion( |
| globals.fs.path.join( |
| engineOutPath, |
| 'flutter_embedding_$buildMode.pom', |
| ) |
| ); |
| for (final String artifact in const <String>['pom', 'jar']) { |
| // The Android embedding artifacts. |
| _createSymlink( |
| globals.fs.path.join( |
| engineOutPath, |
| 'flutter_embedding_$buildMode.$artifact', |
| ), |
| globals.fs.path.join( |
| localEngineRepo.path, |
| 'io', |
| 'flutter', |
| 'flutter_embedding_$buildMode', |
| artifactVersion, |
| 'flutter_embedding_$buildMode-$artifactVersion.$artifact', |
| ), |
| ); |
| // The engine artifacts (libflutter.so). |
| _createSymlink( |
| globals.fs.path.join( |
| engineOutPath, |
| '${abi}_$buildMode.$artifact', |
| ), |
| globals.fs.path.join( |
| localEngineRepo.path, |
| 'io', |
| 'flutter', |
| '${abi}_$buildMode', |
| artifactVersion, |
| '${abi}_$buildMode-$artifactVersion.$artifact', |
| ), |
| ); |
| } |
| 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; |
| } |