Skip to content

[AGP 9.1.0 Migration #9] Move aar_init_script to public AGP APIs - #193962

Merged
auto-submit[bot] merged 25 commits into
flutter:masterfrom
reidbaker-agent:agp-aar-init-script
Oct 20, 2026
Merged

auto-submit[bot] merged 25 commits into
flutter:masterfrom
reidbaker-agent:agp-aar-init-script

Conversation

@reidbaker-agent

@reidbaker-agent reidbaker-agent commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

PR 7 (#193693) merged to master as ecd1ab06865 and PR 8 (#193717) as 4249ccbc18a. This branch merges master at 4521d8d6634, so the Files tab shows only PR 9's changes.

PR 9 commits:

  • 86523c549fc Move aar_init_script to public AGP APIs
  • 3edc95d1fa0 Check requested AAR variants in the module and each plugin
  • da9646aad53 Address review: derive requested variants from components, register AAR tasks lazily
  • 94a29907065 Clarify aar_init_script comments from the adversarial review
  • ea08280d886 Merge commit '23ddd6b74e6' into agp-aar-init-script (PR 8's head with master at ecd1ab06865; no conflicts, no change to PR 9's files)
  • a17fafcc35e Merge commit '5ba8961b517' into agp-aar-init-script (master with PR 8 at 4249ccbc18a; no conflicts, no change to PR 9's files)
  • 77ba449ea4a Filter requested AAR components before adding tasks
  • adbfb9cd640 Drop the AGP version list from the duplicate-variant comment
  • f2b7111c51e Docs: state the AAR variant requirement in plain words
  • cd10f766b5b Register an AAR task for every variant, publish only requested ones
  • b9493299916 Merge commit '4521d8d6634' into agp-aar-init-script (master, including [flutter_tools] Migrate gradle_errors and deferred_components_prebuild_validator away from globals #193649; no conflicts, no change to PR 9's files)

Description

This is PR 9 of 11 in the AGP 9.1.0 / public gradle-api migration stack (#180137, #166550).

flutter build aar passes packages/flutter_tools/gradle/aar_init_script.gradle to Gradle with -I. The script read android.libraryVariants and AGP's internal publishing.singleVariants. With AGP 9's android.newDsl=true, flutter build aar on a module fails on 5ba8961b517 with Can't generate AAR on a non Android library project. (script line 13), and then at line 178 (Could not get unknown property 'libraryVariants'). With this PR the script uses only pluginManager.hasPlugin, Publishing.singleVariant, software components and tasks.register, and the build succeeds with newDsl on and off, on AGP 9.3.1 and on master's minimum AGP 9.1.1, and on AGP 8.11.1 when FGP's version check is skipped.

Breaking: no. Builds that succeed on 5ba8961b517 succeed with the same artifacts. Two failing cases change: a module or plugin build file that declares singleVariant for only some variants builds, and a variant mismatch between the module and a plugin fails with a Flutter error instead of Task with name … not found.

Base: master at 4521d8d6634. aar_init_script.gradle, build_aar_module_test.dart and both docs are the same at 4521d8d6634 and 5ba8961b517, so the comparisons below against 5ba8961b517 hold for 4521d8d6634.

Agent authored details

Changes

All in aar_init_script.gradle unless noted. Line numbers are at b9493299916.

  1. Library check (:13): project.pluginManager.hasPlugin("com.android.library") replaces android.hasProperty("libraryVariants"). The error text is unchanged.
  2. Publishing declarations (afterProject, :185-219): Flutter calls android.publishing.singleVariant(<variant>) { withSourcesJar(); withJavadocJar() } for each variant, one variant at a time. If the project's build file already declared that variant, AGP throws, and isDuplicateSingleVariantError (:125-134) matches AGP's exact message, so the build file's declaration is kept. Its comment links the AGP source of the message (AbstractPublishing.kt at studio-2025.3.2, AGP 9.1.0). Any other exception is rethrown (:201-203). The guard that read publishing.singleVariants and skipped every Flutter declaration once the build file declared any variant is deleted.
  3. No module-to-plugin dependsOn (projectsEvaluated): the loop over moduleProject.android.libraryVariants that made each module assembleAar<Variant> depend on the plugin's task (with eager tasks.named(...).get()) is deleted. The tool requests assembleAar<Variant> without a project path (gradle.dart buildGradleAar, command.add(aarTask)), and Gradle runs a task selector that is only a task name in every project that has the task (Gradle docs: "You can also run a task for all subprojects using a task selector that consists of only the task name."). Each plugin publishes from its own task. The comment at :247-248 says this.
  4. Tasks, publications and task name (configureProject :27-37; aarTaskName :93-97; requestedAarComponents :99-103): AGP adds one software component for each singleVariant declaration, with the variant's name. registerAarTask (:105-110) registers assembleAar<Variant> for every component, so a misspelled --flavor gets Gradle's candidate list (Some candidates are: 'assembleAarDebug', ...). requestedAarComponents returns the components whose assembleAar<Variant> name is in startParameter.taskNames; only those get a publication (addAarPublication, :112-123), because publish publishes every publication in the project. The check uses the same function. aarTaskName is the only place that builds assembleAar<Variant>; its comment points to the tool's getAarTaskFor (gradle.dart). Nothing parses a task name back into a variant. The component.name != "all" filter from 5ba8961b517 is deleted (see "Why removing the deleted code is safe").
  5. Lazy task registration (registerAarTask): project.tasks.register replaces project.tasks.create(name:). The configuration action only adds finalizedBy "publish". publications.create runs in addAarPublication, not in the action, because maven-publish registers tasks for each publication and Gradle rejects that while it realizes a task (DefaultTaskContainer#register(String, Class, Action) on task set cannot be executed in the current context., seen when publications.create was inside the action).
  6. Requested-variant check (checkProjectsHaveRequestedVariants, :136-175, called at :252): Gradle skips projects that lack the requested task, so the script checks the module and each Android plugin. The requested variants are the names of the components that requestedAarComponents returns for those projects. A project without a requested variant fails the build with a GradleException that names the variant and the project:
    • module: Flutter can't build the AAR for variant 'debug' because module project ':flutter' has no variant with that name. Its variants are: demoDebug, demoProfile, demoRelease, fullDebug, fullProfile, fullRelease. Pass the module's product flavor with --flavor, or omit --flavor if the module has no product flavors.
    • plugin: Flutter can't build the AAR for variant 'demoDebug' because plugin project ':plugin_a' has no variant with that name, so the plugin AAR that the module depends on can't be published. flutter build aar requires the module and each of its Android plugins to declare the same product flavors.
    • A requested task that matches no project's variant (for example --flavor foo) is left to Gradle: Task 'assembleAarFooDebug' not found in root project 'android_generated' and its subprojects. Some candidates are: 'assembleAarDebug', 'assembleAarDemoDebug', 'assembleAarFullDebug'. That is the same error as on 5ba8961b517. The check's doc says so. (3edc95d1fa0 parsed the task-name prefix and reported a Flutter module error in this case; da9646aad53 drops the parsing per review.)
  7. Docs. Both docs describe behavior only.
    • Migrating-Flutter-Gradle-Plugin-to-AGP-public-API.md: the P8 row of the phase table and "Features that must break" item 8 are rewritten. The "Why" starting-state list (L38-39) is unchanged, as in other PRs of the stack.
    • website-page-draft.md (publishing tracked in #193713): the flutter build aar section is rewritten. It said Flutter "detects the overlap and reports it with an actionable error". The code keeps the build file's declaration instead, so the docs were changed to match the code. It says (L234-237) that when a build file declares singleVariant for a variant, Flutter keeps that declaration and declares the other variants, and (L238-241) that the module and each Android plugin need a variant with the built name.
  8. Tests. devicelab build_aar_module_test gets two final sections:
    • With android.newDsl=true and android.builtInKotlin=true in .android/gradle.properties, and android.publishing.singleVariant("release") { withSourcesJar() } in .android/Flutter/build.gradle, it runs flutter build aar --no-profile. It checks that the edits survive, that release has the AAR and sources jar and no javadoc jar, that debug has the javadoc jar, and that the plugin AARs exist for release and debug.
    • It replaces that declaration with a demo product flavor and checks both errors: flutter build aar --no-profile --no-release fails with the module error, and the same with --flavor demo fails with the plugin error.

da9646aad53's commit message says the check's doc "says when it returns and when it throws". That doc sentence was added in 94a29907065.

Bug history (moved out of the docs)

The docs describe behavior only, so the history that 3edc95d1fa0 put in them is here:

  • On 5ba8961b517, the afterProject guard skipped all of Flutter's singleVariant declarations for a project whose build file declared any variant. For a module with at least one Android plugin that declared only release, every flutter build aar failed with Task with name 'assembleAarDebug' not found in project ':flutter'. With this PR that build succeeds (table 1 below).
  • On 5ba8961b517, the dependsOn loop failed with Task with name 'assembleAar<Variant>' not found in project ':<plugin>' whenever a plugin lacked any module variant, requested or not, and nothing checked that the module had the requested variant. With this PR only requested variants are checked, in the module and in each plugin (table 2 below).

Audit resolution

PR 9 draft audit, and stack audit items M4 and L8:

Item Resolution
1. Eager .get() and cross-project task lookup by name The wiring is deleted, not made lazy: it was redundant (Changes 3). No .get() or tasks.named in the script. The check reads software components, and registerAarTask uses tasks.register.
2. Broad catch (Exception); overstated multipleVariants warning Only AGP's exact duplicate message is ignored (:130-134); everything else is rethrown (:201-203). No warning is logged, so there is no multipleVariants claim.
3. Ext property without flutter.internal. No ext property. afterProject fires once per project (instrumented: count 1 for :, :app, :flutter, :plugin_a and the included FGP build), and a second call would only hit ignored duplicates. The "fires more than once in composite settings" claim is dropped.

Why the message match and not the type: AGP reports the duplicate from AbstractPublishing.checkSingleVariantUniqueness through SyncIssueReporterImpl, which throws com.android.builder.errors.EvalIssueException in STANDARD mode. That class is in com.android.tools.build:builder, not in gradle-api, and is not on the init script's classpath. javap -v on AbstractPublishing shows the same text in AGP 8.11.1, 9.1.0 and 9.3.1. AGP checks before it adds the component (8.11.1 AbstractPublishing.kt:27-35), so the build file's declaration stays. If AGP changes the text, the exception is rethrown and the build fails loudly; devicelab build_aar_module_test exercises the path on the template AGP.

Why not declare first or query: the public Publishing DSL in gradle-api-9.1.0.jar has only singleVariant(String) and singleVariant(String, Function1), and LibraryPublishing adds only multipleVariants(...) (javap), so there is no public way to list declared variants. Declaring Flutter's variants in beforeProject + pluginManager.withPlugin("com.android.library") makes the build file fail at its own singleVariant("release") line with the duplicate error (probe).

Why not AGP types in the init script: import com.android.build.api.dsl.LibraryExtension fails with unable to resolve class. With gradle-api:9.3.1 on the initscript classpath, the init script gets its own copy of the class from a different classloader, so instanceof is false and extensions.getByType(LibraryExtension) throws Extension of type 'LibraryExtension' does not exist. Currently registered extension types: [..., LibraryExtension, ...] (probes).

multipleVariants: PublishingUtil.ensureComponentNameUniqueness errors only when a multipleVariants component name equals a singleVariant name. Flutter declares the same singleVariants on 5ba8961b517 when the build file declares none, so that behavior is unchanged. It is not tested here.

Why removing the deleted code is safe

  • android.libraryVariants does not exist with newDsl=true (reproduced above). AGP 9.1.0 marks its class, com.android.build.gradle.LibraryExtension, @Deprecated ("This class is not used for the public extensions in AGP when android.newDsl=true, which is the default in AGP 9.0, and will be removed in AGP 10.0."), and the class is not in gradle-api-9.1.0.jar. singleVariants is an internal property (getSingleVariants() on com.android.build.gradle.internal.dsl.AbstractPublishing, javap). It resolves from Groovy with newDsl=true, so it was not a failure, only internal API.
  • The dependsOn: with it removed, the task graph for assembleAarRelease on case (a) has :plugin_a:assembleAarRelease, :plugin_a:bundleReleaseAar, :plugin_a:publishReleasePublicationToMavenRepository and :plugin_a:publish. :flutter:generatePomFileForReleasePublication depends on no :plugin_a task, and :flutter:publishReleasePublicationToMavenRepository depends only on the plugin's compile-classpath tasks, so the order between the module's and the plugin's publishing doesn't matter. The module POM refers to the plugin by coordinates (plugin_a_release). devicelab build_aar_module_test, build_android_host_app_with_module_aar and android_java17_build_android_host_app_with_module_aar pass (they check the plugin AAR/POM and that the module POM depends on plugin_with_android_release).
  • The all filter (component.name != "all", added in Use the maven-publish plugin to publish AAR files. #101891 606e91ad63d): AGP 7.x created an automatic multi-variant component named all when a project used no publishing DSL (PublishingUtil.kt at studio-2021.3.1, createPublishingInfoForLibrary and publishingFeatureOptIn; same in the AGP 7.3.1 jar). Use the maven-publish plugin to publish AAR files. #101891 published it separately with classifiers and skipped it for the _<variant> publications; Revert file naming convention of .aar files to support fuzzy matching in build.gradle #112149 (8e30cc9c56b) removed that publication and kept the skip. AGP 8.0 disabled automatic components by default (AGP 8.0.0 release notes), and the AGP 8.11.1, 9.1.1 and 9.3.1 jars list android.disableAutomaticComponentCreation as removed, with no all component code. This script declares singleVariant for every variant, which also turns automatic components off on AGP 7.x. A probe that prints project.components after projectsEvaluated on all four matrix configurations shows only per-variant names ([debug, profile, release] for :flutter, :app and each plugin, [demoDebug, …, fullRelease] for the flavored module, [freeDebug, freeProfile, freeRelease] for plugin_flavored, [debug, release] for the root), never all.
  • tasks.register: the publication is created at configuration time only for a requested task name, as with tasks.create, whose configuration closure ran immediately. Only the task object is created lazily. Table rows below and the devicelab runs show the same artifacts.

Removed comments, assertions and code

Removed Reason
afterProject comment item 2, "The project has already defined which variants to publish…", and the singleVariants.size() guard it described The guard read internal API and skipped all of Flutter's declarations once a build file declared any. Item 1 of that comment became "Exit early if the project doesn't have the Android Gradle plugin applied."
The projectsEvaluated comment "Configure the assembleAar<variantName> task for each plugin's projects and make the module's equivalent task depend on the plugin's task.", with the libraryVariants.all loop, both tasks.named(...).get() calls, both assert(... != null) and the dependsOn The dependency is redundant (see above). tasks.named(...).get() throws before the asserts could fail, so they never checked anything.
addAarTask and its variantName local Split into registerAarTask (every component) and addAarPublication (requested components). The task name comes from aarTaskName, which the check also uses.
addAarTask comment "This check is required to be able to configure the archives before publish runs.", with the check The check is requestedAarComponents, applied by the caller before addAarPublication. Its reason is the comment on addAarPublication: "publish publishes every publication in the project, so only requested components get one."
The component.name != "all" filter in configureProject, and the fragment "(except "all" since that is used to publish artifacts in the new way)" from "Create a default MavenPublication for the variant" Nothing creates an all component where this script runs (see "Why removing the deleted code is safe").
From the migration doc item 8 and the website page draft: the sentences about the deleted guard, the Task with name … not found failures, and what the deleted code did and did not check The docs describe behavior only. The history is in "Bug history" above.

Behavioral and compatibility notes

Matrix projects (local scratch): (a) module + Android plugin plugin_a; (b) module with flavors demo/full + plugin_a; (c) module + plugin_a + plugin_flavored (flavor free); (d) module declaring singleVariant("release") { withSourcesJar() } + plugin_a.

  1. A build file that declares singleVariant for some variants builds. Case (d), flutter build aar:

    Script Result
    5ba8961b517 (AGP 9.3.1 newDsl=false) exit 1: Task with name 'assembleAarDebug' not found in project ':flutter'.
    This PR (all four configurations below) exit 0. Release has the AAR, POM and sources jar, and no javadoc jar (the build file's declaration). Debug and profile have sources and javadoc jars. Plugin AAR/POM for all three.
  2. Variant mismatches fail with a Flutter error that names the project. Only requested variants are checked. The 5ba8961b517 column is AGP 9.3.1 with newDsl=false.

    Case 5ba8961b517 This PR (all four configurations)
    (b) --flavor demo Task with name 'assembleAarDemoDebug' not found in project ':plugin_a'. plugin error for demoDebug and :plugin_a
    (c) default Task with name 'assembleAarDebug' not found in project ':plugin_flavored'. plugin error for debug and :plugin_flavored
    (b) no --flavor, --no-profile --no-release Task with name 'assembleAarDemoDebug' not found in project ':plugin_a'. module error for debug, listing the module's variants
    (c) --flavor free, --no-profile --no-release Task with name 'assembleAarDebug' not found in project ':plugin_flavored'. module error for freeDebug
    (b) --flavor foo Gradle Task 'assembleAarFooDebug' not found in root project … same Gradle error

    Without the module check (86523c549fc), the module-error cases exit 0 with "✓ Built" and a repo that has only the plugin AAR.

  3. Path-qualified requests. ./gradlew -I aar_init_script.gradle … :flutter:assembleAarRelease exits 0 and publishes nothing, on 5ba8961b517 and on this PR: a publication is created only when startParameter.taskNames contains the bare name. The tool sends only the bare name (gradle.dart command.add(aarTask)). Not changed here.

  4. No warnings. flutter build aar hides Gradle output on success unless -v (ProcessUtils.run buffers it; gradle.dart prints it only on failure, and verified with a logger.error probe). Keeping a build file's declaration needs no action, so nothing is logged. Errors are shown.

  5. Configuration cache: with org.gradle.configuration-cache=true in case (a)'s .android/gradle.properties (AGP 9.3.1 / Gradle 9.5.0), flutter build aar reports the same single problem with the 5ba8961b517 script and with this PR: Task `:flutter:compileFlutterBuildDebug` of type `com.flutter.gradle.tasks.FlutterTask`: invocation of 'Task.project' at execution time is unsupported with the configuration cache. None comes from the init script.

  6. newDsl=true with builtInKotlin=false fails before the script with any Kotlin plugin (The 'org.jetbrains.kotlin.android' plugin is not compatible with AGP's 9.0 new DSL). The devicelab section and matrix set builtInKotlin=true.

  7. Related open work: no overlap with #191681 (gradle.dart, gradle_errors.dart and tests). No open PR touches aar_init_script.gradle.

Follow-up candidates (pre-existing, not changed here)

  • allprojects { apply plugin: "maven-publish" } applies maven-publish to every project, including ones that never publish.
  • The afterProject and projectsEvaluated hooks reach across projects, which Gradle Isolated Projects does not allow.
  • The -SNAPSHOT rewrite of project.version in configureProject.
  • The engine.realm and engine.stamp file reads at configuration time in configureProject.
  • The is-plugin branch of projectsEvaluated looks unreachable from the tool.
  • The configuration cache problem in FGP FlutterTask (note 5).
  • newDsl=true with KGP (builtInKotlin=false), for PR 10.
  • Mapping between flavored module variants and plugin variants (for example, plugin release for module demoRelease).
  • singleVariant is declared on the root project and :app, which never publish.
  • Gradle output is hidden on success, so nothing the script logs at info or warn level reaches the user without -v.

Integration test coverage of the changed paths

Searched dev/devicelab/bin/tasks and packages/flutter_tools/test/integration.shard for build aar and assembleAar.

Path Tests
Library check, publishing declarations, lazy task registration, plugin publishing without dependsOn (newDsl=false) devicelab build_aar_module_test, build_android_host_app_with_module_aar, android_java17_build_android_host_app_with_module_aar; android_obfuscate_test
newDsl=true, a build file's singleVariant declaration kept devicelab build_aar_module_test, newDsl=true section
Module and plugin variant mismatch errors devicelab build_aar_module_test, product flavor section
AGP 9.1.1 and 8.11.1; a variant that no project has Manual matrix only

Linux build_aar_module_test runs in presubmit for this PR: in .ci.yaml its runIf includes dev/** and packages/flutter_tools/**, and it sets neither presubmit: false nor bringup. It also ran locally on the head (below) with cd dev/devicelab && ../../bin/dart bin/test_runner.dart test -t build_aar_module_test --exit.

Tests run locally (macOS, JDK 17)

On the head (b9493299916; master merged at 4521d8d6634):

  • gradle test --rerun-tasks in packages/flutter_tools/gradle, with the engine's third_party/gradle binary that the README names: 33 suites, 277 tests, 0 failures.
  • dart format and dart analyze --fatal-infos on build_aar_module_test.dart: clean (unchanged since 3edc95d1fa0).
  • Tool tests: test/commands.shard/hermetic/build_aar_test.dart, test/commands.shard/permeable/build_aar_test.dart and test/general.shard/android/android_gradle_builder_test.dart: 56 tests, all pass.
  • test/integration.shard/android_obfuscate_test.dart: 2 tests pass.
  • devicelab via bin/test_runner.dart test --exit, one at a time: build_aar_module_test (including both added sections), build_android_host_app_with_module_aar, android_java17_build_android_host_app_with_module_aar, build_android_host_app_with_module_source, module_host_with_custom_build_test, all success.
  • The configuration cache comparison (note 5).

Manual matrix on b9493299916, four configurations:

  • 9.3.1 off: AGP 9.3.1 / Gradle 9.5.0, newDsl=false, builtInKotlin=false.
  • 9.3.1 on: AGP 9.3.1 / Gradle 9.5.0, newDsl=true, builtInKotlin=true.
  • 9.1.1 on: AGP 9.1.1 / Gradle 9.3.1 (FGP's minimums on master since Raise Minimum Version to AGP 9.1.1 #193813), newDsl=true, builtInKotlin=true.
  • 8.11.1: AGP 8.11.1 / Gradle 8.14.3, newDsl=false, with skipDependencyChecks=true in .android/gradle.properties. Without it, FGP stops before the init script runs: Your project's Gradle version (8.14.3) is lower than Flutter's minimum supported version of 9.3.1.
Case 9.3.1 off 9.3.1 on 9.1.1 on 8.11.1
(a) exit 0, AAR, POM, sources and javadoc jars for flutter_{debug,profile,release} and plugin_a_{debug,profile,release} same same same
(b) --flavor demo exit 1, plugin error same same same
(b) no --flavor exit 1, module error same same same
(b) --flavor foo exit 1, Gradle Task 'assembleAarFooDebug' not found in root project same same same
(c) exit 1, plugin error same same same
(c) --flavor free exit 1, module error same same same
(d) exit 0, as (a), except module release has the sources jar and no javadoc jar same same same

Case (a)'s flutter_release-1.0.pom depends on dev.pr9.plugin_a:plugin_a_release:1.0 (checked on b9493299916, AGP 9.1.1).

Negative control, the master script (same at 5ba8961b517 and 4521d8d6634), on b9493299916: with newDsl=true (AGP 9.3.1 and AGP 9.1.1), (a)-(d) all fail with Can't generate AAR on a non Android library project.; with newDsl=false on AGP 9.3.1, (a) passes and (b)-(d) fail with Gradle's Task with name 'assembleAar<V>' not found in project ':<p>'. On a17fafcc35e, with only line 13 changed to hasPlugin, newDsl=true fails at line 178 with Could not get unknown property 'libraryVariants' for object of type com.android.build.gradle.internal.dsl.LibraryExtensionImpl$AgpDecorated. devicelab build_aar_module_test with that script fails at the newDsl=true section with the same error.


Pre-launch Checklist

The application path of the Flutter Gradle Plugin no longer uses the
deprecated BaseVariant API (applicationVariants / ApkVariantOutput):

- CopyFlutterApksTask copies the variant's SingleArtifact.APK outputs,
  read through BuiltArtifactsLoader, into build/outputs/flutter-apk under
  the unchanged names app[-abi][-flavor]-<mode>.apk. assemble<Variant>
  depends on it. The task has no Project or variant fields and uses an
  injected FileSystemOperations.
- A single naming function in the task produces both the declared
  @OutputFiles and the copied file names. Its inputs are the ABIs of the
  variant's outputs (what AGP builds), the flavor, and the build mode.
  The task fails if AGP built an APK for an ABI that was not declared.
- Per-ABI versionCode for --split-per-abi is a read-then-set on
  VariantOutput.versionCode in onVariants. This is a documented
  exception to the no-configuration-time-get rule, because the lazy
  form is circular. The read relies on AGP's compatibility mode
  (android.compatibility.enableLegacyApi, on by default through 9.x).
- The applicationVariants.configureEach block and its
  @Suppress("DEPRECATION") markers are deleted. The add-to-app module
  path keeps using libraryVariants until the next phase.

Behavior change: Flutter's onVariants callback runs before an app's own
androidComponents.onVariants block, while ApkVariantOutput's
versionCodeOverride was applied after it. Apps that transform
output.versionCode there transform Flutter's offset value (for example
arm64-v8a, build 42, x10000: 422000 -> 20420000). The migration doc and
the breaking-change page draft describe this and give a recipe that
keeps the 422000-style numbers. No divergence warning is emitted,
because it would fire on the onVariants pattern that AGP recommends.

Part of flutter#166550.
…ures

- Take the per-ABI versionCode base from a finalizeDsl snapshot of the
  DSL versionCodes (DslVersionCodes) instead of reading
  output.versionCode, which AGP rejects during configuration when
  android.compatibility.enableLegacyApi=false. Warn and skip when the
  DSL declares no versionCode.
- Add a strict-mode (enableLegacyApi=false) case to
  flutter_build_apk_split_per_abi_test.
- CopyFlutterApksTask: use Gradle's Copy/Sync caching annotation and
  reason; explain when the metadata can be missing and when the ABI
  check can fail; point both errors at the plugin that transforms
  SingleArtifact.APK instead of asking for a Flutter issue.
- Document what an APK transform must keep in website-page-draft.md.
- Link the website-page-draft.md references to tracking issue flutter#193713.
- Remove temporal wording from KDoc and docs.
Add-to-app module (library) variants use the same androidComponents.onVariants
path as application variants: the compile, assets and jniLibs tasks are
registered lazily per library variant, and the assets and native libraries are
generated source directories of the variant. The host app consumes them like
any Android library.

Deletes the host-project lookup, the libraryVariants x applicationVariants
cross-wiring, the merge<HostVariant>Assets.dependsOn edge,
addFlutterDepsForModule, addCopyFlutterAssetsDependency,
shouldConfigureFlutterTask(Project, Task) and
buildModeFor(com.android.builder.model.BuildType).

flutter.hostAppProjectName has no effect and logs a warning.

The jniLibs copy is wired from the compile task provider, so its input
directory is required in both copy tasks.
…er comments

- CopyFlutterApksTask: state why caching is disabled (local copy; a cache
  entry would duplicate the APKs) and build both errors through one
  transformedApkError helper.
- Trim comments and KDoc added by this PR to what the code does not
  already say.
…er-agent/flutter into agp-add-to-app-module

# Conflicts:
#	packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt
#	packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt
Keep only what the code doesn't say: why library variants are not
gated by the command line, why the copy task inputs are required, and
what the tests guard. Restore PR 7's shouldCompileFlutterForVariant
paragraph verbatim.
Explain why library variants skip the command-line gate, drop the
application/library list from the assets comment, drop the 'public
variant API' clause, and say what library variants lack in
configureApplicationOutputs.
…m_build_test

The devicelab test already builds the same host fixture one task at a
time. Add a debuggable qa build type that falls back to the module's
release variant, and a final section that builds it with
android.newDsl=true and flutter.hostAppProjectName set. It checks the
warning and that the APK has release Flutter artifacts.

Delete android_add_to_app_module_test.dart, which duplicated that
setup.
Add stubTaskRegistration, captureTaskConfiguration and
setCommandLineTasks, and use them in the add-to-app onVariants tests.
Branch on the variant type at the call site so the function receives an
ApplicationVariant and needs no check().
…a section

AGP 9.1 and 9.3 deprecate the option. Its consumers back the variant API
that android.newDsl=true already removes.
The flutter tool runs Gradle with -q, which hides logger.warn. Log the
warning with logger.error, as the plugin's other user-facing warnings
do, and state the consequence (Google Play needs a distinct versionCode
per APK) and the fix.
flutter build aar failed on a module with android.newDsl=true: the
library check read android.libraryVariants, which AGP does not create
with the new DSL ("Can't generate AAR on a non Android library
project."), and the plugin wiring read it again.

- Detect a library project with pluginManager.hasPlugin(
  "com.android.library").
- Declare singleVariant publishing for each variant, and keep a
  declaration that the project's build file already made. AGP's
  duplicate-declaration error is matched by its message, because its
  class is not public AGP API. This replaces the read of AGP's internal
  publishing.singleVariants, which skipped Flutter's declarations for
  every variant once the build file declared any, so a module that
  declared only release failed every flutter build aar.
- Remove the module-to-plugin assembleAar<Variant> dependsOn. The tool
  requests the task by name, so Gradle runs it in every project that
  has it and each plugin publishes its own AAR.
- When an Android plugin has no variant with the requested name
  (different product flavors), fail with an error that names the plugin,
  instead of "Task with name 'assembleAar<Variant>' not found".
- devicelab build_aar_module_test builds with android.newDsl=true and a
  module singleVariant("release") declaration.
- Update the migration doc and the website page draft.
The tool requests assembleAar<Variant> without a project path, so Gradle
skips projects that lack it. Check each requested task against the module
and every Android plugin, so a flavor mismatch in either direction fails
instead of reporting success with an incomplete repo. Cover both errors in
build_aar_module_test and scope the doc claims about the deleted code.
@github-actions github-actions Bot added platform-android Android applications specifically tool Affects the "flutter" command-line tool. See also t: labels. team-android Owned by Android platform team d: docs/ flutter/flutter/docs, for contributors labels Oct 7, 2026
@reidbaker
reidbaker self-requested a review October 7, 2026 20:48
Comment on lines +122 to +125
// The tool requests `assembleAar<Variant>` by name, without a project path. Gradle runs that task
// in every project that has it and skips projects that don't, so a project without the variant
// would leave the repo without its AAR. The module's POM depends on each plugin's AAR for the same
// variant name.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doc should describe how callers can know when the check is a success or failure.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 94a2990 (aar_init_script.gradle:132-136): "Returns normally if they do. Otherwise throws a GradleException that names the variant and the first project without it. A requested task that matches no project's variant (for example a misspelled --flavor) is left to Gradle, which fails with Task '<name>' not found in root project."

// variant name.
void checkProjectsHaveRequestedVariants(Project moduleProject, Collection<Project> pluginProjects) {
List<String> requestedAarTasks = moduleProject.gradle.startParameter.taskNames.findAll {
it.startsWith("assembleAar")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are going to use string identification here instead of something more typesafe (and I would prefer the more typesafe solution) then use a constant that is referenced here and in aarTaskName so that they always match and we know where the prefix comes from.

@reidbaker-agent reidbaker-agent Oct 8, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 94a2990 with no string parsing (see the reply on aarTaskName). The check builds the requested variant set by comparing startParameter.taskNames with aarTaskName(component.name) for each publishing component of the module and the Android plugins. There is no prefix literal, no substring/uncapitalize, and the variant name in the error comes from the component, so flavor names that start with an uppercase letter are reported correctly.

Behavior in the matrix (AGP 9.3.1 newDsl off and on, AGP 9.1.1, AGP 8.11.1 with the version check skipped):

  • flavored module without --flavor → module project ':flutter' has no variant error;
  • --flavor demo when a plugin lacks it → plugin project ':plugin_a' has no variant error.
  • If no project has the requested variant (--flavor foo), the build fails with Gradle's own Task 'assembleAarFooDebug' not found in root project 'android_generated' and its subprojects. Some candidates are: …, as on the base.
  • A path-qualified request (:flutter:assembleAarRelease) isn't checked and publishes nothing. The base script does the same (exit 0, no artifacts). addAarTask creates publications only for the bare name, and the tool sends only the bare name (gradle.dart:951).

// in every project that has it, and each plugin publishes its own AAR.
modulePlugins.each { pluginProject ->
configurePlugin(pluginProject, moduleProject.property("output-dir"))
moduleProject.android.libraryVariants.all { variant ->

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this not needed anymore?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gradle already runs each plugin's task, and the module's publishing doesn't need the plugin's AAR.

  1. The tool requests the task without a project path (the comment at aar_init_script.gradle:243-244 says this). buildGradleAar adds getAarTaskFor(buildInfo) (assembleAar<Variant>) to the command (gradle.dart:143, :951). The Gradle docs say "You can also run a task for all subprojects using a task selector that consists of only the task name." (https://docs.gradle.org/current/userguide/command_line_interface.html#executing_tasks_in_multi_project_builds). With the loop deleted, the task graph has :plugin_a:assembleAarRelease, :plugin_a:bundleReleaseAar, :plugin_a:publishReleasePublicationToMavenRepository and :plugin_a:publish.
  2. Ordering doesn't matter. In the same graph, :flutter:generatePomFileForReleasePublication depends on no :plugin_a task. :flutter:publishReleasePublicationToMavenRepository depends only on the plugin's compile-classpath tasks (compileReleaseKotlin, bundleLibCompileToJarRelease, …), not on bundleReleaseAar or a plugin publish task. The module POM refers to the plugin by coordinates (plugin_a_release). Each project publishes its own files to the shared local repo.
  3. Result. In the manual matrix (case (a), module + one Android plugin) and in devicelab build_aar_module_test, build_android_host_app_with_module_aar and android_java17_build_android_host_app_with_module_aar, the repo has the module and plugin AAR, POM, sources and javadoc for every mode. The devicelab tests also check that the module POM depends on plugin_with_android_release.
  4. Missing variants. The loop's tasks.named(...).get() threw Task with name 'assembleAar<Variant>' not found in project ':<plugin>' when a plugin lacked any module variant. checkProjectsHaveRequestedVariants (:140) covers that case for the requested variant, with a message that names the project and the fix.

…AR tasks lazily

- checkProjectsHaveRequestedVariants compares the requested task names
  with aarTaskName() of each project's publishing components, so no task
  name is parsed back into a variant name. Its doc says when it returns
  and when it throws.
- aarComponents() is the one component filter for task registration and
  the check.
- addAarTask registers its task lazily. The publication is created
  outside the task's configuration action, where maven-publish may
  register its tasks.
- Link AGP's checkSingleVariantUniqueness source (AGP 9.1.0 tag) and say
  why the duplicate error is identified.
- Keep the task-selector explanation once, in projectsEvaluated.
- Docs describe only the behavior.
- checkProjectsHaveRequestedVariants: a requested task that matches no
  project's variant is left to Gradle's task selector error.
- aarComponents: describe what the filter returns.
- addAarTask: state the effect of the requested-task guard, and drop the
  "all" note from the publication comment (the filter is in aarComponents).
@reidbaker reidbaker added the CICD Run CI/CD label Oct 8, 2026
…add-to-app-module

# Conflicts:
#	packages/flutter_tools/gradle/src/main/kotlin/FlutterPlugin.kt
#	packages/flutter_tools/gradle/src/main/kotlin/FlutterPluginUtils.kt
#	packages/flutter_tools/gradle/src/test/kotlin/FlutterPluginTest.kt
@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Oct 9, 2026
@reidbaker reidbaker added the CICD Run CI/CD label Oct 9, 2026
@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Oct 13, 2026
@reidbaker
reidbaker marked this pull request as ready for review October 14, 2026 20:46
@reidbaker
reidbaker requested a review from a team as a code owner October 14, 2026 20:46
@reidbaker
reidbaker requested review from gmackall, jesswrd and mboetger and removed request for a team October 14, 2026 20:46

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the Android Archive (AAR) build process in Flutter to use the public Android Gradle Plugin (AGP) APIs, refactoring aar_init_script.gradle to register AAR tasks and publications per variant, handle duplicate singleVariant declarations gracefully, and verify variant consistency across the module and its plugins. It also adds corresponding integration tests and updates the migration documentation. The review feedback suggests enhancing the task matching logic in aar_init_script.gradle to support qualified task paths (e.g., :assembleAarRelease) in addition to bare task names.

Comment on lines +100 to +103
static Collection<SoftwareComponent> requestedAarComponents(Project project) {
List<String> requestedTasks = project.gradle.startParameter.taskNames
return project.components.findAll { requestedTasks.contains(aarTaskName(it.name)) }
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To make the script more robust when executed manually or by external wrappers, we should support qualified task paths (e.g., :flutter:assembleAarRelease or :assembleAarRelease) in addition to bare task names. We can achieve this by checking if any requested task matches the task name or ends with :<taskName>.

static Collection<SoftwareComponent> requestedAarComponents(Project project) {
    List<String> requestedTasks = project.gradle.startParameter.taskNames
    return project.components.findAll { component ->
        String taskName = aarTaskName(component.name)
        requestedTasks.any { it == taskName || it.endsWith(":$taskName") }
    }
}

@flutter-dashboard flutter-dashboard Bot removed the CICD Run CI/CD label Oct 15, 2026
@reidbaker reidbaker added the CICD Run CI/CD label Oct 15, 2026
@reidbaker reidbaker added the autosubmit Merge PR when tree becomes green via auto submit App label Oct 20, 2026
@auto-submit
auto-submit Bot added this pull request to the merge queue Oct 20, 2026
Merged via the queue into flutter:master with commit 1bdad40 Oct 20, 2026
31 checks passed
@flutter-dashboard flutter-dashboard Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Oct 20, 2026
suryaKommana2662 pushed a commit to suryaKommana2662/flutter that referenced this pull request Oct 29, 2026
…Is (flutter#193962)

PR 7 (flutter#193693) merged to master as `ecd1ab06865` and PR 8 (flutter#193717) as
`4249ccbc18a`. This branch merges master at `4521d8d6634`, so the Files
tab shows only PR 9's changes.

PR 9 commits:
- `86523c549fc` Move aar_init_script to public AGP APIs
- `3edc95d1fa0` Check requested AAR variants in the module and each
plugin
- `da9646aad53` Address review: derive requested variants from
components, register AAR tasks lazily
- `94a29907065` Clarify aar_init_script comments from the adversarial
review
- `ea08280d886` Merge commit '23ddd6b74e6' into agp-aar-init-script (PR
8's head with master at `ecd1ab06865`; no conflicts, no change to PR 9's
files)
- `a17fafcc35e` Merge commit '5ba8961b517' into agp-aar-init-script
(master with PR 8 at `4249ccbc18a`; no conflicts, no change to PR 9's
files)
- `77ba449ea4a` Filter requested AAR components before adding tasks
- `adbfb9cd640` Drop the AGP version list from the duplicate-variant
comment
- `f2b7111c51e` Docs: state the AAR variant requirement in plain words
- `cd10f766b5b` Register an AAR task for every variant, publish only
requested ones
- `b9493299916` Merge commit '4521d8d6634' into agp-aar-init-script
(master, including flutter#193649; no conflicts, no change to PR 9's files)

## Description

This is PR 9 of 11 in the AGP 9.1.0 / public `gradle-api` migration
stack ([flutter#180137](flutter#180137),
[flutter#166550](flutter#166550)).

`flutter build aar` passes
`packages/flutter_tools/gradle/aar_init_script.gradle` to Gradle with
`-I`. The script read `android.libraryVariants` and AGP's internal
`publishing.singleVariants`. With AGP 9's `android.newDsl=true`,
`flutter build aar` on a module fails on `5ba8961b517` with `Can't
generate AAR on a non Android library project.` (script line 13), and
then at line 178 (`Could not get unknown property 'libraryVariants'`).
With this PR the script uses only `pluginManager.hasPlugin`,
`Publishing.singleVariant`, software `components` and `tasks.register`,
and the build succeeds with `newDsl` on and off, on AGP 9.3.1 and on
master's minimum AGP 9.1.1, and on AGP 8.11.1 when FGP's version check
is skipped.

**Breaking:** no. Builds that succeed on `5ba8961b517` succeed with the
same artifacts. Two failing cases change: a module or plugin build file
that declares `singleVariant` for only some variants builds, and a
variant mismatch between the module and a plugin fails with a Flutter
error instead of `Task with name … not found`.

**Base:** `master` at `4521d8d6634`. `aar_init_script.gradle`,
`build_aar_module_test.dart` and both docs are the same at `4521d8d6634`
and `5ba8961b517`, so the comparisons below against `5ba8961b517` hold
for `4521d8d6634`.

<details>
<summary>Agent authored details</summary>

### Changes

All in `aar_init_script.gradle` unless noted. Line numbers are at
`b9493299916`.

1. **Library check** (`:13`):
`project.pluginManager.hasPlugin("com.android.library")` replaces
`android.hasProperty("libraryVariants")`. The error text is unchanged.
2. **Publishing declarations** (`afterProject`, `:185-219`): Flutter
calls `android.publishing.singleVariant(<variant>) { withSourcesJar();
withJavadocJar() }` for each variant, one variant at a time. If the
project's build file already declared that variant, AGP throws, and
`isDuplicateSingleVariantError` (`:125-134`) matches AGP's exact
message, so the build file's declaration is kept. Its comment links the
AGP source of the message ([`AbstractPublishing.kt` at
`studio-2025.3.2`, AGP
9.1.0](https://android.googlesource.com/platform/tools/base/+/refs/tags/studio-2025.3.2/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/dsl/AbstractPublishing.kt#38)).
Any other exception is rethrown (`:201-203`). The guard that read
`publishing.singleVariants` and skipped every Flutter declaration once
the build file declared any variant is deleted.
3. **No module-to-plugin `dependsOn`** (`projectsEvaluated`): the loop
over `moduleProject.android.libraryVariants` that made each module
`assembleAar<Variant>` depend on the plugin's task (with eager
`tasks.named(...).get()`) is deleted. The tool requests
`assembleAar<Variant>` without a project path (`gradle.dart`
`buildGradleAar`, `command.add(aarTask)`), and Gradle runs a task
selector that is only a task name in every project that has the task
([Gradle
docs](https://docs.gradle.org/current/userguide/command_line_interface.html#executing_tasks_in_multi_project_builds):
"You can also run a task for all subprojects using a task selector that
consists of only the task name."). Each plugin publishes from its own
task. The comment at `:247-248` says this.
4. **Tasks, publications and task name** (`configureProject` `:27-37`;
`aarTaskName` `:93-97`; `requestedAarComponents` `:99-103`): AGP adds
one software component for each `singleVariant` declaration, with the
variant's name. `registerAarTask` (`:105-110`) registers
`assembleAar<Variant>` for every component, so a misspelled `--flavor`
gets Gradle's candidate list (`Some candidates are: 'assembleAarDebug',
...`). `requestedAarComponents` returns the components whose
`assembleAar<Variant>` name is in `startParameter.taskNames`; only those
get a publication (`addAarPublication`, `:112-123`), because `publish`
publishes every publication in the project. The check uses the same
function. `aarTaskName` is the only place that builds
`assembleAar<Variant>`; its comment points to the tool's `getAarTaskFor`
(`gradle.dart`). Nothing parses a task name back into a variant. The
`component.name != "all"` filter from `5ba8961b517` is deleted (see "Why
removing the deleted code is safe").
5. **Lazy task registration** (`registerAarTask`):
`project.tasks.register` replaces `project.tasks.create(name:)`. The
configuration action only adds `finalizedBy "publish"`.
`publications.create` runs in `addAarPublication`, not in the action,
because maven-publish registers tasks for each publication and Gradle
rejects that while it realizes a task
(`DefaultTaskContainer#register(String, Class, Action) on task set
cannot be executed in the current context.`, seen when
`publications.create` was inside the action).
6. **Requested-variant check** (`checkProjectsHaveRequestedVariants`,
`:136-175`, called at `:252`): Gradle skips projects that lack the
requested task, so the script checks the module and each Android plugin.
The requested variants are the names of the components that
`requestedAarComponents` returns for those projects. A project without a
requested variant fails the build with a `GradleException` that names
the variant and the project:
- module: `Flutter can't build the AAR for variant 'debug' because
module project ':flutter' has no variant with that name. Its variants
are: demoDebug, demoProfile, demoRelease, fullDebug, fullProfile,
fullRelease. Pass the module's product flavor with --flavor, or omit
--flavor if the module has no product flavors.`
- plugin: `Flutter can't build the AAR for variant 'demoDebug' because
plugin project ':plugin_a' has no variant with that name, so the plugin
AAR that the module depends on can't be published. `flutter build aar`
requires the module and each of its Android plugins to declare the same
product flavors.`
- A requested task that matches no project's variant (for example
`--flavor foo`) is left to Gradle: `Task 'assembleAarFooDebug' not found
in root project 'android_generated' and its subprojects. Some candidates
are: 'assembleAarDebug', 'assembleAarDemoDebug',
'assembleAarFullDebug'.` That is the same error as on `5ba8961b517`. The
check's doc says so. (`3edc95d1fa0` parsed the task-name prefix and
reported a Flutter module error in this case; `da9646aad53` drops the
parsing per review.)
7. **Docs.** Both docs describe behavior only.
- `Migrating-Flutter-Gradle-Plugin-to-AGP-public-API.md`: the P8 row of
the phase table and "Features that must break" item 8 are rewritten. The
"Why" starting-state list (L38-39) is unchanged, as in other PRs of the
stack.
-
[`website-page-draft.md`](https://github.com/flutter/flutter/blob/master/docs/platforms/android/website-page-draft.md)
(publishing tracked in
[flutter#193713](flutter#193713)): the
`flutter build aar` section is rewritten. It said Flutter "detects the
overlap and reports it with an actionable error". The code keeps the
build file's declaration instead, so the docs were changed to match the
code. It says (L234-237) that when a build file declares `singleVariant`
for a variant, Flutter keeps that declaration and declares the other
variants, and (L238-241) that the module and each Android plugin need a
variant with the built name.
8. **Tests.** devicelab `build_aar_module_test` gets two final sections:
- With `android.newDsl=true` and `android.builtInKotlin=true` in
`.android/gradle.properties`, and
`android.publishing.singleVariant("release") { withSourcesJar() }` in
`.android/Flutter/build.gradle`, it runs `flutter build aar
--no-profile`. It checks that the edits survive, that release has the
AAR and sources jar and no javadoc jar, that debug has the javadoc jar,
and that the plugin AARs exist for release and debug.
- It replaces that declaration with a `demo` product flavor and checks
both errors: `flutter build aar --no-profile --no-release` fails with
the module error, and the same with `--flavor demo` fails with the
plugin error.

`da9646aad53`'s commit message says the check's doc "says when it
returns and when it throws". That doc sentence was added in
`94a29907065`.

### Bug history (moved out of the docs)

The docs describe behavior only, so the history that `3edc95d1fa0` put
in them is here:
- On `5ba8961b517`, the `afterProject` guard skipped all of Flutter's
`singleVariant` declarations for a project whose build file declared any
variant. For a module with at least one Android plugin that declared
only `release`, every `flutter build aar` failed with `Task with name
'assembleAarDebug' not found in project ':flutter'`. With this PR that
build succeeds (table 1 below).
- On `5ba8961b517`, the `dependsOn` loop failed with `Task with name
'assembleAar<Variant>' not found in project ':<plugin>'` whenever a
plugin lacked any module variant, requested or not, and nothing checked
that the module had the requested variant. With this PR only requested
variants are checked, in the module and in each plugin (table 2 below).

### Audit resolution

[PR 9 draft
audit](reidbaker-agent#9 (comment)),
and stack audit items M4 and L8:

| Item | Resolution |
| --- | --- |
| 1. Eager `.get()` and cross-project task lookup by name | The wiring
is deleted, not made lazy: it was redundant (Changes 3). No `.get()` or
`tasks.named` in the script. The check reads software components, and
`registerAarTask` uses `tasks.register`. |
| 2. Broad `catch (Exception)`; overstated `multipleVariants` warning |
Only AGP's exact duplicate message is ignored (`:130-134`); everything
else is rethrown (`:201-203`). No warning is logged, so there is no
`multipleVariants` claim. |
| 3. Ext property without `flutter.internal.` | No ext property.
`afterProject` fires once per project (instrumented: count 1 for `:`,
`:app`, `:flutter`, `:plugin_a` and the included FGP build), and a
second call would only hit ignored duplicates. The "fires more than once
in composite settings" claim is dropped. |

Why the message match and not the type: AGP reports the duplicate from
`AbstractPublishing.checkSingleVariantUniqueness` through
`SyncIssueReporterImpl`, which throws
`com.android.builder.errors.EvalIssueException` in STANDARD mode. That
class is in `com.android.tools.build:builder`, not in `gradle-api`, and
is not on the init script's classpath. `javap -v` on
`AbstractPublishing` shows the same text in AGP 8.11.1, 9.1.0 and 9.3.1.
AGP checks before it adds the component (8.11.1
`AbstractPublishing.kt:27-35`), so the build file's declaration stays.
If AGP changes the text, the exception is rethrown and the build fails
loudly; devicelab `build_aar_module_test` exercises the path on the
template AGP.

Why not declare first or query: the public `Publishing` DSL in
`gradle-api-9.1.0.jar` has only `singleVariant(String)` and
`singleVariant(String, Function1)`, and `LibraryPublishing` adds only
`multipleVariants(...)` (`javap`), so there is no public way to list
declared variants. Declaring Flutter's variants in `beforeProject` +
`pluginManager.withPlugin("com.android.library")` makes the build file
fail at its own `singleVariant("release")` line with the duplicate error
(probe).

Why not AGP types in the init script: `import
com.android.build.api.dsl.LibraryExtension` fails with `unable to
resolve class`. With `gradle-api:9.3.1` on the `initscript` classpath,
the init script gets its own copy of the class from a different
classloader, so `instanceof` is `false` and
`extensions.getByType(LibraryExtension)` throws `Extension of type
'LibraryExtension' does not exist. Currently registered extension types:
[..., LibraryExtension, ...]` (probes).

`multipleVariants`: `PublishingUtil.ensureComponentNameUniqueness`
errors only when a `multipleVariants` component name equals a
`singleVariant` name. Flutter declares the same `singleVariant`s on
`5ba8961b517` when the build file declares none, so that behavior is
unchanged. It is not tested here.

### Why removing the deleted code is safe

- `android.libraryVariants` does not exist with `newDsl=true`
(reproduced above). AGP 9.1.0 marks its class,
`com.android.build.gradle.LibraryExtension`, `@Deprecated` ("This class
is not used for the public extensions in AGP when android.newDsl=true,
which is the default in AGP 9.0, and will be removed in AGP 10.0."), and
the class is not in `gradle-api-9.1.0.jar`. `singleVariants` is an
internal property (`getSingleVariants()` on
`com.android.build.gradle.internal.dsl.AbstractPublishing`, `javap`). It
resolves from Groovy with `newDsl=true`, so it was not a failure, only
internal API.
- The `dependsOn`: with it removed, the task graph for
`assembleAarRelease` on case (a) has `:plugin_a:assembleAarRelease`,
`:plugin_a:bundleReleaseAar`,
`:plugin_a:publishReleasePublicationToMavenRepository` and
`:plugin_a:publish`. `:flutter:generatePomFileForReleasePublication`
depends on no `:plugin_a` task, and
`:flutter:publishReleasePublicationToMavenRepository` depends only on
the plugin's compile-classpath tasks, so the order between the module's
and the plugin's publishing doesn't matter. The module POM refers to the
plugin by coordinates (`plugin_a_release`). devicelab
`build_aar_module_test`, `build_android_host_app_with_module_aar` and
`android_java17_build_android_host_app_with_module_aar` pass (they check
the plugin AAR/POM and that the module POM depends on
`plugin_with_android_release`).
- The `all` filter (`component.name != "all"`, added in flutter#101891
`606e91ad63d`): AGP 7.x created an automatic multi-variant component
named `all` when a project used no publishing DSL ([`PublishingUtil.kt`
at
`studio-2021.3.1`](https://android.googlesource.com/platform/tools/base/+/refs/tags/studio-2021.3.1/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/utils/PublishingUtil.kt),
`createPublishingInfoForLibrary` and `publishingFeatureOptIn`; same in
the AGP 7.3.1 jar). flutter#101891 published it separately with classifiers and
skipped it for the `_<variant>` publications; flutter#112149 (`8e30cc9c56b`)
removed that publication and kept the skip. AGP 8.0 disabled automatic
components by default ([AGP 8.0.0 release
notes](https://developer.android.com/build/releases/past-releases/agp-8-0-0-release-notes)),
and the AGP 8.11.1, 9.1.1 and 9.3.1 jars list
`android.disableAutomaticComponentCreation` as removed, with no `all`
component code. This script declares `singleVariant` for every variant,
which also turns automatic components off on AGP 7.x. A probe that
prints `project.components` after `projectsEvaluated` on all four matrix
configurations shows only per-variant names (`[debug, profile, release]`
for `:flutter`, `:app` and each plugin, `[demoDebug, …, fullRelease]`
for the flavored module, `[freeDebug, freeProfile, freeRelease]` for
`plugin_flavored`, `[debug, release]` for the root), never `all`.
- `tasks.register`: the publication is created at configuration time
only for a requested task name, as with `tasks.create`, whose
configuration closure ran immediately. Only the task object is created
lazily. Table rows below and the devicelab runs show the same artifacts.

### Removed comments, assertions and code

| Removed | Reason |
| --- | --- |
| `afterProject` comment item 2, "The project has already defined which
variants to publish…", and the `singleVariants.size()` guard it
described | The guard read internal API and skipped all of Flutter's
declarations once a build file declared any. Item 1 of that comment
became "Exit early if the project doesn't have the Android Gradle plugin
applied." |
| The `projectsEvaluated` comment "Configure the
`assembleAar<variantName>` task for each plugin's projects and make the
module's equivalent task depend on the plugin's task.", with the
`libraryVariants.all` loop, both `tasks.named(...).get()` calls, both
`assert(... != null)` and the `dependsOn` | The dependency is redundant
(see above). `tasks.named(...).get()` throws before the asserts could
fail, so they never checked anything. |
| `addAarTask` and its `variantName` local | Split into
`registerAarTask` (every component) and `addAarPublication` (requested
components). The task name comes from `aarTaskName`, which the check
also uses. |
| `addAarTask` comment "This check is required to be able to configure
the archives before `publish` runs.", with the check | The check is
`requestedAarComponents`, applied by the caller before
`addAarPublication`. Its reason is the comment on `addAarPublication`:
"`publish` publishes every publication in the project, so only requested
components get one." |
| The `component.name != "all"` filter in `configureProject`, and the
fragment "(except "all" since that is used to publish artifacts in the
new way)" from "Create a default MavenPublication for the variant" |
Nothing creates an `all` component where this script runs (see "Why
removing the deleted code is safe"). |
| From the migration doc item 8 and the website page draft: the
sentences about the deleted guard, the `Task with name … not found`
failures, and what the deleted code did and did not check | The docs
describe behavior only. The history is in "Bug history" above. |

### Behavioral and compatibility notes

Matrix projects (local scratch): (a) module + Android plugin `plugin_a`;
(b) module with flavors `demo`/`full` + `plugin_a`; (c) module +
`plugin_a` + `plugin_flavored` (flavor `free`); (d) module declaring
`singleVariant("release") { withSourcesJar() }` + `plugin_a`.

1. **A build file that declares `singleVariant` for some variants
builds.** Case (d), `flutter build aar`:

   | Script | Result |
   | --- | --- |
| `5ba8961b517` (AGP 9.3.1 `newDsl=false`) | exit 1: `Task with name
'assembleAarDebug' not found in project ':flutter'.` |
| This PR (all four configurations below) | exit 0. Release has the AAR,
POM and sources jar, and no javadoc jar (the build file's declaration).
Debug and profile have sources and javadoc jars. Plugin AAR/POM for all
three. |

2. **Variant mismatches fail with a Flutter error that names the
project.** Only requested variants are checked. The `5ba8961b517` column
is AGP 9.3.1 with `newDsl=false`.

   | Case | `5ba8961b517` | This PR (all four configurations) |
   | --- | --- | --- |
| (b) `--flavor demo` | `Task with name 'assembleAarDemoDebug' not found
in project ':plugin_a'.` | plugin error for `demoDebug` and `:plugin_a`
|
| (c) default | `Task with name 'assembleAarDebug' not found in project
':plugin_flavored'.` | plugin error for `debug` and `:plugin_flavored` |
| (b) no `--flavor`, `--no-profile --no-release` | `Task with name
'assembleAarDemoDebug' not found in project ':plugin_a'.` | module error
for `debug`, listing the module's variants |
| (c) `--flavor free`, `--no-profile --no-release` | `Task with name
'assembleAarDebug' not found in project ':plugin_flavored'.` | module
error for `freeDebug` |
| (b) `--flavor foo` | Gradle `Task 'assembleAarFooDebug' not found in
root project …` | same Gradle error |

Without the module check (`86523c549fc`), the module-error cases exit 0
with "✓ Built" and a repo that has only the plugin AAR.
3. **Path-qualified requests.** `./gradlew -I aar_init_script.gradle …
:flutter:assembleAarRelease` exits 0 and publishes nothing, on
`5ba8961b517` and on this PR: a publication is created only when
`startParameter.taskNames` contains the bare name. The tool sends only
the bare name (`gradle.dart` `command.add(aarTask)`). Not changed here.
4. **No warnings.** `flutter build aar` hides Gradle output on success
unless `-v` (`ProcessUtils.run` buffers it; `gradle.dart` prints it only
on failure, and verified with a `logger.error` probe). Keeping a build
file's declaration needs no action, so nothing is logged. Errors are
shown.
5. **Configuration cache:** with `org.gradle.configuration-cache=true`
in case (a)'s `.android/gradle.properties` (AGP 9.3.1 / Gradle 9.5.0),
`flutter build aar` reports the same single problem with the
`5ba8961b517` script and with this PR: ``Task
`:flutter:compileFlutterBuildDebug` of type
`com.flutter.gradle.tasks.FlutterTask`: invocation of 'Task.project' at
execution time is unsupported with the configuration cache.`` None comes
from the init script.
6. `newDsl=true` with `builtInKotlin=false` fails before the script with
any Kotlin plugin (`The 'org.jetbrains.kotlin.android' plugin is not
compatible with AGP's 9.0 new DSL`). The devicelab section and matrix
set `builtInKotlin=true`.
7. Related open work: no overlap with
[flutter#191681](flutter#191681)
(`gradle.dart`, `gradle_errors.dart` and tests). No open PR touches
`aar_init_script.gradle`.

### Follow-up candidates (pre-existing, not changed here)

- `allprojects { apply plugin: "maven-publish" }` applies maven-publish
to every project, including ones that never publish.
- The `afterProject` and `projectsEvaluated` hooks reach across
projects, which Gradle Isolated Projects does not allow.
- The `-SNAPSHOT` rewrite of `project.version` in `configureProject`.
- The `engine.realm` and `engine.stamp` file reads at configuration time
in `configureProject`.
- The `is-plugin` branch of `projectsEvaluated` looks unreachable from
the tool.
- The configuration cache problem in FGP `FlutterTask` (note 5).
- `newDsl=true` with KGP (`builtInKotlin=false`), for PR 10.
- Mapping between flavored module variants and plugin variants (for
example, plugin `release` for module `demoRelease`).
- `singleVariant` is declared on the root project and `:app`, which
never publish.
- Gradle output is hidden on success, so nothing the script logs at info
or warn level reaches the user without `-v`.

### Integration test coverage of the changed paths

Searched `dev/devicelab/bin/tasks` and
`packages/flutter_tools/test/integration.shard` for `build aar` and
`assembleAar`.

| Path | Tests |
| --- | --- |
| Library check, publishing declarations, lazy task registration, plugin
publishing without `dependsOn` (`newDsl=false`) | devicelab
`build_aar_module_test`, `build_android_host_app_with_module_aar`,
`android_java17_build_android_host_app_with_module_aar`;
`android_obfuscate_test` |
| `newDsl=true`, a build file's `singleVariant` declaration kept |
devicelab `build_aar_module_test`, `newDsl=true` section |
| Module and plugin variant mismatch errors | devicelab
`build_aar_module_test`, product flavor section |
| AGP 9.1.1 and 8.11.1; a variant that no project has | Manual matrix
only |

`Linux build_aar_module_test` runs in presubmit for this PR: in
`.ci.yaml` its `runIf` includes `dev/**` and
`packages/flutter_tools/**`, and it sets neither `presubmit: false` nor
`bringup`. It also ran locally on the head (below) with `cd
dev/devicelab && ../../bin/dart bin/test_runner.dart test -t
build_aar_module_test --exit`.

### Tests run locally (macOS, JDK 17)

On the head (`b9493299916`; master merged at `4521d8d6634`):

- `gradle test --rerun-tasks` in `packages/flutter_tools/gradle`, with
the engine's `third_party/gradle` binary that the README names: 33
suites, 277 tests, 0 failures.
- `dart format` and `dart analyze --fatal-infos` on
`build_aar_module_test.dart`: clean (unchanged since `3edc95d1fa0`).
- Tool tests: `test/commands.shard/hermetic/build_aar_test.dart`,
`test/commands.shard/permeable/build_aar_test.dart` and
`test/general.shard/android/android_gradle_builder_test.dart`: 56 tests,
all pass.
- `test/integration.shard/android_obfuscate_test.dart`: 2 tests pass.
- devicelab via `bin/test_runner.dart test --exit`, one at a time:
`build_aar_module_test` (including both added sections),
`build_android_host_app_with_module_aar`,
`android_java17_build_android_host_app_with_module_aar`,
`build_android_host_app_with_module_source`,
`module_host_with_custom_build_test`, all `success`.
- The configuration cache comparison (note 5).

Manual matrix on `b9493299916`, four configurations:
- **9.3.1 off:** AGP 9.3.1 / Gradle 9.5.0, `newDsl=false`,
`builtInKotlin=false`.
- **9.3.1 on:** AGP 9.3.1 / Gradle 9.5.0, `newDsl=true`,
`builtInKotlin=true`.
- **9.1.1 on:** AGP 9.1.1 / Gradle 9.3.1 (FGP's minimums on master since
flutter#193813), `newDsl=true`, `builtInKotlin=true`.
- **8.11.1:** AGP 8.11.1 / Gradle 8.14.3, `newDsl=false`, with
`skipDependencyChecks=true` in `.android/gradle.properties`. Without it,
FGP stops before the init script runs: `Your project's Gradle version
(8.14.3) is lower than Flutter's minimum supported version of 9.3.1.`

| Case | 9.3.1 off | 9.3.1 on | 9.1.1 on | 8.11.1 |
| --- | --- | --- | --- | --- |
| (a) | exit 0, AAR, POM, sources and javadoc jars for
`flutter_{debug,profile,release}` and `plugin_a_{debug,profile,release}`
| same | same | same |
| (b) `--flavor demo` | exit 1, plugin error | same | same | same |
| (b) no `--flavor` | exit 1, module error | same | same | same |
| (b) `--flavor foo` | exit 1, Gradle `Task 'assembleAarFooDebug' not
found in root project` | same | same | same |
| (c) | exit 1, plugin error | same | same | same |
| (c) `--flavor free` | exit 1, module error | same | same | same |
| (d) | exit 0, as (a), except module release has the sources jar and no
javadoc jar | same | same | same |

Case (a)'s `flutter_release-1.0.pom` depends on
`dev.pr9.plugin_a:plugin_a_release:1.0` (checked on `b9493299916`, AGP
9.1.1).

Negative control, the master script (same at `5ba8961b517` and
`4521d8d6634`), on `b9493299916`: with `newDsl=true` (AGP 9.3.1 and AGP
9.1.1), (a)-(d) all fail with `Can't generate AAR on a non Android
library project.`; with `newDsl=false` on AGP 9.3.1, (a) passes and
(b)-(d) fail with Gradle's `Task with name 'assembleAar<V>' not found in
project ':<p>'.` On `a17fafcc35e`, with only line 13 changed to
`hasPlugin`, `newDsl=true` fails at line 178 with `Could not get unknown
property 'libraryVariants' for object of type
com.android.build.gradle.internal.dsl.LibraryExtensionImpl$AgpDecorated.`
devicelab `build_aar_module_test` with that script fails at the
`newDsl=true` section with the same error.

</details>

---

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [AI contribution guidelines] and understand my
responsibilities, or I am not using AI tools.
- [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 in-code documentation (doc comments with
`///`).
- [x] If this PR introduces a new feature or capability, I created and
linked a website documentation issue or PR in [flutter/website] (or
verified none is needed).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[AI contribution guidelines]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#ai-contribution-guidelines
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[flutter/website]: https://github.com/flutter/website
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
@reidbaker reidbaker added the cp: beta cherry pick this pull request to beta release candidate branch label Oct 29, 2026
auto-submit Bot pushed a commit that referenced this pull request Oct 30, 2026
…APIs (#195640)

This pull request is created by [automatic cherry pick workflow](https://github.com/flutter/flutter/blob/main/docs/releases/Flutter-Cherrypick-Process.md#automatically-creates-a-cherry-pick-request)
Please fill in the form below, and a flutter domain expert will evaluate this cherry pick request.

### Issue Link:
What is the link to the issue this cherry-pick is addressing?

#180137

### Impact Description:
What is the impact (ex. visual jank on Samsung phones, app crash, cannot ship an iOS app)?
Does it impact development (ex. flutter doctor crashes when Android Studio is installed),
or the shipping of production apps (the app crashes on launch).
This information is for domain experts and release engineers to understand the consequences of saying yes or no to the cherry pick.

Cherry-pick of #193962 (also tracks #166550).

On Flutter 3.50 beta (`flutter-3.50-candidate.0`), PRs #1–#8 of the Android Gradle Plugin (AGP) 9 public-API migration have already landed, allowing standard Android apps and source-included add-to-app modules to opt into AGP 9's new DSL (`android.newDsl=true` and `android.builtInKotlin=true`). However, `flutter build aar` passes `packages/flutter_tools/gradle/aar_init_script.gradle` to Gradle, which still accesses `android.libraryVariants` and AGP's internal `publishing.singleVariants`. Because AGP 9 removes `BaseExtension` / `libraryVariants` when `android.newDsl=true`, `flutter build aar` fails 100% of the time on Android modules opting into `android.newDsl=true` (`Can't generate AAR on a non Android library project.` / `Could not get unknown property 'libraryVariants'`). Without this cherry-pick, add-to-app teams packaging Flutter modules as AARs cannot adopt AGP 9's new DSL on Flutter 3.50.

### Changelog Description:
Explain this cherry pick:
* In one line that is accessible to most Flutter developers.
* That describes the state prior to the fix.
* That includes which platforms are impacted.
See [best practices](https://github.com/flutter/flutter/blob/main/docs/releases/Hotfix-Documentation-Best-Practices.md) for examples.

[flutter/180137](#180137): When running `flutter build aar` on Android with `android.newDsl=true` (AGP 9), the build fails because `aar_init_script.gradle` references legacy AGP variant APIs.

### Workaround:
Is there a workaround for this issue?

Keep `android.newDsl=false` in `.android/gradle.properties` when running `flutter build aar`, which prevents Flutter module projects from adopting AGP 9's new DSL.

### Risk:
What is the risk level of this cherry-pick?

### Test Coverage:
Are you confident that your fix is well-tested by automated tests?

### Validation Steps:
What are the steps to validate that this fix works?

1. Create a Flutter module (`flutter create --template=module my_module`).
2. Opt `.android` into AGP 9's new DSL by setting `android.builtInKotlin=true` and `android.newDsl=true` in `.android/gradle.properties` (and ensuring `kotlin-android` is not applied).
3. Run `flutter build aar` inside `my_module` and verify that debug, profile, and release AAR artifacts and Maven POMs are generated in `build/host/outputs/repo` without `libraryVariants` or `Can't generate AAR on a non Android library project.` errors (covered in CI by `dev/devicelab/bin/tasks/build_aar_module_test.dart`).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CICD Run CI/CD cp: beta cherry pick this pull request to beta release candidate branch d: docs/ flutter/flutter/docs, for contributors platform-android Android applications specifically team-android Owned by Android platform team tool Affects the "flutter" command-line tool. See also t: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants