gradle_utils.dart 10.7 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7 8
// 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/file_system.dart';
9 10 11 12

import '../base/logger.dart';
import '../base/os.dart';
import '../base/platform.dart';
13 14 15 16
import '../base/utils.dart';
import '../base/version.dart';
import '../build_info.dart';
import '../cache.dart';
17
import '../globals.dart' as globals;
18 19
import '../project.dart';
import '../reporting/reporting.dart';
20
import 'android_sdk.dart';
21

22 23 24 25 26 27 28 29
// These are the versions used in the project templates.
//
// In general, Flutter aims to default to the latest version.
// However, this currently requires to migrate existing integration tests to the latest supported values.
//
// For more information about the latest version, check:
// https://developer.android.com/studio/releases/gradle-plugin#updating-gradle
// https://kotlinlang.org/docs/gradle.html#plugin-and-versions
30 31
const String templateDefaultGradleVersion = '7.4';
const String templateAndroidGradlePluginVersion = '7.1.2';
32
const String templateDefaultGradleVersionForModule = '7.1.2';
33
const String templateKotlinGradlePluginVersion = '1.6.10';
34

35 36 37 38 39 40 41 42 43 44 45
// These versions should match the values in flutter.gradle (FlutterExtension).
// The Flutter Gradle plugin is only applied to app projects, and modules that are built from source
// using (include_flutter.groovy).
// The remaining projects are: plugins, and modules compiled as AARs. In modules, the ephemeral directory
// `.android` is always regenerated after flutter pub get, so new versions are picked up after a
// Flutter upgrade.
const String compileSdkVersion = '31';
const String minSdkVersion = '16';
const String targetSdkVersion = '31';
const String ndkVersion = '21.1.6352462';

46
final RegExp _androidPluginRegExp = RegExp(r'com\.android\.tools\.build:gradle:(\d+\.\d+\.\d+)');
47

48 49
/// Provides utilities to run a Gradle task, such as finding the Gradle executable
/// or constructing a Gradle project.
50
class GradleUtils {
51
  GradleUtils({
52 53 54 55 56
    required Platform platform,
    required Logger logger,
    required FileSystem fileSystem,
    required Cache cache,
    required OperatingSystemUtils operatingSystemUtils,
57 58 59 60 61 62 63 64 65 66 67 68
  }) : _platform = platform,
       _logger = logger,
       _cache = cache,
       _fileSystem = fileSystem,
       _operatingSystemUtils = operatingSystemUtils;

  final Cache _cache;
  final FileSystem _fileSystem;
  final Platform _platform;
  final Logger _logger;
  final OperatingSystemUtils _operatingSystemUtils;

69 70 71 72
  /// 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;
73
    injectGradleWrapperIfNeeded(androidDir);
74 75

    final File gradle = androidDir.childFile(
76
      _platform.isWindows ? 'gradlew.bat' : 'gradlew',
77 78
    );
    if (gradle.existsSync()) {
79
      _logger.printTrace('Using gradle from ${gradle.absolute.path}.');
80 81
      // If the Gradle executable doesn't have execute permission,
      // then attempt to set it.
82
      _operatingSystemUtils.makeExecutable(gradle);
83 84 85 86 87 88 89 90
      return gradle.absolute.path;
    }
    throwToolExit(
      'Unable to locate gradlew script. Please check that ${gradle.path} '
      'exists or that ${gradle.dirname} can be read.'
    );
  }

91 92
  /// Injects the Gradle wrapper files if any of these files don't exist in [directory].
  void injectGradleWrapperIfNeeded(Directory directory) {
93 94
    copyDirectory(
      _cache.getArtifactDirectory('gradle_wrapper'),
95 96 97 98 99
      directory,
      shouldCopyFile: (File sourceFile, File destinationFile) {
        // Don't override the existing files in the project.
        return !destinationFile.existsSync();
      },
100 101 102
      onFileCopied: (File source, File dest) {
        _operatingSystemUtils.makeExecutable(dest);
      }
103 104
    );
    // Add the `gradle-wrapper.properties` file if it doesn't exist.
105 106 107 108 109 110 111 112 113 114 115
    final Directory propertiesDirectory = directory
      .childDirectory(_fileSystem.path.join('gradle', 'wrapper'));
    final File propertiesFile = propertiesDirectory
      .childFile('gradle-wrapper.properties');

    if (propertiesFile.existsSync()) {
      return;
    }
    propertiesDirectory.createSync(recursive: true);
    final String gradleVersion = getGradleVersionForAndroidPlugin(directory, _logger);
    final String propertyContents = '''
116 117 118 119 120
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\\://services.gradle.org/distributions/gradle-$gradleVersion-all.zip
121 122
''';
    propertiesFile.writeAsStringSync(propertyContents);
123 124 125 126 127 128 129 130
  }
}

/// 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.
131
String getGradleVersionForAndroidPlugin(Directory directory, Logger logger) {
132 133
  final File buildFile = directory.childFile('build.gradle');
  if (!buildFile.existsSync()) {
134 135
    logger.printTrace("$buildFile doesn't exist, assuming Gradle version: $templateDefaultGradleVersion");
    return templateDefaultGradleVersion;
136 137 138 139
  }
  final String buildFileContent = buildFile.readAsStringSync();
  final Iterable<Match> pluginMatches = _androidPluginRegExp.allMatches(buildFileContent);
  if (pluginMatches.isEmpty) {
140 141
    logger.printTrace("$buildFile doesn't provide an AGP version, assuming Gradle version: $templateDefaultGradleVersion");
    return templateDefaultGradleVersion;
142
  }
143
  final String? androidPluginVersion = pluginMatches.first.group(1);
144
  logger.printTrace('$buildFile provides AGP version: $androidPluginVersion');
145
  return getGradleVersionFor(androidPluginVersion ?? 'unknown');
146 147 148 149 150
}

/// Returns true if [targetVersion] is within the range [min] and [max] inclusive.
bool _isWithinVersionRange(
  String targetVersion, {
151 152
  required String min,
  required String max,
153 154 155
}) {
  assert(min != null);
  assert(max != null);
156 157 158
  final Version? parsedTargetVersion = Version.parse(targetVersion);
  final Version? minVersion = Version.parse(min);
  final Version? maxVersion = Version.parse(max);
159 160 161 162 163
  return minVersion != null &&
      maxVersion != null &&
      parsedTargetVersion != null &&
      parsedTargetVersion >= minVersion &&
      parsedTargetVersion <= maxVersion;
164 165 166 167 168
}

/// 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
169
@visibleForTesting
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
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';
  }
204 205 206
  if (_isWithinVersionRange(androidPluginVersion, min: '4.0.0', max: '4.1.0')) {
    return '6.7';
  }
207 208 209
  if (_isWithinVersionRange(androidPluginVersion, min: '7.0', max: '7.4')) {
    return '7.4';
  }
210
  throwToolExit('Unsupported Android Plugin version: $androidPluginVersion.');
211 212 213 214 215 216 217 218
}

/// 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({
219 220
  required FlutterProject project,
  BuildInfo? buildInfo,
221 222
  bool requireAndroidSdk = true,
}) {
223
  if (requireAndroidSdk && globals.androidSdk == null) {
224 225 226 227 228 229 230 231 232 233 234 235 236
    exitWithNoSdkMessage();
  }
  final File localProperties = project.android.localPropertiesFile;
  bool changed = false;

  SettingsFile settings;
  if (localProperties.existsSync()) {
    settings = SettingsFile.parseFromFile(localProperties);
  } else {
    settings = SettingsFile();
    changed = true;
  }

237
  void changeIfNecessary(String key, String? value) {
238 239 240 241 242 243 244 245 246 247 248
    if (settings.values[key] == value) {
      return;
    }
    if (value == null) {
      settings.values.remove(key);
    } else {
      settings.values[key] = value;
    }
    changed = true;
  }

249
  final AndroidSdk? androidSdk = globals.androidSdk;
250 251
  if (androidSdk != null) {
    changeIfNecessary('sdk.dir', globals.fsUtils.escapePath(androidSdk.directory.path));
252 253
  }

254
  changeIfNecessary('flutter.sdk', globals.fsUtils.escapePath(Cache.flutterRoot!));
255 256
  if (buildInfo != null) {
    changeIfNecessary('flutter.buildMode', buildInfo.modeName);
257
    final String? buildName = validatedBuildNameForPlatform(
258 259
      TargetPlatform.android_arm,
      buildInfo.buildName ?? project.manifest.buildName,
260
      globals.logger,
261 262
    );
    changeIfNecessary('flutter.versionName', buildName);
263
    final String? buildNumber = validatedBuildNumberForPlatform(
264 265
      TargetPlatform.android_arm,
      buildInfo.buildNumber ?? project.manifest.buildNumber,
266
      globals.logger,
267
    );
268
    changeIfNecessary('flutter.versionCode', buildNumber);
269 270 271 272 273 274 275 276 277 278 279 280
  }

  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();
281
  final AndroidSdk? androidSdk = globals.androidSdk;
282 283
  if (androidSdk != null) {
    settings.values['sdk.dir'] = globals.fsUtils.escapePath(androidSdk.directory.path);
284 285 286 287 288
  }
  settings.writeContents(properties);
}

void exitWithNoSdkMessage() {
289
  BuildEvent('unsupported-project', type: 'gradle', eventError: 'android-sdk-not-found', flutterUsage: globals.flutterUsage).send();
290
  throwToolExit(
291
    '${globals.logger.terminal.warningMark} No Android SDK found. '
292
    'Try setting the ANDROID_SDK_ROOT environment variable.'
293 294
  );
}