Unverified Commit 2383400f authored by Reid Baker's avatar Reid Baker Committed by GitHub

Add Java-Gradle-AGP validation to flutter analyze (#123916)

https://github.com/flutter/flutter/issues/123917

Doc covering a broad set of issues related to android studio updating. 

https://docs.google.com/document/d/1hTXkjbUrBnXgu8NQsth1c3aEqo77rWoEj8CcsQ39wwQ/edit?pli=1#

Specifically this pr: 
- Adds new functions to find a projects AGP, Gradle and java versions,
and tests.
- Adds new functions that take versions and parse if the versions are
compatible with each other, and tests.
- Adds validator for `flutter analyze --suggestions` that evaluates the
java/gradle/agp versions and checks if they are compatible, and
integration test.
- Updates the version of gradle used by
dev/integration_tests/flutter_gallery/ to the minimum supported by java
18 so that the integration tests pass (It is unknown why the java
version is 18.9 instead of 11)
- Moves `isWithinVersionRange` to version.dart, and tests. 
- Adds FakeAndroidStudio to fakes to be used in multiple tests but does
not remove existing copies.

Metrics will be included as part of the definition of done for this bug
but not as part of this cl. It is already too big.

Known work still left in this pr: 
* Understand why analyze integration tests are failing. 


Example output if Java and gradle are not compatible: 
```
┌───────────────────────────────────────────────────────────────────┐
│ General Info                                                      │
│ [✓] App Name: espresso_example                                    │
│ [✓] Supported Platforms: android                                  │
│ [✓] Is Flutter Package: yes                                       │
│ [✓] Uses Material Design: yes                                     │
│ [✓] Is Plugin: no                                                 │
│ [✗] Java/Gradle/Android Gradle Plugin:                            │
│                                                                   │
│ Incompatible Java/Gradle versions.                                │
│                                                                   │
│ Java Version: 17.0.6, Gradle Version: 7.0.2                       │
│                                                                   │
│ See the link below for more information.                          │
│ https://docs.gradle.org/current/userguide/compatibility.html#java │
│                                                                   │
└───────────────────────────────────────────────────────────────────┘
```
Example output if Gradle and AGP are not compatible
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ General Info                                                                │
│ [✓] App Name: espresso_example                                              │
│ [✓] Supported Platforms: android                                            │
│ [✓] Is Flutter Package: yes                                                 │
│ [✓] Uses Material Design: yes                                               │
│ [✓] Is Plugin: no                                                           │
│ [✗] Java/Gradle/Android Gradle Plugin: Incompatible Gradle/AGP versions.    │
│                                                                             │
│ Gradle Version: 7.0.2, AGP Version: 7.4.2                                   │
│                                                                             │
│ Update gradle to at least "7.5".                                            │
│ See the link below for more information:                                    │
│ https://developer.android.com/studio/releases/gradle-plugin#updating-gradle │
│                                                                             │
│ Incompatible Java/Gradle versions.                                          │
│                                                                             │
│ Java Version: 17.0.6, Gradle Version: 7.0.2                                 │
│                                                                             │
│ See the link below for more information:                                    │
│ https://docs.gradle.org/current/userguide/compatibility.html#java           │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
```
Example output if Java/Gradle/Agp are not compatible. 
```

┌─────────────────────────────────────────────────────────────────────────────┐
│ General Info                                                                │
│ [✓] App Name: espresso_example                                              │
│ [✓] Supported Platforms: android                                            │
│ [✓] Is Flutter Package: yes                                                 │
│ [✓] Uses Material Design: yes                                               │
│ [✓] Is Plugin: no                                                           │
│ [✗] Java/Gradle/Android Gradle Plugin: Incompatible Gradle/AGP versions.    │
│                                                                             │
│ Gradle Version: 7.0.2, AGP Version: 7.4.2                                   │
│                                                                             │
│ Update gradle to at least "7.5".                                            │
│ See the link below for more information:                                    │
│ https://developer.android.com/studio/releases/gradle-plugin#updating-gradle │
│                                                                             │
│ Incompatible Java/Gradle versions.                                          │
│                                                                             │
│ Java Version: 17.0.6, Gradle Version: 7.0.2                                 │
│                                                                             │
│ See the link below for more information:                                    │
│ https://docs.gradle.org/current/userguide/compatibility.html#java           │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
```

Commit messages
- Add function to gradle_utils.dart that gets the gradle version from
wrapper or system and add a test for each situation
- Add method to get agp version, add method to validate agp against
gradle version, update documentation, add tests for agp validation.
- Update dart doc for validateGradleAndAgp to describe where the info
came from and corner case behavior, create function to validate java and
gradle and hardcode return to false
- Fill out and test java gradle compatibility function in gradle_utils
- Hook up java gradle evaluateion to hasValidJavaGradleAgpVersions with
hardcoded java version
- Add java --version output parsing and tests
- Add getJavaBinary test
- Update comment in android_sdk for mac behavior with java_home -v

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] All existing and new tests are passing.
parent a32f0bb7
...@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME ...@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:meta/meta.dart';
import '../base/common.dart'; import '../base/common.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
import '../base/os.dart'; import '../base/os.dart';
...@@ -409,6 +411,57 @@ class AndroidSdk { ...@@ -409,6 +411,57 @@ class AndroidSdk {
return null; return null;
} }
/// Returns the version of java in the format \d(.\d)+(.\d)+
/// Returns null if version not found.
String? getJavaVersion({
required AndroidStudio? androidStudio,
required FileSystem fileSystem,
required OperatingSystemUtils operatingSystemUtils,
required Platform platform,
required ProcessUtils processUtils,
}) {
final String? javaBinary = findJavaBinary(
androidStudio: androidStudio,
fileSystem: fileSystem,
operatingSystemUtils: operatingSystemUtils,
platform: platform,
);
if (javaBinary == null) {
globals.printTrace('Could not find java binary to get version.');
return null;
}
final RunResult result = processUtils.runSync(
<String>[javaBinary, '--version'],
environment: sdkManagerEnv,
);
if (result.exitCode != 0) {
globals.printTrace(
'java --version failed: exitCode: ${result.exitCode} stdout: ${result.stdout} stderr: ${result.stderr}');
return null;
}
return parseJavaVersion(result.stdout);
}
/// Extracts JDK version from the output of java --version.
@visibleForTesting
static String? parseJavaVersion(String rawVersionOutput) {
// The contents that matter come in the format '11.0.18' or '1.8.0_202'.
final RegExp jdkVersionRegex = RegExp(r'\d+\.\d+(\.\d+(?:_\d+)?)?');
final Iterable<RegExpMatch> matches =
jdkVersionRegex.allMatches(rawVersionOutput);
if (matches.isEmpty) {
globals.logger.printWarning(_formatJavaVersionWarning(rawVersionOutput));
return null;
}
final String? versionString = matches.first.group(0);
if (versionString == null || versionString.split('_').isEmpty) {
globals.logger.printWarning(_formatJavaVersionWarning(rawVersionOutput));
return null;
}
// Trim away _d+ from versions 1.8 and below.
return versionString.split('_').first;
}
/// First try Java bundled with Android Studio, then sniff JAVA_HOME, then fallback to PATH. /// First try Java bundled with Android Studio, then sniff JAVA_HOME, then fallback to PATH.
static String? findJavaBinary({ static String? findJavaBinary({
required AndroidStudio? androidStudio, required AndroidStudio? androidStudio,
...@@ -417,12 +470,15 @@ class AndroidSdk { ...@@ -417,12 +470,15 @@ class AndroidSdk {
required Platform platform, required Platform platform,
}) { }) {
if (androidStudio?.javaPath != null) { if (androidStudio?.javaPath != null) {
globals.printTrace("Using Android Studio's java.");
return fileSystem.path.join(androidStudio!.javaPath!, 'bin', 'java'); return fileSystem.path.join(androidStudio!.javaPath!, 'bin', 'java');
} }
final String? javaHomeEnv = platform.environment[_javaHomeEnvironmentVariable]; final String? javaHomeEnv =
platform.environment[_javaHomeEnvironmentVariable];
if (javaHomeEnv != null) { if (javaHomeEnv != null) {
// Trust JAVA_HOME. // Trust JAVA_HOME.
globals.printTrace('Using JAVA_HOME.');
return fileSystem.path.join(javaHomeEnv, 'bin', 'java'); return fileSystem.path.join(javaHomeEnv, 'bin', 'java');
} }
...@@ -430,23 +486,48 @@ class AndroidSdk { ...@@ -430,23 +486,48 @@ class AndroidSdk {
// See: http://stackoverflow.com/questions/14292698/how-do-i-check-if-the-java-jdk-is-installed-on-mac. // See: http://stackoverflow.com/questions/14292698/how-do-i-check-if-the-java-jdk-is-installed-on-mac.
if (platform.isMacOS) { if (platform.isMacOS) {
try { try {
final String javaHomeOutput = globals.processUtils.runSync( // -v Filter versions (as if JAVA_VERSION had been set in the environment).
<String>['/usr/libexec/java_home', '-v', '1.8'], // It is unlikley that filtering to java version 1.8 is the right
throwOnError: true, // decision here. That said, trying this on a mac shows the same jdk
hideStdout: true, // path no matter what input is passed.
).stdout.trim(); final String javaHomeOutput = globals.processUtils
.runSync(
<String>['/usr/libexec/java_home', '-v', '1.8'],
throwOnError: true,
hideStdout: true,
)
.stdout
.trim();
if (javaHomeOutput.isNotEmpty) { if (javaHomeOutput.isNotEmpty) {
final String javaHome = javaHomeOutput.split('\n').last.trim(); final String javaHome = javaHomeOutput.split('\n').last.trim();
globals.printTrace('Using mac JAVA_HOME.');
return fileSystem.path.join(javaHome, 'bin', 'java'); return fileSystem.path.join(javaHome, 'bin', 'java');
} }
} on Exception { /* ignore */ } } on Exception {/* ignore */}
} }
// Fallback to PATH based lookup. // Fallback to PATH based lookup.
return operatingSystemUtils.which(_javaExecutable)?.path; final String? pathJava = operatingSystemUtils.which(_javaExecutable)?.path;
if (pathJava != null) {
globals.printTrace('Using java from PATH.');
} else {
globals.printTrace('Could not find java path.');
}
return pathJava;
}
// Returns a user visible String that says the tool failed to parse
// the version of java along with the output.
static String _formatJavaVersionWarning(String javaVersionRaw) {
return 'Could not parse java version from: \n'
'$javaVersionRaw \n'
'If there is a version please look for an existing bug '
'https://github.com/flutter/flutter/issues/'
' and if one does not exist file a new issue.';
} }
Map<String, String>? _sdkManagerEnv; Map<String, String>? _sdkManagerEnv;
/// Returns an environment with the Java folder added to PATH for use in calling /// Returns an environment with the Java folder added to PATH for use in calling
/// Java-based Android SDK commands such as sdkmanager and avdmanager. /// Java-based Android SDK commands such as sdkmanager and avdmanager.
Map<String, String> get sdkManagerEnv { Map<String, String> get sdkManagerEnv {
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
// found in the LICENSE file. // found in the LICENSE file.
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:process/process.dart';
import '../base/common.dart'; import '../base/common.dart';
import '../base/file_system.dart'; import '../base/file_system.dart';
...@@ -43,7 +44,33 @@ const String minSdkVersion = '16'; ...@@ -43,7 +44,33 @@ const String minSdkVersion = '16';
const String targetSdkVersion = '31'; const String targetSdkVersion = '31';
const String ndkVersion = '23.1.7779620'; const String ndkVersion = '23.1.7779620';
final RegExp _androidPluginRegExp = RegExp(r'com\.android\.tools\.build:gradle:(\d+\.\d+\.\d+)'); // Update this when new versions of Gradle come out including minor versions
//
// Supported here means supported by the tooling for
// flutter analyze --suggestions and does not imply broader flutter support.
const String _maxKnownAndSupportedGradleVersion = '8.0.2';
// Update this when new versions of AGP come out.
@visibleForTesting
const String maxKnownAgpVersion = '8.1';
// Expected content:
// "classpath 'com.android.tools.build:gradle:7.3.0'"
// Parentheticals are use to group which helps with version extraction.
// "...build:gradle:(...)" where group(1) should be the version string.
final RegExp _androidGradlePluginRegExp =
RegExp(r'com\.android\.tools\.build:gradle:(\d+\.\d+\.\d+)');
// From https://docs.gradle.org/current/userguide/command_line_interface.html#command_line_interface
const String gradleVersionFlag = r'--version';
// Directory under android/ that gradle uses to store gradle information.
// Regularly used with [gradleWrapperDirectory] and
// [gradleWrapperPropertiesFilename].
// Different from the directory of gradle files stored in
// `_cache.getArtifactDirectory('gradle_wrapper')`
const String gradleDirectoryName = 'gradle';
const String gradleWrapperDirectoryName = 'wrapper';
const String gradleWrapperPropertiesFilename = 'gradle-wrapper.properties';
/// Provides utilities to run a Gradle task, such as finding the Gradle executable /// Provides utilities to run a Gradle task, such as finding the Gradle executable
/// or constructing a Gradle project. /// or constructing a Gradle project.
...@@ -51,17 +78,14 @@ class GradleUtils { ...@@ -51,17 +78,14 @@ class GradleUtils {
GradleUtils({ GradleUtils({
required Platform platform, required Platform platform,
required Logger logger, required Logger logger,
required FileSystem fileSystem,
required Cache cache, required Cache cache,
required OperatingSystemUtils operatingSystemUtils, required OperatingSystemUtils operatingSystemUtils,
}) : _platform = platform, }) : _platform = platform,
_logger = logger, _logger = logger,
_cache = cache, _cache = cache,
_fileSystem = fileSystem,
_operatingSystemUtils = operatingSystemUtils; _operatingSystemUtils = operatingSystemUtils;
final Cache _cache; final Cache _cache;
final FileSystem _fileSystem;
final Platform _platform; final Platform _platform;
final Logger _logger; final Logger _logger;
final OperatingSystemUtils _operatingSystemUtils; final OperatingSystemUtils _operatingSystemUtils;
...@@ -83,9 +107,8 @@ class GradleUtils { ...@@ -83,9 +107,8 @@ class GradleUtils {
return gradle.absolute.path; return gradle.absolute.path;
} }
throwToolExit( throwToolExit(
'Unable to locate gradlew script. Please check that ${gradle.path} ' 'Unable to locate gradlew script. Please check that ${gradle.path} '
'exists or that ${gradle.dirname} can be read.' 'exists or that ${gradle.dirname} can be read.');
);
} }
/// Injects the Gradle wrapper files if any of these files don't exist in [directory]. /// Injects the Gradle wrapper files if any of these files don't exist in [directory].
...@@ -103,15 +126,17 @@ class GradleUtils { ...@@ -103,15 +126,17 @@ class GradleUtils {
); );
// Add the `gradle-wrapper.properties` file if it doesn't exist. // Add the `gradle-wrapper.properties` file if it doesn't exist.
final Directory propertiesDirectory = directory final Directory propertiesDirectory = directory
.childDirectory(_fileSystem.path.join('gradle', 'wrapper')); .childDirectory(gradleDirectoryName)
final File propertiesFile = propertiesDirectory .childDirectory(gradleWrapperDirectoryName);
.childFile('gradle-wrapper.properties'); final File propertiesFile =
propertiesDirectory.childFile(gradleWrapperPropertiesFilename);
if (propertiesFile.existsSync()) { if (propertiesFile.existsSync()) {
return; return;
} }
propertiesDirectory.createSync(recursive: true); propertiesDirectory.createSync(recursive: true);
final String gradleVersion = getGradleVersionForAndroidPlugin(directory, _logger); final String gradleVersion =
getGradleVersionForAndroidPlugin(directory, _logger);
final String propertyContents = ''' final String propertyContents = '''
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
...@@ -131,11 +156,12 @@ distributionUrl=https\\://services.gradle.org/distributions/gradle-$gradleVersio ...@@ -131,11 +156,12 @@ distributionUrl=https\\://services.gradle.org/distributions/gradle-$gradleVersio
String getGradleVersionForAndroidPlugin(Directory directory, Logger logger) { String getGradleVersionForAndroidPlugin(Directory directory, Logger logger) {
final File buildFile = directory.childFile('build.gradle'); final File buildFile = directory.childFile('build.gradle');
if (!buildFile.existsSync()) { if (!buildFile.existsSync()) {
logger.printTrace("$buildFile doesn't exist, assuming Gradle version: $templateDefaultGradleVersion"); logger.printTrace(
"$buildFile doesn't exist, assuming Gradle version: $templateDefaultGradleVersion");
return templateDefaultGradleVersion; return templateDefaultGradleVersion;
} }
final String buildFileContent = buildFile.readAsStringSync(); final String buildFileContent = buildFile.readAsStringSync();
final Iterable<Match> pluginMatches = _androidPluginRegExp.allMatches(buildFileContent); final Iterable<Match> pluginMatches = _androidGradlePluginRegExp.allMatches(buildFileContent);
if (pluginMatches.isEmpty) { if (pluginMatches.isEmpty) {
logger.printTrace("$buildFile doesn't provide an AGP version, assuming Gradle version: $templateDefaultGradleVersion"); logger.printTrace("$buildFile doesn't provide an AGP version, assuming Gradle version: $templateDefaultGradleVersion");
return templateDefaultGradleVersion; return templateDefaultGradleVersion;
...@@ -145,65 +171,426 @@ String getGradleVersionForAndroidPlugin(Directory directory, Logger logger) { ...@@ -145,65 +171,426 @@ String getGradleVersionForAndroidPlugin(Directory directory, Logger logger) {
return getGradleVersionFor(androidPluginVersion ?? 'unknown'); return getGradleVersionFor(androidPluginVersion ?? 'unknown');
} }
/// Returns true if [targetVersion] is within the range [min] and [max] inclusive. /// Returns either the gradle-wrapper.properties value from the passed in
bool _isWithinVersionRange( /// [directory] or if not present the version available in local path.
String targetVersion, { ///
required String min, /// If gradle version is not found null is returned.
required String max, /// [directory] should be and android directory with a build.gradle file.
}) { Future<String?> getGradleVersion(
final Version? parsedTargetVersion = Version.parse(targetVersion); Directory directory, Logger logger, ProcessManager processManager) async {
final Version? minVersion = Version.parse(min); final File propertiesFile = directory
final Version? maxVersion = Version.parse(max); .childDirectory(gradleDirectoryName)
return minVersion != null && .childDirectory(gradleWrapperDirectoryName)
maxVersion != null && .childFile(gradleWrapperPropertiesFilename);
parsedTargetVersion != null &&
parsedTargetVersion >= minVersion && if (propertiesFile.existsSync()) {
parsedTargetVersion <= maxVersion; final String wrapperFileContent = propertiesFile.readAsStringSync();
// Expected content format (with lines above and below).
// Version can have 2 or 3 numbers.
// 'distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-all.zip'
final RegExp distributionUrlRegex =
RegExp(r'distributionUrl\s?=\s?.*\.zip');
final RegExpMatch? distributionUrl =
distributionUrlRegex.firstMatch(wrapperFileContent);
if (distributionUrl != null) {
// Expected content: 'gradle-7.4.2-all.zip'
final String? gradleZip = distributionUrl.group(0);
if (gradleZip != null) {
final List<String> zipParts = gradleZip.split('-');
if (zipParts.length > 2) {
final String gradleVersion = zipParts[1];
return gradleVersion;
} else {
// Did not find gradle zip url. Likely this is a bug in our parsing.
logger.printWarning(_formatParseWarning(wrapperFileContent));
}
} else {
// Did not find gradle zip url. Likely this is a bug in our parsing.
logger.printWarning(_formatParseWarning(wrapperFileContent));
}
} else {
// If no distributionUrl log then treat as if there was no propertiesFile.
logger.printTrace(
'$propertiesFile does not provide a Gradle version falling back to system gradle.');
}
} else {
// Could not find properties file.
logger.printTrace(
'$propertiesFile does not exist falling back to system gradle');
}
// System installed Gradle version.
if (processManager.canRun('gradle')) {
final String gradleVersionVerbose =
(await processManager.run(<String>['gradle', gradleVersionFlag])).stdout
as String;
// Expected format:
/*
------------------------------------------------------------
Gradle 7.6
------------------------------------------------------------
Build time: 2022-11-25 13:35:10 UTC
Revision: daece9dbc5b79370cc8e4fd6fe4b2cd400e150a8
Kotlin: 1.7.10
Groovy: 3.0.13
Ant: Apache Ant(TM) version 1.10.11 compiled on July 10 2021
JVM: 17.0.6 (Homebrew 17.0.6+0)
OS: Mac OS X 13.2.1 aarch64
*/
// Observation shows that the version can have 2 or 3 numbers.
// Inner parentheticals `(\.\d+)?` denote the optional third value.
// Outer parentheticals `Gradle (...)` denote a grouping used to extract
// the version number.
final RegExp gradleVersionRegex = RegExp(r'Gradle\s+(\d+\.\d+(?:\.\d+)?)');
final RegExpMatch? version =
gradleVersionRegex.firstMatch(gradleVersionVerbose);
if (version == null) {
// Most likley a bug in our parse implementation/regex.
logger.printWarning(_formatParseWarning(gradleVersionVerbose));
return null;
}
return version.group(1);
} else {
logger.printTrace('Could not run system gradle');
return null;
}
} }
/// Returns the Gradle version that is required by the given Android Gradle plugin version /// Returns the Android Gradle Plugin (AGP) version that the current project
/// by picking the largest compatible version from /// depends on when found, null otherwise.
/// https://developer.android.com/studio/releases/gradle-plugin#updating-gradle ///
@visibleForTesting /// The Android plugin version is specified in the [build.gradle] file within
String getGradleVersionFor(String androidPluginVersion) { /// the project's Android directory ([androidDirectory]).
if (_isWithinVersionRange(androidPluginVersion, min: '1.0.0', max: '1.1.3')) { String? getAgpVersion(Directory androidDirectory, Logger logger) {
return '2.3'; final File buildFile = androidDirectory.childFile('build.gradle');
if (!buildFile.existsSync()) {
logger.printTrace('Can not find build.gradle in $androidDirectory');
return null;
}
final String buildFileContent = buildFile.readAsStringSync();
final Iterable<Match> pluginMatches =
_androidGradlePluginRegExp.allMatches(buildFileContent);
if (pluginMatches.isEmpty) {
logger.printTrace("$buildFile doesn't provide an AGP version");
return null;
}
final String? androidPluginVersion = pluginMatches.first.group(1);
logger.printTrace('$buildFile provides AGP version: $androidPluginVersion');
return androidPluginVersion;
}
String _formatParseWarning(String content) {
return 'Could not parse gradle version from: \n'
'$content \n'
'If there is a version please look for an existing bug '
'https://github.com/flutter/flutter/issues/'
' and if one does not exist file a new issue.';
}
// Validate that Gradle version and AGP are compatible with each other.
//
// Returns true if versions are compatible.
// Null Gradle version or AGP version returns false.
// If compatibility can not be evaluated returns false.
// If versions are newer than the max known version a warning is logged and true
// returned.
//
// Source of truth found here:
// https://developer.android.com/studio/releases/gradle-plugin#updating-gradle
// AGP has a minimim version of gradle required but no max starting at
// AGP version 2.3.0+.
bool validateGradleAndAgp(Logger logger,
{required String? gradleV, required String? agpV}) {
const String oldestSupportedAgpVersion = '3.3.0';
const String oldestSupportedGradleVersion = '4.10.1';
if (gradleV == null || agpV == null) {
logger
.printTrace('Gradle version or AGP version unknown ($gradleV, $agpV).');
return false;
}
// First check if versions are too old.
if (isWithinVersionRange(agpV,
min: '0.0', max: oldestSupportedAgpVersion, inclusiveMax: false)) {
logger.printTrace('AGP Version: $agpV is too old.');
return false;
}
if (isWithinVersionRange(gradleV,
min: '0.0', max: oldestSupportedGradleVersion, inclusiveMax: false)) {
logger.printTrace('Gradle Version: $gradleV is too old.');
return false;
}
// Check highest supported version before checking unknown versions.
if (isWithinVersionRange(agpV, min: '8.0', max: maxKnownAgpVersion)) {
return isWithinVersionRange(gradleV,
min: '8.0', max: _maxKnownAndSupportedGradleVersion);
}
// Check if versions are newer than the max known versions.
if (isWithinVersionRange(agpV,
min: _maxKnownAndSupportedGradleVersion, max: '100.100')) {
// Assume versions we do not know about are valid but log.
final bool validGradle =
isWithinVersionRange(gradleV, min: '8.0', max: '100.00');
logger.printTrace('Newer than known AGP version ($agpV), gradle ($gradleV).'
'\n Treating as valid configuration.');
return validGradle;
}
// Begin Known Gradle <-> AGP validation.
// Max agp here is a made up version to contain all 7.4 changes.
if (isWithinVersionRange(agpV, min: '7.4', max: '7.5')) {
return isWithinVersionRange(gradleV,
min: '7.5', max: _maxKnownAndSupportedGradleVersion);
} }
if (_isWithinVersionRange(androidPluginVersion, min: '1.2.0', max: '1.3.1')) { if (isWithinVersionRange(agpV,
return '2.9'; min: '7.3', max: '7.4', inclusiveMax: false)) {
return isWithinVersionRange(gradleV,
min: '7.4', max: _maxKnownAndSupportedGradleVersion);
} }
if (_isWithinVersionRange(androidPluginVersion, min: '1.5.0', max: '1.5.0')) { if (isWithinVersionRange(agpV,
return '2.2.1'; min: '7.2', max: '7.3', inclusiveMax: false)) {
return isWithinVersionRange(gradleV,
min: '7.3.3', max: _maxKnownAndSupportedGradleVersion);
} }
if (_isWithinVersionRange(androidPluginVersion, min: '2.0.0', max: '2.1.2')) { if (isWithinVersionRange(agpV,
return '2.13'; min: '7.1', max: '7.2', inclusiveMax: false)) {
return isWithinVersionRange(gradleV,
min: '7.2', max: _maxKnownAndSupportedGradleVersion);
} }
if (_isWithinVersionRange(androidPluginVersion, min: '2.1.3', max: '2.2.3')) { if (isWithinVersionRange(agpV,
return '2.14.1'; min: '7.0', max: '7.1', inclusiveMax: false)) {
return isWithinVersionRange(gradleV,
min: '7.0', max: _maxKnownAndSupportedGradleVersion);
} }
if (_isWithinVersionRange(androidPluginVersion, min: '2.3.0', max: '2.9.9')) { if (isWithinVersionRange(agpV,
return '3.3'; min: '4.2.0', max: '7.0', inclusiveMax: false)) {
return isWithinVersionRange(gradleV,
min: '6.7.1', max: _maxKnownAndSupportedGradleVersion);
} }
if (_isWithinVersionRange(androidPluginVersion, min: '3.0.0', max: '3.0.9')) { if (isWithinVersionRange(agpV,
return '4.1'; min: '4.1.0', max: '4.2.0', inclusiveMax: false)) {
return isWithinVersionRange(gradleV,
min: '6.5', max: _maxKnownAndSupportedGradleVersion);
}
if (isWithinVersionRange(agpV,
min: '4.0.0', max: '4.1.0', inclusiveMax: false)) {
return isWithinVersionRange(gradleV,
min: '6.1.1', max: _maxKnownAndSupportedGradleVersion);
}
if (isWithinVersionRange(
agpV,
min: '3.6.0',
max: '3.6.4',
)) {
return isWithinVersionRange(gradleV,
min: '5.6.4', max: _maxKnownAndSupportedGradleVersion);
}
if (isWithinVersionRange(
agpV,
min: '3.5.0',
max: '3.5.4',
)) {
return isWithinVersionRange(gradleV,
min: '5.4.1', max: _maxKnownAndSupportedGradleVersion);
}
if (isWithinVersionRange(
agpV,
min: '3.4.0',
max: '3.4.3',
)) {
return isWithinVersionRange(gradleV,
min: '5.1.1', max: _maxKnownAndSupportedGradleVersion);
}
if (isWithinVersionRange(
agpV,
min: '3.3.0',
max: '3.3.3',
)) {
return isWithinVersionRange(gradleV,
min: '4.10.1', max: _maxKnownAndSupportedGradleVersion);
}
logger.printTrace('Unknown Gradle-Agp compatibility, $gradleV, $agpV');
return false;
}
// Validate that the [javaVersion] and Gradle version are compatible with
// each other.
//
// Source of truth:
// https://docs.gradle.org/current/userguide/compatibility.html#java
bool validateJavaGradle(Logger logger,
{required String? javaV, required String? gradleV}) {
// Update these when new major versions of Java are supported by android.
// Supported means Java <-> Gradle support.
const String oneMajorVersionHigherJavaVersion = '20';
// https://docs.gradle.org/current/userguide/compatibility.html#java
const String oldestSupportedJavaVersion = '1.8';
const String oldestDocumentedJavaGradleCompatibility = '2.0';
// Begin Java <-> Gradle validation.
if (javaV == null || gradleV == null) {
logger.printTrace(
'Java version or Gradle version unknown ($javaV, $gradleV).');
return false;
} }
if (_isWithinVersionRange(androidPluginVersion, min: '3.1.0', max: '3.1.9')) {
return '4.4'; // First check if versions are too old.
if (isWithinVersionRange(javaV,
min: '1.1', max: oldestSupportedJavaVersion, inclusiveMax: false)) {
logger.printTrace('Java Version: $javaV is too old.');
return false;
} }
if (_isWithinVersionRange(androidPluginVersion, min: '3.2.0', max: '3.2.1')) { if (isWithinVersionRange(gradleV,
return '4.6'; min: '0.0', max: oldestDocumentedJavaGradleCompatibility, inclusiveMax: false)) {
logger.printTrace('Gradle Version: $gradleV is too old.');
return false;
} }
if (_isWithinVersionRange(androidPluginVersion, min: '3.3.0', max: '3.3.2')) {
return '4.10.2'; // Check if versions are newer than the max supported versions.
if (isWithinVersionRange(
javaV,
min: oneMajorVersionHigherJavaVersion,
max: '100.100',
)) {
// Assume versions Java versions newer than [maxSupportedJavaVersion]
// required a higher gradle version.
final bool validGradle = isWithinVersionRange(gradleV,
min: _maxKnownAndSupportedGradleVersion, max: '100.00');
logger.printWarning(
'Newer than known valid Java version ($javaV), gradle ($gradleV).'
'\n Treating as valid configuration.');
return validGradle;
} }
if (_isWithinVersionRange(androidPluginVersion, min: '3.4.0', max: '3.5.0')) {
return '5.6.2'; // Begin known Java <-> Gradle evaluation.
final List<JavaGradleCompat> compatList = <JavaGradleCompat>[
JavaGradleCompat(
javaMin: '19',
javaMax: '20',
gradleMin: '7.6',
gradleMax: _maxKnownAndSupportedGradleVersion,
),
JavaGradleCompat(
javaMin: '18',
javaMax: '19',
gradleMin: '7.5',
gradleMax: _maxKnownAndSupportedGradleVersion,
),
JavaGradleCompat(
javaMin: '17',
javaMax: '18',
gradleMin: '7.3',
gradleMax: _maxKnownAndSupportedGradleVersion,
),
JavaGradleCompat(
javaMin: '16',
javaMax: '17',
gradleMin: '7.0',
gradleMax: _maxKnownAndSupportedGradleVersion,
),
JavaGradleCompat(
javaMin: '15',
javaMax: '16',
gradleMin: '6.7',
gradleMax: _maxKnownAndSupportedGradleVersion,
),
JavaGradleCompat(
javaMin: '14',
javaMax: '15',
gradleMin: '6.3',
gradleMax: _maxKnownAndSupportedGradleVersion,
),
JavaGradleCompat(
javaMin: '13',
javaMax: '14',
gradleMin: '6.0',
gradleMax: _maxKnownAndSupportedGradleVersion,
),
JavaGradleCompat(
javaMin: '12',
javaMax: '13',
gradleMin: '5.4',
gradleMax: _maxKnownAndSupportedGradleVersion,
),
JavaGradleCompat(
javaMin: '11',
javaMax: '12',
gradleMin: '5.0',
gradleMax: _maxKnownAndSupportedGradleVersion,
),
// 1.11 is a made up java version to cover everything in 1.10.*
JavaGradleCompat(
javaMin: '1.10',
javaMax: '1.11',
gradleMin: '4.7',
gradleMax: _maxKnownAndSupportedGradleVersion,
),
JavaGradleCompat(
javaMin: '1.9',
javaMax: '1.10',
gradleMin: '4.3',
gradleMax: _maxKnownAndSupportedGradleVersion,
),
JavaGradleCompat(
javaMin: '1.8',
javaMax: '1.9',
gradleMin: '2.0',
gradleMax: _maxKnownAndSupportedGradleVersion,
),
];
for (final JavaGradleCompat data in compatList) {
if (isWithinVersionRange(javaV, min: data.javaMin, max: data.javaMax, inclusiveMax: false)) {
return isWithinVersionRange(gradleV, min: data.gradleMin, max: data.gradleMax);
}
} }
if (_isWithinVersionRange(androidPluginVersion, min: '4.0.0', max: '4.1.0')) {
return '6.7'; logger.printTrace('Unknown Java-Gradle compatibility $javaV, $gradleV');
return false;
}
/// 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) {
final List<GradleForAgp> compatList = <GradleForAgp> [
GradleForAgp(agpMin: '1.0.0', agpMax: '1.1.3', minRequiredGradle: '2.3'),
GradleForAgp(agpMin: '1.2.0', agpMax: '1.3.1', minRequiredGradle: '2.9'),
GradleForAgp(agpMin: '1.5.0', agpMax: '1.5.0', minRequiredGradle: '2.2.1'),
GradleForAgp(agpMin: '2.0.0', agpMax: '2.1.2', minRequiredGradle: '2.13'),
GradleForAgp(agpMin: '2.1.3', agpMax: '2.2.3', minRequiredGradle: '2.14.1'),
GradleForAgp(agpMin: '2.3.0', agpMax: '2.9.9', minRequiredGradle: '3.3'),
GradleForAgp(agpMin: '3.0.0', agpMax: '3.0.9', minRequiredGradle: '4.1'),
GradleForAgp(agpMin: '3.1.0', agpMax: '3.1.9', minRequiredGradle: '4.4'),
GradleForAgp(agpMin: '3.2.0', agpMax: '3.2.1', minRequiredGradle: '4.6'),
GradleForAgp(agpMin: '3.3.0', agpMax: '3.3.2', minRequiredGradle: '4.10.2'),
GradleForAgp(agpMin: '3.4.0', agpMax: '3.5.0', minRequiredGradle: '5.6.2'),
GradleForAgp(agpMin: '4.0.0', agpMax: '4.1.0', minRequiredGradle: '6.7'),
// 7.5 is a made up value to include everything through 7.4.*
GradleForAgp(agpMin: '7.0.0', agpMax: '7.5', minRequiredGradle: '7.5'),
GradleForAgp(agpMin: '7.5.0', agpMax: '100.100', minRequiredGradle: '8.0'),
// Assume if AGP is newer than this code know about return the highest gradle
// version we know about.
GradleForAgp(agpMin: maxKnownAgpVersion, agpMax: maxKnownAgpVersion, minRequiredGradle: _maxKnownAndSupportedGradleVersion),
];
for (final GradleForAgp data in compatList) {
if (isWithinVersionRange(androidPluginVersion, min: data.agpMin, max: data.agpMax)) {
return data.minRequiredGradle;
}
} }
if (_isWithinVersionRange(androidPluginVersion, min: '7.0', max: '7.5')) { if (isWithinVersionRange(androidPluginVersion, min: maxKnownAgpVersion, max: '100.100')) {
return '7.5'; return _maxKnownAndSupportedGradleVersion;
} }
throwToolExit('Unsupported Android Plugin version: $androidPluginVersion.'); throwToolExit('Unsupported Android Plugin version: $androidPluginVersion.');
} }
...@@ -284,9 +671,36 @@ void writeLocalProperties(File properties) { ...@@ -284,9 +671,36 @@ void writeLocalProperties(File properties) {
} }
void exitWithNoSdkMessage() { void exitWithNoSdkMessage() {
BuildEvent('unsupported-project', type: 'gradle', eventError: 'android-sdk-not-found', flutterUsage: globals.flutterUsage).send(); BuildEvent('unsupported-project',
throwToolExit( type: 'gradle',
'${globals.logger.terminal.warningMark} No Android SDK found. ' eventError: 'android-sdk-not-found',
'Try setting the ANDROID_SDK_ROOT environment variable.' flutterUsage: globals.flutterUsage)
); .send();
throwToolExit('${globals.logger.terminal.warningMark} No Android SDK found. '
'Try setting the ANDROID_SDK_ROOT environment variable.');
}
// Data class to hold normal/defined Java <-> Gradle compatability criteria.
class JavaGradleCompat {
JavaGradleCompat({
required this.javaMin,
required this.javaMax,
required this.gradleMin,
required this.gradleMax,
});
final String javaMin;
final String javaMax;
final String gradleMin;
final String gradleMax;
}
class GradleForAgp {
GradleForAgp({
required this.agpMin,
required this.agpMax,
required this.minRequiredGradle,
});
final String agpMin;
final String agpMax;
final String minRequiredGradle;
} }
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
// TODO(reidbaker): Investigate using pub_semver instead of this class.
@immutable @immutable
class Version implements Comparable<Version> { class Version implements Comparable<Version> {
/// Creates a new [Version] object. /// Creates a new [Version] object.
...@@ -119,3 +120,35 @@ class Version implements Comparable<Version> { ...@@ -119,3 +120,35 @@ class Version implements Comparable<Version> {
@override @override
String toString() => _text; String toString() => _text;
} }
/// Returns true if [targetVersion] is within the range [min] and [max]
/// inclusive by default.
///
/// [min] and [max] are evaluated by [Version.parse(text)].
///
/// Pass [inclusiveMin] = false for greater than and not equal to min.
/// Pass [inclusiveMax] = false for less than and not equal to max.
bool isWithinVersionRange(
String targetVersion, {
required String min,
required String max,
bool inclusiveMax = true,
bool inclusiveMin = true,
}) {
final Version? parsedTargetVersion = Version.parse(targetVersion);
final Version? minVersion = Version.parse(min);
final Version? maxVersion = Version.parse(max);
final bool withinMin = minVersion != null &&
parsedTargetVersion != null &&
(inclusiveMin
? parsedTargetVersion >= minVersion
: parsedTargetVersion > minVersion);
final bool withinMax = maxVersion != null &&
parsedTargetVersion != null &&
(inclusiveMax
? parsedTargetVersion <= maxVersion
: parsedTargetVersion < maxVersion);
return withinMin && withinMax;
}
...@@ -236,7 +236,6 @@ Future<T> runInContext<T>( ...@@ -236,7 +236,6 @@ Future<T> runInContext<T>(
fuchsiaArtifacts: globals.fuchsiaArtifacts!, fuchsiaArtifacts: globals.fuchsiaArtifacts!,
), ),
GradleUtils: () => GradleUtils( GradleUtils: () => GradleUtils(
fileSystem: globals.fs,
operatingSystemUtils: globals.os, operatingSystemUtils: globals.os,
logger: globals.logger, logger: globals.logger,
platform: globals.platform, platform: globals.platform,
......
...@@ -20,6 +20,7 @@ import 'flutter_manifest.dart'; ...@@ -20,6 +20,7 @@ import 'flutter_manifest.dart';
import 'flutter_plugins.dart'; import 'flutter_plugins.dart';
import 'globals.dart' as globals; import 'globals.dart' as globals;
import 'platform_plugins.dart'; import 'platform_plugins.dart';
import 'project_validator_result.dart';
import 'reporting/reporting.dart'; import 'reporting/reporting.dart';
import 'template.dart'; import 'template.dart';
import 'xcode_project.dart'; import 'xcode_project.dart';
...@@ -418,6 +419,20 @@ abstract class FlutterProjectPlatform { ...@@ -418,6 +419,20 @@ abstract class FlutterProjectPlatform {
class AndroidProject extends FlutterProjectPlatform { class AndroidProject extends FlutterProjectPlatform {
AndroidProject._(this.parent); AndroidProject._(this.parent);
// User facing string when java/gradle/agp versions are compatible.
@visibleForTesting
static const String validJavaGradleAgpString = 'compatible java/gradle/agp';
// User facing link that describes compatibility between gradle and
// android gradle plugin.
static const String gradleAgpCompatUrl =
'https://developer.android.com/studio/releases/gradle-plugin#updating-gradle';
// User facing link that describes compatibility between java and the first
// version of gradle to support it.
static const String javaGradleCompatUrl =
'https://docs.gradle.org/current/userguide/compatibility.html#java';
/// The parent of this project. /// The parent of this project.
final FlutterProject parent; final FlutterProject parent;
...@@ -510,6 +525,77 @@ class AndroidProject extends FlutterProjectPlatform { ...@@ -510,6 +525,77 @@ class AndroidProject extends FlutterProjectPlatform {
return parent.isModule || _editableHostAppDirectory.existsSync(); return parent.isModule || _editableHostAppDirectory.existsSync();
} }
/// Check if the versions of Java, Gradle and AGP are compatible.
///
/// This is expected to be called from
/// flutter_tools/lib/src/project_validator.dart.
Future<ProjectValidatorResult> validateJavaGradleAgpVersions() async {
// Constructing ProjectValidatorResult happens here and not in
// flutter_tools/lib/src/project_validator.dart because of the additional
// Complexity of variable status values and error string formatting.
const String visibleName = 'Java/Gradle/Android Gradle Plugin';
final CompatibilityResult validJavaGradleAgpVersions =
await hasValidJavaGradleAgpVersions();
return ProjectValidatorResult(
name: visibleName,
value: validJavaGradleAgpVersions.description,
status: validJavaGradleAgpVersions.success
? StatusProjectValidator.success
: StatusProjectValidator.error,
);
}
/// Ensures Java SDK is compatible with the project's Gradle version and
/// the project's Gradle version is compatible with the AGP version used
/// in build.gradle.
Future<CompatibilityResult> hasValidJavaGradleAgpVersions() async {
final String? gradleVersion = await gradle.getGradleVersion(
hostAppGradleRoot, globals.logger, globals.processManager);
final String? agpVersion =
gradle.getAgpVersion(hostAppGradleRoot, globals.logger);
final String? javaVersion = globals.androidSdk?.getJavaVersion(
androidStudio: globals.androidStudio,
fileSystem: globals.fs,
operatingSystemUtils: globals.os,
platform: globals.platform,
processUtils: globals.processUtils,
);
// Assume valid configuration.
String description = validJavaGradleAgpString;
final bool compatibleGradleAgp = gradle.validateGradleAndAgp(globals.logger,
gradleV: gradleVersion, agpV: agpVersion);
final bool compatibleJavaGradle = gradle.validateJavaGradle(globals.logger,
javaV: javaVersion, gradleV: gradleVersion);
// Begin description formatting.
if (!compatibleGradleAgp) {
description = '''
Incompatible Gradle/AGP versions. \n
Gradle Version: $gradleVersion, AGP Version: $agpVersion
Update Gradle to at least "${gradle.getGradleVersionFor(agpVersion!)}".\n
See the link below for more information:
$gradleAgpCompatUrl
''';
}
if (!compatibleJavaGradle) {
// Should contain the agp error (if present) but not the valid String.
description = '''
${compatibleGradleAgp ? '' : description}
Incompatible Java/Gradle versions.
Java Version: $javaVersion, Gradle Version: $gradleVersion\n
See the link below for more information:
$javaGradleCompatUrl
''';
}
return CompatibilityResult(
compatibleJavaGradle && compatibleGradleAgp, description);
}
bool get isUsingGradle { bool get isUsingGradle {
return hostAppGradleRoot.childFile('build.gradle').existsSync(); return hostAppGradleRoot.childFile('build.gradle').existsSync();
} }
...@@ -570,12 +656,17 @@ class AndroidProject extends FlutterProjectPlatform { ...@@ -570,12 +656,17 @@ class AndroidProject extends FlutterProjectPlatform {
Future<void> _regenerateLibrary() async { Future<void> _regenerateLibrary() async {
ErrorHandlingFileSystem.deleteIfExists(ephemeralDirectory, recursive: true); ErrorHandlingFileSystem.deleteIfExists(ephemeralDirectory, recursive: true);
await _overwriteFromTemplate(
globals.fs.path.join(
'module',
'android',
'library_new_embedding',
),
ephemeralDirectory);
await _overwriteFromTemplate(globals.fs.path.join( await _overwriteFromTemplate(globals.fs.path.join(
'module', 'module',
'android', 'android',
'library_new_embedding', 'gradle'), ephemeralDirectory);
), ephemeralDirectory);
await _overwriteFromTemplate(globals.fs.path.join('module', 'android', 'gradle'), ephemeralDirectory);
globals.gradleUtils?.injectGradleWrapperIfNeeded(ephemeralDirectory); globals.gradleUtils?.injectGradleWrapperIfNeeded(ephemeralDirectory);
} }
...@@ -786,3 +877,12 @@ class FuchsiaProject { ...@@ -786,3 +877,12 @@ class FuchsiaProject {
Directory get meta => Directory get meta =>
_meta ??= editableHostAppDirectory.childDirectory('meta'); _meta ??= editableHostAppDirectory.childDirectory('meta');
} }
// Combines success and a description into one object that can be returned
// together.
@visibleForTesting
class CompatibilityResult {
CompatibilityResult(this.success, this.description);
final bool success;
final String description;
}
...@@ -224,6 +224,7 @@ class GeneralInfoProjectValidator extends ProjectValidator{ ...@@ -224,6 +224,7 @@ class GeneralInfoProjectValidator extends ProjectValidator{
result.add(_materialDesignResult(flutterManifest)); result.add(_materialDesignResult(flutterManifest));
result.add(_pluginValidatorResult(flutterManifest)); result.add(_pluginValidatorResult(flutterManifest));
} }
result.add(await project.android.validateJavaGradleAgpVersions());
return result; return result;
} }
......
...@@ -4,13 +4,18 @@ ...@@ -4,13 +4,18 @@
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:flutter_tools/src/android/android_sdk.dart'; import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/android/android_studio.dart';
import 'package:flutter_tools/src/base/config.dart'; import 'package:flutter_tools/src/base/config.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/process.dart';
import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/globals.dart' as globals;
import '../../integration.shard/test_utils.dart';
import '../../src/common.dart'; import '../../src/common.dart';
import '../../src/context.dart'; import '../../src/context.dart';
import '../../src/fakes.dart' show FakeAndroidStudio, FakeOperatingSystemUtils;
void main() { void main() {
late MemoryFileSystem fileSystem; late MemoryFileSystem fileSystem;
...@@ -342,6 +347,132 @@ void main() { ...@@ -342,6 +347,132 @@ void main() {
Config: () => config, Config: () => config,
}); });
}); });
group('java version', () {
const String exampleJdk8Output = '''
java version "1.8.0_202"
Java(TM) SE Runtime Environment (build 1.8.0_202-b10)
Java HotSpot(TM) 64-Bit Server VM (build 25.202-b10, mixed mode)
''';
// Example strings came from actual terminal output.
testWithoutContext('parses jdk 8', () {
expect(AndroidSdk.parseJavaVersion(exampleJdk8Output), '1.8.0');
});
testWithoutContext('parses jdk 11 windows', () {
const String exampleJdkOutput = '''
java version "11.0.14"
Java(TM) SE Runtime Environment (build 11.0.14+10-b13)
Java HotSpot(TM) 64-Bit Server VM (build 11.0.14+10-b13, mixed mode)
''';
expect(AndroidSdk.parseJavaVersion(exampleJdkOutput), '11.0.14');
});
testWithoutContext('parses jdk 11 mac/linux', () {
const String exampleJdkOutput = '''
openjdk version "11.0.18" 2023-01-17 LTS
OpenJDK Runtime Environment Zulu11.62+17-CA (build 11.0.18+10-LTS)
OpenJDK 64-Bit Server VM Zulu11.62+17-CA (build 11.0.18+10-LTS, mixed mode)
''';
expect(AndroidSdk.parseJavaVersion(exampleJdkOutput), '11.0.18');
});
testWithoutContext('parses jdk 17', () {
const String exampleJdkOutput = '''
openjdk 17.0.6 2023-01-17
OpenJDK Runtime Environment (build 17.0.6+0-17.0.6b802.4-9586694)
OpenJDK 64-Bit Server VM (build 17.0.6+0-17.0.6b802.4-9586694, mixed mode)
''';
expect(AndroidSdk.parseJavaVersion(exampleJdkOutput), '17.0.6');
});
testWithoutContext('parses jdk 19', () {
const String exampleJdkOutput = '''
openjdk 19.0.2 2023-01-17
OpenJDK Runtime Environment Homebrew (build 19.0.2)
OpenJDK 64-Bit Server VM Homebrew (build 19.0.2, mixed mode, sharing)
''';
expect(AndroidSdk.parseJavaVersion(exampleJdkOutput), '19.0.2');
});
// https://chrome-infra-packages.appspot.com/p/flutter/java/openjdk/
testWithoutContext('parses jdk output from ci', () {
const String exampleJdkOutput = '''
openjdk 11.0.2 2019-01-15
OpenJDK Runtime Environment 18.9 (build 11.0.2+9)
OpenJDK 64-Bit Server VM 18.9 (build 11.0.2+9, mixed mode)
''';
expect(AndroidSdk.parseJavaVersion(exampleJdkOutput), '11.0.2');
});
testWithoutContext('parses jdk two number versions', () {
const String exampleJdkOutput = 'openjdk 19.0 2023-01-17';
expect(AndroidSdk.parseJavaVersion(exampleJdkOutput), '19.0');
});
testUsingContext('getJavaBinary with AS install', () {
final Directory sdkDir = createSdkDirectory(fileSystem: fileSystem);
config.setValue('android-sdk', sdkDir.path);
final AndroidStudio androidStudio = FakeAndroidStudio();
final String javaPath = AndroidSdk.findJavaBinary(
androidStudio: androidStudio,
fileSystem: fileSystem,
operatingSystemUtils: FakeOperatingSystemUtils(),
platform: platform)!;
// Built from the implementation of findJavaBinary android studio case.
final String expectedJavaPath = '${androidStudio.javaPath}/bin/java';
expect(javaPath, expectedJavaPath);
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
Config: () => config,
Platform: () => FakePlatform(environment: <String, String>{}),
});
group('java', () {
late AndroidStudio androidStudio;
setUp(() {
androidStudio = FakeAndroidStudio();
});
testUsingContext('getJavaVersion finds AS java and parses version', () {
final Directory sdkDir = createSdkDirectory(fileSystem: fileSystem);
config.setValue('android-sdk', sdkDir.path);
final ProcessUtils processUtils = ProcessUtils(
processManager: processManager, logger: BufferLogger.test());
// Built from the implementation of findJavaBinary android studio case.
final String expectedJavaPath = '${androidStudio.javaPath}/bin/java';
processManager.addCommand(FakeCommand(
command: <String>[
expectedJavaPath,
'--version',
],
stdout: exampleJdk8Output,
));
final AndroidSdk sdk = AndroidSdk.locateAndroidSdk()!;
final String? javaVersion = sdk.getJavaVersion(
androidStudio: androidStudio,
fileSystem: fileSystem,
operatingSystemUtils: FakeOperatingSystemUtils(),
platform: FakePlatform(),
processUtils: processUtils,
);
expect(javaVersion, '1.8.0');
}, overrides: <Type, Generator>{
FileSystem: () => fileSystem,
ProcessManager: () => processManager,
AndroidStudio: () => androidStudio,
Config: () => config,
Platform: () => FakePlatform(environment: <String, String>{}),
});
});
});
} }
/// A broken SDK installation. /// A broken SDK installation.
......
...@@ -16,6 +16,7 @@ import 'package:flutter_tools/src/doctor_validator.dart'; ...@@ -16,6 +16,7 @@ import 'package:flutter_tools/src/doctor_validator.dart';
import 'package:test/fake.dart'; import 'package:test/fake.dart';
import '../../src/common.dart'; import '../../src/common.dart';
import '../../src/context.dart';
import '../../src/fake_process_manager.dart'; import '../../src/fake_process_manager.dart';
import '../../src/fakes.dart'; import '../../src/fakes.dart';
...@@ -425,7 +426,7 @@ Review licenses that have not been accepted (y/N)? ...@@ -425,7 +426,7 @@ Review licenses that have not been accepted (y/N)?
expect(licenseMessage.message, UserMessages().androidSdkLicenseOnly(kAndroidHome)); expect(licenseMessage.message, UserMessages().androidSdkLicenseOnly(kAndroidHome));
}); });
testWithoutContext('detects minimum required SDK and buildtools', () async { testUsingContext('detects minimum required SDK and buildtools', () async {
processManager.addCommand(const FakeCommand( processManager.addCommand(const FakeCommand(
command: <String>[ command: <String>[
'which', 'which',
...@@ -523,7 +524,7 @@ Review licenses that have not been accepted (y/N)? ...@@ -523,7 +524,7 @@ Review licenses that have not been accepted (y/N)?
expect(cmdlineMessage.message, errorMessage); expect(cmdlineMessage.message, errorMessage);
}); });
testWithoutContext('detects minimum required java version', () async { testUsingContext('detects minimum required java version', () async {
// Test with older version of JDK // Test with older version of JDK
const String javaVersionText = 'openjdk version "1.7.0_212"'; const String javaVersionText = 'openjdk version "1.7.0_212"';
processManager.addCommand(const FakeCommand( processManager.addCommand(const FakeCommand(
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:flutter_tools/src/android/android_sdk.dart'; import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/android/gradle.dart'; import 'package:flutter_tools/src/android/gradle.dart';
import 'package:flutter_tools/src/android/gradle_utils.dart'; import 'package:flutter_tools/src/android/gradle_utils.dart' as gradle_utils;
import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/artifacts.dart';
import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/common.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
...@@ -196,7 +196,7 @@ void main() { ...@@ -196,7 +196,7 @@ void main() {
group('gradle build', () { group('gradle build', () {
testUsingContext('do not crash if there is no Android SDK', () async { testUsingContext('do not crash if there is no Android SDK', () async {
expect(() { expect(() {
updateLocalProperties(project: FlutterProject.fromDirectoryTest(globals.fs.currentDirectory)); gradle_utils.updateLocalProperties(project: FlutterProject.fromDirectoryTest(globals.fs.currentDirectory));
}, throwsToolExit( }, throwsToolExit(
message: '${globals.logger.terminal.warningMark} No Android SDK found. Try setting the ANDROID_SDK_ROOT environment variable.', message: '${globals.logger.terminal.warningMark} No Android SDK found. Try setting the ANDROID_SDK_ROOT environment variable.',
)); ));
...@@ -241,7 +241,7 @@ void main() { ...@@ -241,7 +241,7 @@ void main() {
manifestFile.writeAsStringSync(manifest); manifestFile.writeAsStringSync(manifest);
updateLocalProperties( gradle_utils.updateLocalProperties(
project: FlutterProject.fromDirectoryTest(globals.fs.directory('path/to/project')), project: FlutterProject.fromDirectoryTest(globals.fs.directory('path/to/project')),
buildInfo: buildInfo, buildInfo: buildInfo,
requireAndroidSdk: false, requireAndroidSdk: false,
...@@ -415,55 +415,57 @@ flutter: ...@@ -415,55 +415,57 @@ flutter:
}); });
}); });
group('gradle version', () { group('gradgradle_utils.le version', () {
testWithoutContext('should be compatible with the Android plugin version', () { testWithoutContext('should be compatible with the Android plugin version', () {
// Granular versions. // Grangradle_utils.ular versions.
expect(getGradleVersionFor('1.0.0'), '2.3'); expect(gradle_utils.getGradleVersionFor('1.0.0'), '2.3');
expect(getGradleVersionFor('1.0.1'), '2.3'); expect(gradle_utils.getGradleVersionFor('1.0.1'), '2.3');
expect(getGradleVersionFor('1.0.2'), '2.3'); expect(gradle_utils.getGradleVersionFor('1.0.2'), '2.3');
expect(getGradleVersionFor('1.0.4'), '2.3'); expect(gradle_utils.getGradleVersionFor('1.0.4'), '2.3');
expect(getGradleVersionFor('1.0.8'), '2.3'); expect(gradle_utils.getGradleVersionFor('1.0.8'), '2.3');
expect(getGradleVersionFor('1.1.0'), '2.3'); expect(gradle_utils.getGradleVersionFor('1.1.0'), '2.3');
expect(getGradleVersionFor('1.1.2'), '2.3'); expect(gradle_utils.getGradleVersionFor('1.1.2'), '2.3');
expect(getGradleVersionFor('1.1.2'), '2.3'); expect(gradle_utils.getGradleVersionFor('1.1.2'), '2.3');
expect(getGradleVersionFor('1.1.3'), '2.3'); expect(gradle_utils.getGradleVersionFor('1.1.3'), '2.3');
// Version Ranges. // Versgradle_utils.ion Ranges.
expect(getGradleVersionFor('1.2.0'), '2.9'); expect(gradle_utils.getGradleVersionFor('1.2.0'), '2.9');
expect(getGradleVersionFor('1.3.1'), '2.9'); expect(gradle_utils.getGradleVersionFor('1.3.1'), '2.9');
expect(getGradleVersionFor('1.5.0'), '2.2.1'); expect(gradle_utils.getGradleVersionFor('1.5.0'), '2.2.1');
expect(getGradleVersionFor('2.0.0'), '2.13'); expect(gradle_utils.getGradleVersionFor('2.0.0'), '2.13');
expect(getGradleVersionFor('2.1.2'), '2.13'); expect(gradle_utils.getGradleVersionFor('2.1.2'), '2.13');
expect(getGradleVersionFor('2.1.3'), '2.14.1'); expect(gradle_utils.getGradleVersionFor('2.1.3'), '2.14.1');
expect(getGradleVersionFor('2.2.3'), '2.14.1'); expect(gradle_utils.getGradleVersionFor('2.2.3'), '2.14.1');
expect(getGradleVersionFor('2.3.0'), '3.3'); expect(gradle_utils.getGradleVersionFor('2.3.0'), '3.3');
expect(getGradleVersionFor('3.0.0'), '4.1'); expect(gradle_utils.getGradleVersionFor('3.0.0'), '4.1');
expect(getGradleVersionFor('3.1.0'), '4.4'); expect(gradle_utils.getGradleVersionFor('3.1.0'), '4.4');
expect(getGradleVersionFor('3.2.0'), '4.6'); expect(gradle_utils.getGradleVersionFor('3.2.0'), '4.6');
expect(getGradleVersionFor('3.2.1'), '4.6'); expect(gradle_utils.getGradleVersionFor('3.2.1'), '4.6');
expect(getGradleVersionFor('3.3.0'), '4.10.2'); expect(gradle_utils.getGradleVersionFor('3.3.0'), '4.10.2');
expect(getGradleVersionFor('3.3.2'), '4.10.2'); expect(gradle_utils.getGradleVersionFor('3.3.2'), '4.10.2');
expect(getGradleVersionFor('3.4.0'), '5.6.2'); expect(gradle_utils.getGradleVersionFor('3.4.0'), '5.6.2');
expect(getGradleVersionFor('3.5.0'), '5.6.2'); expect(gradle_utils.getGradleVersionFor('3.5.0'), '5.6.2');
expect(getGradleVersionFor('4.0.0'), '6.7'); expect(gradle_utils.getGradleVersionFor('4.0.0'), '6.7');
expect(getGradleVersionFor('4.1.0'), '6.7'); expect(gradle_utils.getGradleVersionFor('4.1.0'), '6.7');
expect(getGradleVersionFor('7.0'), '7.5'); expect(gradle_utils.getGradleVersionFor('7.0'), '7.5');
expect(getGradleVersionFor('7.1.2'), '7.5'); expect(gradle_utils.getGradleVersionFor('7.1.2'), '7.5');
expect(getGradleVersionFor('7.2'), '7.5'); expect(gradle_utils.getGradleVersionFor('7.2'), '7.5');
expect(gradle_utils.getGradleVersionFor('8.0'), '8.0');
expect(gradle_utils.getGradleVersionFor(gradle_utils.maxKnownAgpVersion), '8.0');
}); });
testWithoutContext('throws on unsupported versions', () { testWithoutContext('throws on unsupported versions', () {
expect(() => getGradleVersionFor('3.6.0'), expect(() => gradle_utils.getGradleVersionFor('3.6.0'),
throwsA(predicate<Exception>((Exception e) => e is ToolExit))); throwsA(predicate<Exception>((Exception e) => e is ToolExit)));
}); });
}); });
......
...@@ -14,30 +14,31 @@ import '../../src/fake_process_manager.dart'; ...@@ -14,30 +14,31 @@ import '../../src/fake_process_manager.dart';
import '../../src/fakes.dart'; import '../../src/fakes.dart';
void main() { void main() {
group('injectGradleWrapperIfNeeded', () { group('injectGradleWrapperIfNeeded', () {
late MemoryFileSystem fileSystem; late MemoryFileSystem fileSystem;
late Directory gradleWrapperDirectory; late Directory gradleWrapperDirectory;
late GradleUtils gradleUtils; late GradleUtils gradleUtils;
setUp(() { setUp(() {
fileSystem = MemoryFileSystem.test(); fileSystem = MemoryFileSystem.test();
gradleWrapperDirectory = fileSystem.directory('cache/bin/cache/artifacts/gradle_wrapper'); gradleWrapperDirectory =
fileSystem.directory('cache/bin/cache/artifacts/gradle_wrapper');
gradleWrapperDirectory.createSync(recursive: true); gradleWrapperDirectory.createSync(recursive: true);
gradleWrapperDirectory gradleWrapperDirectory
.childFile('gradlew') .childFile('gradlew')
.writeAsStringSync('irrelevant'); .writeAsStringSync('irrelevant');
gradleWrapperDirectory gradleWrapperDirectory
.childDirectory('gradle') .childDirectory('gradle')
.childDirectory('wrapper') .childDirectory('wrapper')
.createSync(recursive: true); .createSync(recursive: true);
gradleWrapperDirectory gradleWrapperDirectory
.childDirectory('gradle') .childDirectory('gradle')
.childDirectory('wrapper') .childDirectory('wrapper')
.childFile('gradle-wrapper.jar') .childFile('gradle-wrapper.jar')
.writeAsStringSync('irrelevant'); .writeAsStringSync('irrelevant');
gradleUtils = GradleUtils( gradleUtils = GradleUtils(
cache: Cache.test(processManager: FakeProcessManager.any(), fileSystem: fileSystem), cache: Cache.test(
fileSystem: fileSystem, processManager: FakeProcessManager.any(), fileSystem: fileSystem),
platform: FakePlatform(environment: <String, String>{}), platform: FakePlatform(environment: <String, String>{}),
logger: BufferLogger.test(), logger: BufferLogger.test(),
operatingSystemUtils: FakeOperatingSystemUtils(), operatingSystemUtils: FakeOperatingSystemUtils(),
...@@ -45,43 +46,52 @@ void main() { ...@@ -45,43 +46,52 @@ void main() {
}); });
testWithoutContext('injects the wrapper when all files are missing', () { testWithoutContext('injects the wrapper when all files are missing', () {
final Directory sampleAppAndroid = fileSystem.directory('/sample-app/android'); final Directory sampleAppAndroid =
fileSystem.directory('/sample-app/android');
sampleAppAndroid.createSync(recursive: true); sampleAppAndroid.createSync(recursive: true);
gradleUtils.injectGradleWrapperIfNeeded(sampleAppAndroid); gradleUtils.injectGradleWrapperIfNeeded(sampleAppAndroid);
expect(sampleAppAndroid.childFile('gradlew').existsSync(), isTrue); expect(sampleAppAndroid.childFile('gradlew').existsSync(), isTrue);
expect(sampleAppAndroid expect(
.childDirectory('gradle') sampleAppAndroid
.childDirectory('wrapper') .childDirectory('gradle')
.childFile('gradle-wrapper.jar') .childDirectory('wrapper')
.existsSync(), isTrue); .childFile('gradle-wrapper.jar')
.existsSync(),
expect(sampleAppAndroid isTrue);
.childDirectory('gradle')
.childDirectory('wrapper') expect(
.childFile('gradle-wrapper.properties') sampleAppAndroid
.existsSync(), isTrue); .childDirectory('gradle')
.childDirectory('wrapper')
expect(sampleAppAndroid .childFile('gradle-wrapper.properties')
.childDirectory('gradle') .existsSync(),
.childDirectory('wrapper') isTrue);
.childFile('gradle-wrapper.properties')
.readAsStringSync(), expect(
'distributionBase=GRADLE_USER_HOME\n' sampleAppAndroid
'distributionPath=wrapper/dists\n' .childDirectory('gradle')
'zipStoreBase=GRADLE_USER_HOME\n' .childDirectory('wrapper')
'zipStorePath=wrapper/dists\n' .childFile('gradle-wrapper.properties')
'distributionUrl=https\\://services.gradle.org/distributions/gradle-7.5-all.zip\n'); .readAsStringSync(),
'distributionBase=GRADLE_USER_HOME\n'
'distributionPath=wrapper/dists\n'
'zipStoreBase=GRADLE_USER_HOME\n'
'zipStorePath=wrapper/dists\n'
'distributionUrl=https\\://services.gradle.org/distributions/gradle-7.5-all.zip\n');
}); });
testWithoutContext('injects the wrapper when some files are missing', () { testWithoutContext('injects the wrapper when some files are missing', () {
final Directory sampleAppAndroid = fileSystem.directory('/sample-app/android'); final Directory sampleAppAndroid =
fileSystem.directory('/sample-app/android');
sampleAppAndroid.createSync(recursive: true); sampleAppAndroid.createSync(recursive: true);
// There's an existing gradlew // There's an existing gradlew
sampleAppAndroid.childFile('gradlew').writeAsStringSync('existing gradlew'); sampleAppAndroid
.childFile('gradlew')
.writeAsStringSync('existing gradlew');
gradleUtils.injectGradleWrapperIfNeeded(sampleAppAndroid); gradleUtils.injectGradleWrapperIfNeeded(sampleAppAndroid);
...@@ -89,23 +99,28 @@ void main() { ...@@ -89,23 +99,28 @@ void main() {
expect(sampleAppAndroid.childFile('gradlew').readAsStringSync(), expect(sampleAppAndroid.childFile('gradlew').readAsStringSync(),
equals('existing gradlew')); equals('existing gradlew'));
expect(sampleAppAndroid expect(
.childDirectory('gradle') sampleAppAndroid
.childDirectory('wrapper') .childDirectory('gradle')
.childFile('gradle-wrapper.jar') .childDirectory('wrapper')
.existsSync(), isTrue); .childFile('gradle-wrapper.jar')
.existsSync(),
expect(sampleAppAndroid isTrue);
.childDirectory('gradle')
.childDirectory('wrapper') expect(
.childFile('gradle-wrapper.properties') sampleAppAndroid
.existsSync(), isTrue); .childDirectory('gradle')
.childDirectory('wrapper')
expect(sampleAppAndroid .childFile('gradle-wrapper.properties')
.childDirectory('gradle') .existsSync(),
.childDirectory('wrapper') isTrue);
.childFile('gradle-wrapper.properties')
.readAsStringSync(), expect(
sampleAppAndroid
.childDirectory('gradle')
.childDirectory('wrapper')
.childFile('gradle-wrapper.properties')
.readAsStringSync(),
'distributionBase=GRADLE_USER_HOME\n' 'distributionBase=GRADLE_USER_HOME\n'
'distributionPath=wrapper/dists\n' 'distributionPath=wrapper/dists\n'
'zipStoreBase=GRADLE_USER_HOME\n' 'zipStoreBase=GRADLE_USER_HOME\n'
...@@ -113,7 +128,9 @@ void main() { ...@@ -113,7 +128,9 @@ void main() {
'distributionUrl=https\\://services.gradle.org/distributions/gradle-7.5-all.zip\n'); 'distributionUrl=https\\://services.gradle.org/distributions/gradle-7.5-all.zip\n');
}); });
testWithoutContext('injects the wrapper and the Gradle version is derivated from the AGP version', () { testWithoutContext(
'injects the wrapper and the Gradle version is derivated from the AGP version',
() {
const Map<String, String> testCases = <String, String>{ const Map<String, String> testCases = <String, String>{
// AGP version : Gradle version // AGP version : Gradle version
'1.0.0': '2.3', '1.0.0': '2.3',
...@@ -132,10 +149,9 @@ void main() { ...@@ -132,10 +149,9 @@ void main() {
}; };
for (final MapEntry<String, String> entry in testCases.entries) { for (final MapEntry<String, String> entry in testCases.entries) {
final Directory sampleAppAndroid = fileSystem.systemTempDirectory.createTempSync('flutter_android.'); final Directory sampleAppAndroid =
sampleAppAndroid fileSystem.systemTempDirectory.createTempSync('flutter_android.');
.childFile('build.gradle') sampleAppAndroid.childFile('build.gradle').writeAsStringSync('''
.writeAsStringSync('''
buildscript { buildscript {
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:${entry.key}' classpath 'com.android.tools.build:gradle:${entry.key}'
...@@ -146,33 +162,39 @@ void main() { ...@@ -146,33 +162,39 @@ void main() {
expect(sampleAppAndroid.childFile('gradlew').existsSync(), isTrue); expect(sampleAppAndroid.childFile('gradlew').existsSync(), isTrue);
expect(sampleAppAndroid expect(
.childDirectory('gradle') sampleAppAndroid
.childDirectory('wrapper') .childDirectory('gradle')
.childFile('gradle-wrapper.jar') .childDirectory('wrapper')
.existsSync(), isTrue); .childFile('gradle-wrapper.jar')
.existsSync(),
isTrue);
expect(sampleAppAndroid expect(
.childDirectory('gradle') sampleAppAndroid
.childDirectory('wrapper') .childDirectory('gradle')
.childFile('gradle-wrapper.properties') .childDirectory('wrapper')
.existsSync(), isTrue); .childFile('gradle-wrapper.properties')
.existsSync(),
isTrue);
expect(sampleAppAndroid expect(
.childDirectory('gradle') sampleAppAndroid
.childDirectory('wrapper') .childDirectory('gradle')
.childFile('gradle-wrapper.properties') .childDirectory('wrapper')
.readAsStringSync(), .childFile('gradle-wrapper.properties')
'distributionBase=GRADLE_USER_HOME\n' .readAsStringSync(),
'distributionPath=wrapper/dists\n' 'distributionBase=GRADLE_USER_HOME\n'
'zipStoreBase=GRADLE_USER_HOME\n' 'distributionPath=wrapper/dists\n'
'zipStorePath=wrapper/dists\n' 'zipStoreBase=GRADLE_USER_HOME\n'
'distributionUrl=https\\://services.gradle.org/distributions/gradle-${entry.value}-all.zip\n'); 'zipStorePath=wrapper/dists\n'
'distributionUrl=https\\://services.gradle.org/distributions/gradle-${entry.value}-all.zip\n');
} }
}); });
testWithoutContext('returns the gradlew path', () { testWithoutContext('returns the gradlew path', () {
final Directory androidDirectory = fileSystem.directory('/android')..createSync(); final Directory androidDirectory = fileSystem.directory('/android')
..createSync();
androidDirectory.childFile('gradlew').createSync(); androidDirectory.childFile('gradlew').createSync();
androidDirectory.childFile('gradlew.bat').createSync(); androidDirectory.childFile('gradlew.bat').createSync();
androidDirectory.childFile('gradle.properties').createSync(); androidDirectory.childFile('gradle.properties').createSync();
...@@ -182,9 +204,394 @@ void main() { ...@@ -182,9 +204,394 @@ void main() {
fileSystem: fileSystem, fileSystem: fileSystem,
).fromDirectory(fileSystem.currentDirectory); ).fromDirectory(fileSystem.currentDirectory);
expect(gradleUtils.getExecutable(flutterProject), expect(
gradleUtils.getExecutable(flutterProject),
androidDirectory.childFile('gradlew').path, androidDirectory.childFile('gradlew').path,
); );
}); });
testWithoutContext('returns the gradle wrapper version', () async {
const String expectedVersion = '7.4.2';
final Directory androidDirectory = fileSystem.directory('/android')
..createSync();
final Directory wrapperDirectory = androidDirectory
.childDirectory('gradle')
.childDirectory('wrapper')
..createSync(recursive: true);
wrapperDirectory
.childFile('gradle-wrapper.properties')
.writeAsStringSync('''
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\\://services.gradle.org/distributions/gradle-$expectedVersion-all.zip
''');
expect(
await getGradleVersion(
androidDirectory, BufferLogger.test(), FakeProcessManager.empty()),
expectedVersion,
);
});
testWithoutContext('returns gradlew version, whitespace, location', () async {
const String expectedVersion = '7.4.2';
final Directory androidDirectory = fileSystem.directory('/android')
..createSync();
final Directory wrapperDirectory = androidDirectory
.childDirectory('gradle')
.childDirectory('wrapper')
..createSync(recursive: true);
// Distribution url is not the last line.
// Whitespace around distribution url.
wrapperDirectory
.childFile('gradle-wrapper.properties')
.writeAsStringSync('''
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl = https\\://services.gradle.org/distributions/gradle-$expectedVersion-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
''');
expect(
await getGradleVersion(
androidDirectory, BufferLogger.test(), FakeProcessManager.empty()),
expectedVersion,
);
});
testWithoutContext('does not crash on hypothetical new format', () async {
final Directory androidDirectory = fileSystem.directory('/android')
..createSync();
final Directory wrapperDirectory = androidDirectory
.childDirectory('gradle')
.childDirectory('wrapper')
..createSync(recursive: true);
// Distribution url is not the last line.
// Whitespace around distribution url.
wrapperDirectory
.childFile('gradle-wrapper.properties')
.writeAsStringSync(r'distributionUrl=https\://services.gradle.org/distributions/gradle_7.4.2_all.zip');
// FakeProcessManager.any is used here and not in other getGradleVersion
// tests because this test does not care about process fallback logic.
expect(
await getGradleVersion(
androidDirectory, BufferLogger.test(), FakeProcessManager.any()),
isNull,
);
});
testWithoutContext('returns the installed gradle version', () async {
const String expectedVersion = '7.4.2';
const String gradleOutput = '''
------------------------------------------------------------
Gradle $expectedVersion
------------------------------------------------------------
Build time: 2022-03-31 15:25:29 UTC
Revision: 540473b8118064efcc264694cbcaa4b677f61041
Kotlin: 1.5.31
Groovy: 3.0.9
Ant: Apache Ant(TM) version 1.10.11 compiled on July 10 2021
JVM: 11.0.18 (Azul Systems, Inc. 11.0.18+10-LTS)
OS: Mac OS X 13.2.1 aarch64
''';
final Directory androidDirectory = fileSystem.directory('/android')
..createSync();
final ProcessManager processManager = FakeProcessManager.empty()
..addCommand(const FakeCommand(
command: <String>['gradle', gradleVersionFlag],
stdout: gradleOutput));
expect(
await getGradleVersion(
androidDirectory,
BufferLogger.test(),
processManager,
),
expectedVersion,
);
});
testWithoutContext('returns the installed gradle with whitespace formatting', () async {
const String expectedVersion = '7.4.2';
const String gradleOutput = 'Gradle $expectedVersion';
final Directory androidDirectory = fileSystem.directory('/android')
..createSync();
final ProcessManager processManager = FakeProcessManager.empty()
..addCommand(const FakeCommand(
command: <String>['gradle', gradleVersionFlag],
stdout: gradleOutput));
expect(
await getGradleVersion(
androidDirectory,
BufferLogger.test(),
processManager,
),
expectedVersion,
);
});
testWithoutContext('returns the AGP version when set', () async {
const String expectedVersion = '7.3.0';
final Directory androidDirectory = fileSystem.directory('/android')
..createSync();
androidDirectory.childFile('build.gradle').writeAsStringSync('''
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:$expectedVersion'
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
''');
expect(
getAgpVersion(androidDirectory, BufferLogger.test()),
expectedVersion,
);
});
testWithoutContext('returns null when AGP version not set', () async {
final Directory androidDirectory = fileSystem.directory('/android')
..createSync();
androidDirectory.childFile('build.gradle').writeAsStringSync('''
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
''');
expect(
getAgpVersion(androidDirectory, BufferLogger.test()),
null,
);
});
testWithoutContext('returns the AGP version when beta', () async {
final Directory androidDirectory = fileSystem.directory('/android')
..createSync();
androidDirectory.childFile('build.gradle').writeAsStringSync(r'''
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.3.0-beta03'
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
''');
expect(
getAgpVersion(androidDirectory, BufferLogger.test()),
'7.3.0',
);
});
group('validates gradle/agp versions', () {
final List<GradleAgpTestData> testData = <GradleAgpTestData>[
// Values too new *these need to update* when
// max known gradle and max known agp versions are updated:
// Newer tools version supports max gradle version.
GradleAgpTestData(true, agpVersion: '8.2', gradleVersion: '8.0'),
// Newer tools version does not even meet current gradle version requiremnts.
GradleAgpTestData(false, agpVersion: '8.2', gradleVersion: '7.3'),
// Newer tools version requires newer gradle version.
GradleAgpTestData(true, agpVersion: '8.3', gradleVersion: '8.1'),
// Minimims as defined in
// https://developer.android.com/studio/releases/gradle-plugin#updating-gradle
GradleAgpTestData(true, agpVersion: '8.1', gradleVersion: '8.0'),
GradleAgpTestData(true, agpVersion: '8.0', gradleVersion: '8.0'),
GradleAgpTestData(true, agpVersion: '7.4', gradleVersion: '7.5'),
GradleAgpTestData(true, agpVersion: '7.3', gradleVersion: '7.4'),
GradleAgpTestData(true, agpVersion: '7.2', gradleVersion: '7.3.3'),
GradleAgpTestData(true, agpVersion: '7.1', gradleVersion: '7.2'),
GradleAgpTestData(true, agpVersion: '7.0', gradleVersion: '7.0'),
GradleAgpTestData(true, agpVersion: '4.2.0', gradleVersion: '6.7.1'),
GradleAgpTestData(true, agpVersion: '4.1.0', gradleVersion: '6.5'),
GradleAgpTestData(true, agpVersion: '4.0.0', gradleVersion: '6.1.1'),
GradleAgpTestData(true, agpVersion: '3.6.0', gradleVersion: '5.6.4'),
GradleAgpTestData(true, agpVersion: '3.5.0', gradleVersion: '5.4.1'),
GradleAgpTestData(true, agpVersion: '3.4.0', gradleVersion: '5.1.1'),
GradleAgpTestData(true, agpVersion: '3.3.0', gradleVersion: '4.10.1'),
// Values too old:
GradleAgpTestData(false, agpVersion: '3.3.0', gradleVersion: '4.9'),
GradleAgpTestData(false, agpVersion: '7.3', gradleVersion: '7.2'),
GradleAgpTestData(false, agpVersion: '3.0.0', gradleVersion: '7.2'),
// Null values:
// ignore: avoid_redundant_argument_values
GradleAgpTestData(false, agpVersion: null, gradleVersion: '7.2'),
// ignore: avoid_redundant_argument_values
GradleAgpTestData(false, agpVersion: '3.0.0', gradleVersion: null),
// ignore: avoid_redundant_argument_values
GradleAgpTestData(false, agpVersion: null, gradleVersion: null),
// Middle AGP cases:
GradleAgpTestData(true, agpVersion: '8.0.1', gradleVersion: '8.0'),
GradleAgpTestData(true, agpVersion: '7.4.1', gradleVersion: '7.5'),
GradleAgpTestData(true, agpVersion: '7.3.1', gradleVersion: '7.4'),
GradleAgpTestData(true, agpVersion: '7.2.1', gradleVersion: '7.3.3'),
GradleAgpTestData(true, agpVersion: '7.1.1', gradleVersion: '7.2'),
GradleAgpTestData(true, agpVersion: '7.0.1', gradleVersion: '7.0'),
GradleAgpTestData(true, agpVersion: '4.2.1', gradleVersion: '6.7.1'),
GradleAgpTestData(true, agpVersion: '4.1.1', gradleVersion: '6.5'),
GradleAgpTestData(true, agpVersion: '4.0.1', gradleVersion: '6.1.1'),
GradleAgpTestData(true, agpVersion: '3.6.1', gradleVersion: '5.6.4'),
GradleAgpTestData(true, agpVersion: '3.5.1', gradleVersion: '5.4.1'),
GradleAgpTestData(true, agpVersion: '3.4.1', gradleVersion: '5.1.1'),
GradleAgpTestData(true, agpVersion: '3.3.1', gradleVersion: '4.10.1'),
// Higher gradle cases:
GradleAgpTestData(true, agpVersion: '7.4', gradleVersion: '8.0'),
GradleAgpTestData(true, agpVersion: '7.3', gradleVersion: '7.5'),
GradleAgpTestData(true, agpVersion: '7.2', gradleVersion: '7.4'),
GradleAgpTestData(true, agpVersion: '7.1', gradleVersion: '7.3.3'),
GradleAgpTestData(true, agpVersion: '7.0', gradleVersion: '7.2'),
GradleAgpTestData(true, agpVersion: '4.2.0', gradleVersion: '7.0'),
GradleAgpTestData(true, agpVersion: '4.1.0', gradleVersion: '6.7.1'),
GradleAgpTestData(true, agpVersion: '4.0.0', gradleVersion: '6.5'),
GradleAgpTestData(true, agpVersion: '3.6.0', gradleVersion: '6.1.1'),
GradleAgpTestData(true, agpVersion: '3.5.0', gradleVersion: '5.6.4'),
GradleAgpTestData(true, agpVersion: '3.4.0', gradleVersion: '5.4.1'),
GradleAgpTestData(true, agpVersion: '3.3.0', gradleVersion: '5.1.1'),
];
for (final GradleAgpTestData data in testData) {
test('(gradle, agp): (${data.gradleVersion}, ${data.agpVersion})', () {
expect(
validateGradleAndAgp(
BufferLogger.test(),
gradleV: data.gradleVersion,
agpV: data.agpVersion,
),
data.validPair ? isTrue : isFalse,
reason: 'G: ${data.gradleVersion}, AGP: ${data.agpVersion}');
});
}
});
group('validates java/gradle versions', () {
final List<JavaGradleTestData> testData = <JavaGradleTestData>[
// Values too new *these need to update* when
// max supported java and max known gradle versions are updated:
// Newer tools version does not even meet current gradle version requiremnts.
JavaGradleTestData(false, javaVersion: '20', gradleVersion: '7.5'),
// Newer tools version requires newer gradle version.
JavaGradleTestData(true, javaVersion: '20', gradleVersion: '8.1'),
// Max known unsupported java version.
JavaGradleTestData(true, javaVersion: '24', gradleVersion: '8.1'),
// Minimims as defined in
// https://docs.gradle.org/current/userguide/compatibility.html#java
JavaGradleTestData(true, javaVersion: '19', gradleVersion: '7.6'),
JavaGradleTestData(true, javaVersion: '18', gradleVersion: '7.5'),
JavaGradleTestData(true, javaVersion: '17', gradleVersion: '7.3'),
JavaGradleTestData(true, javaVersion: '16', gradleVersion: '7.0'),
JavaGradleTestData(true, javaVersion: '15', gradleVersion: '6.7'),
JavaGradleTestData(true, javaVersion: '14', gradleVersion: '6.3'),
JavaGradleTestData(true, javaVersion: '13', gradleVersion: '6.0'),
JavaGradleTestData(true, javaVersion: '12', gradleVersion: '5.4'),
JavaGradleTestData(true, javaVersion: '11', gradleVersion: '5.0'),
JavaGradleTestData(true, javaVersion: '1.10', gradleVersion: '4.7'),
JavaGradleTestData(true, javaVersion: '1.9', gradleVersion: '4.3'),
JavaGradleTestData(true, javaVersion: '1.8', gradleVersion: '2.0'),
// Gradle too old for java version.
JavaGradleTestData(false, javaVersion: '19', gradleVersion: '6.7'),
JavaGradleTestData(false, javaVersion: '11', gradleVersion: '4.10.1'),
JavaGradleTestData(false, javaVersion: '1.9', gradleVersion: '4.1'),
// Null values:
// ignore: avoid_redundant_argument_values
JavaGradleTestData(false, javaVersion: null, gradleVersion: '7.2'),
// ignore: avoid_redundant_argument_values
JavaGradleTestData(false, javaVersion: '11', gradleVersion: null),
// ignore: avoid_redundant_argument_values
JavaGradleTestData(false, javaVersion: null, gradleVersion: null),
// Middle Java cases:
// https://www.java.com/releases/
JavaGradleTestData(true, javaVersion: '19.0.2', gradleVersion: '8.0.2'),
JavaGradleTestData(true, javaVersion: '19.0.2', gradleVersion: '8.0.0'),
JavaGradleTestData(true, javaVersion: '18.0.2', gradleVersion: '8.0.2'),
JavaGradleTestData(true, javaVersion: '17.0.3', gradleVersion: '7.5'),
JavaGradleTestData(true, javaVersion: '16.0.1', gradleVersion: '7.3'),
JavaGradleTestData(true, javaVersion: '15.0.2', gradleVersion: '7.3'),
JavaGradleTestData(true, javaVersion: '14.0.1', gradleVersion: '7.0'),
JavaGradleTestData(true, javaVersion: '13.0.2', gradleVersion: '6.7'),
JavaGradleTestData(true, javaVersion: '12.0.2', gradleVersion: '6.3'),
JavaGradleTestData(true, javaVersion: '11.0.18', gradleVersion: '6.0'),
// Higher gradle cases:
JavaGradleTestData(true, javaVersion: '19', gradleVersion: '8.0'),
JavaGradleTestData(true, javaVersion: '18', gradleVersion: '8.0'),
JavaGradleTestData(true, javaVersion: '17', gradleVersion: '7.5'),
JavaGradleTestData(true, javaVersion: '16', gradleVersion: '7.3'),
JavaGradleTestData(true, javaVersion: '15', gradleVersion: '7.3'),
JavaGradleTestData(true, javaVersion: '14', gradleVersion: '7.0'),
JavaGradleTestData(true, javaVersion: '13', gradleVersion: '6.7'),
JavaGradleTestData(true, javaVersion: '12', gradleVersion: '6.3'),
JavaGradleTestData(true, javaVersion: '11', gradleVersion: '6.0'),
JavaGradleTestData(true, javaVersion: '1.10', gradleVersion: '5.4'),
JavaGradleTestData(true, javaVersion: '1.9', gradleVersion: '5.0'),
JavaGradleTestData(true, javaVersion: '1.8', gradleVersion: '4.3'),
];
for (final JavaGradleTestData data in testData) {
testWithoutContext(
'(Java, gradle): (${data.javaVersion}, ${data.gradleVersion})', () {
expect(
validateJavaGradle(
BufferLogger.test(),
javaV: data.javaVersion,
gradleV: data.gradleVersion,
),
data.validPair ? isTrue : isFalse,
reason: 'J: ${data.javaVersion}, G: ${data.gradleVersion}');
});
}
});
}); });
} }
class GradleAgpTestData {
GradleAgpTestData(this.validPair, {this.gradleVersion, this.agpVersion});
final String? gradleVersion;
final String? agpVersion;
final bool validPair;
}
class JavaGradleTestData {
JavaGradleTestData(this.validPair, {this.javaVersion, this.gradleVersion});
final String? gradleVersion;
final String? javaVersion;
final bool validPair;
}
...@@ -4,9 +4,14 @@ ...@@ -4,9 +4,14 @@
import 'package:file/file.dart'; import 'package:file/file.dart';
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/android/android_studio.dart';
import 'package:flutter_tools/src/android/gradle_utils.dart' as gradle_utils;
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/os.dart'; import 'package:flutter_tools/src/base/os.dart';
import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/base/process.dart';
import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/build_info.dart';
import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/cache.dart';
import 'package:flutter_tools/src/convert.dart'; import 'package:flutter_tools/src/convert.dart';
...@@ -401,6 +406,256 @@ void main() { ...@@ -401,6 +406,256 @@ void main() {
}); });
}); });
group('java gradle agp compatibility', () {
Future<FlutterProject?> configureJavaGradleAgpForTest(
FakeAndroidSdkWithDir androidSdk, {
required String javaV,
required String gradleV,
required String agpV,
}) async {
final FlutterProject project = await someProject();
addRootGradleFile(project.directory, gradleFileContent: () {
return '''
dependencies {
classpath 'com.android.tools.build:gradle:$agpV'
}
''';
});
addGradleWrapperFile(project.directory, gradleV);
androidSdk.javaVersion = javaV;
return project;
}
// Tests in this group that use overrides and _testInMemory should
// be placed in their own group to avoid test pollution. This is
// especially important for filesystem.
group('_', () {
final FakeProcessManager processManager;
final AndroidStudio androidStudio;
final FakeAndroidSdkWithDir androidSdk;
final FileSystem fileSystem = getFileSystemForPlatform();
processManager = FakeProcessManager.empty();
androidStudio = FakeAndroidStudio();
androidSdk =
FakeAndroidSdkWithDir(fileSystem.currentDirectory, androidStudio);
fileSystem.currentDirectory
.childDirectory(androidStudio.javaPath!)
.createSync();
_testInMemory(
'flamingo values are compatible',
() async {
final FlutterProject? project = await configureJavaGradleAgpForTest(
androidSdk,
javaV: '17.0.2',
gradleV: '8.0',
agpV: '7.4.2',
);
final CompatibilityResult value =
await project!.android.hasValidJavaGradleAgpVersions();
expect(value.success, isTrue);
},
androidStudio: androidStudio,
processManager: processManager,
androidSdk: androidSdk,
);
});
group('_', () {
final FakeProcessManager processManager;
final AndroidStudio androidStudio;
final FakeAndroidSdkWithDir androidSdk;
final FileSystem fileSystem = getFileSystemForPlatform();
processManager = FakeProcessManager.empty();
androidStudio = FakeAndroidStudio();
androidSdk =
FakeAndroidSdkWithDir(fileSystem.currentDirectory, androidStudio);
fileSystem.currentDirectory
.childDirectory(androidStudio.javaPath!)
.createSync();
_testInMemory(
'java 8 era values are compatible',
() async {
final FlutterProject? project = await configureJavaGradleAgpForTest(
androidSdk,
javaV: '1.8.0_242',
gradleV: '6.7.1',
agpV: '4.2.0',
);
final CompatibilityResult value =
await project!.android.hasValidJavaGradleAgpVersions();
expect(value.success, isTrue);
},
androidStudio: androidStudio,
processManager: processManager,
androidSdk: androidSdk,
);
});
group('_', () {
final FakeProcessManager processManager;
final AndroidStudio androidStudio;
final FakeAndroidSdkWithDir androidSdk;
final FileSystem fileSystem = getFileSystemForPlatform();
processManager = FakeProcessManager.empty();
androidStudio = FakeAndroidStudio();
androidSdk =
FakeAndroidSdkWithDir(fileSystem.currentDirectory, androidStudio);
fileSystem.currentDirectory
.childDirectory(androidStudio.javaPath!)
.createSync();
_testInMemory(
'electric eel era values are compatible',
() async {
final FlutterProject? project = await configureJavaGradleAgpForTest(
androidSdk,
javaV: '11.0.14',
gradleV: '7.3.3',
agpV: '7.2.0',
);
final CompatibilityResult value =
await project!.android.hasValidJavaGradleAgpVersions();
expect(value.success, isTrue);
},
androidStudio: androidStudio,
processManager: processManager,
androidSdk: androidSdk,
);
});
group('_', () {
final FakeProcessManager processManager;
final AndroidStudio androidStudio;
final FakeAndroidSdkWithDir androidSdk;
final FileSystem fileSystem = getFileSystemForPlatform();
processManager = FakeProcessManager.empty();
androidStudio = FakeAndroidStudio();
androidSdk =
FakeAndroidSdkWithDir(fileSystem.currentDirectory, androidStudio);
fileSystem.currentDirectory
.childDirectory(androidStudio.javaPath!)
.createSync();
_testInMemory(
'incompatible everything',
() async {
const String javaV = '17.0.2';
const String gradleV = '6.7.3';
const String agpV = '7.2.0';
final FlutterProject? project = await configureJavaGradleAgpForTest(
androidSdk,
javaV: javaV,
gradleV: gradleV,
agpV: agpV,
);
final CompatibilityResult value =
await project!.android.hasValidJavaGradleAgpVersions();
expect(value.success, isFalse);
// Should not have the valid string
expect(
value.description,
isNot(
contains(RegExp(AndroidProject.validJavaGradleAgpString))));
// On gradle/agp error print help url and gradle and agp versions.
expect(value.description,
contains(RegExp(AndroidProject.gradleAgpCompatUrl)));
expect(value.description, contains(RegExp(gradleV)));
expect(value.description, contains(RegExp(agpV)));
// On gradle/agp error print help url and java and gradle versions.
expect(value.description,
contains(RegExp(AndroidProject.javaGradleCompatUrl)));
expect(value.description, contains(RegExp(javaV)));
expect(value.description, contains(RegExp(gradleV)));
},
androidStudio: androidStudio,
processManager: processManager,
androidSdk: androidSdk,
);
});
group('_', () {
final FakeProcessManager processManager;
final AndroidStudio androidStudio;
final FakeAndroidSdkWithDir androidSdk;
final FileSystem fileSystem = getFileSystemForPlatform();
processManager = FakeProcessManager.empty();
androidStudio = FakeAndroidStudio();
androidSdk =
FakeAndroidSdkWithDir(fileSystem.currentDirectory, androidStudio);
fileSystem.currentDirectory
.childDirectory(androidStudio.javaPath!)
.createSync();
_testInMemory(
'incompatible java/gradle only',
() async {
const String javaV = '17.0.2';
const String gradleV = '6.7.3';
const String agpV = '4.2.0';
final FlutterProject? project = await configureJavaGradleAgpForTest(
androidSdk,
javaV: javaV,
gradleV: gradleV,
agpV: agpV,
);
final CompatibilityResult value =
await project!.android.hasValidJavaGradleAgpVersions();
expect(value.success, isFalse);
// Should not have the valid string.
expect(
value.description,
isNot(
contains(RegExp(AndroidProject.validJavaGradleAgpString))));
// On gradle/agp error print help url and java and gradle versions.
expect(value.description,
contains(RegExp(AndroidProject.javaGradleCompatUrl)));
expect(value.description, contains(RegExp(javaV)));
expect(value.description, contains(RegExp(gradleV)));
},
androidStudio: androidStudio,
processManager: processManager,
androidSdk: androidSdk,
);
});
group('_', () {
final FakeProcessManager processManager;
final AndroidStudio androidStudio;
final FakeAndroidSdkWithDir androidSdk;
final FileSystem fileSystem = getFileSystemForPlatform();
processManager = FakeProcessManager.empty();
androidStudio = FakeAndroidStudio();
androidSdk =
FakeAndroidSdkWithDir(fileSystem.currentDirectory, androidStudio);
fileSystem.currentDirectory
.childDirectory(androidStudio.javaPath!)
.createSync();
_testInMemory(
'incompatible gradle/agp only',
() async {
const String javaV = '11.0.2';
const String gradleV = '7.0.3';
const String agpV = '7.1.0';
final FlutterProject? project = await configureJavaGradleAgpForTest(
androidSdk,
javaV: javaV,
gradleV: gradleV,
agpV: agpV,
);
final CompatibilityResult value =
await project!.android.hasValidJavaGradleAgpVersions();
expect(value.success, isFalse);
// Should not have the valid string.
expect(
value.description,
isNot(
contains(RegExp(AndroidProject.validJavaGradleAgpString))));
// On gradle/agp error print help url and gradle and agp versions.
expect(value.description,
contains(RegExp(AndroidProject.gradleAgpCompatUrl)));
expect(value.description, contains(RegExp(gradleV)));
expect(value.description, contains(RegExp(agpV)));
},
androidStudio: androidStudio,
processManager: processManager,
androidSdk: androidSdk,
);
});
});
group('language', () { group('language', () {
late XcodeProjectInterpreter xcodeProjectInterpreter; late XcodeProjectInterpreter xcodeProjectInterpreter;
late MemoryFileSystem fs; late MemoryFileSystem fs;
...@@ -1025,42 +1280,52 @@ flutter: ...@@ -1025,42 +1280,52 @@ flutter:
/// Executes the [testMethod] in a context where the file system /// Executes the [testMethod] in a context where the file system
/// is in memory. /// is in memory.
@isTest @isTest
void _testInMemory(String description, Future<void> Function() testMethod) { void _testInMemory(
String description,
Future<void> Function() testMethod, {
FileSystem? fileSystem,
AndroidStudio? androidStudio,
ProcessManager? processManager,
AndroidSdk? androidSdk,
}) {
Cache.flutterRoot = getFlutterRoot(); Cache.flutterRoot = getFlutterRoot();
final FileSystem testFileSystem = MemoryFileSystem( final FileSystem testFileSystem = fileSystem ?? getFileSystemForPlatform();
style: globals.platform.isWindows ? FileSystemStyle.windows : FileSystemStyle.posix, testFileSystem.directory('.dart_tool').childFile('package_config.json')
);
testFileSystem
.directory('.dart_tool')
.childFile('package_config.json')
..createSync(recursive: true) ..createSync(recursive: true)
..writeAsStringSync('{"configVersion":2,"packages":[]}'); ..writeAsStringSync('{"configVersion":2,"packages":[]}');
// Transfer needed parts of the Flutter installation folder // Transfer needed parts of the Flutter installation folder
// to the in-memory file system used during testing. // to the in-memory file system used during testing.
final Logger logger = BufferLogger.test(); final Logger logger = BufferLogger.test();
transfer(Cache( transfer(
fileSystem: globals.fs, Cache(
logger: logger, fileSystem: globals.fs,
artifacts: <ArtifactSet>[], logger: logger,
osUtils: OperatingSystemUtils( artifacts: <ArtifactSet>[],
fileSystem: globals.fs, osUtils: OperatingSystemUtils(
logger: logger, fileSystem: globals.fs,
platform: globals.platform, logger: logger,
processManager: globals.processManager, platform: globals.platform,
), processManager: globals.processManager,
platform: globals.platform, ),
).getArtifactDirectory('gradle_wrapper'), testFileSystem); platform: globals.platform,
transfer(globals.fs.directory(Cache.flutterRoot) ).getArtifactDirectory('gradle_wrapper'),
.childDirectory('packages') testFileSystem);
.childDirectory('flutter_tools') transfer(
.childDirectory('templates'), testFileSystem); globals.fs
.directory(Cache.flutterRoot)
.childDirectory('packages')
.childDirectory('flutter_tools')
.childDirectory('templates'),
testFileSystem);
// Set up enough of the packages to satisfy the templating code. // Set up enough of the packages to satisfy the templating code.
final File packagesFile = testFileSystem.directory(Cache.flutterRoot) final File packagesFile = testFileSystem
.directory(Cache.flutterRoot)
.childDirectory('packages') .childDirectory('packages')
.childDirectory('flutter_tools') .childDirectory('flutter_tools')
.childDirectory('.dart_tool') .childDirectory('.dart_tool')
.childFile('package_config.json'); .childFile('package_config.json');
final Directory dummyTemplateImagesDirectory = testFileSystem.directory(Cache.flutterRoot).parent; final Directory dummyTemplateImagesDirectory =
testFileSystem.directory(Cache.flutterRoot).parent;
dummyTemplateImagesDirectory.createSync(recursive: true); dummyTemplateImagesDirectory.createSync(recursive: true);
packagesFile.createSync(recursive: true); packagesFile.createSync(recursive: true);
packagesFile.writeAsStringSync(json.encode(<String, Object>{ packagesFile.writeAsStringSync(json.encode(<String, Object>{
...@@ -1080,18 +1345,21 @@ void _testInMemory(String description, Future<void> Function() testMethod) { ...@@ -1080,18 +1345,21 @@ void _testInMemory(String description, Future<void> Function() testMethod) {
testMethod, testMethod,
overrides: <Type, Generator>{ overrides: <Type, Generator>{
FileSystem: () => testFileSystem, FileSystem: () => testFileSystem,
ProcessManager: () => FakeProcessManager.any(), ProcessManager: () => processManager ?? FakeProcessManager.any(),
AndroidStudio: () => androidStudio ?? FakeAndroidStudio(),
// Intentionlly null if not set. Some ios tests fail if this is a fake.
AndroidSdk: () => androidSdk,
Cache: () => Cache( Cache: () => Cache(
logger: globals.logger, logger: globals.logger,
fileSystem: testFileSystem, fileSystem: testFileSystem,
osUtils: globals.os, osUtils: globals.os,
platform: globals.platform, platform: globals.platform,
artifacts: <ArtifactSet>[], artifacts: <ArtifactSet>[],
), ),
FlutterProjectFactory: () => FlutterProjectFactory( FlutterProjectFactory: () => FlutterProjectFactory(
fileSystem: testFileSystem, fileSystem: testFileSystem,
logger: globals.logger, logger: globals.logger,
), ),
}, },
); );
} }
...@@ -1133,8 +1401,40 @@ void addAndroidGradleFile(Directory directory, { required String Function() grad ...@@ -1133,8 +1401,40 @@ void addAndroidGradleFile(Directory directory, { required String Function() grad
.childDirectory('android') .childDirectory('android')
.childDirectory('app') .childDirectory('app')
.childFile('build.gradle') .childFile('build.gradle')
..createSync(recursive: true) ..createSync(recursive: true)
..writeAsStringSync(gradleFileContent()); ..writeAsStringSync(gradleFileContent());
}
void addRootGradleFile(Directory directory,
{required String Function() gradleFileContent}) {
directory.childDirectory('android').childFile('build.gradle')
..createSync(recursive: true)
..writeAsStringSync(gradleFileContent());
}
void addGradleWrapperFile(Directory directory, String gradleVersion) {
directory
.childDirectory('android')
.childDirectory(gradle_utils.gradleDirectoryName)
.childDirectory(gradle_utils.gradleWrapperDirectoryName)
.childFile(gradle_utils.gradleWrapperPropertiesFilename)
..createSync(recursive: true)
// ignore: unnecessary_string_escapes
..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
''');
}
FileSystem getFileSystemForPlatform() {
return MemoryFileSystem(
style: globals.platform.isWindows
? FileSystemStyle.windows
: FileSystemStyle.posix,
);
} }
void addAndroidWithGroup(Directory directory, String id) { void addAndroidWithGroup(Directory directory, String id) {
...@@ -1245,3 +1545,39 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete ...@@ -1245,3 +1545,39 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete
@override @override
bool get isInstalled => true; bool get isInstalled => true;
} }
class FakeAndroidSdkWithDir extends Fake implements AndroidSdk {
FakeAndroidSdkWithDir(this._directory, AndroidStudio _androidStudio) {
_javaPath = '${_androidStudio.javaPath}/bin/java';
}
late String _javaPath;
String? javaVersion;
final Directory _directory;
@override
late bool platformToolsAvailable;
@override
late bool licensesAvailable;
@override
AndroidSdkVersion? latestVersion;
@override
Directory get directory => _directory;
@override
Map<String, String> get sdkManagerEnv => <String, String>{'PATH': _javaPath};
@override
String? getJavaVersion({
required AndroidStudio? androidStudio,
required FileSystem fileSystem,
required OperatingSystemUtils operatingSystemUtils,
required Platform platform,
required ProcessUtils processUtils,
}) {
return javaVersion;
}
}
...@@ -56,6 +56,57 @@ baz=qux ...@@ -56,6 +56,57 @@ baz=qux
expect(Version.parse('Preview2.2'), isNull); expect(Version.parse('Preview2.2'), isNull);
}); });
group('isWithinVersionRange', () {
test('unknown not included', () {
expect(isWithinVersionRange('unknown', min: '1.0.0', max: '1.1.3'),
isFalse);
});
test('pre java 8 format included', () {
expect(isWithinVersionRange('1.0.0_201', min: '1.0.0', max: '1.1.3'),
isTrue);
});
test('min included by default', () {
expect(
isWithinVersionRange('1.0.0', min: '1.0.0', max: '1.1.3'), isTrue);
});
test('max included by default', () {
expect(
isWithinVersionRange('1.1.3', min: '1.0.0', max: '1.1.3'), isTrue);
});
test('inclusive min excluded', () {
expect(
isWithinVersionRange('1.0.0',
min: '1.0.0', max: '1.1.3', inclusiveMin: false),
isFalse);
});
test('inclusive max excluded', () {
expect(
isWithinVersionRange('1.1.3',
min: '1.0.0', max: '1.1.3', inclusiveMax: false),
isFalse);
});
test('lower value excluded', () {
expect(
isWithinVersionRange('0.1.0', min: '1.0.0', max: '1.1.3'), isFalse);
});
test('higher value excluded', () {
expect(
isWithinVersionRange('1.1.4', min: '1.0.0', max: '1.1.3'), isFalse);
});
test('middle value included', () {
expect(
isWithinVersionRange('1.1.0', min: '1.0.0', max: '1.1.3'), isTrue);
});
});
}); });
group('Misc', () { group('Misc', () {
......
...@@ -11,6 +11,7 @@ import 'package:flutter_tools/src/base/logger.dart'; ...@@ -11,6 +11,7 @@ import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/platform.dart';
import 'package:flutter_tools/src/commands/analyze.dart'; import 'package:flutter_tools/src/commands/analyze.dart';
import 'package:flutter_tools/src/globals.dart' as globals; import 'package:flutter_tools/src/globals.dart' as globals;
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/project_validator.dart'; import 'package:flutter_tools/src/project_validator.dart';
import '../src/common.dart'; import '../src/common.dart';
...@@ -57,6 +58,7 @@ void main() { ...@@ -57,6 +58,7 @@ void main() {
'│ [✓] Is Flutter Package: yes │\n' '│ [✓] Is Flutter Package: yes │\n'
'│ [✓] Uses Material Design: yes │\n' '│ [✓] Uses Material Design: yes │\n'
'│ [✓] Is Plugin: no │\n' '│ [✓] Is Plugin: no │\n'
'│ [✓] Java/Gradle/Android Gradle Plugin: ${AndroidProject.validJavaGradleAgpString}\n'
'└───────────────────────────────────────────────────────────────────┘\n'; '└───────────────────────────────────────────────────────────────────┘\n';
expect(loggerTest.statusText, contains(expected)); expect(loggerTest.statusText, contains(expected));
......
...@@ -6,6 +6,7 @@ import 'dart:async'; ...@@ -6,6 +6,7 @@ import 'dart:async';
import 'dart:io' as io show IOSink, ProcessSignal, Stdout, StdoutException; import 'dart:io' as io show IOSink, ProcessSignal, Stdout, StdoutException;
import 'package:flutter_tools/src/android/android_sdk.dart'; import 'package:flutter_tools/src/android/android_sdk.dart';
import 'package:flutter_tools/src/android/android_studio.dart';
import 'package:flutter_tools/src/base/bot_detector.dart'; import 'package:flutter_tools/src/base/bot_detector.dart';
import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/file_system.dart';
import 'package:flutter_tools/src/base/io.dart'; import 'package:flutter_tools/src/base/io.dart';
...@@ -596,3 +597,8 @@ class FakeAndroidSdk extends Fake implements AndroidSdk { ...@@ -596,3 +597,8 @@ class FakeAndroidSdk extends Fake implements AndroidSdk {
@override @override
AndroidSdkVersion? latestVersion; AndroidSdkVersion? latestVersion;
} }
class FakeAndroidStudio extends Fake implements AndroidStudio {
@override
String get javaPath => 'java';
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment