gradle_utils.dart 9.37 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_null_migrated.dart' as globals;
18 19
import '../project.dart';
import '../reporting/reporting.dart';
20
import 'android_sdk.dart';
21

22
const String _defaultGradleVersion = '6.7';
23

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

26 27
/// Provides utilities to run a Gradle task, such as finding the Gradle executable
/// or constructing a Gradle project.
28
class GradleUtils {
29
  GradleUtils({
30 31 32 33 34
    required Platform platform,
    required Logger logger,
    required FileSystem fileSystem,
    required Cache cache,
    required OperatingSystemUtils operatingSystemUtils,
35 36 37 38 39 40 41 42 43 44 45 46
  }) : _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;

47 48 49 50
  /// 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;
51
    injectGradleWrapperIfNeeded(androidDir);
52 53

    final File gradle = androidDir.childFile(
54
      _platform.isWindows ? 'gradlew.bat' : 'gradlew',
55 56
    );
    if (gradle.existsSync()) {
57
      _logger.printTrace('Using gradle from ${gradle.absolute.path}.');
58 59
      // If the Gradle executable doesn't have execute permission,
      // then attempt to set it.
60
      _operatingSystemUtils.makeExecutable(gradle);
61 62 63 64 65 66 67 68
      return gradle.absolute.path;
    }
    throwToolExit(
      'Unable to locate gradlew script. Please check that ${gradle.path} '
      'exists or that ${gradle.dirname} can be read.'
    );
  }

69 70
  /// Injects the Gradle wrapper files if any of these files don't exist in [directory].
  void injectGradleWrapperIfNeeded(Directory directory) {
71 72
    copyDirectory(
      _cache.getArtifactDirectory('gradle_wrapper'),
73 74 75 76 77
      directory,
      shouldCopyFile: (File sourceFile, File destinationFile) {
        // Don't override the existing files in the project.
        return !destinationFile.existsSync();
      },
78 79 80
      onFileCopied: (File source, File dest) {
        _operatingSystemUtils.makeExecutable(dest);
      }
81 82
    );
    // Add the `gradle-wrapper.properties` file if it doesn't exist.
83 84 85 86 87 88 89 90 91 92 93
    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 = '''
94 95 96 97 98
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\\://services.gradle.org/distributions/gradle-$gradleVersion-all.zip
99 100
''';
    propertiesFile.writeAsStringSync(propertyContents);
101 102 103 104 105 106 107 108
  }
}

/// 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.
109
String getGradleVersionForAndroidPlugin(Directory directory, Logger logger) {
110 111
  final File buildFile = directory.childFile('build.gradle');
  if (!buildFile.existsSync()) {
112
    logger.printTrace("$buildFile doesn't exist, assuming AGP version: $_defaultGradleVersion");
113 114 115 116 117
    return _defaultGradleVersion;
  }
  final String buildFileContent = buildFile.readAsStringSync();
  final Iterable<Match> pluginMatches = _androidPluginRegExp.allMatches(buildFileContent);
  if (pluginMatches.isEmpty) {
118
    logger.printTrace("$buildFile doesn't provide an AGP version, assuming AGP version: $_defaultGradleVersion");
119 120
    return _defaultGradleVersion;
  }
121
  final String? androidPluginVersion = pluginMatches.first.group(1);
122
  logger.printTrace('$buildFile provides AGP version: $androidPluginVersion');
123
  return getGradleVersionFor(androidPluginVersion ?? 'unknown');
124 125 126 127 128
}

/// Returns true if [targetVersion] is within the range [min] and [max] inclusive.
bool _isWithinVersionRange(
  String targetVersion, {
129 130
  required String min,
  required String max,
131 132 133
}) {
  assert(min != null);
  assert(max != null);
134 135 136
  final Version? parsedTargetVersion = Version.parse(targetVersion);
  final Version? minVersion = Version.parse(min);
  final Version? maxVersion = Version.parse(max);
137 138 139 140 141
  return minVersion != null &&
      maxVersion != null &&
      parsedTargetVersion != null &&
      parsedTargetVersion >= minVersion &&
      parsedTargetVersion <= maxVersion;
142 143 144 145 146
}

/// 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
147
@visibleForTesting
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
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';
  }
182 183 184
  if (_isWithinVersionRange(androidPluginVersion, min: '4.0.0', max: '4.1.0')) {
    return '6.7';
  }
185
  throwToolExit('Unsupported Android Plugin version: $androidPluginVersion.');
186 187 188 189 190 191 192 193
}

/// 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({
194 195
  required FlutterProject project,
  BuildInfo? buildInfo,
196 197
  bool requireAndroidSdk = true,
}) {
198
  if (requireAndroidSdk && globals.androidSdk == null) {
199 200 201 202 203 204 205 206 207 208 209 210 211
    exitWithNoSdkMessage();
  }
  final File localProperties = project.android.localPropertiesFile;
  bool changed = false;

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

212
  void changeIfNecessary(String key, String? value) {
213 214 215 216 217 218 219 220 221 222 223
    if (settings.values[key] == value) {
      return;
    }
    if (value == null) {
      settings.values.remove(key);
    } else {
      settings.values[key] = value;
    }
    changed = true;
  }

224
  final AndroidSdk? androidSdk = globals.androidSdk;
225 226
  if (androidSdk != null) {
    changeIfNecessary('sdk.dir', globals.fsUtils.escapePath(androidSdk.directory.path));
227 228
  }

229
  changeIfNecessary('flutter.sdk', globals.fsUtils.escapePath(Cache.flutterRoot!));
230 231
  if (buildInfo != null) {
    changeIfNecessary('flutter.buildMode', buildInfo.modeName);
232
    final String? buildName = validatedBuildNameForPlatform(
233 234
      TargetPlatform.android_arm,
      buildInfo.buildName ?? project.manifest.buildName,
235
      globals.logger,
236 237
    );
    changeIfNecessary('flutter.versionName', buildName);
238
    final String? buildNumber = validatedBuildNumberForPlatform(
239 240
      TargetPlatform.android_arm,
      buildInfo.buildNumber ?? project.manifest.buildNumber,
241
      globals.logger,
242
    );
243
    changeIfNecessary('flutter.versionCode', buildNumber);
244 245 246 247 248 249 250 251 252 253 254 255
  }

  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();
256
  final AndroidSdk? androidSdk = globals.androidSdk;
257 258
  if (androidSdk != null) {
    settings.values['sdk.dir'] = globals.fsUtils.escapePath(androidSdk.directory.path);
259 260 261 262 263
  }
  settings.writeContents(properties);
}

void exitWithNoSdkMessage() {
264
  BuildEvent('unsupported-project', type: 'gradle', eventError: 'android-sdk-not-found', flutterUsage: globals.flutterUsage).send();
265
  throwToolExit(
266
    '${globals.logger.terminal.warningMark} No Android SDK found. '
267
    'Try setting the ANDROID_SDK_ROOT environment variable.'
268 269
  );
}