| // 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 = '5.6.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.6.2'; |
| } |
| 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)}'; |
| } |
| } |