diff --git a/dev/integration_tests/flutter_gallery/android/gradle/wrapper/gradle-wrapper.properties b/dev/integration_tests/flutter_gallery/android/gradle/wrapper/gradle-wrapper.properties index f338a880848d1..ceccc3a854030 100644 --- a/dev/integration_tests/flutter_gallery/android/gradle/wrapper/gradle-wrapper.properties +++ b/dev/integration_tests/flutter_gallery/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME 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 diff --git a/packages/flutter_tools/lib/src/android/android_sdk.dart b/packages/flutter_tools/lib/src/android/android_sdk.dart index 0165887cb98fd..7a3bb66804438 100644 --- a/packages/flutter_tools/lib/src/android/android_sdk.dart +++ b/packages/flutter_tools/lib/src/android/android_sdk.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:meta/meta.dart'; + import '../base/common.dart'; import '../base/file_system.dart'; import '../base/os.dart'; @@ -409,6 +411,57 @@ class AndroidSdk { 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( + [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 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. static String? findJavaBinary({ required AndroidStudio? androidStudio, @@ -417,12 +470,15 @@ class AndroidSdk { required Platform platform, }) { if (androidStudio?.javaPath != null) { + globals.printTrace("Using Android Studio's java."); return fileSystem.path.join(androidStudio!.javaPath!, 'bin', 'java'); } - final String? javaHomeEnv = platform.environment[_javaHomeEnvironmentVariable]; + final String? javaHomeEnv = + platform.environment[_javaHomeEnvironmentVariable]; if (javaHomeEnv != null) { // Trust JAVA_HOME. + globals.printTrace('Using JAVA_HOME.'); return fileSystem.path.join(javaHomeEnv, 'bin', 'java'); } @@ -430,23 +486,48 @@ class AndroidSdk { // See: http://stackoverflow.com/questions/14292698/how-do-i-check-if-the-java-jdk-is-installed-on-mac. if (platform.isMacOS) { try { - final String javaHomeOutput = globals.processUtils.runSync( - ['/usr/libexec/java_home', '-v', '1.8'], - throwOnError: true, - hideStdout: true, - ).stdout.trim(); + // -v Filter versions (as if JAVA_VERSION had been set in the environment). + // It is unlikley that filtering to java version 1.8 is the right + // decision here. That said, trying this on a mac shows the same jdk + // path no matter what input is passed. + final String javaHomeOutput = globals.processUtils + .runSync( + ['/usr/libexec/java_home', '-v', '1.8'], + throwOnError: true, + hideStdout: true, + ) + .stdout + .trim(); if (javaHomeOutput.isNotEmpty) { final String javaHome = javaHomeOutput.split('\n').last.trim(); + globals.printTrace('Using mac JAVA_HOME.'); return fileSystem.path.join(javaHome, 'bin', 'java'); } - } on Exception { /* ignore */ } + } on Exception {/* ignore */} } // 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? _sdkManagerEnv; + /// Returns an environment with the Java folder added to PATH for use in calling /// Java-based Android SDK commands such as sdkmanager and avdmanager. Map get sdkManagerEnv { diff --git a/packages/flutter_tools/lib/src/android/gradle_utils.dart b/packages/flutter_tools/lib/src/android/gradle_utils.dart index 6d40ab170a006..6ea8c0fc501a2 100644 --- a/packages/flutter_tools/lib/src/android/gradle_utils.dart +++ b/packages/flutter_tools/lib/src/android/gradle_utils.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:meta/meta.dart'; +import 'package:process/process.dart'; import '../base/common.dart'; import '../base/file_system.dart'; @@ -43,7 +44,33 @@ const String minSdkVersion = '16'; const String targetSdkVersion = '31'; 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 /// or constructing a Gradle project. @@ -51,17 +78,14 @@ class GradleUtils { GradleUtils({ required Platform platform, required Logger logger, - required FileSystem fileSystem, required Cache cache, required OperatingSystemUtils operatingSystemUtils, - }) : _platform = platform, + }) : _platform = platform, _logger = logger, _cache = cache, - _fileSystem = fileSystem, _operatingSystemUtils = operatingSystemUtils; final Cache _cache; - final FileSystem _fileSystem; final Platform _platform; final Logger _logger; final OperatingSystemUtils _operatingSystemUtils; @@ -83,9 +107,8 @@ class GradleUtils { return gradle.absolute.path; } throwToolExit( - 'Unable to locate gradlew script. Please check that ${gradle.path} ' - 'exists or that ${gradle.dirname} can be read.' - ); + 'Unable to locate gradlew script. Please check that ${gradle.path} ' + 'exists or that ${gradle.dirname} can be read.'); } /// Injects the Gradle wrapper files if any of these files don't exist in [directory]. @@ -103,15 +126,17 @@ class GradleUtils { ); // Add the `gradle-wrapper.properties` file if it doesn't exist. final Directory propertiesDirectory = directory - .childDirectory(_fileSystem.path.join('gradle', 'wrapper')); - final File propertiesFile = propertiesDirectory - .childFile('gradle-wrapper.properties'); + .childDirectory(gradleDirectoryName) + .childDirectory(gradleWrapperDirectoryName); + final File propertiesFile = + propertiesDirectory.childFile(gradleWrapperPropertiesFilename); if (propertiesFile.existsSync()) { return; } propertiesDirectory.createSync(recursive: true); - final String gradleVersion = getGradleVersionForAndroidPlugin(directory, _logger); + final String gradleVersion = + getGradleVersionForAndroidPlugin(directory, _logger); final String propertyContents = ''' distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists @@ -131,11 +156,12 @@ distributionUrl=https\\://services.gradle.org/distributions/gradle-$gradleVersio String getGradleVersionForAndroidPlugin(Directory directory, Logger logger) { final File buildFile = directory.childFile('build.gradle'); 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; } final String buildFileContent = buildFile.readAsStringSync(); - final Iterable pluginMatches = _androidPluginRegExp.allMatches(buildFileContent); + final Iterable pluginMatches = _androidGradlePluginRegExp.allMatches(buildFileContent); if (pluginMatches.isEmpty) { logger.printTrace("$buildFile doesn't provide an AGP version, assuming Gradle version: $templateDefaultGradleVersion"); return templateDefaultGradleVersion; @@ -145,65 +171,426 @@ String getGradleVersionForAndroidPlugin(Directory directory, Logger logger) { return getGradleVersionFor(androidPluginVersion ?? 'unknown'); } -/// Returns true if [targetVersion] is within the range [min] and [max] inclusive. -bool _isWithinVersionRange( - String targetVersion, { - required String min, - required String max, -}) { - final Version? parsedTargetVersion = Version.parse(targetVersion); - final Version? minVersion = Version.parse(min); - final Version? maxVersion = Version.parse(max); - return minVersion != null && - maxVersion != null && - parsedTargetVersion != null && - parsedTargetVersion >= minVersion && - parsedTargetVersion <= maxVersion; +/// Returns either the gradle-wrapper.properties value from the passed in +/// [directory] or if not present the version available in local path. +/// +/// If gradle version is not found null is returned. +/// [directory] should be and android directory with a build.gradle file. +Future getGradleVersion( + Directory directory, Logger logger, ProcessManager processManager) async { + final File propertiesFile = directory + .childDirectory(gradleDirectoryName) + .childDirectory(gradleWrapperDirectoryName) + .childFile(gradleWrapperPropertiesFilename); + + if (propertiesFile.existsSync()) { + 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 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(['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 -/// by picking the largest compatible version from -/// https://developer.android.com/studio/releases/gradle-plugin#updating-gradle -@visibleForTesting -String getGradleVersionFor(String androidPluginVersion) { - if (_isWithinVersionRange(androidPluginVersion, min: '1.0.0', max: '1.1.3')) { - return '2.3'; +/// Returns the Android Gradle Plugin (AGP) version that the current project +/// depends on when found, null otherwise. +/// +/// The Android plugin version is specified in the [build.gradle] file within +/// the project's Android directory ([androidDirectory]). +String? getAgpVersion(Directory androidDirectory, Logger logger) { + 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 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')) { - return '2.9'; + if (isWithinVersionRange(agpV, + 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')) { - return '2.2.1'; + if (isWithinVersionRange(agpV, + 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')) { - return '2.13'; + if (isWithinVersionRange(agpV, + 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')) { - return '2.14.1'; + if (isWithinVersionRange(agpV, + 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')) { - return '3.3'; + if (isWithinVersionRange(agpV, + 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')) { - return '4.1'; + if (isWithinVersionRange(agpV, + 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')) { - return '4.6'; + if (isWithinVersionRange(gradleV, + 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 compatList = [ + 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 compatList = [ + 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')) { - return '7.5'; + if (isWithinVersionRange(androidPluginVersion, min: maxKnownAgpVersion, max: '100.100')) { + return _maxKnownAndSupportedGradleVersion; } throwToolExit('Unsupported Android Plugin version: $androidPluginVersion.'); } @@ -284,9 +671,36 @@ void writeLocalProperties(File properties) { } void exitWithNoSdkMessage() { - BuildEvent('unsupported-project', type: 'gradle', eventError: 'android-sdk-not-found', flutterUsage: globals.flutterUsage).send(); - throwToolExit( - '${globals.logger.terminal.warningMark} No Android SDK found. ' - 'Try setting the ANDROID_SDK_ROOT environment variable.' - ); + BuildEvent('unsupported-project', + type: 'gradle', + eventError: 'android-sdk-not-found', + 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; } diff --git a/packages/flutter_tools/lib/src/base/version.dart b/packages/flutter_tools/lib/src/base/version.dart index 89895995f76aa..b82d992a9d6af 100644 --- a/packages/flutter_tools/lib/src/base/version.dart +++ b/packages/flutter_tools/lib/src/base/version.dart @@ -4,6 +4,7 @@ import 'package:meta/meta.dart'; +// TODO(reidbaker): Investigate using pub_semver instead of this class. @immutable class Version implements Comparable { /// Creates a new [Version] object. @@ -119,3 +120,35 @@ class Version implements Comparable { @override 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; +} diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart index fffa29da4edef..1f7c6fc24bf05 100644 --- a/packages/flutter_tools/lib/src/context_runner.dart +++ b/packages/flutter_tools/lib/src/context_runner.dart @@ -236,7 +236,6 @@ Future runInContext( fuchsiaArtifacts: globals.fuchsiaArtifacts!, ), GradleUtils: () => GradleUtils( - fileSystem: globals.fs, operatingSystemUtils: globals.os, logger: globals.logger, platform: globals.platform, diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index 9a69bd3bb8c04..6d1f5665f67cd 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart @@ -20,6 +20,7 @@ import 'flutter_manifest.dart'; import 'flutter_plugins.dart'; import 'globals.dart' as globals; import 'platform_plugins.dart'; +import 'project_validator_result.dart'; import 'reporting/reporting.dart'; import 'template.dart'; import 'xcode_project.dart'; @@ -418,6 +419,20 @@ abstract class FlutterProjectPlatform { class AndroidProject extends FlutterProjectPlatform { 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. final FlutterProject parent; @@ -510,6 +525,77 @@ class AndroidProject extends FlutterProjectPlatform { 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 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 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 { return hostAppGradleRoot.childFile('build.gradle').existsSync(); } @@ -570,12 +656,17 @@ class AndroidProject extends FlutterProjectPlatform { Future _regenerateLibrary() async { ErrorHandlingFileSystem.deleteIfExists(ephemeralDirectory, recursive: true); + await _overwriteFromTemplate( + globals.fs.path.join( + 'module', + 'android', + 'library_new_embedding', + ), + ephemeralDirectory); await _overwriteFromTemplate(globals.fs.path.join( 'module', 'android', - 'library_new_embedding', - ), ephemeralDirectory); - await _overwriteFromTemplate(globals.fs.path.join('module', 'android', 'gradle'), ephemeralDirectory); + 'gradle'), ephemeralDirectory); globals.gradleUtils?.injectGradleWrapperIfNeeded(ephemeralDirectory); } @@ -786,3 +877,12 @@ class FuchsiaProject { Directory get 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; +} diff --git a/packages/flutter_tools/lib/src/project_validator.dart b/packages/flutter_tools/lib/src/project_validator.dart index 365e4db029eae..791642a19bf8c 100644 --- a/packages/flutter_tools/lib/src/project_validator.dart +++ b/packages/flutter_tools/lib/src/project_validator.dart @@ -224,6 +224,7 @@ class GeneralInfoProjectValidator extends ProjectValidator{ result.add(_materialDesignResult(flutterManifest)); result.add(_pluginValidatorResult(flutterManifest)); } + result.add(await project.android.validateJavaGradleAgpVersions()); return result; } diff --git a/packages/flutter_tools/test/general.shard/android/android_sdk_test.dart b/packages/flutter_tools/test/general.shard/android/android_sdk_test.dart index 9e7ed95ea2396..1401f43478a3f 100644 --- a/packages/flutter_tools/test/general.shard/android/android_sdk_test.dart +++ b/packages/flutter_tools/test/general.shard/android/android_sdk_test.dart @@ -4,13 +4,18 @@ 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/base/config.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/process.dart'; import 'package:flutter_tools/src/globals.dart' as globals; +import '../../integration.shard/test_utils.dart'; import '../../src/common.dart'; import '../../src/context.dart'; +import '../../src/fakes.dart' show FakeAndroidStudio, FakeOperatingSystemUtils; void main() { late MemoryFileSystem fileSystem; @@ -342,6 +347,132 @@ void main() { 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: { + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + Config: () => config, + Platform: () => FakePlatform(environment: {}), + }); + + 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: [ + 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: { + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + AndroidStudio: () => androidStudio, + Config: () => config, + Platform: () => FakePlatform(environment: {}), + }); + }); + }); } /// A broken SDK installation. diff --git a/packages/flutter_tools/test/general.shard/android/android_workflow_test.dart b/packages/flutter_tools/test/general.shard/android/android_workflow_test.dart index e86165c023a00..43a3e3bc16306 100644 --- a/packages/flutter_tools/test/general.shard/android/android_workflow_test.dart +++ b/packages/flutter_tools/test/general.shard/android/android_workflow_test.dart @@ -16,6 +16,7 @@ import 'package:flutter_tools/src/doctor_validator.dart'; import 'package:test/fake.dart'; import '../../src/common.dart'; +import '../../src/context.dart'; import '../../src/fake_process_manager.dart'; import '../../src/fakes.dart'; @@ -425,7 +426,7 @@ Review licenses that have not been accepted (y/N)? 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( command: [ 'which', @@ -523,7 +524,7 @@ Review licenses that have not been accepted (y/N)? expect(cmdlineMessage.message, errorMessage); }); - testWithoutContext('detects minimum required java version', () async { + testUsingContext('detects minimum required java version', () async { // Test with older version of JDK const String javaVersionText = 'openjdk version "1.7.0_212"'; processManager.addCommand(const FakeCommand( diff --git a/packages/flutter_tools/test/general.shard/android/gradle_test.dart b/packages/flutter_tools/test/general.shard/android/gradle_test.dart index 643d5faf74167..5b32f927997f0 100644 --- a/packages/flutter_tools/test/general.shard/android/gradle_test.dart +++ b/packages/flutter_tools/test/general.shard/android/gradle_test.dart @@ -5,7 +5,7 @@ import 'package:file/memory.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_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/base/common.dart'; import 'package:flutter_tools/src/base/file_system.dart'; @@ -196,7 +196,7 @@ void main() { group('gradle build', () { testUsingContext('do not crash if there is no Android SDK', () async { expect(() { - updateLocalProperties(project: FlutterProject.fromDirectoryTest(globals.fs.currentDirectory)); + gradle_utils.updateLocalProperties(project: FlutterProject.fromDirectoryTest(globals.fs.currentDirectory)); }, throwsToolExit( message: '${globals.logger.terminal.warningMark} No Android SDK found. Try setting the ANDROID_SDK_ROOT environment variable.', )); @@ -241,7 +241,7 @@ void main() { manifestFile.writeAsStringSync(manifest); - updateLocalProperties( + gradle_utils.updateLocalProperties( project: FlutterProject.fromDirectoryTest(globals.fs.directory('path/to/project')), buildInfo: buildInfo, requireAndroidSdk: false, @@ -415,55 +415,57 @@ flutter: }); }); - group('gradle version', () { + group('gradgradle_utils.le version', () { testWithoutContext('should be compatible with the Android plugin version', () { - // Granular versions. - expect(getGradleVersionFor('1.0.0'), '2.3'); - expect(getGradleVersionFor('1.0.1'), '2.3'); - expect(getGradleVersionFor('1.0.2'), '2.3'); - expect(getGradleVersionFor('1.0.4'), '2.3'); - expect(getGradleVersionFor('1.0.8'), '2.3'); - expect(getGradleVersionFor('1.1.0'), '2.3'); - expect(getGradleVersionFor('1.1.2'), '2.3'); - expect(getGradleVersionFor('1.1.2'), '2.3'); - expect(getGradleVersionFor('1.1.3'), '2.3'); - // Version Ranges. - expect(getGradleVersionFor('1.2.0'), '2.9'); - expect(getGradleVersionFor('1.3.1'), '2.9'); + // Grangradle_utils.ular versions. + expect(gradle_utils.getGradleVersionFor('1.0.0'), '2.3'); + expect(gradle_utils.getGradleVersionFor('1.0.1'), '2.3'); + expect(gradle_utils.getGradleVersionFor('1.0.2'), '2.3'); + expect(gradle_utils.getGradleVersionFor('1.0.4'), '2.3'); + expect(gradle_utils.getGradleVersionFor('1.0.8'), '2.3'); + expect(gradle_utils.getGradleVersionFor('1.1.0'), '2.3'); + expect(gradle_utils.getGradleVersionFor('1.1.2'), '2.3'); + expect(gradle_utils.getGradleVersionFor('1.1.2'), '2.3'); + expect(gradle_utils.getGradleVersionFor('1.1.3'), '2.3'); + // Versgradle_utils.ion Ranges. + expect(gradle_utils.getGradleVersionFor('1.2.0'), '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(getGradleVersionFor('2.1.2'), '2.13'); + expect(gradle_utils.getGradleVersionFor('2.0.0'), '2.13'); + expect(gradle_utils.getGradleVersionFor('2.1.2'), '2.13'); - expect(getGradleVersionFor('2.1.3'), '2.14.1'); - expect(getGradleVersionFor('2.2.3'), '2.14.1'); + expect(gradle_utils.getGradleVersionFor('2.1.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(getGradleVersionFor('3.2.1'), '4.6'); + expect(gradle_utils.getGradleVersionFor('3.2.0'), '4.6'); + expect(gradle_utils.getGradleVersionFor('3.2.1'), '4.6'); - expect(getGradleVersionFor('3.3.0'), '4.10.2'); - expect(getGradleVersionFor('3.3.2'), '4.10.2'); + expect(gradle_utils.getGradleVersionFor('3.3.0'), '4.10.2'); + expect(gradle_utils.getGradleVersionFor('3.3.2'), '4.10.2'); - expect(getGradleVersionFor('3.4.0'), '5.6.2'); - expect(getGradleVersionFor('3.5.0'), '5.6.2'); + expect(gradle_utils.getGradleVersionFor('3.4.0'), '5.6.2'); + expect(gradle_utils.getGradleVersionFor('3.5.0'), '5.6.2'); - expect(getGradleVersionFor('4.0.0'), '6.7'); - expect(getGradleVersionFor('4.1.0'), '6.7'); + expect(gradle_utils.getGradleVersionFor('4.0.0'), '6.7'); + expect(gradle_utils.getGradleVersionFor('4.1.0'), '6.7'); - expect(getGradleVersionFor('7.0'), '7.5'); - expect(getGradleVersionFor('7.1.2'), '7.5'); - expect(getGradleVersionFor('7.2'), '7.5'); + expect(gradle_utils.getGradleVersionFor('7.0'), '7.5'); + expect(gradle_utils.getGradleVersionFor('7.1.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', () { - expect(() => getGradleVersionFor('3.6.0'), + expect(() => gradle_utils.getGradleVersionFor('3.6.0'), throwsA(predicate((Exception e) => e is ToolExit))); }); }); diff --git a/packages/flutter_tools/test/general.shard/android/gradle_utils_test.dart b/packages/flutter_tools/test/general.shard/android/gradle_utils_test.dart index db04086956d88..418cf2d2eef84 100644 --- a/packages/flutter_tools/test/general.shard/android/gradle_utils_test.dart +++ b/packages/flutter_tools/test/general.shard/android/gradle_utils_test.dart @@ -14,30 +14,31 @@ import '../../src/fake_process_manager.dart'; import '../../src/fakes.dart'; void main() { - group('injectGradleWrapperIfNeeded', () { + group('injectGradleWrapperIfNeeded', () { late MemoryFileSystem fileSystem; late Directory gradleWrapperDirectory; late GradleUtils gradleUtils; setUp(() { 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 - .childFile('gradlew') - .writeAsStringSync('irrelevant'); + .childFile('gradlew') + .writeAsStringSync('irrelevant'); gradleWrapperDirectory - .childDirectory('gradle') - .childDirectory('wrapper') - .createSync(recursive: true); + .childDirectory('gradle') + .childDirectory('wrapper') + .createSync(recursive: true); gradleWrapperDirectory - .childDirectory('gradle') - .childDirectory('wrapper') - .childFile('gradle-wrapper.jar') - .writeAsStringSync('irrelevant'); + .childDirectory('gradle') + .childDirectory('wrapper') + .childFile('gradle-wrapper.jar') + .writeAsStringSync('irrelevant'); gradleUtils = GradleUtils( - cache: Cache.test(processManager: FakeProcessManager.any(), fileSystem: fileSystem), - fileSystem: fileSystem, + cache: Cache.test( + processManager: FakeProcessManager.any(), fileSystem: fileSystem), platform: FakePlatform(environment: {}), logger: BufferLogger.test(), operatingSystemUtils: FakeOperatingSystemUtils(), @@ -45,43 +46,52 @@ void main() { }); 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); gradleUtils.injectGradleWrapperIfNeeded(sampleAppAndroid); expect(sampleAppAndroid.childFile('gradlew').existsSync(), isTrue); - expect(sampleAppAndroid - .childDirectory('gradle') - .childDirectory('wrapper') - .childFile('gradle-wrapper.jar') - .existsSync(), isTrue); - - expect(sampleAppAndroid - .childDirectory('gradle') - .childDirectory('wrapper') - .childFile('gradle-wrapper.properties') - .existsSync(), isTrue); - - expect(sampleAppAndroid - .childDirectory('gradle') - .childDirectory('wrapper') - .childFile('gradle-wrapper.properties') - .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'); + expect( + sampleAppAndroid + .childDirectory('gradle') + .childDirectory('wrapper') + .childFile('gradle-wrapper.jar') + .existsSync(), + isTrue); + + expect( + sampleAppAndroid + .childDirectory('gradle') + .childDirectory('wrapper') + .childFile('gradle-wrapper.properties') + .existsSync(), + isTrue); + + expect( + sampleAppAndroid + .childDirectory('gradle') + .childDirectory('wrapper') + .childFile('gradle-wrapper.properties') + .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', () { - final Directory sampleAppAndroid = fileSystem.directory('/sample-app/android'); + final Directory sampleAppAndroid = + fileSystem.directory('/sample-app/android'); sampleAppAndroid.createSync(recursive: true); // There's an existing gradlew - sampleAppAndroid.childFile('gradlew').writeAsStringSync('existing gradlew'); + sampleAppAndroid + .childFile('gradlew') + .writeAsStringSync('existing gradlew'); gradleUtils.injectGradleWrapperIfNeeded(sampleAppAndroid); @@ -89,23 +99,28 @@ void main() { expect(sampleAppAndroid.childFile('gradlew').readAsStringSync(), equals('existing gradlew')); - expect(sampleAppAndroid - .childDirectory('gradle') - .childDirectory('wrapper') - .childFile('gradle-wrapper.jar') - .existsSync(), isTrue); - - expect(sampleAppAndroid - .childDirectory('gradle') - .childDirectory('wrapper') - .childFile('gradle-wrapper.properties') - .existsSync(), isTrue); - - expect(sampleAppAndroid - .childDirectory('gradle') - .childDirectory('wrapper') - .childFile('gradle-wrapper.properties') - .readAsStringSync(), + expect( + sampleAppAndroid + .childDirectory('gradle') + .childDirectory('wrapper') + .childFile('gradle-wrapper.jar') + .existsSync(), + isTrue); + + expect( + sampleAppAndroid + .childDirectory('gradle') + .childDirectory('wrapper') + .childFile('gradle-wrapper.properties') + .existsSync(), + isTrue); + + expect( + sampleAppAndroid + .childDirectory('gradle') + .childDirectory('wrapper') + .childFile('gradle-wrapper.properties') + .readAsStringSync(), 'distributionBase=GRADLE_USER_HOME\n' 'distributionPath=wrapper/dists\n' 'zipStoreBase=GRADLE_USER_HOME\n' @@ -113,7 +128,9 @@ void main() { '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 testCases = { // AGP version : Gradle version '1.0.0': '2.3', @@ -132,10 +149,9 @@ void main() { }; for (final MapEntry entry in testCases.entries) { - final Directory sampleAppAndroid = fileSystem.systemTempDirectory.createTempSync('flutter_android.'); - sampleAppAndroid - .childFile('build.gradle') - .writeAsStringSync(''' + final Directory sampleAppAndroid = + fileSystem.systemTempDirectory.createTempSync('flutter_android.'); + sampleAppAndroid.childFile('build.gradle').writeAsStringSync(''' buildscript { dependencies { classpath 'com.android.tools.build:gradle:${entry.key}' @@ -146,33 +162,39 @@ void main() { expect(sampleAppAndroid.childFile('gradlew').existsSync(), isTrue); - expect(sampleAppAndroid - .childDirectory('gradle') - .childDirectory('wrapper') - .childFile('gradle-wrapper.jar') - .existsSync(), isTrue); + expect( + sampleAppAndroid + .childDirectory('gradle') + .childDirectory('wrapper') + .childFile('gradle-wrapper.jar') + .existsSync(), + isTrue); - expect(sampleAppAndroid - .childDirectory('gradle') - .childDirectory('wrapper') - .childFile('gradle-wrapper.properties') - .existsSync(), isTrue); + expect( + sampleAppAndroid + .childDirectory('gradle') + .childDirectory('wrapper') + .childFile('gradle-wrapper.properties') + .existsSync(), + isTrue); - expect(sampleAppAndroid - .childDirectory('gradle') - .childDirectory('wrapper') - .childFile('gradle-wrapper.properties') - .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-${entry.value}-all.zip\n'); + expect( + sampleAppAndroid + .childDirectory('gradle') + .childDirectory('wrapper') + .childFile('gradle-wrapper.properties') + .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-${entry.value}-all.zip\n'); } }); 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.bat').createSync(); androidDirectory.childFile('gradle.properties').createSync(); @@ -182,9 +204,394 @@ void main() { fileSystem: fileSystem, ).fromDirectory(fileSystem.currentDirectory); - expect(gradleUtils.getExecutable(flutterProject), + expect( + gradleUtils.getExecutable(flutterProject), 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: ['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: ['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 testData = [ + // 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 testData = [ + // 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; +} diff --git a/packages/flutter_tools/test/general.shard/project_test.dart b/packages/flutter_tools/test/general.shard/project_test.dart index 485646360980b..924b7b4b05118 100644 --- a/packages/flutter_tools/test/general.shard/project_test.dart +++ b/packages/flutter_tools/test/general.shard/project_test.dart @@ -4,9 +4,14 @@ import 'package:file/file.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/logger.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/cache.dart'; import 'package:flutter_tools/src/convert.dart'; @@ -401,6 +406,256 @@ void main() { }); }); + group('java gradle agp compatibility', () { + Future 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', () { late XcodeProjectInterpreter xcodeProjectInterpreter; late MemoryFileSystem fs; @@ -1025,42 +1280,52 @@ flutter: /// Executes the [testMethod] in a context where the file system /// is in memory. @isTest -void _testInMemory(String description, Future Function() testMethod) { +void _testInMemory( + String description, + Future Function() testMethod, { + FileSystem? fileSystem, + AndroidStudio? androidStudio, + ProcessManager? processManager, + AndroidSdk? androidSdk, +}) { Cache.flutterRoot = getFlutterRoot(); - final FileSystem testFileSystem = MemoryFileSystem( - style: globals.platform.isWindows ? FileSystemStyle.windows : FileSystemStyle.posix, - ); - testFileSystem - .directory('.dart_tool') - .childFile('package_config.json') + final FileSystem testFileSystem = fileSystem ?? getFileSystemForPlatform(); + testFileSystem.directory('.dart_tool').childFile('package_config.json') ..createSync(recursive: true) ..writeAsStringSync('{"configVersion":2,"packages":[]}'); // Transfer needed parts of the Flutter installation folder // to the in-memory file system used during testing. final Logger logger = BufferLogger.test(); - transfer(Cache( - fileSystem: globals.fs, - logger: logger, - artifacts: [], - osUtils: OperatingSystemUtils( - fileSystem: globals.fs, - logger: logger, - platform: globals.platform, - processManager: globals.processManager, - ), - platform: globals.platform, - ).getArtifactDirectory('gradle_wrapper'), testFileSystem); - transfer(globals.fs.directory(Cache.flutterRoot) - .childDirectory('packages') - .childDirectory('flutter_tools') - .childDirectory('templates'), testFileSystem); + transfer( + Cache( + fileSystem: globals.fs, + logger: logger, + artifacts: [], + osUtils: OperatingSystemUtils( + fileSystem: globals.fs, + logger: logger, + platform: globals.platform, + processManager: globals.processManager, + ), + platform: globals.platform, + ).getArtifactDirectory('gradle_wrapper'), + testFileSystem); + transfer( + globals.fs + .directory(Cache.flutterRoot) + .childDirectory('packages') + .childDirectory('flutter_tools') + .childDirectory('templates'), + testFileSystem); // 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('flutter_tools') .childDirectory('.dart_tool') .childFile('package_config.json'); - final Directory dummyTemplateImagesDirectory = testFileSystem.directory(Cache.flutterRoot).parent; + final Directory dummyTemplateImagesDirectory = + testFileSystem.directory(Cache.flutterRoot).parent; dummyTemplateImagesDirectory.createSync(recursive: true); packagesFile.createSync(recursive: true); packagesFile.writeAsStringSync(json.encode({ @@ -1080,18 +1345,21 @@ void _testInMemory(String description, Future Function() testMethod) { testMethod, overrides: { 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( - logger: globals.logger, - fileSystem: testFileSystem, - osUtils: globals.os, - platform: globals.platform, - artifacts: [], - ), + logger: globals.logger, + fileSystem: testFileSystem, + osUtils: globals.os, + platform: globals.platform, + artifacts: [], + ), FlutterProjectFactory: () => FlutterProjectFactory( - fileSystem: testFileSystem, - logger: globals.logger, - ), + fileSystem: testFileSystem, + logger: globals.logger, + ), }, ); } @@ -1133,8 +1401,40 @@ void addAndroidGradleFile(Directory directory, { required String Function() grad .childDirectory('android') .childDirectory('app') .childFile('build.gradle') - ..createSync(recursive: true) - ..writeAsStringSync(gradleFileContent()); + ..createSync(recursive: true) + ..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) { @@ -1245,3 +1545,39 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete @override 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 get sdkManagerEnv => {'PATH': _javaPath}; + + @override + String? getJavaVersion({ + required AndroidStudio? androidStudio, + required FileSystem fileSystem, + required OperatingSystemUtils operatingSystemUtils, + required Platform platform, + required ProcessUtils processUtils, + }) { + return javaVersion; + } +} diff --git a/packages/flutter_tools/test/general.shard/utils_test.dart b/packages/flutter_tools/test/general.shard/utils_test.dart index 5e0f859e65b25..a2594533a523a 100644 --- a/packages/flutter_tools/test/general.shard/utils_test.dart +++ b/packages/flutter_tools/test/general.shard/utils_test.dart @@ -56,6 +56,57 @@ baz=qux 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', () { diff --git a/packages/flutter_tools/test/integration.shard/analyze_suggestions_integration_test.dart b/packages/flutter_tools/test/integration.shard/analyze_suggestions_integration_test.dart index e99813aef1958..c4c37ab48a55b 100644 --- a/packages/flutter_tools/test/integration.shard/analyze_suggestions_integration_test.dart +++ b/packages/flutter_tools/test/integration.shard/analyze_suggestions_integration_test.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/commands/analyze.dart'; 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 '../src/common.dart'; @@ -57,6 +58,7 @@ void main() { '│ [✓] Is Flutter Package: yes │\n' '│ [✓] Uses Material Design: yes │\n' '│ [✓] Is Plugin: no │\n' + '│ [✓] Java/Gradle/Android Gradle Plugin: ${AndroidProject.validJavaGradleAgpString} │\n' '└───────────────────────────────────────────────────────────────────┘\n'; expect(loggerTest.statusText, contains(expected)); diff --git a/packages/flutter_tools/test/src/fakes.dart b/packages/flutter_tools/test/src/fakes.dart index 808d5c6bf8833..87e4acbdd6c8a 100644 --- a/packages/flutter_tools/test/src/fakes.dart +++ b/packages/flutter_tools/test/src/fakes.dart @@ -6,6 +6,7 @@ import 'dart:async'; 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_studio.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/io.dart'; @@ -596,3 +597,8 @@ class FakeAndroidSdk extends Fake implements AndroidSdk { @override AndroidSdkVersion? latestVersion; } + +class FakeAndroidStudio extends Fake implements AndroidStudio { + @override + String get javaPath => 'java'; +}