From 6794e375cca688d82f82a4f3c0cbe38016ca1083 Mon Sep 17 00:00:00 2001 From: Reid Baker Date: Wed, 5 Apr 2023 14:36:05 -0400 Subject: [PATCH] Add Java-Gradle-AGP validation to flutter analyze (#123916) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../lib/src/android/android_sdk.dart | 97 ++- .../lib/src/android/gradle_utils.dart | 542 +++++++++++++++-- .../flutter_tools/lib/src/base/version.dart | 33 + .../flutter_tools/lib/src/context_runner.dart | 1 - packages/flutter_tools/lib/src/project.dart | 106 +++- .../lib/src/project_validator.dart | 1 + .../android/android_sdk_test.dart | 131 ++++ .../android/android_workflow_test.dart | 5 +- .../general.shard/android/gradle_test.dart | 76 +-- .../android/gradle_utils_test.dart | 571 +++++++++++++++--- .../test/general.shard/project_test.dart | 410 +++++++++++-- .../test/general.shard/utils_test.dart | 51 ++ .../analyze_suggestions_integration_test.dart | 2 + packages/flutter_tools/test/src/fakes.dart | 6 + 15 files changed, 1799 insertions(+), 235 deletions(-) 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'; +}