// Copyright 2016 The Chromium 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 '../android/android_sdk.dart'; import '../artifacts.dart'; import '../base/common.dart'; import '../base/context.dart'; import '../base/file_system.dart'; import '../base/io.dart'; import '../base/logger.dart'; import '../base/os.dart'; import '../base/platform.dart'; import '../base/process.dart'; import '../base/terminal.dart'; import '../base/utils.dart'; import '../base/version.dart'; import '../build_info.dart'; import '../cache.dart'; import '../features.dart'; import '../flutter_manifest.dart'; import '../globals.dart'; import '../project.dart'; import '../reporting/reporting.dart'; import 'android_sdk.dart'; import 'android_studio.dart'; /// Gradle utils in the current [AppContext]. GradleUtils get gradleUtils => context.get<GradleUtils>(); /// Provides utilities to run a Gradle task, /// such as finding the Gradle executable or constructing a Gradle project. class GradleUtils { /// Empty constructor. GradleUtils(); String _cachedExecutable; /// Gets the Gradle executable path. /// This is the `gradlew` or `gradlew.bat` script in the `android/` directory. Future<String> getExecutable(FlutterProject project) async { _cachedExecutable ??= await _initializeGradle(project); return _cachedExecutable; } GradleProject _cachedAppProject; /// Gets the [GradleProject] for the current [FlutterProject] if built as an app. Future<GradleProject> get appProject async { _cachedAppProject ??= await _readGradleProject(isLibrary: false); return _cachedAppProject; } GradleProject _cachedLibraryProject; /// Gets the [GradleProject] for the current [FlutterProject] if built as a library. Future<GradleProject> get libraryProject async { _cachedLibraryProject ??= await _readGradleProject(isLibrary: true); return _cachedLibraryProject; } } final RegExp _assembleTaskPattern = RegExp(r'assemble(\S+)'); enum FlutterPluginVersion { none, v1, v2, managed, } // Investigation documented in #13975 suggests the filter should be a subset // of the impact of -q, but users insist they see the error message sometimes // anyway. If we can prove it really is impossible, delete the filter. // This technically matches everything *except* the NDK message, since it's // passed to a function that filters out all lines that don't match a filter. final RegExp ndkMessageFilter = RegExp(r'^(?!NDK is missing a ".*" directory' r'|If you are not using NDK, unset the NDK variable from ANDROID_NDK_HOME or local.properties to remove this warning' r'|If you are using NDK, verify the ndk.dir is set to a valid NDK directory. It is currently set to .*)'); // This regex is intentionally broad. AndroidX errors can manifest in multiple // different ways and each one depends on the specific code config and // filesystem paths of the project. Throwing the broadest net possible here to // catch all known and likely cases. // // Example stack traces: // // https://github.com/flutter/flutter/issues/27226 "AAPT: error: resource android:attr/fontVariationSettings not found." // https://github.com/flutter/flutter/issues/27106 "Android resource linking failed|Daemon: AAPT2|error: failed linking references" // https://github.com/flutter/flutter/issues/27493 "error: cannot find symbol import androidx.annotation.NonNull;" // https://github.com/flutter/flutter/issues/23995 "error: package android.support.annotation does not exist import android.support.annotation.NonNull;" final RegExp androidXFailureRegex = RegExp(r'(AAPT|androidx|android\.support)'); final RegExp androidXPluginWarningRegex = RegExp(r'\*{57}' r"|WARNING: This version of (\w+) will break your Android build if it or its dependencies aren't compatible with AndroidX." r'|See https://goo.gl/CP92wY for more information on the problem and how to fix it.' r'|This warning prints for all Android build failures. The real root cause of the error may be unrelated.'); FlutterPluginVersion getFlutterPluginVersion(AndroidProject project) { final File plugin = project.hostAppGradleRoot.childFile( fs.path.join('buildSrc', 'src', 'main', 'groovy', 'FlutterPlugin.groovy')); if (plugin.existsSync()) { final String packageLine = plugin.readAsLinesSync().skip(4).first; if (packageLine == 'package io.flutter.gradle') { return FlutterPluginVersion.v2; } return FlutterPluginVersion.v1; } final File appGradle = project.hostAppGradleRoot.childFile( fs.path.join('app', 'build.gradle')); if (appGradle.existsSync()) { for (String line in appGradle.readAsLinesSync()) { if (line.contains(RegExp(r'apply from: .*/flutter.gradle'))) { return FlutterPluginVersion.managed; } if (line.contains("def flutterPluginVersion = 'managed'")) { return FlutterPluginVersion.managed; } } } return FlutterPluginVersion.none; } /// Returns the apk file created by [buildGradleProject] Future<File> getGradleAppOut(AndroidProject androidProject) async { switch (getFlutterPluginVersion(androidProject)) { case FlutterPluginVersion.none: // Fall through. Pretend we're v1, and just go with it. case FlutterPluginVersion.v1: return androidProject.gradleAppOutV1File; case FlutterPluginVersion.managed: // Fall through. The managed plugin matches plugin v2 for now. case FlutterPluginVersion.v2: final GradleProject gradleProject = await gradleUtils.appProject; return fs.file(gradleProject.apkDirectory.childFile('app.apk')); } return null; } /// Runs `gradlew dependencies`, ensuring that dependencies are resolved and /// potentially downloaded. Future<void> checkGradleDependencies() async { final Status progress = logger.startProgress('Ensuring gradle dependencies are up to date...', timeout: timeoutConfiguration.slowOperation); final FlutterProject flutterProject = FlutterProject.current(); final String gradlew = await gradleUtils.getExecutable(flutterProject); await processUtils.run( <String>[gradlew, 'dependencies'], throwOnError: true, workingDirectory: flutterProject.android.hostAppGradleRoot.path, environment: _gradleEnv, ); 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. 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 = fs.path.relative(newSettingsFile.path); final Status status = logger.startProgress('✏️ Creating `$newSettingsRelativeFile`...', timeout: timeoutConfiguration.fastOperation); final String flutterRoot = fs.path.absolute(Cache.flutterRoot); final File deprecatedFile = fs.file(fs.path.join(flutterRoot, 'packages','flutter_tools', 'gradle', 'deprecated_settings.gradle')); assert(deprecatedFile.existsSync()); final String settingsAarContent = fs.file(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 (String fileContentVariant in existingVariants) { if (currentFileContent.trim() == fileContentVariant.trim()) { exactMatch = true; break; } } if (!exactMatch) { status.cancel(); printError('*******************************************************************************************'); printError('Flutter tried to create the file `$newSettingsRelativeFile`, but failed.'); // Print how to manually update the file. printError(fs.file(fs.path.join(flutterRoot, 'packages','flutter_tools', 'gradle', 'manual_migration_settings.gradle.md')).readAsStringSync()); printError('*******************************************************************************************'); throwToolExit('Please create the file and run this command again.'); } // Copy the new file. newSettingsFile.writeAsStringSync(settingsAarContent); status.stop(); printStatus('✅ `$newSettingsRelativeFile` created successfully.'); } // Note: Dependencies are resolved and possibly downloaded as a side-effect // of calculating the app properties using Gradle. This may take minutes. Future<GradleProject> _readGradleProject({bool isLibrary = false}) async { final FlutterProject flutterProject = FlutterProject.current(); final String gradlew = await gradleUtils.getExecutable(flutterProject); updateLocalProperties(project: flutterProject); final FlutterManifest manifest = flutterProject.manifest; final Directory hostAppGradleRoot = flutterProject.android.hostAppGradleRoot; if (featureFlags.isPluginAsAarEnabled && !manifest.isPlugin && !manifest.isModule) { createSettingsAarGradle(hostAppGradleRoot); } if (manifest.isPlugin) { assert(isLibrary); return GradleProject( <String>['debug', 'profile', 'release'], <String>[], // Plugins don't have flavors. flutterProject.directory.childDirectory('build').path, ); } final Status status = logger.startProgress('Resolving dependencies...', timeout: timeoutConfiguration.slowOperation); GradleProject project; // Get the properties and tasks from Gradle, so we can determinate the `buildDir`, // flavors and build types defined in the project. If gradle fails, then check if the failure is due to t try { final RunResult propertiesRunResult = await processUtils.run( <String>[gradlew, isLibrary ? 'properties' : 'app:properties'], throwOnError: true, workingDirectory: hostAppGradleRoot.path, environment: _gradleEnv, ); final RunResult tasksRunResult = await processUtils.run( <String>[gradlew, isLibrary ? 'tasks': 'app:tasks', '--all', '--console=auto'], throwOnError: true, workingDirectory: hostAppGradleRoot.path, environment: _gradleEnv, ); project = GradleProject.fromAppProperties(propertiesRunResult.stdout, tasksRunResult.stdout); } catch (exception) { if (getFlutterPluginVersion(flutterProject.android) == FlutterPluginVersion.managed) { status.cancel(); // Handle known exceptions. throwToolExitIfLicenseNotAccepted(exception); // Print a general Gradle error and exit. printError('* Error running Gradle:\n$exception\n'); throwToolExit('Please review your Gradle project setup in the android/ folder.'); } // Fall back to the default project = GradleProject( <String>['debug', 'profile', 'release'], <String>[], fs.path.join(flutterProject.android.hostAppGradleRoot.path, 'app', 'build') ); } status.stop(); return project; } /// Handle Gradle error thrown when Gradle needs to download additional /// Android SDK components (e.g. Platform Tools), and the license /// for that component has not been accepted. void throwToolExitIfLicenseNotAccepted(Exception exception) { const String licenseNotAcceptedMatcher = r'You have not accepted the license agreements of the following SDK components:' r'\s*\[(.+)\]'; final RegExp licenseFailure = RegExp(licenseNotAcceptedMatcher, multiLine: true); final Match licenseMatch = licenseFailure.firstMatch(exception.toString()); if (licenseMatch != null) { final String missingLicenses = licenseMatch.group(1); final String errorMessage = '\n\n* Error running Gradle:\n' 'Unable to download needed Android SDK components, as the following licenses have not been accepted:\n' '$missingLicenses\n\n' 'To resolve this, please run the following command in a Terminal:\n' 'flutter doctor --android-licenses'; throwToolExit(errorMessage); } } String _locateGradlewExecutable(Directory directory) { final File gradle = directory.childFile( platform.isWindows ? 'gradlew.bat' : 'gradlew', ); if (gradle.existsSync()) { return gradle.absolute.path; } return null; } // Note: Gradle may be bootstrapped and possibly downloaded as a side-effect // of validating the Gradle executable. This may take several seconds. Future<String> _initializeGradle(FlutterProject project) async { final Directory android = project.android.hostAppGradleRoot; final Status status = logger.startProgress('Initializing gradle...', timeout: timeoutConfiguration.slowOperation); // Update the project if needed. // TODO(egarciad): https://github.com/flutter/flutter/issues/40460. migrateToR8(android); injectGradleWrapperIfNeeded(android); final String gradle = _locateGradlewExecutable(android); if (gradle == null) { status.stop(); throwToolExit('Unable to locate gradlew script'); } printTrace('Using gradle from $gradle.'); // Validates the Gradle executable by asking for its version. // Makes Gradle Wrapper download and install Gradle distribution, if needed. try { await processUtils.run( <String>[gradle, '-v'], throwOnError: true, environment: _gradleEnv, ); } on ProcessException catch (e) { final String error = e.toString(); if (error.contains('java.io.FileNotFoundException: https://downloads.gradle.org') || error.contains('java.io.IOException: Unable to tunnel through proxy')) { throwToolExit('$gradle threw an error while trying to update itself.\n$e'); } rethrow; } finally { status.stop(); } return gradle; } /// Migrates the Android's [directory] to R8. /// https://developer.android.com/studio/build/shrink-code @visibleForTesting void migrateToR8(Directory directory) { final File gradleProperties = directory.childFile('gradle.properties'); if (!gradleProperties.existsSync()) { throwToolExit('Expected file ${gradleProperties.path}.'); } final String propertiesContent = gradleProperties.readAsStringSync(); if (propertiesContent.contains('android.enableR8')) { printTrace('gradle.properties already sets `android.enableR8`'); return; } printTrace('set `android.enableR8=true` in gradle.properties'); try { gradleProperties .writeAsStringSync('android.enableR8=true\n', mode: FileMode.append); } on FileSystemException { throwToolExit( 'The tool failed to add `android.enableR8=true` to ${gradleProperties.path}. ' 'Please update the file manually and try this command again.' ); } } /// Injects the Gradle wrapper files if any of these files don't exist in [directory]. void injectGradleWrapperIfNeeded(Directory directory) { copyDirectorySync( cache.getArtifactDirectory('gradle_wrapper'), directory, shouldCopyFile: (File sourceFile, File destinationFile) { // Don't override the existing files in the project. return !destinationFile.existsSync(); }, onFileCopied: (File sourceFile, File destinationFile) { final String modes = sourceFile.statSync().modeString(); if (modes != null && modes.contains('x')) { os.makeExecutable(destinationFile); } }, ); // Add the `gradle-wrapper.properties` file if it doesn't exist. final File propertiesFile = directory.childFile( fs.path.join('gradle', 'wrapper', 'gradle-wrapper.properties')); if (!propertiesFile.existsSync()) { final String gradleVersion = getGradleVersionForAndroidPlugin(directory); propertiesFile.writeAsStringSync(''' distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists distributionUrl=https\\://services.gradle.org/distributions/gradle-$gradleVersion-all.zip ''', flush: true, ); } } /// Returns true if [targetVersion] is within the range [min] and [max] inclusive. bool _isWithinVersionRange(String targetVersion, {String min, String max}) { final Version parsedTargetVersion = Version.parse(targetVersion); return parsedTargetVersion >= Version.parse(min) && parsedTargetVersion <= Version.parse(max); } const String defaultGradleVersion = '4.10.2'; /// Returns the Gradle version that is required by the given Android Gradle plugin version /// by picking the largest compatible version from /// https://developer.android.com/studio/releases/gradle-plugin#updating-gradle String getGradleVersionFor(String androidPluginVersion) { if (_isWithinVersionRange(androidPluginVersion, min: '1.0.0', max: '1.1.3')) { return '2.3'; } if (_isWithinVersionRange(androidPluginVersion, min: '1.2.0', max: '1.3.1')) { return '2.9'; } if (_isWithinVersionRange(androidPluginVersion, min: '1.5.0', max: '1.5.0')) { return '2.2.1'; } if (_isWithinVersionRange(androidPluginVersion, min: '2.0.0', max: '2.1.2')) { return '2.13'; } if (_isWithinVersionRange(androidPluginVersion, min: '2.1.3', max: '2.2.3')) { return '2.14.1'; } if (_isWithinVersionRange(androidPluginVersion, min: '2.3.0', max: '2.9.9')) { return '3.3'; } if (_isWithinVersionRange(androidPluginVersion, min: '3.0.0', max: '3.0.9')) { return '4.1'; } if (_isWithinVersionRange(androidPluginVersion, min: '3.1.0', max: '3.1.9')) { return '4.4'; } if (_isWithinVersionRange(androidPluginVersion, min: '3.2.0', max: '3.2.1')) { return '4.6'; } if (_isWithinVersionRange(androidPluginVersion, min: '3.3.0', max: '3.3.2')) { return '4.10.2'; } if (_isWithinVersionRange(androidPluginVersion, min: '3.4.0', max: '3.5.0')) { return '5.1.1'; } throwToolExit('Unsuported Android Plugin version: $androidPluginVersion.'); return ''; } final RegExp _androidPluginRegExp = RegExp('com\.android\.tools\.build\:gradle\:(\\d+\.\\d+\.\\d+\)'); /// Returns the Gradle version that the current Android plugin depends on when found, /// otherwise it returns a default version. /// /// The Android plugin version is specified in the [build.gradle] file within /// the project's Android directory. String getGradleVersionForAndroidPlugin(Directory directory) { final File buildFile = directory.childFile('build.gradle'); if (!buildFile.existsSync()) { return defaultGradleVersion; } final String buildFileContent = buildFile.readAsStringSync(); final Iterable<Match> pluginMatches = _androidPluginRegExp.allMatches(buildFileContent); if (pluginMatches.isEmpty) { return defaultGradleVersion; } final String androidPluginVersion = pluginMatches.first.group(1); return getGradleVersionFor(androidPluginVersion); } /// Overwrite local.properties in the specified Flutter project's Android /// sub-project, if needed. /// /// If [requireAndroidSdk] is true (the default) and no Android SDK is found, /// this will fail with a [ToolExit]. void updateLocalProperties({ @required FlutterProject project, BuildInfo buildInfo, bool requireAndroidSdk = true, }) { if (requireAndroidSdk) { _exitIfNoAndroidSdk(); } final File localProperties = project.android.localPropertiesFile; bool changed = false; SettingsFile settings; if (localProperties.existsSync()) { settings = SettingsFile.parseFromFile(localProperties); } else { settings = SettingsFile(); changed = true; } void changeIfNecessary(String key, String value) { if (settings.values[key] != value) { if (value == null) { settings.values.remove(key); } else { settings.values[key] = value; } changed = true; } } final FlutterManifest manifest = project.manifest; if (androidSdk != null) { changeIfNecessary('sdk.dir', escapePath(androidSdk.directory)); } changeIfNecessary('flutter.sdk', escapePath(Cache.flutterRoot)); if (buildInfo != null) { changeIfNecessary('flutter.buildMode', buildInfo.modeName); final String buildName = validatedBuildNameForPlatform(TargetPlatform.android_arm, buildInfo.buildName ?? manifest.buildName); changeIfNecessary('flutter.versionName', buildName); final String buildNumber = validatedBuildNumberForPlatform(TargetPlatform.android_arm, buildInfo.buildNumber ?? manifest.buildNumber); changeIfNecessary('flutter.versionCode', buildNumber?.toString()); } if (changed) { settings.writeContents(localProperties); } } /// Writes standard Android local properties to the specified [properties] file. /// /// Writes the path to the Android SDK, if known. void writeLocalProperties(File properties) { final SettingsFile settings = SettingsFile(); if (androidSdk != null) { settings.values['sdk.dir'] = escapePath(androidSdk.directory); } settings.writeContents(properties); } /// Throws a ToolExit, if the path to the Android SDK is not known. void _exitIfNoAndroidSdk() { if (androidSdk == null) { throwToolExit('Unable to locate Android SDK. Please run `flutter doctor` for more details.'); } } Future<void> buildGradleProject({ @required FlutterProject project, @required AndroidBuildInfo androidBuildInfo, @required String target, @required bool isBuildingBundle, }) async { // Update the local.properties file with the build mode, version name and code. // FlutterPlugin v1 reads local.properties to determine build mode. Plugin v2 // uses the standard Android way to determine what to build, but we still // update local.properties, in case we want to use it in the future. // Version name and number are provided by the pubspec.yaml file // and can be overwritten with flutter build command. // The default Gradle script reads the version name and number // from the local.properties file. updateLocalProperties(project: project, buildInfo: androidBuildInfo.buildInfo); switch (getFlutterPluginVersion(project.android)) { case FlutterPluginVersion.none: // Fall through. Pretend it's v1, and just go for it. case FlutterPluginVersion.v1: return _buildGradleProjectV1(project); case FlutterPluginVersion.managed: // Fall through. Managed plugin builds the same way as plugin v2. case FlutterPluginVersion.v2: return _buildGradleProjectV2(project, androidBuildInfo, target, isBuildingBundle); } } Future<void> buildGradleAar({ @required FlutterProject project, @required AndroidBuildInfo androidBuildInfo, @required String target, @required String outputDir, }) async { final FlutterManifest manifest = project.manifest; GradleProject gradleProject; if (manifest.isModule) { gradleProject = await gradleUtils.appProject; } else if (manifest.isPlugin) { gradleProject = await gradleUtils.libraryProject; } else { throwToolExit('AARs can only be built for plugin or module projects.'); } if (outputDir != null && outputDir.isNotEmpty) { gradleProject.buildDirectory = outputDir; } final String aarTask = gradleProject.aarTaskFor(androidBuildInfo.buildInfo); if (aarTask == null) { printUndefinedTask(gradleProject, androidBuildInfo.buildInfo); throwToolExit('Gradle build aborted.'); } final Status status = logger.startProgress( 'Running Gradle task \'$aarTask\'...', timeout: timeoutConfiguration.slowOperation, multilineOutput: true, ); final String gradlew = await gradleUtils.getExecutable(project); final String flutterRoot = fs.path.absolute(Cache.flutterRoot); final String initScript = fs.path.join(flutterRoot, 'packages','flutter_tools', 'gradle', 'aar_init_script.gradle'); final List<String> command = <String>[ gradlew, '-I=$initScript', '-Pflutter-root=$flutterRoot', '-Poutput-dir=${gradleProject.buildDirectory}', '-Pis-plugin=${manifest.isPlugin}', '-Dbuild-plugins-as-aars=true', ]; if (target != null && target.isNotEmpty) { command.add('-Ptarget=$target'); } if (androidBuildInfo.targetArchs.isNotEmpty) { final String targetPlatforms = androidBuildInfo.targetArchs .map(getPlatformNameForAndroidArch).join(','); command.add('-Ptarget-platform=$targetPlatforms'); } if (artifacts is LocalEngineArtifacts) { final LocalEngineArtifacts localEngineArtifacts = artifacts; printTrace('Using local engine: ${localEngineArtifacts.engineOutPath}'); command.add('-PlocalEngineOut=${localEngineArtifacts.engineOutPath}'); } command.add(aarTask); final Stopwatch sw = Stopwatch()..start(); int exitCode = 1; try { exitCode = await processUtils.stream( command, workingDirectory: project.android.hostAppGradleRoot.path, allowReentrantFlutter: true, environment: _gradleEnv, mapFunction: (String line) { // Always print the full line in verbose mode. if (logger.isVerbose) { return line; } return null; }, ); } finally { status.stop(); } flutterUsage.sendTiming('build', 'gradle-aar', Duration(milliseconds: sw.elapsedMilliseconds)); if (exitCode != 0) { throwToolExit('Gradle task $aarTask failed with exit code $exitCode', exitCode: exitCode); } final Directory repoDirectory = gradleProject.repoDirectory; if (!repoDirectory.existsSync()) { throwToolExit('Gradle task $aarTask failed to produce $repoDirectory', exitCode: exitCode); } printStatus('Built ${fs.path.relative(repoDirectory.path)}.', color: TerminalColor.green); } Future<void> _buildGradleProjectV1(FlutterProject project) async { final String gradlew = await gradleUtils.getExecutable(project); // Run 'gradlew build'. final Status status = logger.startProgress( 'Running \'gradlew build\'...', timeout: timeoutConfiguration.slowOperation, multilineOutput: true, ); final Stopwatch sw = Stopwatch()..start(); final int exitCode = await processUtils.stream( <String>[fs.file(gradlew).absolute.path, 'build'], workingDirectory: project.android.hostAppGradleRoot.path, allowReentrantFlutter: true, environment: _gradleEnv, ); status.stop(); flutterUsage.sendTiming('build', 'gradle-v1', Duration(milliseconds: sw.elapsedMilliseconds)); if (exitCode != 0) { throwToolExit('Gradle build failed: $exitCode', exitCode: exitCode); } printStatus('Built ${fs.path.relative(project.android.gradleAppOutV1File.path)}.'); } String _hex(List<int> bytes) { final StringBuffer result = StringBuffer(); for (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(); printTrace('calculateSha: reading file took ${sw.elapsedMilliseconds}us'); flutterUsage.sendTiming('build', 'apk-sha-read', Duration(milliseconds: sw.elapsedMilliseconds)); sw.reset(); final String sha = _hex(sha1.convert(bytes).bytes); printTrace('calculateSha: computing sha took ${sw.elapsedMilliseconds}us'); flutterUsage.sendTiming('build', 'apk-sha-calc', Duration(milliseconds: sw.elapsedMilliseconds)); return sha; } void printUndefinedTask(GradleProject project, BuildInfo buildInfo) { printError(''); printError('The Gradle project does not define a task suitable for the requested build.'); if (!project.buildTypes.contains(buildInfo.modeName)) { printError('Review the android/app/build.gradle file and ensure it defines a ${buildInfo.modeName} build type.'); return; } if (project.productFlavors.isEmpty) { printError('The android/app/build.gradle file does not define any custom product flavors.'); printError('You cannot use the --flavor option.'); } else { printError('The android/app/build.gradle file defines product flavors: ${project.productFlavors.join(', ')}'); printError('You must specify a --flavor option to select one of them.'); } } Future<void> _buildGradleProjectV2( FlutterProject flutterProject, AndroidBuildInfo androidBuildInfo, String target, bool isBuildingBundle, ) async { final String gradlew = await gradleUtils.getExecutable(flutterProject); final GradleProject project = await gradleUtils.appProject; final BuildInfo buildInfo = androidBuildInfo.buildInfo; String assembleTask; if (isBuildingBundle) { assembleTask = project.bundleTaskFor(buildInfo); } else { assembleTask = project.assembleTaskFor(buildInfo); } if (assembleTask == null) { printUndefinedTask(project, buildInfo); throwToolExit('Gradle build aborted.'); } final Status status = logger.startProgress( 'Running Gradle task \'$assembleTask\'...', timeout: timeoutConfiguration.slowOperation, multilineOutput: true, ); final List<String> command = <String>[gradlew]; if (logger.isVerbose) { command.add('-Pverbose=true'); } else { command.add('-q'); } if (artifacts is LocalEngineArtifacts) { final LocalEngineArtifacts localEngineArtifacts = artifacts; printTrace('Using local engine: ${localEngineArtifacts.engineOutPath}'); command.add('-PlocalEngineOut=${localEngineArtifacts.engineOutPath}'); } 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}'); } 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.targetArchs.isNotEmpty) { final String targetPlatforms = androidBuildInfo.targetArchs .map(getPlatformNameForAndroidArch).join(','); command.add('-Ptarget-platform=$targetPlatforms'); } if (featureFlags.isPluginAsAarEnabled) { // 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'); if (!flutterProject.manifest.isModule) { command.add('--settings-file=settings_aar.gradle'); } } command.add(assembleTask); bool potentialAndroidXFailure = false; bool potentialR8Failure = false; final Stopwatch sw = Stopwatch()..start(); int exitCode = 1; try { exitCode = await processUtils.stream( command, workingDirectory: flutterProject.android.hostAppGradleRoot.path, allowReentrantFlutter: true, environment: _gradleEnv, // TODO(mklim): if AndroidX warnings are no longer required, this // mapFunction and all its associated variabled can be replaced with just // `filter: ndkMessagefilter`. mapFunction: (String line) { final bool isAndroidXPluginWarning = androidXPluginWarningRegex.hasMatch(line); if (!isAndroidXPluginWarning && androidXFailureRegex.hasMatch(line)) { potentialAndroidXFailure = true; } // R8 errors include references to this package. if (!potentialR8Failure && androidBuildInfo.shrink && line.contains('com.android.tools.r8')) { potentialR8Failure = true; } // Always print the full line in verbose mode. if (logger.isVerbose) { return line; } else if (isAndroidXPluginWarning || !ndkMessageFilter.hasMatch(line)) { return null; } return line; }, ); } finally { status.stop(); } if (exitCode != 0) { if (potentialR8Failure) { final String exclamationMark = terminal.color('[!]', TerminalColor.red); printStatus('$exclamationMark The shrinker may have failed to optimize the Java bytecode.', emphasis: true); printStatus('To disable the shrinker, pass the `--no-shrink` flag to this command.', indent: 4); printStatus('To learn more, see: https://developer.android.com/studio/build/shrink-code', indent: 4); BuildEvent('r8-failure').send(); } else if (potentialAndroidXFailure) { printStatus('AndroidX incompatibilities may have caused this build to fail. See https://goo.gl/CP92wY.'); BuildEvent('android-x-failure').send(); } throwToolExit('Gradle task $assembleTask failed with exit code $exitCode', exitCode: exitCode); } flutterUsage.sendTiming('build', 'gradle-v2', Duration(milliseconds: sw.elapsedMilliseconds)); if (!isBuildingBundle) { final Iterable<File> apkFiles = findApkFiles(project, androidBuildInfo); if (apkFiles.isEmpty) { throwToolExit('Gradle build failed to produce an Android package.'); } // Copy the first APK to app.apk, so `flutter run`, `flutter install`, etc. can find it. // TODO(blasten): Handle multiple APKs. apkFiles.first.copySync(project.apkDirectory.childFile('app.apk').path); printTrace('calculateSha: ${project.apkDirectory}/app.apk'); final File apkShaFile = project.apkDirectory.childFile('app.apk.sha1'); apkShaFile.writeAsStringSync(_calculateSha(apkFiles.first)); for (File apkFile in apkFiles) { String appSize; if (buildInfo.mode == BuildMode.debug) { appSize = ''; } else { appSize = ' (${getSizeAsMB(apkFile.lengthSync())})'; } printStatus('Built ${fs.path.relative(apkFile.path)}$appSize.', color: TerminalColor.green); } } else { final File bundleFile = findBundleFile(project, buildInfo); if (bundleFile == null) { throwToolExit('Gradle build failed to produce an Android bundle package.'); } String appSize; if (buildInfo.mode == BuildMode.debug) { appSize = ''; } else { appSize = ' (${getSizeAsMB(bundleFile.lengthSync())})'; } printStatus('Built ${fs.path.relative(bundleFile.path)}$appSize.', color: TerminalColor.green); } } @visibleForTesting Iterable<File> findApkFiles(GradleProject project, AndroidBuildInfo androidBuildInfo) { final Iterable<String> apkFileNames = project.apkFilesFor(androidBuildInfo); if (apkFileNames.isEmpty) { return const <File>[]; } return apkFileNames.expand<File>((String apkFileName) { File apkFile = project.apkDirectory.childFile(apkFileName); if (apkFile.existsSync()) { return <File>[apkFile]; } final BuildInfo buildInfo = androidBuildInfo.buildInfo; final String modeName = camelCase(buildInfo.modeName); apkFile = project.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 = project.apkDirectory .childDirectory(buildInfo.flavor) .childDirectory(modeName) .childFile(apkFileName); if (apkFile.existsSync()) { return <File>[apkFile]; } } return const <File>[]; }); } @visibleForTesting File findBundleFile(GradleProject project, BuildInfo buildInfo) { final List<File> fileCandidates = <File>[ project.bundleDirectory .childDirectory(camelCase(buildInfo.modeName)) .childFile('app.aab'), project.bundleDirectory .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( project.bundleDirectory .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( project.bundleDirectory .childDirectory('${buildInfo.flavor}${camelCase('_' + buildInfo.modeName)}') .childFile('app-${buildInfo.flavor}-${buildInfo.modeName}.aab')); } for (final File bundleFile in fileCandidates) { if (bundleFile.existsSync()) { return bundleFile; } } return null; } Map<String, String> get _gradleEnv { final Map<String, String> env = Map<String, String>.from(platform.environment); if (javaPath != null) { // Use java bundled with Android Studio. env['JAVA_HOME'] = javaPath; } // Don't log analytics for downstream Flutter commands. // e.g. `flutter build bundle`. env['FLUTTER_SUPPRESS_ANALYTICS'] = 'true'; return env; } class GradleProject { GradleProject( this.buildTypes, this.productFlavors, this.buildDirectory, ); factory GradleProject.fromAppProperties(String properties, String tasks) { // Extract build directory. final String buildDirectory = properties .split('\n') .firstWhere((String s) => s.startsWith('buildDir: ')) .substring('buildDir: '.length) .trim(); // Extract build types and product flavors. final Set<String> variants = <String>{}; for (String s in tasks.split('\n')) { final Match match = _assembleTaskPattern.matchAsPrefix(s); if (match != null) { final String variant = match.group(1).toLowerCase(); if (!variant.endsWith('test')) { variants.add(variant); } } } final Set<String> buildTypes = <String>{}; final Set<String> productFlavors = <String>{}; for (final String variant1 in variants) { for (final String variant2 in variants) { if (variant2.startsWith(variant1) && variant2 != variant1) { final String buildType = variant2.substring(variant1.length); if (variants.contains(buildType)) { buildTypes.add(buildType); productFlavors.add(variant1); } } } } if (productFlavors.isEmpty) { buildTypes.addAll(variants); } return GradleProject( buildTypes.toList(), productFlavors.toList(), buildDirectory, ); } /// The build types such as [release] or [debug]. final List<String> buildTypes; /// The product flavors defined in build.gradle. final List<String> productFlavors; /// The build directory. This is typically <project>build/. String buildDirectory; /// The directory where the APK artifact is generated. Directory get apkDirectory { return fs.directory(fs.path.join(buildDirectory, 'outputs', 'apk')); } /// The directory where the app bundle artifact is generated. Directory get bundleDirectory { return fs.directory(fs.path.join(buildDirectory, 'outputs', 'bundle')); } /// The directory where the repo is generated. /// Only applicable to AARs. Directory get repoDirectory { return fs.directory(fs.path.join(buildDirectory, 'outputs', 'repo')); } String _buildTypeFor(BuildInfo buildInfo) { final String modeName = camelCase(buildInfo.modeName); if (buildTypes.contains(modeName.toLowerCase())) { return modeName; } return null; } String _productFlavorFor(BuildInfo buildInfo) { if (buildInfo.flavor == null) { return productFlavors.isEmpty ? '' : null; } else if (productFlavors.contains(buildInfo.flavor)) { return buildInfo.flavor; } return null; } String assembleTaskFor(BuildInfo buildInfo) { final String buildType = _buildTypeFor(buildInfo); final String productFlavor = _productFlavorFor(buildInfo); if (buildType == null || productFlavor == null) { return null; } return 'assemble${toTitleCase(productFlavor)}${toTitleCase(buildType)}'; } Iterable<String> apkFilesFor(AndroidBuildInfo androidBuildInfo) { final String buildType = _buildTypeFor(androidBuildInfo.buildInfo); final String productFlavor = _productFlavorFor(androidBuildInfo.buildInfo); if (buildType == null || productFlavor == null) { return const <String>[]; } 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']; } String bundleTaskFor(BuildInfo buildInfo) { final String buildType = _buildTypeFor(buildInfo); final String productFlavor = _productFlavorFor(buildInfo); if (buildType == null || productFlavor == null) { return null; } return 'bundle${toTitleCase(productFlavor)}${toTitleCase(buildType)}'; } String aarTaskFor(BuildInfo buildInfo) { final String buildType = _buildTypeFor(buildInfo); final String productFlavor = _productFlavorFor(buildInfo); if (buildType == null || productFlavor == null) { return null; } return 'assembleAar${toTitleCase(productFlavor)}${toTitleCase(buildType)}'; } }