From e5a922fed4bd2a2efacfe2849020a68c6630e8bb Mon Sep 17 00:00:00 2001 From: Ann Marie Mossman <233583+mossmana@users.noreply.github.com> Date: Mon, 5 Feb 2024 13:47:39 -0800 Subject: [PATCH] Update AGP version validation code to support KGP and kotlin build files. (#142357) Addresses: https://github.com/flutter/flutter/issues/141410 --- .../lib/src/android/gradle_utils.dart | 90 +++++----- .../android/gradle_utils_test.dart | 165 ++++++++++++++---- 2 files changed, 181 insertions(+), 74 deletions(-) diff --git a/packages/flutter_tools/lib/src/android/gradle_utils.dart b/packages/flutter_tools/lib/src/android/gradle_utils.dart index 4bec2eaa725..8b3e6ee49c5 100644 --- a/packages/flutter_tools/lib/src/android/gradle_utils.dart +++ b/packages/flutter_tools/lib/src/android/gradle_utils.dart @@ -75,19 +75,25 @@ const String oldestDocumentedJavaAgpCompatibilityVersion = '4.2'; // [_settingsAndroidGradlePluginRegExp] to identify the version section. const String _versionGroupName = 'version'; -// AGP can be defined in build.gradle -// Expected content: -// "classpath 'com.android.tools.build:gradle:7.3.0'" +// AGP can be defined in the dependencies block of [build.gradle] or [build.gradle.kts]. +// Expected content (covers both classpath and compileOnly cases): +// Groovy DSL with single quotes - 'com.android.tools.build:gradle:{{agpVersion}}' +// Groovy DSL with double quotes - "com.android.tools.build:gradle:{{agpVersion}}" +// Kotlin DSL - ("com.android.tools.build.gradle:{{agpVersion}}") // ? is used to name the version group which helps with extraction. -final RegExp _buildAndroidGradlePluginRegExp = - RegExp(r'com\.android\.tools\.build:gradle:(?\d+\.\d+\.\d+)'); +final RegExp _androidGradlePluginRegExpFromDependencies = RegExp( + r"""[^\/]*\s*((\bclasspath\b)|(\bcompileOnly\b))\s*\(?['"]com\.android\.tools\.build:gradle:(?\d+(\.\d+){1,2})\)?""", + multiLine: true); -// AGP can be defined in settings.gradle. +// AGP can be defined in the plugins block of [build.gradle], +// [build.gradle.kts], [settings.gradle], or [settings.gradle.kts]. // Expected content: -// "id "com.android.application" version "{{agpVersion}}"" +// Groovy DSL with single quotes - id 'com.android.application' version '{{agpVersion}}' +// Groovy DSL with double quotes - id "com.android.application" version "{{agpVersion}}" +// Kotlin DSL - id("com.android.application") version "{{agpVersion}}" // ? is used to name the version group which helps with extraction. -final RegExp _settingsAndroidGradlePluginRegExp = RegExp( - r'^\s+id\s+"com.android.application"\s+version\s+"(?\d+\.\d+\.\d+)"', +final RegExp _androidGradlePluginRegExpFromId = RegExp( + r"""[^\/]*s*id\s*\(?['"]com\.android\.application['"]\)?\s+version\s+['"](?\d+(\.\d+){1,2})\)?""", multiLine: true); // Expected content format (with lines above and below). @@ -203,30 +209,17 @@ distributionUrl=https\\://services.gradle.org/distributions/gradle-$gradleVersio /// Returns the Gradle version that the current Android plugin depends on when found, /// otherwise it returns a default version. /// -/// The Android plugin version is specified in the [build.gradle] file within +/// The Android plugin version is specified in the [build.gradle], +/// [build.gradle.kts], [settings.gradle], or [settings.gradle.kts] file within /// the project's Android directory. String getGradleVersionForAndroidPlugin(Directory directory, Logger logger) { - const String buildFileName = 'build.gradle/build.gradle.kts'; - - File buildFile = directory.childFile('build.gradle'); - if (!buildFile.existsSync()) { - buildFile = directory.childFile('build.gradle.kts'); - } - - if (!buildFile.existsSync()) { + final String? androidPluginVersion = getAgpVersion(directory, logger); + if (androidPluginVersion == null) { logger.printTrace( - "$buildFileName doesn't exist, assuming Gradle version: $templateDefaultGradleVersion"); + 'AGP version cannot be determined, assuming Gradle version: $templateDefaultGradleVersion'); return templateDefaultGradleVersion; } - final String buildFileContent = buildFile.readAsStringSync(); - final Iterable pluginMatches = _buildAndroidGradlePluginRegExp.allMatches(buildFileContent); - if (pluginMatches.isEmpty) { - logger.printTrace("$buildFileName doesn't provide an AGP version, assuming Gradle version: $templateDefaultGradleVersion"); - return templateDefaultGradleVersion; - } - final String? androidPluginVersion = pluginMatches.first.group(1); - logger.printTrace('$buildFileName provides AGP version: $androidPluginVersion'); - return getGradleVersionFor(androidPluginVersion ?? 'unknown'); + return getGradleVersionFor(androidPluginVersion); } /// Returns the gradle file from the top level directory. @@ -329,38 +322,51 @@ OS: Mac OS X 13.2.1 aarch64 /// 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] or -/// [settings.gradle] file within the project's -/// Android directory ([androidDirectory]). +/// The Android plugin version is specified in the [build.gradle], +/// [build.gradle.kts], [settings.gradle] or [settings.gradle.kts] +/// file within the project's Android directory ([androidDirectory]). String? getAgpVersion(Directory androidDirectory, Logger logger) { File buildFile = androidDirectory.childFile('build.gradle'); if (!buildFile.existsSync()) { buildFile = androidDirectory.childFile('build.gradle.kts'); } - if (!buildFile.existsSync()) { - logger.printTrace('Can not find build.gradle/build.gradle.kts in $androidDirectory'); + logger.printTrace( + 'Cannot find build.gradle/build.gradle.kts in $androidDirectory'); return null; } final String buildFileContent = buildFile.readAsStringSync(); - final RegExpMatch? buildMatch = - _buildAndroidGradlePluginRegExp.firstMatch(buildFileContent); - if (buildMatch != null) { + final RegExpMatch? buildMatchClasspath = + _androidGradlePluginRegExpFromDependencies.firstMatch(buildFileContent); + if (buildMatchClasspath != null) { final String? androidPluginVersion = - buildMatch.namedGroup(_versionGroupName); - logger.printTrace('$buildFile provides AGP version: $androidPluginVersion'); + buildMatchClasspath.namedGroup(_versionGroupName); + logger.printTrace('$buildFile provides AGP version from classpath: $androidPluginVersion'); return androidPluginVersion; } + final RegExpMatch? buildMatchId = + _androidGradlePluginRegExpFromId.firstMatch(buildFileContent); + if (buildMatchId != null) { + final String? androidPluginVersion = + buildMatchId.namedGroup(_versionGroupName); + logger.printTrace('$buildFile provides AGP version from plugin id: $androidPluginVersion'); + return androidPluginVersion; + } + logger.printTrace( "$buildFile doesn't provide an AGP version. Checking settings."); - final File settingsFile = androidDirectory.childFile('settings.gradle'); + File settingsFile = androidDirectory.childFile('settings.gradle'); if (!settingsFile.existsSync()) { - logger.printTrace('$settingsFile does not exist.'); + settingsFile = androidDirectory.childFile('settings.gradle.kts'); + } + if (!settingsFile.existsSync()) { + logger.printTrace( + 'Cannot find settings.gradle/settings.gradle.kts in $androidDirectory'); return null; } final String settingsFileContent = settingsFile.readAsStringSync(); final RegExpMatch? settingsMatch = - _settingsAndroidGradlePluginRegExp.firstMatch(settingsFileContent); + _androidGradlePluginRegExpFromId.firstMatch(settingsFileContent); if (settingsMatch != null) { final String? androidPluginVersion = @@ -385,7 +391,7 @@ String _formatParseWarning(String content) { // // Returns true if versions are compatible. // Null Gradle version or AGP version returns false. -// If compatibility can not be evaluated returns false. +// If compatibility cannot be evaluated returns false. // If versions are newer than the max known version a warning is logged and true // returned. // 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 82893bb31b3..0e3522edb27 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 @@ -387,7 +387,9 @@ OS: Mac OS X 13.2.1 aarch64 ); }); - testWithoutContext('returns the AGP version when set in Groovy', () async { + testWithoutContext( + 'returns the AGP version when set in Groovy build file as classpath with single quotes and commented line', + () async { const String expectedVersion = '7.3.0'; final Directory androidDirectory = fileSystem.directory('/android') ..createSync(); @@ -399,6 +401,8 @@ buildscript { } dependencies { + // Decoy value to ensure we ignore commented out lines. + // classpath 'com.android.application' version '6.1.0' apply false classpath 'com.android.tools.build:gradle:$expectedVersion' } } @@ -417,7 +421,9 @@ allprojects { ); }); - testWithoutContext('returns the AGP version when set in Kotlin', () async { + testWithoutContext( + 'returns the AGP version when set in Kotlin build file as classpath', + () async { const String expectedVersion = '7.3.0'; final Directory androidDirectory = fileSystem.directory('/android') ..createSync(); @@ -447,7 +453,77 @@ allprojects { ); }); - testWithoutContext('prefers the AGP version when set in Groovy, ignores Kotlin', () async { + + testWithoutContext( + 'returns the AGP version when set in Groovy build file as compileOnly with double quotes', + () async { + const String expectedVersion = '7.1.0'; + final Directory androidDirectory = fileSystem.directory('/android') + ..createSync(); + androidDirectory.childFile('build.gradle.kts').writeAsStringSync(''' +dependencies { + compileOnly "com.android.tools.build:gradle:$expectedVersion" +} +'''); + + expect( + getAgpVersion(androidDirectory, BufferLogger.test()), + expectedVersion, + ); + }); + testWithoutContext( + 'returns the AGP version when set in Kotlin build file as compileOnly', + () async { + const String expectedVersion = '7.1.0'; + final Directory androidDirectory = fileSystem.directory('/android') + ..createSync(); + androidDirectory.childFile('build.gradle.kts').writeAsStringSync(''' +dependencies { + compileOnly("com.android.tools.build:gradle:$expectedVersion") +} +'''); + + expect( + getAgpVersion(androidDirectory, BufferLogger.test()), + expectedVersion, + ); + }); + testWithoutContext( + 'returns the AGP version when set in Groovy build file as plugin', + () async { + const String expectedVersion = '6.8'; + final Directory androidDirectory = fileSystem.directory('/android') + ..createSync(); + androidDirectory.childFile('build.gradle').writeAsStringSync(''' +plugins { + id 'com.android.application' version '$expectedVersion' apply false +} + '''); + expect( + getAgpVersion(androidDirectory, BufferLogger.test()), + expectedVersion, + ); + }); + + testWithoutContext( + 'returns the AGP version when set in Kotlin build file as plugin', + () async { + const String expectedVersion = '7.2.0'; + final Directory androidDirectory = fileSystem.directory('/android') + ..createSync(); + androidDirectory.childFile('build.gradle.kts').writeAsStringSync(''' +plugins { + id("com.android.application") version "$expectedVersion" apply false +} + '''); + expect( + getAgpVersion(androidDirectory, BufferLogger.test()), + expectedVersion, + ); + }); + + testWithoutContext( + 'prefers the AGP version when set in Groovy, ignores Kotlin', () async { const String versionInGroovy = '7.3.0'; const String versionInKotlin = '7.4.2'; final Directory androidDirectory = fileSystem.directory('/android') @@ -554,48 +630,73 @@ allprojects { ); }); - testWithoutContext('returns the AGP version when in settings', () async { + testWithoutContext('returns the AGP version when in Groovy settings as plugin', + () async { final Directory androidDirectory = fileSystem.directory('/android') ..createSync(); // File must exist and can not have agp defined. androidDirectory.childFile('build.gradle').writeAsStringSync(r''); androidDirectory.childFile('settings.gradle').writeAsStringSync(r''' pluginManagement { - def flutterSdkPath = { - def properties = new Properties() - file("local.properties").withInputStream { properties.load(it) } - def flutterSdkPath = properties.getProperty("flutter.sdk") - assert flutterSdkPath != null, "flutter.sdk not set in local.properties" - return flutterSdkPath - } - settings.ext.flutterSdkPath = flutterSdkPath() - - includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") - - repositories { - google() - mavenCentral() - gradlePluginPortal() - } - plugins { - id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false + id 'dev.flutter.flutter-gradle-plugin' version '1.0.0' apply false + id 'dev.flutter.flutter-plugin-loader' version '1.0.0' + // Decoy value to ensure we ignore commented out lines. + // id 'com.android.application' version '6.1.0' apply false + id 'com.android.application' version '8.1.0' apply false } } - -plugins { - id "dev.flutter.flutter-plugin-loader" version "1.0.0" - // Decoy value to ensure we ignore commented out lines. - // id "com.android.application" version "6.1.0" apply false - id "com.android.application" version "7.3.0" apply false -} - -include ":app" '''); expect( getAgpVersion(androidDirectory, BufferLogger.test()), - '7.3.0', + '8.1.0', + ); + }); + + testWithoutContext( + 'returns the AGP version when in Kotlin settings as plugin', () async { + final Directory androidDirectory = fileSystem.directory('/android') + ..createSync(); + // File must exist and cannot have agp defined. + androidDirectory.childFile('build.gradle.kts').writeAsStringSync(r''); + androidDirectory.childFile('settings.gradle.kts').writeAsStringSync(r''' +pluginManagement { + plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + // Decoy value to ensure we ignore commented out lines. + // id("com.android.application") version "6.1.0" apply false + id("com.android.application") version "7.5.0" apply false + } +} +'''); + + expect( + getAgpVersion(androidDirectory, BufferLogger.test()), + '7.5.0', + ); + }); + + testWithoutContext( + 'returns null when agp version is misconfigured', + () async { + final Directory androidDirectory = fileSystem.directory('/android') + ..createSync(); + androidDirectory.childFile('build.gradle.kts').writeAsStringSync(''' +plugins { + `java-gradle-plugin` + `groovy` +} + +dependencies { + // intentional typo + compileOnl("com.android.tools.build:gradle:7.3.0") +} +'''); + + expect( + getAgpVersion(androidDirectory, BufferLogger.test()), + null, ); });