Cannot apply plugins to subprojects from within a settings plugin

After updating Gradle from 8 to 9, the plugin stopped working. I now get the error:

Execution failed for task ‘:extensions:nunl:processReleaseResources’.

Cannot mutate the dependencies of configuration ‘:extensions:nunl:releaseCompileClasspath’ after the configuration was resolved. After a configuration has been observed, it should not be modified.

I cannot pinpoint what causes it.

The plugin I am writing works in this fashion:

A settings plugin applies plugins to subprojects respectively. These plugins then configure dependencies and share artifacts to each other. Namely, “extension” projects supply artifacts to the single “patches” project. Related code:

private fun Settings.configureProjects(extension: SettingsExtension) {
    // region Include the projects

    val extensionsProjectPath = extension.extensions.projectsPath

    if (extensionsProjectPath != null) {
        objectFactory.fileTree().from(rootDir.resolve(extensionsProjectPath)).matching {
            it.include("**/build.gradle.kts", "**/build.gradle")
        }.forEach {
            include(it.parentFile.relativeTo(rootDir).toPath().joinToString(":"))
        }
    }

    include(extension.patchesProjectPath)

    // endregion

    // region Apply the plugins

    gradle.rootProject { rootProject ->
        if (extensionsProjectPath != null) {
            val extensionsProject = try {
                rootProject.project(extensionsProjectPath)
            } catch (e: UnknownProjectException) {
                null
            }

            extensionsProject?.subprojects { extensionProject ->
                if (
                    extensionProject.buildFile.exists() &&
                    !extensionProject.parent!!.plugins.hasPlugin(ExtensionPlugin::class.java)
                ) {
                    extensionProject.pluginManager.apply(ExtensionPlugin::class.java)
                }
            }
        }

        // Needs to be applied after the extension plugin
        // so that their extensionConfiguration is available for consumption.
        rootProject.project(extension.patchesProjectPath).pluginManager.apply(PatchesPlugin::class.java)
    }   

The patches plugin:

/**
 * Configures the project to consume the extension artifacts and add them to the resources of the patches project.
 */
private fun Project.configureConsumeExtensions(patchesExtension: PatchesExtension) {
    val extensionsProject = try {
        project(patchesExtension.extensionsProjectPath ?: return)
    } catch (e: UnknownProjectException) {
        return
    }

    val extensionProjects = extensionsProject.subprojects.filter { extensionProject ->
        extensionProject.plugins.hasPlugin(ExtensionPlugin::class.java)
    }

    val extensionsDependencyScopeConfiguration =
        configurations.dependencyScope("extensionsDependencyScope").get()
    val extensionsConfiguration = configurations.resolvable("extensionConfiguration").apply {
        configure { it.extendsFrom(extensionsDependencyScopeConfiguration) }
    }

    project.dependencies.apply {
        extensionProjects.forEach { extensionProject ->
            add(
                extensionsDependencyScopeConfiguration.name,
                project(
                    mapOf(
                        "path" to extensionProject.path,
                        "configuration" to "extensionConfiguration",
                    ),
                ),
            )
        }
    }

    extensions.configure<SourceSetContainer>("sourceSets") { sources ->
        sources.named("main") { main ->
            main.resources.srcDir(extensionsConfiguration)
        }
    }
}

The extension plugin:

private fun Project.configureArtifactSharing(extension: ExtensionExtension) {
    val androidExtension = extensions.getByType<BaseAppModuleExtension>()
    val syncExtensionTask = tasks.register<Sync>("syncExtension") {
        val dexTaskName = if (androidExtension.buildTypes.getByName("release").isMinifyEnabled) {
            "minifyReleaseWithR8"
        } else {
            "mergeDexRelease"
        }

        val dexTask = tasks.getByName(dexTaskName)

        dependsOn(dexTask)

        val extensionName = if (extension.name != null) {
            Path(extension.name!!)
        } else {
            projectDir.resolveSibling(project.name + ".rve").relativeTo(rootDir).toPath()
        }

        from(dexTask.outputs.files.asFileTree.matching { include("**/*.dex") })
        into(layout.buildDirectory.dir("revanced/${extensionName.parent.pathString}"))

        rename { extensionName.fileName.toString() }
    }

    configurations.create("extensionConfiguration").apply {
        isCanBeResolved = false
        isCanBeConsumed = true

        outgoing.artifact(layout.buildDirectory.dir("revanced")) {
            it.builtBy(syncExtensionTask)
        }
    }
}

Mind that there are likely bad practices involved, which could also be the source of the issue; however, I did not find any other way to achieve the behaviour I had with this in Gradle 8. Ideally, I can restore the behaviour I saw on Gradle 8.

I did not review all your code, but if you get that error you mentioned, that is just the symptom.
Something resolved the configuration you try to configure before you try to configure it.
So you would need to find out / have a breakpoint when the configuration you try to modify is locked by being resolved.
Then you can check whether that is a legit resolve.

This problem often arises because something resolves a configuration at configuration time which is seldomly a good idea.

When I omit the call to configureConsumeExtensions, the symptom no longer occurs. I will try to use breakpoints to find the relevant lines. Maybe a stacktrace could show where the issue occurs? I don’t think a breakpoint would show which line causes the error, because the stack trace didn’t either

Yes, it would, if you have read what I wrote closely. :wink:

The error happens because you try to add some dependency to a configuration that already took part in dependency resolution, which in Gradle 9 was made a hard error.

But this error is usually just the symptom, which is also why the stacktrace is not helping in any way, and also a breakpoint at the spot where it fails would of course not help, but that is not what I said.

I said that most often the actual problem is, that someone already resolved the configuration prematurely, for example at configuration time and before you try to add dependencies. So you have to use breakpoints to find the spot that resolves the configuration and thus locks it for change.

Do I set the breakpoints anywhere in my Gradle plugin? How do I know whether the configuration resolves at the breakpoint? Is there one configuration or multiple? How do I see the configuration and if it is resolved? Is there an API to get the configuration when I break?

No, of course not in your plugin, you don’t know what causes the resolve, so that would be pointless.
Find the condition in the Gradle code when the exception that is thrown is triggered.
Find where this condition is turned to being true.
Set a breakpoint there to see when the configuration is resolved.

Oh wait, your error says “Execution failed for task ‘:extensions:nunl:processReleaseResources’.”, so you are in the execution phase already.

Forget what I said, it seems to not be the typical case but really your error.
You should not change any configuration options at task execution time.

How do I know which configuration I am changing at execution time? I have to share an artifact between modules which happens after one project built it. How do determine which part of the code in the plugin is responsible for changing the configuration?

To share an artifact, create an outgoing variant for it and depend on that variant from the other projects. You must not change the configuration at execution time for that.

As far as I am concerned, I do that. How do I know that I am not doing that and the error is actually caused by it? Note when I remove the call to consume the artifact, the error disappears, maybe thats an indicator

From last time I checked the docs, it has changed a little. I’ll give replicating it a shot and see if that happens to resolve the issue: How to Share Artifacts Between Projects with Gradle

I skimmed over your code now.
As you said, there are some bad practices (doing cross-project configuration, doing cross-project mutable state reading, using configuration = instead of attributes to consume the artifact from the other project, requiring a specific plugin application order, using .plugins, …), but no idea whether those cause it or whether you maybe are following a red herring.

Your error says it fails at processReleaseResources, and if you look at the --stacktrace you should also see where exactly. That should hopefully give an indication where the problem stems from.