| // 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 'package:meta/meta.dart'; |
| |
| import '../base/common.dart'; |
| import '../base/context.dart'; |
| import '../base/file_system.dart'; |
| import '../base/terminal.dart'; |
| import '../base/utils.dart'; |
| import '../base/version.dart'; |
| import '../build_info.dart'; |
| import '../cache.dart'; |
| import '../globals.dart' as globals; |
| import '../project.dart'; |
| import '../reporting/reporting.dart'; |
| import 'android_studio.dart'; |
| |
| /// The environment variables needed to run Gradle. |
| Map<String, String> get gradleEnvironment { |
| final Map<String, String> environment = Map<String, String>.of(globals.platform.environment); |
| if (javaPath != null) { |
| // Use java bundled with Android Studio. |
| environment['JAVA_HOME'] = javaPath; |
| } |
| // Don't log analytics for downstream Flutter commands. |
| // e.g. `flutter build bundle`. |
| environment['FLUTTER_SUPPRESS_ANALYTICS'] = 'true'; |
| return environment; |
| } |
| |
| /// 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 { |
| /// Gets the Gradle executable path and prepares the Gradle project. |
| /// This is the `gradlew` or `gradlew.bat` script in the `android/` directory. |
| String getExecutable(FlutterProject project) { |
| final Directory androidDir = project.android.hostAppGradleRoot; |
| // Update the project if needed. |
| // TODO(egarciad): https://github.com/flutter/flutter/issues/40460 |
| gradleUtils.migrateToR8(androidDir); |
| gradleUtils.injectGradleWrapperIfNeeded(androidDir); |
| |
| final File gradle = androidDir.childFile( |
| globals.platform.isWindows ? 'gradlew.bat' : 'gradlew', |
| ); |
| if (gradle.existsSync()) { |
| globals.printTrace('Using gradle from ${gradle.absolute.path}.'); |
| // If the Gradle executable doesn't have execute permission, |
| // then attempt to set it. |
| _giveExecutePermissionIfNeeded(gradle); |
| return gradle.absolute.path; |
| } |
| throwToolExit( |
| 'Unable to locate gradlew script. Please check that ${gradle.path} ' |
| 'exists or that ${gradle.dirname} can be read.' |
| ); |
| return null; |
| } |
| |
| /// 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}. ' |
| 'Please ensure that this file exists or that ${gradleProperties.dirname} can be read.' |
| ); |
| } |
| final String propertiesContent = gradleProperties.readAsStringSync(); |
| if (propertiesContent.contains('android.enableR8')) { |
| globals.printTrace('gradle.properties already sets `android.enableR8`'); |
| return; |
| } |
| globals.printTrace('set `android.enableR8=true` in gradle.properties'); |
| try { |
| if (propertiesContent.isNotEmpty && !propertiesContent.endsWith('\n')) { |
| // Add a new line if the file doesn't end with a new line. |
| gradleProperties.writeAsStringSync('\n', mode: FileMode.append); |
| } |
| 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) { |
| globals.fsUtils.copyDirectorySync( |
| globals.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) { |
| if (_hasAnyExecutableFlagSet(sourceFile)) { |
| _giveExecutePermissionIfNeeded(destinationFile); |
| } |
| }, |
| ); |
| // Add the `gradle-wrapper.properties` file if it doesn't exist. |
| final Directory propertiesDirectory = directory.childDirectory( |
| globals.fs.path.join('gradle', 'wrapper')); |
| final File propertiesFile = propertiesDirectory.childFile('gradle-wrapper.properties'); |
| if (!propertiesFile.existsSync()) { |
| propertiesDirectory.createSync(recursive: true); |
| 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, |
| ); |
| } |
| } |
| } |
| const String _defaultGradleVersion = '5.6.2'; |
| |
| final RegExp _androidPluginRegExp = RegExp(r'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); |
| } |
| |
| const int _kExecPermissionMask = 0x49; // a+x |
| |
| /// Returns [true] if [executable] has all executable flag set. |
| bool _hasAllExecutableFlagSet(File executable) { |
| final FileStat stat = executable.statSync(); |
| assert(stat.type != FileSystemEntityType.notFound); |
| globals.printTrace('${executable.path} mode: ${stat.mode} ${stat.modeString()}.'); |
| return stat.mode & _kExecPermissionMask == _kExecPermissionMask; |
| } |
| |
| /// Returns [true] if [executable] has any executable flag set. |
| bool _hasAnyExecutableFlagSet(File executable) { |
| final FileStat stat = executable.statSync(); |
| assert(stat.type != FileSystemEntityType.notFound); |
| globals.printTrace('${executable.path} mode: ${stat.mode} ${stat.modeString()}.'); |
| return stat.mode & _kExecPermissionMask != 0; |
| } |
| |
| /// Gives execute permission to [executable] if it doesn't have it already. |
| void _giveExecutePermissionIfNeeded(File executable) { |
| if (!_hasAllExecutableFlagSet(executable)) { |
| globals.printTrace('Trying to give execute permission to ${executable.path}.'); |
| globals.os.makeExecutable(executable); |
| } |
| } |
| |
| /// Returns true if [targetVersion] is within the range [min] and [max] inclusive. |
| bool _isWithinVersionRange( |
| String targetVersion, { |
| @required String min, |
| @required String max, |
| }) { |
| assert(min != null); |
| assert(max != null); |
| final Version parsedTargetVersion = Version.parse(targetVersion); |
| return parsedTargetVersion >= Version.parse(min) && |
| parsedTargetVersion <= Version.parse(max); |
| } |
| |
| /// 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('Unsupported Android Plugin version: $androidPluginVersion.'); |
| return ''; |
| } |
| |
| /// 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 && globals.androidSdk == null) { |
| exitWithNoSdkMessage(); |
| } |
| 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) { |
| return; |
| } |
| if (value == null) { |
| settings.values.remove(key); |
| } else { |
| settings.values[key] = value; |
| } |
| changed = true; |
| } |
| |
| if (globals.androidSdk != null) { |
| changeIfNecessary('sdk.dir', globals.fsUtils.escapePath(globals.androidSdk.directory)); |
| } |
| |
| changeIfNecessary('flutter.sdk', globals.fsUtils.escapePath(Cache.flutterRoot)); |
| if (buildInfo != null) { |
| changeIfNecessary('flutter.buildMode', buildInfo.modeName); |
| final String buildName = validatedBuildNameForPlatform( |
| TargetPlatform.android_arm, |
| buildInfo.buildName ?? project.manifest.buildName, |
| globals.logger, |
| ); |
| changeIfNecessary('flutter.versionName', buildName); |
| final String buildNumber = validatedBuildNumberForPlatform( |
| TargetPlatform.android_arm, |
| buildInfo.buildNumber ?? project.manifest.buildNumber, |
| globals.logger, |
| ); |
| 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 (globals.androidSdk != null) { |
| settings.values['sdk.dir'] = globals.fsUtils.escapePath(globals.androidSdk.directory); |
| } |
| settings.writeContents(properties); |
| } |
| |
| void exitWithNoSdkMessage() { |
| BuildEvent('unsupported-project', eventError: 'android-sdk-not-found', flutterUsage: globals.flutterUsage).send(); |
| throwToolExit( |
| '$warningMark No Android SDK found. ' |
| 'Try setting the ANDROID_SDK_ROOT environment variable.' |
| ); |
| } |