diff --git a/dev/devicelab/lib/framework/dependency_smoke_test_task_definition.dart b/dev/devicelab/lib/framework/dependency_smoke_test_task_definition.dart index 4c9d468563532..d34fb917fd2b8 100644 --- a/dev/devicelab/lib/framework/dependency_smoke_test_task_definition.dart +++ b/dev/devicelab/lib/framework/dependency_smoke_test_task_definition.dart @@ -158,7 +158,10 @@ Future buildFlutterApkWithSpecifiedDependencyVersions({ section('Add a dependency on a plugin'); await flutter( 'pub', - options: ['add', 'shared_preferences_android:2.4.7'], // Chosen randomly. + options: [ + 'add', + 'shared_preferences_android:2.4.24', + ], // Smallest version jump supporting AGP 9. workingDirectory: appPath, ); diff --git a/dev/integration_tests/android_hardware_smoke_test/android/app/test_result_embedder.gradle.kts b/dev/integration_tests/android_hardware_smoke_test/android/app/test_result_embedder.gradle.kts index add0d13bbc152..ecd4545eba1b0 100644 --- a/dev/integration_tests/android_hardware_smoke_test/android/app/test_result_embedder.gradle.kts +++ b/dev/integration_tests/android_hardware_smoke_test/android/app/test_result_embedder.gradle.kts @@ -33,9 +33,23 @@ tasks.register("embedTestResultImages") { val packageId = "com.example.android_hardware_smoke_test" val discoveredTests = mutableListOf() - // Resolve binary safe adb executable from Android Gradle Plugin BaseExtension - val android = project.extensions.getByType(com.android.build.gradle.BaseExtension::class.java) - val adbPath = android.adbExecutable.absolutePath + // Resolve binary-safe adb executable from Android Gradle Plugin (new variant API with BaseExtension fallback) + val adbPath = + try { + val androidComponents = + project.extensions.getByType( + com.android.build.api.variant.ApplicationAndroidComponentsExtension::class.java + ) + androidComponents.sdkComponents.adb + .get() + .asFile.absolutePath + } catch (_: org.gradle.api.UnknownDomainObjectException) { + val android = project.extensions.getByType(com.android.build.gradle.BaseExtension::class.java) + android.adbExecutable.absolutePath + } catch (_: IllegalArgumentException) { + val android = project.extensions.getByType(com.android.build.gradle.BaseExtension::class.java) + android.adbExecutable.absolutePath + } println("Resolved binary-safe adb executable from AGP: $adbPath") // 1. Query the device sandbox to list all files in cache/results/ using ProcessBuilder diff --git a/docs/platforms/android/Migrating-Flutter-Gradle-Plugin-to-AGP-public-API.md b/docs/platforms/android/Migrating-Flutter-Gradle-Plugin-to-AGP-public-API.md index 5a828aef49313..94c62b12ca27f 100644 --- a/docs/platforms/android/Migrating-Flutter-Gradle-Plugin-to-AGP-public-API.md +++ b/docs/platforms/android/Migrating-Flutter-Gradle-Plugin-to-AGP-public-API.md @@ -97,7 +97,15 @@ projects. That opt-out dies with AGP 10. `outputs/flutter-apk` directory, because a shared `@OutputDirectory` would overlap between variants by construction. A runtime warning reports produced names outside the predicted set. -6. **P3 pre-spike (afterEvaluate DSL mutation under newDsl).** The planned scratch-app +6. **Stable-channel add-migrator guard (release management, not in this repo's + changes).** The stable channel still carries the old `DisableNewDslMigration`, + which ADDS `android.newDsl=false`. Once the P9 removal migrator reaches a + channel, switching between that channel and stable would flip-flop + gradle.properties. Before P9 is released, cherry-pick a guard onto the + stable-side add-migrator (skip adding the opt-out when the project's recorded + Flutter/AGP versions indicate the new-DSL-capable plugin) or accept the + flip-flop for the overlap window and document it in the release notes. +7. **P3 pre-spike (afterEvaluate DSL mutation under newDsl).** The planned scratch-app spike (AGP 9.1 + `newDsl=true` + custom build type, verifying that build-type creation from `pluginProject.afterEvaluate` still works) could not run in the implementation sandbox (no AGP artifact access). The `initWith` copy landed on the diff --git a/docs/platforms/android/website-page-draft.md b/docs/platforms/android/website-page-draft.md index de89c1b5ba0c5..ae61dca4002fe 100644 --- a/docs/platforms/android/website-page-draft.md +++ b/docs/platforms/android/website-page-draft.md @@ -3,7 +3,12 @@ *Draft breaking-change page for `docs.flutter.dev/release/breaking-changes/`. This file is the source of truth until the page is published to flutter/website; publishing must complete before the newDsl flip reaches the -beta channel. Contributor-facing details live in +beta channel. The page MUST be published at +`https://docs.flutter.dev/release/breaking-changes/android-agp-new-dsl` — +that URL is hard-coded as `kNewDslBreakingChangeDocsUrl` in +`packages/flutter_tools/lib/src/android/gradle_errors.dart` and is printed by +the legacy-variant-API error handler and the opt-out removal migrator. +Contributor-facing details live in [Migrating-Flutter-Gradle-Plugin-to-AGP-public-API.md](Migrating-Flutter-Gradle-Plugin-to-AGP-public-API.md).* ## Summary diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart index be7e0313e6a1b..8fd3fc2b05a5a 100644 --- a/packages/flutter_tools/lib/src/android/gradle.dart +++ b/packages/flutter_tools/lib/src/android/gradle.dart @@ -42,9 +42,9 @@ import 'java.dart'; import 'migrations/android_studio_java_gradle_conflict_migration.dart'; import 'migrations/cmake_android_16k_pages_migration.dart'; import 'migrations/disable_built_in_kotlin_migration.dart'; -import 'migrations/disable_new_dsl_migration.dart'; import 'migrations/min_sdk_version_migration.dart'; import 'migrations/multidex_removal_migration.dart'; +import 'migrations/remove_new_dsl_opt_out_migration.dart'; import 'migrations/top_level_gradle_build_file_migration.dart'; /// The regex to grab variant names from printBuildVariants gradle task @@ -518,7 +518,7 @@ To fix this, you can either: MultidexRemovalMigration(project.android, _logger), CmakeAndroid16kPagesMigration(project.android, _logger), DisableBuiltInKotlinMigration(project.android, _logger), - DisableNewDslMigration(project.android, _logger), + RemoveNewDslOptOutMigration(project.android, _logger), ]; final migration = ProjectMigration(migrators); diff --git a/packages/flutter_tools/lib/src/android/gradle_errors.dart b/packages/flutter_tools/lib/src/android/gradle_errors.dart index f58462d5d6361..7bde2b9ef6833 100644 --- a/packages/flutter_tools/lib/src/android/gradle_errors.dart +++ b/packages/flutter_tools/lib/src/android/gradle_errors.dart @@ -85,7 +85,7 @@ final gradleErrors = [ jlinkErrorWithJava21AndSourceCompatibility, missingNdkSourcePropertiesFile, applyingKotlinAndroidPluginErrorHandler, - useNewAgpDslErrorHandler, + legacyVariantApiUsageErrorHandler, incompatibleKotlinVersionHandler, // This handler should always be last, as its key log output is sometimes in error messages with other root causes. ]; @@ -689,9 +689,11 @@ final missingNdkSourcePropertiesFile = GradleHandledError( const String kMigrateToBuiltInKotlinDocsUrl = 'https://docs.flutter.dev/release/breaking-changes/migrate-to-built-in-kotlin'; -/// The URL for documentation on opting out of the new AGP DSL. -const String kOptOutOfNewDslDocsUrl = - 'https://developer.android.com/build/releases/agp-9-0-0-release-notes'; +/// The URL for the breaking-change page covering the Android Gradle Plugin new-DSL +/// migration (legacy variant API removal), including before/after recipes and the +/// temporary `android.newDsl=false` escape hatch. +const String kNewDslBreakingChangeDocsUrl = + 'https://docs.flutter.dev/release/breaking-changes/android-agp-new-dsl'; /// Handler when applying the kotlin-android plugin results in a build failure. This failure occurs when /// using AGP 9+ because built-in Kotlin has become the default behavior. @@ -715,29 +717,32 @@ To resolve this, migrate to built-in Kotlin. eventLabel: 'applying-kotlin-android-plugin-error', ); -/// Handler when using the new AGP DSL interfaces. Starting AGP 9+, only the new -/// DSL interfaces are used. This results in a failure because we still depend -/// on old DSL types. +/// Handler for Gradle build scripts that use the removed legacy AGP variant API +/// (`applicationVariants`, `libraryVariants`, `variantFilter`). Flutter projects build +/// with the Android Gradle Plugin's new DSL, under which these APIs do not exist. @visibleForTesting -final useNewAgpDslErrorHandler = GradleHandledError( +final legacyVariantApiUsageErrorHandler = GradleHandledError( test: _lineMatcher(const [ - "> Failed to apply plugin 'dev.flutter.flutter-gradle-plugin'", - '> java.lang.NullPointerException (no error message)', + "Could not get unknown property 'applicationVariants'", + "Could not get unknown property 'libraryVariants'", + "Could not get unknown property 'testVariants'", + "Could not get unknown property 'variantFilter'", + 'Could not find method applicationVariants(', + 'Could not find method libraryVariants(', + 'Could not find method variantFilter(', ]), handler: ({required String line, required FlutterProject project, required bool usesAndroidX}) async { - final File appGradleFile = project.android.appGradleFile; globals.printBox( ''' -${globals.logger.terminal.warningMark} Starting AGP 9+, only the new DSL interface will be read. -This results in a build failure when applying the Flutter Gradle plugin at ${appGradleFile.path}. -\nTo resolve this update flutter or opt out of `android.newDsl`. -For instructions on how to opt out, see: $kOptOutOfNewDslDocsUrl -\nIf you are not upgrading to AGP 9+, run `flutter analyze --suggestions` to check for incompatible dependencies.''', +${globals.logger.terminal.warningMark} A Gradle build script in this project uses the Android Gradle Plugin's legacy variant API (for example `android.applicationVariants`), which does not exist under the new Android Gradle Plugin DSL that Flutter projects now build with. +\nTo resolve this, migrate the build script to the variant API (`androidComponents.onVariants`). +For before/after examples of common patterns, see: $kNewDslBreakingChangeDocsUrl +\nAs a temporary escape hatch, you can add `android.newDsl=false` to android/gradle.properties, but this stops working with Android Gradle Plugin 10, which removes the legacy API entirely.''', title: _boxTitle, ); return GradleBuildStatus.exit; }, - eventLabel: 'use-new-agp-dsl-error', + eventLabel: 'legacy-variant-api-usage', ); diff --git a/packages/flutter_tools/lib/src/android/gradle_utils.dart b/packages/flutter_tools/lib/src/android/gradle_utils.dart index d0c2af17a1818..04188699beceb 100644 --- a/packages/flutter_tools/lib/src/android/gradle_utils.dart +++ b/packages/flutter_tools/lib/src/android/gradle_utils.dart @@ -50,6 +50,8 @@ const templateAndroidGradlePluginVersionForModule = '9.1.0'; // * KGP jvm constant in packages/flutter_tools/gradle/src/test/kotlin/DependencyVersionCheckerTest.kt // See https://kotlinlang.org/docs/releases.html#release-details const templateKotlinGradlePluginVersion = '2.4.0'; +const templateJunitJupiterEngineVersion = '5.10.2'; +const templateJunitPlatformLauncherVersion = '1.10.2'; // The Flutter Gradle Plugin is only applied to app projects, and modules that // are built from source using (`include_flutter.groovy`). The remaining diff --git a/packages/flutter_tools/lib/src/android/migrations/disable_new_dsl_migration.dart b/packages/flutter_tools/lib/src/android/migrations/disable_new_dsl_migration.dart deleted file mode 100644 index e63f2f29f491b..0000000000000 --- a/packages/flutter_tools/lib/src/android/migrations/disable_new_dsl_migration.dart +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright 2014 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import '../../base/file_system.dart'; -import '../../base/project_migrator.dart'; -import '../../project.dart'; - -const String _newDslFlagText = ''' -# This newDsl flag was added automatically by Flutter migrator -android.newDsl=false'''; - -// Gradle Properties are case sensitive so the AGP config must be this exact flag -final RegExp _newDslRegex = RegExp(r'^\s*android\.newDsl(?=[ \t=:])', multiLine: true); - -/// Migrate from enabled new DSL by default to disabled new DSL by default. -/// For more details see: https://developer.android.com/build/releases/agp-9-0-0-release-notes#android-gradle-plugin-changed-dsl -class DisableNewDslMigration extends ProjectMigrator { - DisableNewDslMigration(AndroidProject project, super.logger) - : _gradlePropertiesFile = project.hostAppGradleRoot.childFile('gradle.properties'); - - final File _gradlePropertiesFile; - - @override - Future migrate() async { - if (!_gradlePropertiesFile.existsSync()) { - logger.printTrace( - 'The gradle.properties file was not found. Creating it with a disabled new DSL flag.', - ); - try { - await _gradlePropertiesFile.writeAsString('$_newDslFlagText\n'); - } on FileSystemException catch (e) { - logger.printError('Failed to write to the gradle.properties during migration: $e'); - } - return; - } - - String contents; - - try { - contents = await _gradlePropertiesFile.readAsString(); - } on FileSystemException catch (e) { - logger.printError('Failed to read gradle.properties during migration: $e'); - return; - } - - // Skip migration if the newDsl flag already exists - if (contents.contains(_newDslRegex)) { - logger.printTrace( - 'The developer has already configured the new DSL flag, skipping migration.', - ); - return; - } - - processFileLines(_gradlePropertiesFile); - } - - @override - String migrateFileContents(String fileContents) { - logger.printTrace('Migrating to disable new DSL by default.'); - - final bool hasNewDsl = fileContents.contains(_newDslRegex); - - if (hasNewDsl) { - return fileContents; - } - - final propertyToAppend = StringBuffer(); - propertyToAppend.writeln(_newDslFlagText); - - final prefix = fileContents.isEmpty || fileContents.endsWith('\n') ? '' : '\n'; - - return '$fileContents$prefix$propertyToAppend'; - } -} diff --git a/packages/flutter_tools/lib/src/android/migrations/remove_new_dsl_opt_out_migration.dart b/packages/flutter_tools/lib/src/android/migrations/remove_new_dsl_opt_out_migration.dart new file mode 100644 index 0000000000000..ad518abbf447a --- /dev/null +++ b/packages/flutter_tools/lib/src/android/migrations/remove_new_dsl_opt_out_migration.dart @@ -0,0 +1,77 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../../base/file_system.dart'; +import '../../base/project_migrator.dart'; +import '../../project.dart'; +import '../gradle_errors.dart'; + +/// The marker comment the project templates placed above the opt-out. +const String _templateMarkerComment = '# This newDsl flag was added by the Flutter template'; + +/// The marker comment [the former DisableNewDslMigration] placed above the opt-out. +const String _migratorMarkerComment = + '# This newDsl flag was added automatically by Flutter migrator'; + +/// Matches the opt-out line Flutter wrote (tolerating whitespace and the `:` separator). +/// Deliberately does NOT match `android.newDsl=true` or other values: if the developer +/// edited the line, it is theirs now and stays. +final RegExp _newDslOptOutPattern = RegExp(r'^\s*android\.newDsl\s*[=:]\s*false\s*$'); + +/// Removes the `android.newDsl=false` opt-out that Flutter previously added to +/// `gradle.properties` (through the project templates and through the former +/// `DisableNewDslMigration`), now that the Flutter Gradle Plugin supports the Android +/// Gradle Plugin's new DSL. +/// +/// Only line pairs Flutter wrote are removed: one of the two known marker comments +/// immediately followed by the `android.newDsl=false` property line. The removal is +/// anchored on the `android.newDsl` property line - never on marker wording alone - +/// so the adjacent `android.builtInKotlin` marker/flag lines (owned by the separate +/// built-in Kotlin migration) and hand-added opt-outs are never touched. +class RemoveNewDslOptOutMigration extends ProjectMigrator { + RemoveNewDslOptOutMigration(AndroidProject project, super.logger) + : _gradlePropertiesFile = project.hostAppGradleRoot.childFile('gradle.properties'); + + final File _gradlePropertiesFile; + + @override + Future migrate() async { + if (!_gradlePropertiesFile.existsSync()) { + // Nothing to remove. (The former DisableNewDslMigration created this file when it + // was missing, writing only the marker and the flag; that case is handled by the + // pair removal below, leaving an empty file.) + return; + } + processFileLines(_gradlePropertiesFile); + } + + @override + String migrateFileContents(String fileContents) { + final List lines = fileContents.split('\n'); + final result = []; + var removed = false; + for (var i = 0; i < lines.length; i++) { + final String trimmed = lines[i].trim(); + final bool isFlutterMarker = + trimmed == _templateMarkerComment || trimmed == _migratorMarkerComment; + if (isFlutterMarker && i + 1 < lines.length && _newDslOptOutPattern.hasMatch(lines[i + 1])) { + // Skip the marker comment and the opt-out line it annotates. + i++; + removed = true; + continue; + } + result.add(lines[i]); + } + if (!removed) { + return fileContents; + } + logger.printStatus( + 'Removed the android.newDsl opt-out that Flutter previously added to ' + '${_gradlePropertiesFile.path}; Android builds now use the Android Gradle ' + "Plugin's new DSL. If your Android build fails after this change, see " + '$kNewDslBreakingChangeDocsUrl', + ); + return result.join('\n'); + } +} diff --git a/packages/flutter_tools/lib/src/commands/create_base.dart b/packages/flutter_tools/lib/src/commands/create_base.dart index 6f9caf3080c5d..809f52cc8e1a0 100644 --- a/packages/flutter_tools/lib/src/commands/create_base.dart +++ b/packages/flutter_tools/lib/src/commands/create_base.dart @@ -393,6 +393,8 @@ mixin CreateBase on FlutterCommand { 'agpVersion': agpVersion, 'agpVersionForModule': gradle.templateAndroidGradlePluginVersionForModule, 'kotlinVersion': kotlinVersion, + 'junitJupiterEngineVersion': gradle.templateJunitJupiterEngineVersion, + 'junitPlatformLauncherVersion': gradle.templateJunitPlatformLauncherVersion, 'gradleVersion': gradleVersion, 'compileSdkVersion': gradle.compileSdkVersion, 'minSdkVersion': gradle.minSdkVersion, diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index 0cbfe1435c031..37dbac23ab70a 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart @@ -990,6 +990,8 @@ See the link below for more information: 'agpVersion': gradle.templateAndroidGradlePluginVersion, 'agpVersionForModule': gradle.templateAndroidGradlePluginVersionForModule, 'kotlinVersion': gradle.templateKotlinGradlePluginVersion, + 'junitJupiterEngineVersion': gradle.templateJunitJupiterEngineVersion, + 'junitPlatformLauncherVersion': gradle.templateJunitPlatformLauncherVersion, 'gradleVersion': gradle.templateDefaultGradleVersion, 'compileSdkVersion': gradle.compileSdkVersion, 'minSdkVersion': gradle.minSdkVersion, diff --git a/packages/flutter_tools/templates/app/android.tmpl/gradle.properties.tmpl b/packages/flutter_tools/templates/app/android.tmpl/gradle.properties.tmpl index e96108cfe4c4d..22af923a63937 100644 --- a/packages/flutter_tools/templates/app/android.tmpl/gradle.properties.tmpl +++ b/packages/flutter_tools/templates/app/android.tmpl/gradle.properties.tmpl @@ -1,6 +1,4 @@ org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true -# This newDsl flag was added by the Flutter template -android.newDsl=false # This builtInKotlin flag was added by the Flutter template android.builtInKotlin=false diff --git a/packages/flutter_tools/templates/module/android/gradle/gradle.properties.tmpl b/packages/flutter_tools/templates/module/android/gradle/gradle.properties.tmpl index e96108cfe4c4d..22af923a63937 100644 --- a/packages/flutter_tools/templates/module/android/gradle/gradle.properties.tmpl +++ b/packages/flutter_tools/templates/module/android/gradle/gradle.properties.tmpl @@ -1,6 +1,4 @@ org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true -# This newDsl flag was added by the Flutter template -android.newDsl=false # This builtInKotlin flag was added by the Flutter template android.builtInKotlin=false diff --git a/packages/flutter_tools/templates/plugin/android-kotlin.tmpl/build.gradle.kts.tmpl b/packages/flutter_tools/templates/plugin/android-kotlin.tmpl/build.gradle.kts.tmpl index a2ccf176fd147..c3520c6308923 100644 --- a/packages/flutter_tools/templates/plugin/android-kotlin.tmpl/build.gradle.kts.tmpl +++ b/packages/flutter_tools/templates/plugin/android-kotlin.tmpl/build.gradle.kts.tmpl @@ -72,6 +72,9 @@ kotlin { } dependencies { - testImplementation("org.jetbrains.kotlin:kotlin-test") + testImplementation("org.jetbrains.kotlin:kotlin-test:{{kotlinVersion}}") + testImplementation("org.jetbrains.kotlin:kotlin-test-junit5:{{kotlinVersion}}") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:{{junitJupiterEngineVersion}}") + testRuntimeOnly("org.junit.platform:junit-platform-launcher:{{junitPlatformLauncherVersion}}") testImplementation("org.mockito:mockito-core:5.0.0") } diff --git a/packages/flutter_tools/test/general.shard/android/android_project_migration_test.dart b/packages/flutter_tools/test/general.shard/android/android_project_migration_test.dart index ecfacbeb62530..77ac41d9667b8 100644 --- a/packages/flutter_tools/test/general.shard/android/android_project_migration_test.dart +++ b/packages/flutter_tools/test/general.shard/android/android_project_migration_test.dart @@ -5,12 +5,13 @@ import 'package:file/file.dart'; import 'package:file/memory.dart'; import 'package:flutter_tools/src/android/android_studio.dart'; +import 'package:flutter_tools/src/android/gradle_errors.dart'; import 'package:flutter_tools/src/android/gradle_utils.dart'; import 'package:flutter_tools/src/android/migrations/android_studio_java_gradle_conflict_migration.dart'; import 'package:flutter_tools/src/android/migrations/disable_built_in_kotlin_migration.dart'; -import 'package:flutter_tools/src/android/migrations/disable_new_dsl_migration.dart'; import 'package:flutter_tools/src/android/migrations/min_sdk_version_migration.dart'; import 'package:flutter_tools/src/android/migrations/multidex_removal_migration.dart'; +import 'package:flutter_tools/src/android/migrations/remove_new_dsl_opt_out_migration.dart'; import 'package:flutter_tools/src/android/migrations/top_level_gradle_build_file_migration.dart'; import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/version.dart'; @@ -498,192 +499,121 @@ android.builtInKotlin false ); }); - group('Migrate to opt-out of new DSL', () { - testUsingContext('skip if new DSL flag exists', () async { + group('Remove the new DSL opt-out', () { + testUsingContext('removes the template-added opt-out and keeps builtInKotlin', () async { topLevelGradlePropertiesFile.writeAsStringSync(''' +org.gradle.jvmargs=-Xmx8G +android.useAndroidX=true +# This newDsl flag was added by the Flutter template android.newDsl=false +# This builtInKotlin flag was added by the Flutter template +android.builtInKotlin=false '''); - expect( - topLevelGradlePropertiesFile.readAsStringSync().contains('android.newDsl=false'), - isTrue, - ); - final androidProjectMigration = DisableNewDslMigration(project, bufferLogger); + final androidProjectMigration = RemoveNewDslOptOutMigration(project, bufferLogger); await androidProjectMigration.migrate(); - expect(topLevelGradlePropertiesFile.existsSync(), isTrue); + + final String fileContents = topLevelGradlePropertiesFile.readAsStringSync(); + expect(fileContents, isNot(contains('android.newDsl'))); + expect(fileContents, isNot(contains('newDsl flag'))); expect( - bufferLogger.traceText, - contains('The developer has already configured the new DSL flag, skipping migration.'), + fileContents, + contains('# This builtInKotlin flag was added by the Flutter template'), ); + expect(fileContents, contains('android.builtInKotlin=false')); + expect(fileContents, contains('android.useAndroidX=true')); + expect( + bufferLogger.statusText, + contains('Removed the android.newDsl opt-out that Flutter previously added'), + ); + expect(bufferLogger.statusText, contains(kNewDslBreakingChangeDocsUrl)); }); - testUsingContext('skip if new DSL flag uses a nonstandard separator and exists', () async { + testUsingContext('removes the migrator-added opt-out', () async { topLevelGradlePropertiesFile.writeAsStringSync(''' -android.newDsl : false +android.useAndroidX=true +# This newDsl flag was added automatically by Flutter migrator +android.newDsl=false '''); - expect( - topLevelGradlePropertiesFile.readAsStringSync().contains('android.newDsl : false'), - isTrue, - ); - final androidProjectMigration = DisableNewDslMigration(project, bufferLogger); + final androidProjectMigration = RemoveNewDslOptOutMigration(project, bufferLogger); await androidProjectMigration.migrate(); - expect(topLevelGradlePropertiesFile.existsSync(), isTrue); - expect( - bufferLogger.traceText, - contains('The developer has already configured the new DSL flag, skipping migration.'), - ); - }); - testUsingContext( - 'create gradle.properties file and add the new DSL flag if gradle.properties file is missing', - () async { - final androidProjectMigration = DisableNewDslMigration(project, bufferLogger); - expect(topLevelGradlePropertiesFile.existsSync(), isFalse); - await androidProjectMigration.migrate(); - expect(topLevelGradlePropertiesFile.existsSync(), isTrue); - expect( - bufferLogger.traceText, - contains( - 'The gradle.properties file was not found. Creating it with a disabled new DSL flag.', - ), - ); - expect( - topLevelGradlePropertiesFile.readAsStringSync().contains('android.newDsl=false'), - isTrue, - ); - }, - ); + final String fileContents = topLevelGradlePropertiesFile.readAsStringSync(); + expect(fileContents, isNot(contains('android.newDsl'))); + expect(fileContents, contains('android.useAndroidX=true')); + }); testUsingContext( - 'logs an error if the gradle.properties file cannot be written to', + 'empties a gradle.properties the former migrator created with only the flag', () async { - final projectWithUnwritablePropertiesFile = FakeAndroidProject( - root: errorThrowingFileSystemForWrite.currentDirectory.childDirectory('android') - ..createSync(), - ); - - final File unwritablePropertiesFile = projectWithUnwritablePropertiesFile - .hostAppGradleRoot - .childFile('gradle.properties'); - - expect(unwritablePropertiesFile.existsSync(), isFalse); - - final androidProjectMigration = DisableNewDslMigration( - projectWithUnwritablePropertiesFile, - bufferLogger, + topLevelGradlePropertiesFile.writeAsStringSync( + '# This newDsl flag was added automatically by Flutter migrator\n' + 'android.newDsl=false\n', ); + final androidProjectMigration = RemoveNewDslOptOutMigration(project, bufferLogger); await androidProjectMigration.migrate(); + expect(topLevelGradlePropertiesFile.existsSync(), isTrue); expect( - bufferLogger.traceText, - contains( - 'The gradle.properties file was not found. Creating it with a disabled new DSL flag.', - ), - ); - - expect( - bufferLogger.errorText, - contains('Failed to write to the gradle.properties during migration'), + topLevelGradlePropertiesFile.readAsStringSync().trim(), + isEmpty, ); }, - overrides: { - FileSystem: () => errorThrowingFileSystemForWrite, - ProcessManager: () => FakeProcessManager.any(), - }, ); - testUsingContext( - 'logs an error and aborts if the gradle.properties file cannot be read', - () async { - final projectWithUnreadablePropertiesFile = FakeAndroidProject( - root: errorThrowingFileSystemForRead.currentDirectory.childDirectory('android') - ..createSync(), - ); - - final File unreadablePropertiesFile = - projectWithUnreadablePropertiesFile.hostAppGradleRoot.childFile('gradle.properties') - ..createSync(recursive: true); - - final androidProjectMigration = DisableNewDslMigration( - projectWithUnreadablePropertiesFile, - bufferLogger, - ); + testUsingContext('leaves a hand-added opt-out without a Flutter marker alone', () async { + topLevelGradlePropertiesFile.writeAsStringSync(''' +# I really want the legacy API +android.newDsl=false +'''); + final androidProjectMigration = RemoveNewDslOptOutMigration(project, bufferLogger); - expect(unreadablePropertiesFile.existsSync(), isTrue); - await androidProjectMigration.migrate(); - expect( - bufferLogger.errorText, - contains('Failed to read gradle.properties during migration:'), - ); - }, + await androidProjectMigration.migrate(); - overrides: { - FileSystem: () => errorThrowingFileSystemForRead, - ProcessManager: () => FakeProcessManager.any(), - }, - ); + final String fileContents = topLevelGradlePropertiesFile.readAsStringSync(); + expect(fileContents, contains('android.newDsl=false')); + expect(fileContents, contains('# I really want the legacy API')); + expect(bufferLogger.statusText, isNot(contains('Removed the android.newDsl opt-out'))); + }); - testUsingContext( - 'add new DSL flag if it does not exist in gradle.properties file', - () async { - topLevelGradlePropertiesFile.writeAsStringSync(''' + testUsingContext('leaves a Flutter marker whose flag the developer edited alone', () async { + topLevelGradlePropertiesFile.writeAsStringSync(''' +# This newDsl flag was added by the Flutter template +android.newDsl=true '''); - expect(topLevelGradlePropertiesFile.existsSync(), isTrue); - expect( - topLevelGradlePropertiesFile.readAsStringSync().contains('android.newDsl=false'), - isFalse, - ); - final androidProjectMigration = DisableNewDslMigration(project, bufferLogger); - - await androidProjectMigration.migrate(); - - expect(bufferLogger.traceText, contains('Migrating to disable new DSL by default.')); + final androidProjectMigration = RemoveNewDslOptOutMigration(project, bufferLogger); - final String fileContents = topLevelGradlePropertiesFile.readAsStringSync(); - expect( - fileContents.contains( - '# This newDsl flag was added automatically by Flutter migrator', - ), - isTrue, - ); - expect(fileContents.contains('android.newDsl=false'), isTrue); - }, - ); + await androidProjectMigration.migrate(); - testUsingContext( - 'logs an error if processFileLines fails to write the migrated file', - () async { - final projectWithProcessError = FakeAndroidProject( - root: errorThrowingFileSystemForProcessFile.currentDirectory.childDirectory('android') - ..createSync(), - ); + final String fileContents = topLevelGradlePropertiesFile.readAsStringSync(); + expect(fileContents, contains('android.newDsl=true')); + }); - final File topLevelGradlePropertiesFile = projectWithProcessError.hostAppGradleRoot - .childFile('gradle.properties'); + testUsingContext('does nothing when gradle.properties is missing', () async { + expect(topLevelGradlePropertiesFile.existsSync(), isFalse); + final androidProjectMigration = RemoveNewDslOptOutMigration(project, bufferLogger); - topLevelGradlePropertiesFile.writeAsStringSync(''); + await androidProjectMigration.migrate(); - final androidProjectMigration = DisableNewDslMigration( - projectWithProcessError, - bufferLogger, - ); + expect(topLevelGradlePropertiesFile.existsSync(), isFalse); + }); - await androidProjectMigration.migrate(); + testUsingContext('removes an opt-out written with a nonstandard separator', () async { + topLevelGradlePropertiesFile.writeAsStringSync(''' +# This newDsl flag was added by the Flutter template +android.newDsl : false +'''); + final androidProjectMigration = RemoveNewDslOptOutMigration(project, bufferLogger); - expect(bufferLogger.traceText, contains('Migrating to disable new DSL by default.')); + await androidProjectMigration.migrate(); - expect( - bufferLogger.errorText, - contains('Failed to process/migrate gradle.properties during migration:'), - ); - }, - overrides: { - FileSystem: () => errorThrowingFileSystemForProcessFile, - ProcessManager: () => FakeProcessManager.any(), - }, - ); + expect( + topLevelGradlePropertiesFile.readAsStringSync(), + isNot(contains('android.newDsl')), + ); + }); }); }); diff --git a/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart b/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart index ddc9b1afa81c7..b695d9096e91e 100644 --- a/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart +++ b/packages/flutter_tools/test/general.shard/android/gradle_errors_test.dart @@ -57,7 +57,7 @@ void main() { jlinkErrorWithJava21AndSourceCompatibility, missingNdkSourcePropertiesFile, applyingKotlinAndroidPluginErrorHandler, - useNewAgpDslErrorHandler, + legacyVariantApiUsageErrorHandler, incompatibleKotlinVersionHandler, ]), ); @@ -1704,50 +1704,66 @@ An exception occurred applying plugin request [id: 'kotlin-android'] }, ); - testUsingContext( - 'Failure to apply kotlin-android plugin', - () async { - const useNewAgpDslErrorHandlerExample = r''' -FAILURE: Build failed with an exception. - -* Where: -Build file '/Users/jesswon/Desktop/fresh_flutter_app/android/app/build.gradle.kts' - + group('legacy variant API usage', () { + testWithoutContext('matches the Gradle output of common legacy API usages', () { + const errorExamples = [ + ''' * What went wrong: -An exception occurred applying plugin request [id: 'dev.flutter.flutter-gradle-plugin'] -> Failed to apply plugin 'dev.flutter.flutter-gradle-plugin'. - > java.lang.NullPointerException (no error message) - '''; +A problem occurred configuring project ':app'. +> Could not get unknown property 'applicationVariants' for extension 'android' of type com.android.build.gradle.internal.dsl.ApplicationExtensionImpl. +''', + ''' +* What went wrong: +A problem occurred evaluating project ':some_plugin'. +> Could not get unknown property 'libraryVariants' for extension 'android' of type com.android.build.gradle.internal.dsl.LibraryExtensionImpl. +''', + r''' +* What went wrong: +A problem occurred evaluating project ':app'. +> Could not find method applicationVariants() for arguments [build_2i3f1jrzcvsfaqonvzchd6n1k$_run_closure2$_closure8@2cff667e] on extension 'android'. +''', + r''' +* What went wrong: +A problem occurred evaluating project ':app'. +> Could not find method variantFilter() for arguments [build_abc$_run_closure1@12345678] on extension 'android'. +''', + ]; + for (final example in errorExamples) { + expect( + formatTestErrorMessage(example, legacyVariantApiUsageErrorHandler), + isTrue, + reason: 'expected handler to match:\n$example', + ); + } + }); - final FlutterProject project = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); - await useNewAgpDslErrorHandler.handler( - line: useNewAgpDslErrorHandlerExample, - project: project, - usesAndroidX: true, - ); + testUsingContext( + 'suggests migrating to the variant API and mentions the escape hatch', + () async { + final FlutterProject project = FlutterProject.fromDirectoryTest( + fileSystem.currentDirectory, + ); + await legacyVariantApiUsageErrorHandler.handler( + line: "> Could not get unknown property 'applicationVariants' for extension 'android'.", + project: project, + usesAndroidX: true, + ); - expect( - testLogger.statusText, - contains('Starting AGP 9+, only the new DSL interface will be read.'), - ); - expect( - testLogger.statusText, - contains('This results in a build failure when applying the Flutter Gradle plugin'), - ); - expect(testLogger.statusText, contains('For instructions on how to opt out, see:')); - expect(testLogger.statusText, contains(kOptOutOfNewDslDocsUrl)); - expect( - testLogger.statusText, - contains('If you are not upgrading to AGP 9+, run `flutter analyze --suggestions`'), - ); - }, - overrides: { - GradleUtils: () => FakeGradleUtils(), - Platform: () => fakePlatform('android'), - FileSystem: () => fileSystem, - ProcessManager: () => processManager, - }, - ); + // The printBox output wraps lines, so assert on fragments that do not wrap. + expect(testLogger.statusText, contains('legacy variant API')); + expect(testLogger.statusText, contains('androidComponents.onVariants')); + expect(testLogger.statusText, contains(kNewDslBreakingChangeDocsUrl)); + expect(testLogger.statusText, contains('android.newDsl=false')); + expect(testLogger.statusText, contains('Android Gradle Plugin 10')); + }, + overrides: { + GradleUtils: () => FakeGradleUtils(), + Platform: () => fakePlatform('android'), + FileSystem: () => fileSystem, + ProcessManager: () => processManager, + }, + ); + }); } bool formatTestErrorMessage(String errorMessage, GradleHandledError error) {