gradle_utils.dart 10.6 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
// 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
29
// https://kotlinlang.org/docs/releases.html#release-details
30
const String templateDefaultGradleVersion = '7.5';
31 32
const String templateAndroidGradlePluginVersion = '7.3.0';
const String templateDefaultGradleVersionForModule = '7.3.0';
33
const String templateKotlinGradlePluginVersion = '1.7.10';
34

35 36 37 38 39 40 41 42 43
// 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';
44
const String ndkVersion = '23.1.7779620';
45

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 156
  final Version? parsedTargetVersion = Version.parse(targetVersion);
  final Version? minVersion = Version.parse(min);
  final Version? maxVersion = Version.parse(max);
157 158 159 160 161
  return minVersion != null &&
      maxVersion != null &&
      parsedTargetVersion != null &&
      parsedTargetVersion >= minVersion &&
      parsedTargetVersion <= maxVersion;
162 163 164 165 166
}

/// 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
167
@visibleForTesting
168 169 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
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';
  }
202 203 204
  if (_isWithinVersionRange(androidPluginVersion, min: '4.0.0', max: '4.1.0')) {
    return '6.7';
  }
205 206
  if (_isWithinVersionRange(androidPluginVersion, min: '7.0', max: '7.5')) {
    return '7.5';
207
  }
208
  throwToolExit('Unsupported Android Plugin version: $androidPluginVersion.');
209 210 211 212 213 214 215 216
}

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

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

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

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

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

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

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