Multi-project Gradle plugin

I have a project of the following structure:

root:

  • consumer
  • producers
    • producer1
    • producer2

The consumer is a Kotlin/JVM project. The producers are Android projects.
My plugin is applied to the consumer.
I want to add an extension to each producer using that plugin to configure the Android extension.
The plugin should also apply the necessary Android plugin to the producers to reduce boilerplate.
It should create an extension in the producer in which the Android extension block can be configured. A default configuration of the Android extension should be present.

I also need a plugin for the project settings. Now I was wondering if it is possible to apply a plugin to the consumer and each producer from within a Settings plugin. This way, I would not need to apply a separate plugin to the consumer.

I have come across several issues when trying to realize all of this.
My consumer project is called “patches”. Producers are called “extensions”.

  1. I have created a Settings plugin:

  1. I have created a Patches plugin:

image

  1. In my Settings plugin I try to apply the Patches plugin to the patches project:

The Settings plugin does not depend on the Patches plugin, so I get this error:

If I depend on it and use the plugin in my project I get this error:

Instead, if the Settings plugin does not depend on the Patches plugin, does not apply the Patches plugin to the Patches project and instead I apply it manually to the Patches project, this issue disappears.

The Settings plugin is supposed to configure the entire multi-project. This means it should configure the dependencies and plugins and include the patches project and the extensions inside a specified directory.

Without the plugin, I have to create this boilerplate in the gradle.settings.kts file:

Instead, with the plugin I want to reduce boilerplate so that the gradle.settings.kts file looks like this:

To do that I tried to create the Settings plugin like this:

abstract class PatchesSettingsPlugin : Plugin<Settings> {
    override fun apply(settings: Settings) {
        val extension = settings.extensions.create("patches", PatchesSettingsExtension::class.java)

        settings.configureDependencies()
        settings.configureProjects(extension)
        settings.configureBuildCache()
    }

    private fun Settings.configureDependencies() {
        pluginManagement.repositories.apply {
            google()
            mavenCentral()
        }

        @Suppress("UnstableApiUsage")
        dependencyResolutionManagement.apply {
            repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)

            repositories.apply {
                google()
                mavenCentral()
                mavenLocal()
                maven {
                    it.url = URI("https://maven.pkg.github.com/revanced/registry")
                    it.credentials.apply {
                        username = providers.gradleProperty("gpr.user").orNull
                            ?: System.getenv("GITHUB_ACTOR")
                        password = providers.gradleProperty("gpr.key").orNull
                            ?: System.getenv("GITHUB_TOKEN")
                    }
                }
            }
        }
    }

    private fun Settings.configureProjects(extension: PatchesSettingsExtension) {
        include(extension.patchesProjectPath)

        gradle.rootProject.project(extension.patchesProjectPath).pluginManager.apply(PatchesPlugin::class.java)

        // TODO("Add extension projects")
        // fileTree(extension.extensionsProjectPath.get()).matching {
        //     include("**/build.gradle.kts")
        // }.forEach {
        //     include(it.relativeTo(settingsDir).toPath().joinToString(":"))
        // }
    }

    private fun Settings.configureBuildCache() {
        buildCache.local.isEnabled = "CI" !in System.getenv()
    }
}

My next issue is including the extension projects using the “fileTree” API. The API seems to be available only in the Gradle kts files and not in the Plugin.

In the project Gradle kts file, I can use it as follows:

But in my Settings plugin, I can’t use it:

Do I need to add a specific dependency to my Settings plugin?

The next issue is that I want to configure the extension projects from within the plugin that is applied to the consumer. The plugin should add plugins to the extension projects and create an extension in which the android block can be configured.

For that I iterate through the subprojects and apply the necessary plugins:

But when I apply the Patches plugin to my consumer project, it does not configure the plugin of the extension projects:

When I manually add the plugins, the errors disappear.

The next challenge I had is to add an extension in which the Android block is configured by default. The user should be able to make changes to it though if necessary additionally so the following boilerplate can be removed from each extension project:

Instead, an extension block should configure the Android extension. For that I create the extension in my Patches plugin for each extension project:

The extension is created, but it does not seem I can set any properties there, even though I specified the class of the extension:

image

The next question would be how to configure the android block by default and allow adjusting it in the extensionConfig block if necessary.

I thought of adding this to the extension:

This way, the android block can be configured. Then I would preconfigure the block like this:

To sum up everything:

  • How can I merge the Settings and Project plugins into a single Settings plugin
  • How can I use APIs such as fileTree in a plugin
  • How can I apply plugins to the extensions projects from within a plugin I apply to the patches project
  • How can I add an extension to the extensions projects from within a plugin I apply to the patches project

Some other questions I have would be:

  • Do I need to add:
    dependencies {
        implementation(gradleApi())
    }
    
    to the plugin project? Because It works without this dependency just as it does with the dependency
  • Do I need to add plugins as a dependency to my plugin if I want to apply these plugins to projects via my plugin
  • I have seen the use of afterEvaluate in plugins, but I don’t understand the need for it. So far, I did not seem to need it. What would I need it for
  • I have seen people add properties like this to extensions:
    abstract val property: Property<String>
    
    but it seems its also possible to add properties like this
    var property: String
    
    Which one is the correct way/ What’s the former used for?

I will likely have follow-up questions after these, but thanks if you have made it this far!

Do … not … post … screenshots … of … text … … … PLEASE!

Now I was wondering if it is possible to apply a plugin to the consumer and each producer from within a Settings plugin.

Yes

In my Settings plugin I try to apply the Patches plugin to the patches project
The Settings plugin does not depend on the Patches plugin, so I get this error:

If you would not get that error, you would get the error “The root project is not yet available for build.” as you are still in the initialization phase and it is only available in the configuration phase. You could instead register an action to be done as soon as it is available using gradle.rootProject { ... }, or gradle.allprojects { ... }.

The Settings plugin does not depend on the Patches plugin, so I get this error:

So you have the patches and settings plugins in different projects?
Why?
If you want to apply the one from the other, you should probably just have them in the same project.
But depending on it should also work.

If I depend on it and use the plugin in my project I get this error:

Hard to say, screenshot is not good readable, and you cut off most of the error message. Optimally share a build --scan URL if possible.

mavenLocal()

Be aware that this is highly discouraged, broken by design in Maven already, makes your builds slower, makes your builds flaky at best, …
See here for more information: Declaring repositories

My next issue is including the extension projects using the “fileTree” API. The API seems to be available only in the Gradle kts files and not in the Plugin.

Inject an instance of ObjectFactory to your settings plugin, then you can call fileTree on that.

The next issue is that I want to configure the extension projects from within the plugin that is applied to the consumer.

Don’t do that.
Using in the settings plugin gradle.allprojects { ... } and then checking in which project you are might be acceptable instead.

But when I apply the Patches plugin to my consumer project, it does not configure the plugin of the extension projects:

It probably did, but your problem is, that you only get type-safe accessors for things added by plugins applied in the plugins { ... } block in the same script. If you do cross-project configuration from the root project or also from the settings script, or use the legacy apply(...) method, you do not get type-safe accessors generated. You could provide such accessors in your plugin as code, but then they are available for all projects, whether the respective plugin is applied or not.

The next challenge I had

Maybe you should stop overloading a single thread.
This thread should probably have been 10 individual threads.
Having 10 topics in one thread makes it much harder to discuss individual topics and to follow.

is to add an extension in which the Android block is configured by default.

So?
Configure the extension in your plugin after you applied the plugin.
You of course also here cannot use the accessor, but have to configure the extension by type for example like configure<ApplicationExtension> { ... }.

The extension is created, but it does not seem I can set any properties there, even though I specified the class of the extension:

The extension is probably not accessible by the build script, for example because you made it internal or a local class or similar.
Make it in its own ...kt file and it will most probably work.

Then I would preconfigure the block like this:

You should not create the extension the android plugin is creating.
You are not the Android plugin.
Apply the Android plugin and after that you can safely access the extension the plugin added without any fallback.
If it is not there for whatever reason, something went wrong and you need to check what and not just create the extension on your own which can only make problems later on.

Do I need to add
Because It works without this dependency just as it does with the dependency

You probably apply some plugin that already adds this dependency for you like kotlin-dsl or java-gradle-plugin.

Do I need to add plugins as a dependency to my plugin if I want to apply these plugins to projects via my plugin

Yes

I have seen the use of afterEvaluate in plugins, but I don’t understand the need for it. So far, I did not seem to need it. What would I need it for

  • to introduce ordering problems
  • to introduce timing problems
  • to introduce race conditions

There are very rare cases where you need it, most often if you need to cooperate with other plugins that evilly use it.
Avoid it wherever you can and optimally never ever use it.
It is most often just a symptom treatment, delaying the actual problem to a later, harder to reproduce, harder to debug, harder to fix point in time.
It is like calling Platform.runLater or `SwingUtilities.invokeLater to “fix” a GUI problem.
Veeery bad idea.

Which one is the correct way/ What’s the former used for?

Always use Propertys and its relatives.
And always treat them lazy, meaning reading it optimally only at execution time, never at configuration time.
Never use primitve properties, or you get into ordering problems where you or the consumer then need to use afterEvaluate or fight with afterEvaluates against each other to get the ordering right.
With Propertys and Providers and alike you can wire things together, only querying the actual value at execution time, so it doesn’t matter when at configuration time the value is set or changed, as you do only read it after all configuration is finished.
They were introduced to stop the afterEvaluate madness with all its problems.
Read more about it at Configuring Tasks Lazily.

Old post, see post below because it didn't fit in one post

If I don’t I get the same error I experience when I depend on the patches plugin (see OP).

What do you mean by “register an action”. When I apply the plugin?

I added the Patches plugin project as a dependency:

dependencies {
    implementation(gradleApi())
    implementation(project(":plugin"))
}

Now I can apply the plugin from within my Settings plugin:

gradle.rootProject.project(extension.patchesProjectPath).pluginManager.apply(PatchesPlugin::class.java)

Here is the build scan, when I try to build the patches project:

19:23:07: Executing 'build --scan'...


FAILURE: Build failed with an exception.

* What went wrong:
Could not resolve all artifacts for configuration 'classpath'.
> Could not find com.gradle:gradle-enterprise-gradle-plugin:3.16.2.
  Required by:
      unspecified:unspecified:unspecified
> Could not find org.jetbrains.kotlin:kotlin-reflect:1.9.22.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/org/jetbrains/kotlin/kotlin-reflect/1.9.22/kotlin-reflect-1.9.22.pom
    - https://maven.pkg.github.com/revanced/registry/org/jetbrains/kotlin/kotlin-reflect/1.9.22/kotlin-reflect-1.9.22.pom
  Required by:
      unspecified:unspecified:unspecified
> Could not find com.android.tools.smali:smali:3.0.5.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/com/android/tools/smali/smali/3.0.5/smali-3.0.5.pom
    - https://maven.pkg.github.com/revanced/registry/com/android/tools/smali/smali/3.0.5/smali-3.0.5.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0 > app.revanced:revanced-patcher:20.0.0
> Could not find com.google.guava:guava:33.1.0-jre.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/com/google/guava/guava/33.1.0-jre/guava-33.1.0-jre.pom
    - https://maven.pkg.github.com/revanced/registry/com/google/guava/guava/33.1.0-jre/guava-33.1.0-jre.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0
> Could not find org.jetbrains.kotlinx.binary-compatibility-validator:org.jetbrains.kotlinx.binary-compatibility-validator.gradle.plugin:0.14.0.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/org/jetbrains/kotlinx/binary-compatibility-validator/org.jetbrains.kotlinx.binary-compatibility-validator.gradle.plugin/0.14.0/org.jetbrains.kotlinx.binary-compatibility-validator.gradle.plugin-0.14.0.pom
    - https://maven.pkg.github.com/revanced/registry/org/jetbrains/kotlinx/binary-compatibility-validator/org.jetbrains.kotlinx.binary-compatibility-validator.gradle.plugin/0.14.0/org.jetbrains.kotlinx.binary-compatibility-validator.gradle.plugin-0.14.0.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0
> Could not find org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.0.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/org/jetbrains/kotlin/kotlin-gradle-plugin/2.0.0/kotlin-gradle-plugin-2.0.0.pom
    - https://maven.pkg.github.com/revanced/registry/org/jetbrains/kotlin/kotlin-gradle-plugin/2.0.0/kotlin-gradle-plugin-2.0.0.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0
> Could not find org.jetbrains.kotlin.android:org.jetbrains.kotlin.android.gradle.plugin:2.0.0.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/org/jetbrains/kotlin/android/org.jetbrains.kotlin.android.gradle.plugin/2.0.0/org.jetbrains.kotlin.android.gradle.plugin-2.0.0.pom
    - https://maven.pkg.github.com/revanced/registry/org/jetbrains/kotlin/android/org.jetbrains.kotlin.android.gradle.plugin/2.0.0/org.jetbrains.kotlin.android.gradle.plugin-2.0.0.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0
> Could not find com.android.application:com.android.application.gradle.plugin:8.2.2.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/com/android/application/com.android.application.gradle.plugin/8.2.2/com.android.application.gradle.plugin-8.2.2.pom
    - https://maven.pkg.github.com/revanced/registry/com/android/application/com.android.application.gradle.plugin/8.2.2/com.android.application.gradle.plugin-8.2.2.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0
> Could not find com.android.library:com.android.library.gradle.plugin:8.2.2.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/com/android/library/com.android.library.gradle.plugin/8.2.2/com.android.library.gradle.plugin-8.2.2.pom
    - https://maven.pkg.github.com/revanced/registry/com/android/library/com.android.library.gradle.plugin/8.2.2/com.android.library.gradle.plugin-8.2.2.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0
> Could not find org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.7.3/kotlinx-coroutines-core-1.7.3.pom
    - https://maven.pkg.github.com/revanced/registry/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.7.3/kotlinx-coroutines-core-1.7.3.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0 > app.revanced:revanced-patcher:20.0.0
> Could not find xpp3:xpp3:1.1.4c.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/xpp3/xpp3/1.1.4c/xpp3-1.1.4c.pom
    - https://maven.pkg.github.com/revanced/registry/xpp3/xpp3/1.1.4c/xpp3-1.1.4c.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0 > app.revanced:revanced-patcher:20.0.0
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0 > app.revanced:revanced-patcher:20.0.0 > app.revanced:apktool-lib:2.9.3
> Could not find org.jetbrains.kotlin:kotlin-reflect:1.9.22.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/org/jetbrains/kotlin/kotlin-reflect/1.9.22/kotlin-reflect-1.9.22.pom
    - https://maven.pkg.github.com/revanced/registry/org/jetbrains/kotlin/kotlin-reflect/1.9.22/kotlin-reflect-1.9.22.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0 > app.revanced:revanced-patcher:20.0.0
> Could not find com.android.tools.smali:smali-dexlib2:3.0.3.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/com/android/tools/smali/smali-dexlib2/3.0.3/smali-dexlib2-3.0.3.pom
    - https://maven.pkg.github.com/revanced/registry/com/android/tools/smali/smali-dexlib2/3.0.3/smali-dexlib2-3.0.3.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0 > app.revanced:revanced-patcher:20.0.0 > app.revanced:multidexlib2:3.0.3.r3
> Could not find com.google.guava:guava:33.1.0-jre.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/com/google/guava/guava/33.1.0-jre/guava-33.1.0-jre.pom
    - https://maven.pkg.github.com/revanced/registry/com/google/guava/guava/33.1.0-jre/guava-33.1.0-jre.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0 > app.revanced:revanced-patcher:20.0.0 > app.revanced:multidexlib2:3.0.3.r3
> Could not find com.android.tools.smali:smali-baksmali:3.0.3.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/com/android/tools/smali/smali-baksmali/3.0.3/smali-baksmali-3.0.3.pom
    - https://maven.pkg.github.com/revanced/registry/com/android/tools/smali/smali-baksmali/3.0.3/smali-baksmali-3.0.3.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0 > app.revanced:revanced-patcher:20.0.0 > app.revanced:apktool-lib:2.9.3
> Could not find com.android.tools.smali:smali:3.0.5.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/com/android/tools/smali/smali/3.0.5/smali-3.0.5.pom
    - https://maven.pkg.github.com/revanced/registry/com/android/tools/smali/smali/3.0.5/smali-3.0.5.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0 > app.revanced:revanced-patcher:20.0.0 > app.revanced:apktool-lib:2.9.3
> Could not find com.google.guava:guava:33.1.0-jre.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/com/google/guava/guava/33.1.0-jre/guava-33.1.0-jre.pom
    - https://maven.pkg.github.com/revanced/registry/com/google/guava/guava/33.1.0-jre/guava-33.1.0-jre.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0 > app.revanced:revanced-patcher:20.0.0 > app.revanced:apktool-lib:2.9.3
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0 > app.revanced:revanced-patcher:20.0.0 > app.revanced:apktool-lib:2.9.3 > app.revanced:brut.j.util:2.9.3
> Could not find org.apache.commons:commons-lang3:3.14.0.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/org/apache/commons/commons-lang3/3.14.0/commons-lang3-3.14.0.pom
    - https://maven.pkg.github.com/revanced/registry/org/apache/commons/commons-lang3/3.14.0/commons-lang3-3.14.0.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0 > app.revanced:revanced-patcher:20.0.0 > app.revanced:apktool-lib:2.9.3
> Could not find commons-io:commons-io:2.15.1.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/commons-io/commons-io/2.15.1/commons-io-2.15.1.pom
    - https://maven.pkg.github.com/revanced/registry/commons-io/commons-io/2.15.1/commons-io-2.15.1.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0 > app.revanced:revanced-patcher:20.0.0 > app.revanced:apktool-lib:2.9.3
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0 > app.revanced:revanced-patcher:20.0.0 > app.revanced:apktool-lib:2.9.3 > app.revanced:brut.j.dir:2.9.3
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0 > app.revanced:revanced-patcher:20.0.0 > app.revanced:apktool-lib:2.9.3 > app.revanced:brut.j.util:2.9.3
> Could not find org.apache.commons:commons-text:1.11.0.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/org/apache/commons/commons-text/1.11.0/commons-text-1.11.0.pom
    - https://maven.pkg.github.com/revanced/registry/org/apache/commons/commons-text/1.11.0/commons-text-1.11.0.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0 > app.revanced:revanced-patcher:20.0.0 > app.revanced:apktool-lib:2.9.3

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.

BUILD FAILED in 3s
19:23:11: Execution finished 'build --scan'.

Why? I believe this block would be called for every project. And every time it is called, I would have to check if the plugin should be applied. Instead I iterate through every project I want to apply the plugin and apply the plugin.

So should I instead of applying the plugin from my patches plugin to the extension plugins, instead create a separate plugin and apply it to each of the extension projects by hand using plugins { ... }?

How would I do that? Right now, I can not set any property in my extension:

extensionConfig<Any> {
    prop = "" // Prop not found
}

If my goal is to apply plugins from a Settings plugin wouldn’t I have this problem with type safe accessors in all my projects such as the patches project as well as the extensions projects?

The separate topics are related to each other, especially if I progress with the current ones and with most sorted out quickly, the need for multiple threads liquidates.

Configure the extension in your plugin after you applied the plugin.

I want to configure the extension in my own extension:

extensionConfig {
  android { ... }
}

and not

extensionConfig { ... }
android { ... }

The extension and its properties are public and inside their own kt:

package app.revanced.patches.gradle

import com.android.build.gradle.internal.dsl.BaseAppModuleExtension
import org.gradle.api.provider.Property

abstract class ExtensionExtension {
    abstract val extensionPath: Property<String>
    abstract val android: Property<BaseAppModuleExtension.() -> Unit>

    init {
        extensionPath.convention(":extensions")
        android.convention { }
    }
}

I want my plugin to configure the android extension for the extension projects. Otherwise I would have to duplicate the same android block in every extension project.

Access it where. In my plugin or in the project. Because I access it in my plugin after I apply the Android project and accessing it in the project defeats my goal with my plugin.

The extension project extension I created is not typed. The Android plugin extension is there, but only if I manually apply the Android plugin to the extension project. If I only apply it from within my plugin, the Android plugin extension does not appear.

And always treat them lazy, meaning reading it optimally only at execution time, never at configuration time.

What does it mean to read them at execution time?
For example, in my settings plugin I have an extension in which you can name the paths to the patches plugin and the directory in which the extension projects are inside. The plugin then reads the extension property and includes the projects at said paths. How would I move this to execution time?

I still stand where I did before. I am unable to merge the settings and patches plugin. The settings plugin cannot apply the patches plugin to the patches project. The extensions are still not typed and even if I manage to apply the plugins from my settings plugin, which I apply to the settings of the root project, I would not have type-safe extensions in my patches or extension projects.

What do you suggest from here on? Should I manually apply plugins to the root settings, the patches, and each extension project?

Edit:

I have now managed to get a bit further with the plugins. I still have 3 plugins I manually need to apply to the settings, the patches and the extension projects.

The plugins configure other extensions such as the extension plugin configuring the Android extension and the patches plugin configuring the publications.

But when I try to build the project I get this exception:

Could not resolve all artifacts for configuration 'classpath'.
> Could not find com.gradle:gradle-enterprise-gradle-plugin:3.16.2.
  Required by:
      unspecified:unspecified:unspecified

Possible solution:
 - Declare repository providing the artifact, see the documentation at https://docs.gradle.org/current/userguide/declaring_repositories.html

Additionally I would like to provide an extension with my extension plugin in which the user can set the Android extension namespace property for example.

Right now the user has to add this block to the extension project:

android {
    namespace = "app.revanced.extension"

    defaultConfig {
        applicationId = "app.revanced.extension"
    }
}

Instead I want:

extensionConfig {
  android {
    namespace = "app.revanced.extension"

    defaultConfig {
        applicationId = "app.revanced.extension"
    }
}

As you can see, I provide a function in my extension to set specific properties. The plugin should then set these properties in the Android extension.

In general, how can I create an extension, where the user sets the properties and then get the property values in my plugin to configure other extensions for example.

Edit2:

I have solved the error:

Could not resolve all artifacts for configuration 'classpath'.
> Could not find com.gradle:gradle-enterprise-gradle-plugin:3.16.2.
  Required by:
      unspecified:unspecified:unspecified

Possible solution:
 - Declare repository providing the artifact, see the documentation at https://docs.gradle.org/current/userguide/declaring_repositories.html

The issue only appears when I run the build task with --scan.
I would like to understand how I would know that given the error message, because I discovered this solution by luck.

So I now have a working build system. But I still have a couple of questions.
I have three plugins. One is applied to settings, one to patches and one to the extensions each. I would like to reduce this down to one plugin, but when I merge the plugin projects, I get the exception:

19:23:07: Executing 'build --scan'...


FAILURE: Build failed with an exception.

* What went wrong:
Could not resolve all artifacts for configuration 'classpath'.
> Could not find com.gradle:gradle-enterprise-gradle-plugin:3.16.2.
  Required by:
      unspecified:unspecified:unspecified
> Could not find org.jetbrains.kotlin:kotlin-reflect:1.9.22.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/org/jetbrains/kotlin/kotlin-reflect/1.9.22/kotlin-reflect-1.9.22.pom
    - https://maven.pkg.github.com/revanced/registry/org/jetbrains/kotlin/kotlin-reflect/1.9.22/kotlin-reflect-1.9.22.pom
  Required by:
      unspecified:unspecified:unspecified
> Could not find com.android.tools.smali:smali:3.0.5.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/com/android/tools/smali/smali/3.0.5/smali-3.0.5.pom
    - https://maven.pkg.github.com/revanced/registry/com/android/tools/smali/smali/3.0.5/smali-3.0.5.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0 > app.revanced:revanced-patcher:20.0.0
> Could not find com.google.guava:guava:33.1.0-jre.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/com/google/guava/guava/33.1.0-jre/guava-33.1.0-jre.pom
    - https://maven.pkg.github.com/revanced/registry/com/google/guava/guava/33.1.0-jre/guava-33.1.0-jre.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0
> Could not find org.jetbrains.kotlinx.binary-compatibility-validator:org.jetbrains.kotlinx.binary-compatibility-validator.gradle.plugin:0.14.0.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/org/jetbrains/kotlinx/binary-compatibility-validator/org.jetbrains.kotlinx.binary-compatibility-validator.gradle.plugin/0.14.0/org.jetbrains.kotlinx.binary-compatibility-validator.gradle.plugin-0.14.0.pom
    - https://maven.pkg.github.com/revanced/registry/org/jetbrains/kotlinx/binary-compatibility-validator/org.jetbrains.kotlinx.binary-compatibility-validator.gradle.plugin/0.14.0/org.jetbrains.kotlinx.binary-compatibility-validator.gradle.plugin-0.14.0.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0
> Could not find org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.0.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/org/jetbrains/kotlin/kotlin-gradle-plugin/2.0.0/kotlin-gradle-plugin-2.0.0.pom
    - https://maven.pkg.github.com/revanced/registry/org/jetbrains/kotlin/kotlin-gradle-plugin/2.0.0/kotlin-gradle-plugin-2.0.0.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0
> Could not find org.jetbrains.kotlin.android:org.jetbrains.kotlin.android.gradle.plugin:2.0.0.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/org/jetbrains/kotlin/android/org.jetbrains.kotlin.android.gradle.plugin/2.0.0/org.jetbrains.kotlin.android.gradle.plugin-2.0.0.pom
    - https://maven.pkg.github.com/revanced/registry/org/jetbrains/kotlin/android/org.jetbrains.kotlin.android.gradle.plugin/2.0.0/org.jetbrains.kotlin.android.gradle.plugin-2.0.0.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0
> Could not find com.android.application:com.android.application.gradle.plugin:8.2.2.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/com/android/application/com.android.application.gradle.plugin/8.2.2/com.android.application.gradle.plugin-8.2.2.pom
    - https://maven.pkg.github.com/revanced/registry/com/android/application/com.android.application.gradle.plugin/8.2.2/com.android.application.gradle.plugin-8.2.2.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0
> Could not find com.android.library:com.android.library.gradle.plugin:8.2.2.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/com/android/library/com.android.library.gradle.plugin/8.2.2/com.android.library.gradle.plugin-8.2.2.pom
    - https://maven.pkg.github.com/revanced/registry/com/android/library/com.android.library.gradle.plugin/8.2.2/com.android.library.gradle.plugin-8.2.2.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0
> Could not find org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.7.3/kotlinx-coroutines-core-1.7.3.pom
    - https://maven.pkg.github.com/revanced/registry/org/jetbrains/kotlinx/kotlinx-coroutines-core/1.7.3/kotlinx-coroutines-core-1.7.3.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0 > app.revanced:revanced-patcher:20.0.0
> Could not find xpp3:xpp3:1.1.4c.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/xpp3/xpp3/1.1.4c/xpp3-1.1.4c.pom
    - https://maven.pkg.github.com/revanced/registry/xpp3/xpp3/1.1.4c/xpp3-1.1.4c.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0 > app.revanced:revanced-patcher:20.0.0
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0 > app.revanced:revanced-patcher:20.0.0 > app.revanced:apktool-lib:2.9.3
> Could not find org.jetbrains.kotlin:kotlin-reflect:1.9.22.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/org/jetbrains/kotlin/kotlin-reflect/1.9.22/kotlin-reflect-1.9.22.pom
    - https://maven.pkg.github.com/revanced/registry/org/jetbrains/kotlin/kotlin-reflect/1.9.22/kotlin-reflect-1.9.22.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0 > app.revanced:revanced-patcher:20.0.0
> Could not find com.android.tools.smali:smali-dexlib2:3.0.3.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/com/android/tools/smali/smali-dexlib2/3.0.3/smali-dexlib2-3.0.3.pom
    - https://maven.pkg.github.com/revanced/registry/com/android/tools/smali/smali-dexlib2/3.0.3/smali-dexlib2-3.0.3.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0 > app.revanced:revanced-patcher:20.0.0 > app.revanced:multidexlib2:3.0.3.r3
> Could not find com.google.guava:guava:33.1.0-jre.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/com/google/guava/guava/33.1.0-jre/guava-33.1.0-jre.pom
    - https://maven.pkg.github.com/revanced/registry/com/google/guava/guava/33.1.0-jre/guava-33.1.0-jre.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0 > app.revanced:revanced-patcher:20.0.0 > app.revanced:multidexlib2:3.0.3.r3
> Could not find com.android.tools.smali:smali-baksmali:3.0.3.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/com/android/tools/smali/smali-baksmali/3.0.3/smali-baksmali-3.0.3.pom
    - https://maven.pkg.github.com/revanced/registry/com/android/tools/smali/smali-baksmali/3.0.3/smali-baksmali-3.0.3.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0 > app.revanced:revanced-patcher:20.0.0 > app.revanced:apktool-lib:2.9.3
> Could not find com.android.tools.smali:smali:3.0.5.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/com/android/tools/smali/smali/3.0.5/smali-3.0.5.pom
    - https://maven.pkg.github.com/revanced/registry/com/android/tools/smali/smali/3.0.5/smali-3.0.5.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0 > app.revanced:revanced-patcher:20.0.0 > app.revanced:apktool-lib:2.9.3
> Could not find com.google.guava:guava:33.1.0-jre.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/com/google/guava/guava/33.1.0-jre/guava-33.1.0-jre.pom
    - https://maven.pkg.github.com/revanced/registry/com/google/guava/guava/33.1.0-jre/guava-33.1.0-jre.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0 > app.revanced:revanced-patcher:20.0.0 > app.revanced:apktool-lib:2.9.3
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0 > app.revanced:revanced-patcher:20.0.0 > app.revanced:apktool-lib:2.9.3 > app.revanced:brut.j.util:2.9.3
> Could not find org.apache.commons:commons-lang3:3.14.0.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/org/apache/commons/commons-lang3/3.14.0/commons-lang3-3.14.0.pom
    - https://maven.pkg.github.com/revanced/registry/org/apache/commons/commons-lang3/3.14.0/commons-lang3-3.14.0.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0 > app.revanced:revanced-patcher:20.0.0 > app.revanced:apktool-lib:2.9.3
> Could not find commons-io:commons-io:2.15.1.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/commons-io/commons-io/2.15.1/commons-io-2.15.1.pom
    - https://maven.pkg.github.com/revanced/registry/commons-io/commons-io/2.15.1/commons-io-2.15.1.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0 > app.revanced:revanced-patcher:20.0.0 > app.revanced:apktool-lib:2.9.3
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0 > app.revanced:revanced-patcher:20.0.0 > app.revanced:apktool-lib:2.9.3 > app.revanced:brut.j.dir:2.9.3
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0 > app.revanced:revanced-patcher:20.0.0 > app.revanced:apktool-lib:2.9.3 > app.revanced:brut.j.util:2.9.3
> Could not find org.apache.commons:commons-text:1.11.0.
  Searched in the following locations:
    - file:/C:/Users/oSumAtrIX/.m2/repository/org/apache/commons/commons-text/1.11.0/commons-text-1.11.0.pom
    - https://maven.pkg.github.com/revanced/registry/org/apache/commons/commons-text/1.11.0/commons-text-1.11.0.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:plugin:1.0.0 > app.revanced:revanced-patcher:20.0.0 > app.revanced:apktool-lib:2.9.3

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.

BUILD FAILED in 3s
19:23:11: Execution finished 'build --scan'.

My next question is, does Gradle support applying plugins in the root project with apply false so it is available quickly in the sub-projects? But now that my plugin applies the plugins, should I still keep it there? If so, I would have to update the dependencies manually in the project again instead of just having the plugin do that for me. If I try to remove the plugin from the root project, I get an error:

Build file 'D:\ReVanced\revanced-patches\extensions\shared\stub\build.gradle.kts' line: 1

Error resolving plugin [id: 'com.android.library', version: '8.2.2']
> The request for this plugin could not be satisfied because the plugin is already on the classpath with an unknown version, so compatibility cannot be checked.

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.
Deprecated Gradle features were used in this build, making it incompatible with Gradle 9.0.
You can use '--warning-mode all' to show the individual deprecation warnings and determine if they come from your own scripts or plugins.
For more on this, please refer to https://docs.gradle.org/8.7/userguide/command_line_interface.html#sec:command_line_warnings in the Gradle documentation.
BUILD FAILED in 4s

If I add back the plugins block to my root project, the issue goes away:

plugins {
    // Extensions
    alias(libs.plugins.android.library) apply false
}

My next question is, that I add dependencies to projects via my plugin. For that I use the Gradle catalog:

    private fun Project.configureDependencies() {
        val catalog = extensions.getByType(VersionCatalogsExtension::class.java).named("libs")
        operator fun String.invoke(versionAlias: String) = dependencies.add(
            "implementation",
            "$this:" + catalog.findVersion(versionAlias).orElseThrow {
                IllegalArgumentException("Version with alias $versionAlias not found in version catalog")
            },
        )

        afterEvaluate {
            "app.revanced:revanced-patcher"("revanced-patcher")
            "com.android.tools.smali:smali"("smali")
        }
    }

The reason I am doing this, is because the user would have to add the dependency block manually. Is this approach fine? The code above also doesn’t work without afterEvaluate.

The next question I had was that I want to configure the Android extension in my own extension like this:

extensionConfig {
  android { ... }
}

Right now, my extension configures the android extension and then the user has to configure the android extension like this:

extensionConfig { ... }
android { ... }

Similarly I want to provide properties to set in my extension block which my plugin would set the JAR manifest properties with. So instead of the user doing:

tasks.withType<Jar> {
    exclude("app/revanced/meta")
    manifest {
        attributes["Name"] = "ReVanced Patches"
        attributes["Description"] = "Patches for ReVanced."
        attributes["Source"] = "git@github.com:revanced/revanced-patches.git"
        attributes["Author"] = "ReVanced"
        attributes["Contact"] = "contact@revanced.app"
        attributes["Origin"] = "https://revanced.app"
        attributes["License"] = "GNU General Public License v3.0"
    }
}

I want them to specify the attributes like this:

patches {
 about {
    name = ""
     ...
  }
}

Similarly, I want to provide a block to configure a publication of the project instead of having the user do it via the publishing block. So, the plugin preconfigured the publication, and then the user could change anything they wanted before it was created and added.

The question is, is this incorrect practice?

but when I merge the plugin projects, I get the exception

Maybe because different repository settings?
Wherever you have it now you have mavenLocal() (always a bad idea) and GitHub Package Repository and in neither the things that are searched for are found.

does Gradle support applying plugins in the root project with apply false so it is available quickly in the sub-projects?

I have no idea what you mean with “available quickly”, but yes, you can do that.
But if it is already in the settings plugin, it is simply unnecessary.
All that would do is adding the plugin to the classpath, but as you already have it applied to the settings script, it is already available through the settings class loader.

But now that my plugin applies the plugins, should I still keep it there?

You should never have it there unless you have a real reason like using a shared build service that wouldn’t work properly otherwise. Without reason it is just visual clutter. And if the settings plugin already brings the plugin to the classloader then even that reason is void as the plugin is then already available through the settings classloader, whether it is applied by the settings plugin or not.

The request for this plugin could not be satisfied because the plugin is already on the classpath with an unknown version, so compatibility cannot be checked.

It complains because you bring the plugin to the classpath via the settings script class loader, and then again try to apply it with version. For the plugin coming through the settings classloader Gradle does not know at that point which version the plugin has and thus cannot check whether you try to apply the same version. Just remove the version and it should work. It is not necessary as you already brought the version you want to the classpath throug the settings classloader.

The reason I am doing this, is because the user would have to add the dependency block manually. Is this approach fine? The code above also doesn’t work without afterEvaluate .

Well, if it needs afterEvaluate, then the approach is probably not fine. afterEvaluate is always evil and its main effect is adding ordering problems, adding timing problems, and adding race conditions. How to do it differently is hardly guessable from the snippets you shared, especially as you did not say in which way it fails without.

Right now, my extension configures the android extension and then the user has to configure the android extension like this:

If you apply the android plugin, the top level android extension is there and your users can configure it, full-stop.
If your extension does not have an extension with name android or a function called android with compatible signature, your users can also configure the top-level android extension within your extension block, it is just unnecessary visual clutter. But if you just want the top-level configuration be configured within your block, just tell them to do that, even if it is bad practice.

Similarly I want to provide properties to set in my extension block which my plugin would set the JAR manifest properties with.

Make about a function in patches that has a parameter Action<AboutInformation>. Then in the function body you create an AboutInformation, give it to the supplied action, and then you can read the values and configure the manifest accordingly.

The question is, is this incorrect practice?

Not really. If you provide some DSL to your consumers that better fits in your domain and then translate that to other configurations, that’s fine, as long as you do it properly and nowhere use afterEvaluate or other discouraged practices.

I made sure to merge the repos, as in, didn’t remove any repositories.

I have maven local for testing my unpublished plugins and maven libraries. I removed it from the plugin, it wasn’t supposed to be there.

I now have removed the patches plugin from my patches project, and instead apply it via the settings plugin using. For that i first added the patches and extension plugins to my settings plugin as a dependency. I then applied the patches plugin to via the settings plugin to my patches project.
I then removed manually adding the plugin to the patches project. Right now I have:

settings.gradle.kts:


pluginManagement {
    repositories {
        gradlePluginPortal()
        google()
        mavenLocal()
        maven {
            // A repository must be specified for some reason. "registry" is a dummy.
            url = uri("https://maven.pkg.github.com/revanced/registry")
            credentials {
                username = providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR")
                password = providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN")
            }
        }
    }
}

@Suppress("UnstableApiUsage")
dependencyResolutionManagement.apply {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)

    repositories.apply {
        mavenCentral()
        mavenLocal()
        google()
        maven {
            url = URI("https://maven.pkg.github.com/revanced/registry")
            credentials.apply {
                username = providers.gradleProperty("gpr.user").orNull ?: System.getenv("GITHUB_ACTOR")
                password = providers.gradleProperty("gpr.key").orNull ?: System.getenv("GITHUB_TOKEN")
            }
        }
    }
}

plugins {
    id("revanced-patches-gradle-settings-plugin") version "1.0.0"
}

The build.gradle.kts of my settings plugin:

dependencies {
    implementation(gradleApi())
    implementation(project(":patches-plugin"))
    implementation(project(":extension-plugin"))
    implementation(project(":patches-plugin"))
}

A configuration in my settings plugin to configure the repositories and plugin repos:

   private fun Settings.configureDependencies() {
        pluginManagement.repositories.apply {
            gradlePluginPortal()
            google()
        }

        @Suppress("UnstableApiUsage")
        dependencyResolutionManagement.apply {
            repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)

            repositories.apply {
                mavenCentral()
                google()
                maven {
                    it.url = URI("https://maven.pkg.github.com/revanced/registry")
                    it.credentials.apply {
                        username = providers.gradleProperty("gpr.user").orNull
                            ?: System.getenv("GITHUB_ACTOR")
                        password = providers.gradleProperty("gpr.key").orNull
                            ?: System.getenv("GITHUB_TOKEN")
                    }
                }
            }
        }
    }

And a configuration to apply the plugins to the patches and extensions:

    private fun Settings.configurePlugins(extension: SettingsExtension) {
        gradle.rootProject {
            it.project(extension.patchesProjectPath.get()).pluginManager.apply(PatchesPlugin::class.java)

            it.project(extension.extensionsProjectPath.get()).let { extensionsProject ->
                extensionsProject.subprojects { extensionProject ->
                    if (extensionProject.parent != extensionsProject) return@subprojects

                    extensionProject.pluginManager.apply(ExtensionPlugin::class.java)
                }
            }
        }
    }

One of my extension projects also depends on another compile only project. This project uses the Android library plugin. It is not an extension project but instead a project that an extension project depends on. It currently has this in build.gradle.kts:

plugins {
    id(libs.plugins.android.library.get().pluginId)
}

And the project builds.
If I use:

plugins {
    alias(libs.plugins.android.library)
}

I get:

Error resolving plugin [id: 'com.android.library', version: '8.2.2']
> The request for this plugin could not be satisfied because the plugin is already on the classpath with an unknown version, so compatibility cannot be checked.

I have checked my plugins as well as my projects and I do not add the library plugin anywhere as a dependency or similar. The only place I can see a reference to this plugin is in my projects Gradle catalog as well as in the plugins block above.

As you have seen my settings plugins adds plugin repositories. But when I remove these two blocks from my settings.gradle.kts, because my plugin is supposed to already have done it, I get this error:

Could not resolve all artifacts for configuration 'classpath'.
> Could not find org.jetbrains.kotlin:kotlin-reflect:1.9.22.
  Searched in the following locations:
    - https://dl.google.com/dl/android/maven2/org/jetbrains/kotlin/kotlin-reflect/1.9.22/kotlin-reflect-1.9.22.pom
    - file:/C:/Users/oSumAtrIX/.m2/repository/org/jetbrains/kotlin/kotlin-reflect/1.9.22/kotlin-reflect-1.9.22.pom
  Required by:
      unspecified:unspecified:unspecified
> Could not find org.jetbrains.kotlinx.binary-compatibility-validator:org.jetbrains.kotlinx.binary-compatibility-validator.gradle.plugin:0.14.0.
  Searched in the following locations:
    - https://dl.google.com/dl/android/maven2/org/jetbrains/kotlinx/binary-compatibility-validator/org.jetbrains.kotlinx.binary-compatibility-validator.gradle.plugin/0.14.0/org.jetbrains.kotlinx.binary-compatibility-validator.gradle.plugin-0.14.0.pom
    - file:/C:/Users/oSumAtrIX/.m2/repository/org/jetbrains/kotlinx/binary-compatibility-validator/org.jetbrains.kotlinx.binary-compatibility-validator.gradle.plugin/0.14.0/org.jetbrains.kotlinx.binary-compatibility-validator.gradle.plugin-0.14.0.pom
  Required by:
      unspecified:unspecified:unspecified > revanced-patches-gradle-settings-plugin:revanced-patches-gradle-settings-plugin.gradle.plugin:1.0.0 > app.revanced:settings-plugin:1.0.0 > app.revanced:patches-plugin:1.0.0

Possible solution:
 - Declare repository providing the artifact, see the documentation at https://docs.gradle.org/current/userguide/declaring_repositories.html

I truncated it. The error disappears if I add back:

pluginManagement {
    repositories {
        gradlePluginPortal()
        google()
    }
}

But my plugin is adding these two repositories:

    private fun Settings.configureDependencies() {
        pluginManagement.repositories.apply {
            gradlePluginPortal()
            google()
        }

So far I have been using afterEvaluate here:

    private fun Project.configureDependencies() {
        afterEvaluate {
            val catalog = extensions.getByType(VersionCatalogsExtension::class.java).named("libs")

            operator fun String.invoke(versionAlias: String) = dependencies.add(
                "implementation",
                "$this:" + catalog.findVersion(versionAlias).orElseThrow {
                    IllegalArgumentException("Version with alias $versionAlias not found in version catalog")
                },
            )

            "app.revanced:revanced-patcher"("revanced-patcher")
            "com.android.tools.smali:smali"("smali")
        }
    }
 private fun Project.configureArtifactSharing(extension: ExtensionExtension) {
        afterEvaluate {
            val extensionPath = Path(extension.name.get())

            val syncExtensionTask = tasks.register("syncExtension", Sync::class.java) {
                it.apply {
                    dependsOn("minifyReleaseWithR8")

                    from(layout.buildDirectory.file("intermediates/dex/release/minifyReleaseWithR8/classes.dex"))
                    into(layout.buildDirectory.dir("revanced/${extensionPath.parent.pathString}"))

                    rename("classes.dex", extensionPath.fileName.toString())
                }
            }

            configurations.consumable("extensionConfiguration").also { configuration ->
                artifacts.add(
                    configuration.name,
                    layout.buildDirectory.dir("revanced"),
                ) { artifact -> artifact.builtBy(syncExtensionTask) }
            }
        }
    }
 private fun Project.configureSigning() {
        pluginManager.apply("signing")

        afterEvaluate {
            extensions.configure<SigningExtension>("signing") {
                it.useGpgCmd()
                it.sign(
                    extensions.getByType(PublishingExtension::class.java)
                        .publications.getByName("revanced-patches-publication"),
                )
            }
        }
    }
afterEvaluate {
            val about = patchesExtension.about
            extensions.configure(PublishingExtension::class.java) { extension ->
                extension.publications { container ->
                    container.create("revanced-patches-publication", MavenPublication::class.java) {
                        it.from(components["java"])

                        it.pom { pom ->
                            pom.name.set(about.name)
                            pom.description.set(about.description)
                            pom.url.set(about.website)

                            pom.licenses { licenses ->
                                licenses.license { license ->
                                    license.name.set(about.license)
                                }
                            }
                            pom.developers { developers ->
                                developers.developer { developer ->
                                    developer.name.set(about.author)
                                    developer.email.set(about.contact)
                                }
                            }
                            pom.scm { scm ->
                                scm.url.set(about.source)
                            }
                        }
                    }
                }
            }
        }

and

 private fun Project.configureDependencies() {
        afterEvaluate {
            val catalog = extensions.getByType(VersionCatalogsExtension::class.java).named("libs")

            operator fun String.invoke(versionAlias: String) = dependencies.add(
                "implementation",
                "$this:" + catalog.findVersion(versionAlias).orElseThrow {
                    IllegalArgumentException("Version with alias $versionAlias not found in version catalog")
                },
            )

            "app.revanced:revanced-patcher"("revanced-patcher")
            "com.android.tools.smali:smali"("smali")
        }
    }

Do you have any ideas how I can no rely on afterEvaluate to make all these work?

Right now I also have all these three separate plugins. One settings, one patches and one extension plugin which are applied separately to each project by my settings plugin. I had initially planned to have one settings plugin and not three plugins. Should I still merge them or is doing it the way I have it right now a better approach?

I made sure to merge the repos, as in, didn’t remove any repositories.

Well, the error says you only have mavenLocal and GHA repositories (as shown after “Searched in the following locations:”) and there those dependencies are not found.

The build.gradle.kts of my settings plugin:

implementation(project(":patches-plugin")) twice?

I have checked my plugins as well as my projects and I do not add the library plugin anywhere as a dependency or similar.

Maybe not the library plugin, but any other plugin from the Android Gradle Plugin suite. Each of the plugins adds all of the plugins.

But my plugin is adding these two repositories:

What your plugin adds is irrelevant as it is too late.
If you have these things as dependencies of your settings plugin, then you need to have the repositories where you can get them in the consumer settings script of that settings plugin.
Because to run the settings plugin, Gradle first needs to get the settings plugin and all its dependencies. So the repositories where to get these dependencies have to be declared before the settings plugin runs, or it cannot be fully resolved and thus not run.

For project plugins this is differently, because the time the project plugins are resolved, the settings plugin already executed and had the chance to add those additional plugin repositories.

That’s a chicken-and-egg kind of problem.

Do you have any ideas how I can no rely on afterEvaluate to make all these work?

Well, there are two problems here:

  1. I’m not going to do your work for you :wink:
  2. I can hardly guess from just these snippets why you at all think afterEvaluate would be necessary.
    If I would know why it is there and have the full code at hand, I could maybe provide alternatives, but I’m really too busy to spoon-feed, sorry.

Should I still merge them or is doing it the way I have it right now a better approach?

I have too less information to make a statement on this.
Depending on the gory details, both could be fine.
But - as I said - doing subproject { ... } or other kinds of cross-project configuration is a big no-go.

Is there any way to see by whom the plugin was added? Note: If I remove the plugin from the plugin block, the plugin will not be applied. I specifically have to apply it without a version. So I am doubting a plugin is applying it.

To make sure I understood this. The settings plugin itself declares repositories to get the dependencies from in its own settings.gradle.kts file. So when I use that plugin in my project, I would need to add the repository which has that plugin. Then the plugin would use its declared repositories to fetch its own dependencies or do I have to declare the repositories again in my project where I use the plugin?

Assuming this, lets say the plugin depends on some dependency which is obtained from some unknown but public maven repository. Would I as the user of the plugin have to add that unknown repo? How would I know without the plugin documenting it? I thought Gradle would transitively use the plugin repositories so I do not have to redeclare them in my project.

If this is the case, then that’s exactly what I also have now. The plugin itself declares repositories to fetch dependencies from. The plugin compiles just fine that way. I publish it to maven local for now, so I add mavenLocal to my projects plugin repos. Now that I have done that, Gradle would transitively lookup the plugins repositories to fetch its dependencies. Using this setup, I was getting the error I mentioned above. I had to explicitly redeclare the repositories for dependencies the plugin was using, and not just the repository where I get the plugin from. I have also not seen any other plugin which had the requirement to add repositories other than the one to get the plugin from. You mentioned that this behavior is exclusive to settings plugins though, but I would assume this transitive behaviour for them as well, (that is Gradle to lookup the plugins repos for the plugins dependencies).

I am trying my best to understand solutions you give me. But I would also like to understand them. So if you for example tell me that an afterEvaluate is discouraged because of side effects, I would have to understand how in my situation these side effects apply or not. Given that with afterEvaluate what I have works and without it it doesn’t, and I do not have any other solution at hand, I am unable to do anything but reply with a question. This is not a call for you to do my work, but rather a follow up question on what you had said before. In this particular case, I was assuming there was a specific correct alternative I could use and not a case-by-case scenario. Assuming it is, can you link me to somewhere, where I could understand which cases need what solution, because so far I do not know how to find an alternative for my afterEvaluate approach.

I was providing snippets about everything I assumed was relevant to have an insight into the issue. Because everything else I did not include in the snippets was either unrelated or something I assumed was unrelated.

Let’s take one of my usages of afterEvaluate:


    private fun Project.configureDependencies() {
        afterEvaluate {
            val catalog = extensions.getByType(VersionCatalogsExtension::class.java).named("libs")

            operator fun String.invoke(versionAlias: String) = dependencies.add(
                "implementation",
                "$this:" + catalog.findVersion(versionAlias).orElseThrow {
                    IllegalArgumentException("Version with alias $versionAlias not found in version catalog")
                },
            )

            "app.revanced:revanced-patcher"("revanced-patcher")
            "com.android.tools.smali:smali"("smali")
        }
    }

This snippet adds the two dependencies to my project. I want my project to give control in choosing the version of the dependencies so that I do not have to update the plugin all the time. Since Gradle catalogs were the modern approach to dependencies I wanted to keep them in my projects which is why my plugin uses the catalog of the project to get the version. Now, this is the same snippet I provided in my previous post. I am unaware of any other missing context. Without afterEvaluate VersionCatalogsExtension can not be used. I would love to be able to find a solution to

Do you have any ideas how I can no rely on afterEvaluate to make all these work?

Do you have any ideas how I can find a way not to rely on afterEvaluate given this example usage of afterEvaluate? What is the reason afterEvaluate is improperly used in this specific example?

With the current approach, I release three plugins. Each of the plugins always sets up the project it is applied to with the exception of the settings plugin applying the other two plugins to subprojects. You have mentioned that settings plugins work a bit different than project plugins, for example in terms of dependencies like replied to above. So me getting errors from merging project plugins into settings plugins, maybe emerges from that fact? Having three separate plugins also does not make sense semantically. Theres no scenario where you would only need one of these plugins, so that’s another reason why it wouldn’t make sense to have three separate plugins.

Previously you suggested to use allprojects {... }. Isn’t that a cross-project configuration? Why is allprojects fine, but not subprojects? I assume you are referring to the situation where type-safety is lost. So this is counter argument in merging the plugins into one settings plugins. So at hand I have the both, reasons not to and reasons to merge. Losing types is a problem though so unless there’s a solution for that, I can not merge the plugins, for semantical reasons.

The project I am working on is not yet open source, so I can give you access to it in a private manner if I am not giving enough context. Alternatively, I can also send snippets here if you can tell me what you need.

Is there any way to see by whom the plugin was added?

Sure, look at a build --scan, use the dependencies task, or use the dependencyInsights task.

Note: If I remove the plugin from the plugin block, the plugin will not be applied.

Of course.

I specifically have to apply it without a version.

Sure

So I am doubting a plugin is applying it.

I never suggested any is.
I said it is brought to the classpath / class loader already.
Again, for example in the Android Gradle Plugin suite are various plugins, for example the Android application plugin and the Android library plugin. If your settings plugin for example depends on the Android application plugin, the Android library plugin is also on the classpath already. Because both live in the same code artifact and you just have sepearate plugin marker artifacts to resolve plugin id to some coordinates by naming convention.

or do I have to declare the repositories again in my project where I use the plugin?

Yes. Repositories are never “forwarded”. A consuming build always have to have the control over where things it uses are coming from. Otherwise it would for example be hard to follow company guidelines like “you must only use internal mirrors that are protected against fraud and audited”. If the repository declarations of plugins you use would be used, this would be impossible to do. A given build always have the say where things are resolved from, so it has to declare all necessary repositories accordingly.

Assuming this, lets say the plugin depends on some dependency which is obtained from some unknown but public maven repository. Would I as the user of the plugin have to add that unknown repo?

Yes.

How would I know without the plugin documenting it?

You don’t, except by error message “cannot resolve X” and then Googling.
Optimally, a plugin should not depend on something that is not found on plugin portal or maven central, resp. within the same repository where it lives itself.

I thought Gradle would transitively use the plugin repositories so I do not have to redeclare them in my project.

Nope, that would maybe be convenient, but a security nightmare. This will most probably never ever happen. Gradle is not Maven. :slight_smile:

You mentioned that this behavior is exclusive to settings plugins though

No, what we now just talked about is the same, whether it is a settings plugin or a project plugin or an init plugin doesn’t matter.

What I understood was, that your settings plugin in its apply adds the plugin repositories where its dependencies could be resolved from and you expected that to be taken into account. And that would work for a project plugin, but not for the settings plugin itself (or any other sibling settings plugin).

I would have to understand how in my situation these side effects apply or not.

They always apply and sooner or later bite you hard in the a**.
As I said, the main effect for doing something not properly but using afterEvaluate is to earn ordering problem, timing problems, and race conditions. It makes your code brittle and flaky and very error-prone and you should avoid it wherever you can at almost any cost.

For example, if you register an extension with properties and then want to immediately read those properties to do something that needs to be done at configuration time, they will not be set yet, as the consumer of the plugin did not have the chance to configure those values yet. Now you could be tempted to use afterEvaluate { ... } and read the values there as then the consumer build had the chance to set the values, and in a trivial case this might work. But just that it had a chance to set the values, does not mean it did it already. It could itself use afterEvaluate { ... } for some reason and be it just brainless StackOverflow-copy-paste and as it used afterEvaluate after you used it, it will be executed after yours and so you again miss the values that should have been configured. Or you get some configured value in afterEvaluate that were already set, but some other afterEvaluate executed later again changes the configured values, so you react to some intermediate value, but not the final one, …

Generally, use *Property, Provider, ConfigureableFileCollection, and so on everywhere, wire properties together at configuration time instead of reading their value at configuration time and only read the values at execution time when no further configuration can happen. And if you need some value at configuration time, do not make them properties, neither the typed ones nor the primitive ones, but instead have functions for example in your extensions that do the configuration with the values given as arguments to parameters.

I am unaware of any other missing context.

The missing context is why you actually think you need afterEvaluate.
Version catalogs are readily available, you do not need afterEvaluate to access them.
If you see different behavior, please provide an MCVE that demonstrates the case.

What is the reason afterEvaluate is improperly used in this specific example?

Because afterEvaluate is practically always improperly used, because using afterEvaluate itself is the improper action that should be avoided at almost any cost.
There are some rare situations where you cannot get around it, mainly if something else uses afterEvaluate and you have to cooperate with that. For example if you need to set some properties on tasks registered by IntelliJ IDEA automatically to execute a main() class. IJ uses afterEvaluate unfortunately to register those and set some values and to override those values, you have to use afterEvaluate yourself or the IJ afterEvaluate would overwrite your values, …
But those should be very rare exceptions and not the rule nowadays.

Previously you suggested to use allprojects {... }

From project, yes.
If you do allprojects { ... } on a project you are configuring other projects from that project which is as bad as subprojects { ... }.
But that is not what I suggested.
I said gradle.allprojects { ... } in the settings script / settings plugin. There I think it should be ok or at least acceptible.

Losing types is a problem though so unless there’s a solution for that, I can not merge the plugins, for semantical reasons.

You are not loosing any types.
You might loose the type-safe accessors.
So you cannot do android { ... } if not a plugin in the plugins { ... } block of that same build script added this extension directly or transitively.
But that is not different in your current solution either.
You can still configure by type like configure<WhateverTheTypeOfAndroidIs> { ... } for example.
Or you could provide an accessor like the generated one in your plugin as code, but then it is always available and not only in the projects where the extension actually is present.

Well, tradeoffs for doing bad-practice, which is not applying a plugin the build script itself but injecting it from outside.

I have looked at the build scan and either I am looking at the wrong place or no other plugin applies it (as expected).

The dependencies task returns this ( trimmed the output):

debugAndroidTestCompileClasspath - Resolved configuration for compilation for variant: debugAndroidTest
\--- project :extensions:shared:stub (*)

debugAndroidTestRuntimeClasspath - Resolved configuration for runtime for variant: debugAndroidTest
\--- project :extensions:shared:stub (*)

debugUnitTestCompileClasspath - Resolved configuration for compilation for variant: debugUnitTest
\--- project :extensions:shared:stub (*)

debugUnitTestRuntimeClasspath - Resolved configuration for runtime for variant: debugUnitTest
\--- project :extensions:shared:stub (*)

releaseUnitTestCompileClasspath - Resolved configuration for compilation for variant: releaseUnitTest
\--- project :extensions:shared:stub (*)

releaseUnitTestRuntimeClasspath - Resolved configuration for runtime for variant: releaseUnitTest
\--- project :extensions:shared:stub (*)

Once again, I can not see any plugin which adds it.

I tried the dependencyInsight task but I do not know what a configuration is or which dependency or configuration to enter when I get this error:

Execution failed for task ':extensions:shared:stub:dependencyInsight'.
> Dependency insight report cannot be generated because the input configuration was not specified. 
  It can be specified from the command line, e.g: ':extensions:shared:stub:dependencyInsight --configuration someConf --dependency someDep'

I need the Android plugin for one subproject and the library plugin for another. I need to independently be able to apply the plugin with a specific version. In my settings plugin I apply the android plugin to one of the subprojects, but I do not want to be limited to use the library plugin already in classpath, if it really is caused by that.

Without the settings plugin I would be able to independently add the application plugin to one project and the library plugin to another, any version I want. The settings plugin is there to reduce boilerplate in projects where the Android application plugin is applied so it applies the plugin for those projects and configures them.

Part of the reason the settings plugin exists is to setup repositories in the multi-module project. I want to prevent having users of the settings plugin to declare all the dependencies the plugin itself also needs. If that is not possible everyone that uses the settings plugin has to declare the repositories and if they need to declare the repositories there’s no point of the settings plugin to use in the first place. So what can I do to prevent users from having to declare the repositories all the time? When I update my settings plugin as well and it starts to depend on a dependency from a new repository, bumping that plugin would break builds.

So why was I never asked to declare repositories apart from the ones to get the certain plugins from? These plugins surely depended on other plugins which came from other repos I did not add myself.

In the example

 private fun Project.configureSigning() {
        pluginManager.apply("signing")

        extensions.configure<SigningExtension>("signing") {
            it.useGpgCmd()
            it.sign(
                extensions.getByType(PublishingExtension::class.java)
                    .publications.getByName("revanced-patches-publication"),
            )
        }
    }

How would I do that for the publication? The publication does not exist yet when the plugin is configured, without an afterEvaluate i can not get the publication.

In the other example I do use get() on the providers but how would else would I do it?

private fun Project.configureArtifactSharing(extension: ExtensionExtension) {
        afterEvaluate {
            val extensionPath = Path(extension.name.get())

            val syncExtensionTask = tasks.register("syncExtension", Sync::class.java) {
                it.apply {
                    dependsOn("minifyReleaseWithR8")

                    from(layout.buildDirectory.file("intermediates/dex/release/minifyReleaseWithR8/classes.dex"))
                    into(layout.buildDirectory.dir("revanced/${extensionPath.parent.pathString}"))

                    rename("classes.dex", extensionPath.fileName.toString())
                }
            }

            configurations.consumable("extensionConfiguration").also { configuration ->
                artifacts.add(
                    configuration.name,
                    layout.buildDirectory.dir("revanced"),
                ) { artifact -> artifact.builtBy(syncExtensionTask) }
            }
        }
    }

I get the error

* What went wrong:
An exception occurred applying plugin request [id: 'patches.extension.gradle.plugin']
> Failed to apply plugin 'patches.extension.gradle.plugin'.
   > Cannot query the value of extension 'extension' property 'name' because it has no value available.

like you described.

Similarly in this example I was previously using get() because I was assuming the values needed to be strings. The value is of type object but I would assume it would convert it to a string eventually. I removed get() and it still worked without an “afterEvaluate”:

private fun Project.configureJarTask(patchesExtension: PatchesExtension) {
        tasks.withType(Jar::class.java).configureEach {
            with(patchesExtension.about) {
                it.manifest.apply {
                    attributes["Name"] = name
                    attributes["Description"] = description
                    attributes["Version"] = version
                    attributes["Timestamp"] = timestamp
                    attributes["Source"] = source
                    attributes["Author"] = author
                    attributes["Contact"] = contact
                    attributes["Website"] = website
                    attributes["License"] = license
                }
            }
            it.archiveExtension.set("rvp")
        }
    }

I don’t know if this is how the properties are intended to be used. Imagine the case where the values would be strictly strings. How would I deal with that here?

I’d like to provide an MCVE but the project is still closed source so I can not publish it easily.
I can provide some more details though:

 private fun Project.configureDependencies() {
        val catalog = extensions.getByType(VersionCatalogsExtension::class.java).named("libs")

        operator fun String.invoke(versionAlias: String) = dependencies.add(
            "implementation",
            "$this:" + catalog.findVersion(versionAlias).orElseThrow {
                IllegalArgumentException("Version with alias $versionAlias not found in version catalog")
            },
        )

        "app.revanced:revanced-patcher"("revanced-patcher")
        "com.android.tools.smali:smali"("smali")
    }

Without wrapping that codeblock into afterEvaluate I get the error:

An exception occurred applying plugin request [id: 'patches.gradle.plugin']
> Failed to apply plugin 'patches.gradle.plugin'.
   > Configuration with name 'implementation' not found.

I have looked up references to that extension on GitHub as well and others were also using afterEvaluate:

I want to only configure projects under a specific project. I would intuitively use thatProject.subprojects and filter for the parent == thatProject like this:

    private fun Settings.configurePlugins(extension: SettingsExtension) {
        gradle.rootProject {
            it.project(extension.patchesProjectPath.get()).pluginManager.apply(PatchesPlugin::class.java)

            it.project(extension.extensionsProjectPath.get()).let { extensionsProject ->
                extensionsProject.subprojects { extensionProject ->
                    if (extensionProject.parent != extensionsProject) return@subprojects

                    extensionProject.pluginManager.apply(ExtensionPlugin::class.java)
                }
            }
        }
    }

I don’t see why I would use gradle.allprojects here instead because allprojects would return projects that are certainly not subprojects to thatProject, whereas allproject returns all projects that also thatProject.subProject returns. So given that thatProject.subProjects is a subset of gradle.allprojects why is the latter suggested over the former?

(Btw, here I also use get() on extension properties, but how else would I be able to use the provider?)

The reason I am doing it from the outside is because otherwise, one would have to apply the plugin every time they create a new subproject. Instead I was imagining that I can setup a specific directory in which projects would be automatically configured. You init a project there, the root project sets it up and you ready to configure and work on it. I am not sure why that is considered bad. To me this sounds like a convenient idea.

With the following in my settings plugin:

dependencies {
    implementation(gradleApi())
    implementation(project(":patches-plugin"))
    implementation(project(":extension-plugin"))
}

as well as

 private fun Settings.configurePlugins(extension: SettingsExtension) {
        gradle.rootProject {
            it.project(extension.patchesProjectPath.get()).pluginManager.apply(PatchesPlugin::class.java)

            it.project(extension.extensionsProjectPath.get()).let { extensionsProject ->
                extensionsProject.subprojects { extensionProject ->
                    if (extensionProject.parent != extensionsProject) return@subprojects

                    extensionProject.pluginManager.apply(ExtensionPlugin::class.java)
                }
            }
        }
    }

I am able to use type-safe accessors in my patches and extension projects without needing to add the plugins manually. So the approach I had works fine. The issue starts to occur only once I merge the plugins. As you can see they are currently separate projects:

dependencies {
implementation(gradleApi())
implementation(project(“:patches-plugin”))
implementation(project(“:extension-plugin”))
}

But given that now I have only one settings plugin I need to apply to the root project, I would like to merge the other two plugins which is where the issue arises.

The other issue is that the settings plugin is well , a settings plugin whereas the other plugins are project plugins. They are slightly different, so would that still work merging them all to a settings plugin?

I have looked at the build scan and either I am looking at the wrong place or no other plugin applies it (as expected).

Scan of the plugin build, not the consumer.
Same for dependencies. Doesn’t make any sense at all to look at the dependencies task of the consumer if you are interested in the build logic.

Once again, I can not see any plugin which adds it.

You will not see any build logic at all, you are looking at your consumer’s build production dependencies.

I tried the dependencyInsight task but I do not know what a configuration is or which dependency or configuration to enter when I get this error:

There is pretty much documentation that explains it, for example Dependency Management Terminology for a short definition and further links. Did you actually try to research this, or are you just asking along? I’m not an interactive manual, you know? :wink:

I need the Android plugin for one subproject and the library plugin for another. I need to independently be able to apply the plugin with a specific version.

Well, as I said, you cannot. There is not “the Android plugin” and “the library plugin”. Even if you meant to say “the Android application plugin” and the “the Android library plugin”. There is only one bunch of android plugins call “the Android Gradle plugin” which comes with the “Android application” plugin, the “Android library” plugin, …, you cannot have either independently, this is simply impossible. But I’m not an Android developer, if you need more in-depth help with that AGP mess, I’m not the one able to help.

Without the settings plugin I would be able to independently add the application plugin to one project and the library plugin to another, any version I want.

If you say so, I’m not Android developer enough to know that.
But as I said, if your settings plugin depends on the Android Gradle plugin, it is in the settings classpath and thus in a parent class loader that is in the ancestry of all build scripts and you will not be able to use different AGP version in different subprojects. I guess this is anyway a bad idea, but you have to clarify that with someone savvy in Android quirks.

If you really want different AGP versions, you probably have to just have it as compileOnly dependency in your settings plugin and reacting to the respective plugins being applied instead of applying them, using pluginManager.withPlugin(...) { ... } and making sure you are compatible with all version you need to be compatible with.

So what can I do to prevent users from having to declare the repositories all the time?

Nothing good.
Maybe do not depend on dependencies from strange repositories.
If it is for some corporate in-house thing, I’d say put up a NXRM, not having one is very bad idea anyway, that could mirror / proxy all the other repositiories, so you only need one to get them all.
If it is for something openly available, you cannot do much in a good way.
The repositories needed to resolve settings plugins and their dependencies have to be defined by the consuming build, full-stop.
But most typically you should only use dependencies available in Gradle Plugin Portal or Maven Central and by that it just works out-of-the-box.

If you really need dependencies from non-canonical repositories, either convince their maintainers to publish to the canonical repositories, find an alternative to use, provide a proxy to all repositories you need to your consumers, or have your consumers declare those repositories.

So why was I never asked to declare repositories apart from the ones to get the certain plugins from? These plugins surely depended on other plugins which came from other repos I did not add myself.

Unliekly, but my crystal ball is broken. If you want to talk about any concrete one, name them. Everything else would just be speculation. Most probably those plugins only depend on things found in the same repository or in any other you have declared.

How would I do that for the publication?

 private fun Project.configureSigning() {
        apply("signing")

        extensions.configure<SigningExtension>("signing") {
            it.useGpgCmd()
            extensions.getByType(PublishingExtension::class.java).publications.named { it == "revanced-patches-publication" }.configureEach(it::sign)
        }
    }

Generally for domain object collections, if the entry you want might not exist yet, react to it being added in the past or future by properly using lazy reactive methods.

without an afterEvaluate i can not get the publication.

Yes you can.
And by using afterEvaluate you might get it, but you also might not get it.
Or you might get it today but not tomorrow.
Again, the main effect earned with using afterEvaluate is timing problems, ordering problems, and race conditions.

In the other example I do use get() on the providers but how would else would I do it?

Just do not use the property value during configuration time:

private fun Project.configureArtifactSharing(extension: ExtensionExtension) {
    val syncExtensionTask = tasks.register("syncExtension", Sync::class.java) {
        it.apply {
            dependsOn("minifyReleaseWithR8")

            from(layout.buildDirectory.file("intermediates/dex/release/minifyReleaseWithR8/classes.dex"))
            into(layout.buildDirectory.zip(extension.name) { buildDirectory, extensionName -> buildDirectory.dir("revanced/${Path(extensionName).parent.pathString}") })

            rename { "${Path(extension.name.get()).fileName}" }
        }
    }

    configurations.consumable("extensionConfiguration").also { configuration ->
        artifacts.add(
            configuration.name,
            layout.buildDirectory.dir("revanced"),
        ) { artifact -> artifact.builtBy(syncExtensionTask) }
    }
}

Alternatively, especially if you really need the value at configuration time - unlike this example - as I said, do not have a property in the extension, but instead have a function configureArtifactSharing(extensionName: String) in your ExtensionExtension and do the necessary configuration in its body.

I removed get() and it still worked without an

What are those fields in About?
If they are Property<String> it will technically work, but not have the result you might expect.
Those manifest attributes are following the old lazyfying approach of calling toString() late.
Hopefully with Gradle 9 and the Propertyfication this changes.
Assuming those fields are Property<String>, you would in this case probably do something like

private fun Project.configureJarTask(patchesExtension: PatchesExtension) {
    tasks.withType(Jar::class.java).configureEach {
        with(patchesExtension.about) {
            it.manifest.apply {
                attributes["Name"] = object { override fun toString() = name.get() }
                attributes["Description"] = object { override fun toString() = description.get() }
                attributes["Version"] = object { override fun toString() = version.get() }
                attributes["Timestamp"] = object { override fun toString() = timestamp.get() }
                attributes["Source"] = object { override fun toString() = source.get() }
                attributes["Author"] = object { override fun toString() = author.get() }
                attributes["Contact"] = object { override fun toString() = contact.get() }
                attributes["Website"] = object { override fun toString() = website.get() }
                attributes["License"] = object { override fun toString() = license.get() }
            }
        }
        it.archiveExtension.set("rvp")
    }
}

Imagine the case where the values would be strictly strings. How would I deal with that here?

As you have seen, highly depends on the actual case.
But in what you indicate, probably again the “use a method, not properties” approach.
Or actually in this specific case you could actually let those values be Property if you like for consistency but have PatchesExtension#about be a method that takes an Action<About>, give an instance to the supplied action and then configure the values from the set values.
But as I said, how to best do it depends from case to case.

I’d like to provide an MCVE but the project is still closed source

Sharing a big fat project would not meet the M of MCVE anyway.

Without wrapping that codeblock into afterEvaluate I get the error:

Ah, see, there is the context you left out.
It does not complain about anything with the version catalog, the version catalog is available just fine.
It complains that implementation configuration is not found.
This happens because you call this before any plugin like java or java-library or whatever is applied that adds this configuration.
So you try to use something before it exists.

So here the solution is simply to react to the plugin that adds this configuration being applied, for example like

 private fun Project.configureDependencies() {
    pluginManager.withPlugin("java") {
        val catalog = extensions.getByType(VersionCatalogsExtension::class.java).named("libs")

        operator fun String.invoke(versionAlias: String) = dependencies.add(
            "implementation",
            "$this:" + catalog.findVersion(versionAlias).orElseThrow {
                IllegalArgumentException("Version with alias $versionAlias not found in version catalog")
            },
        )

        "app.revanced:revanced-patcher"("revanced-patcher")
        "com.android.tools.smali:smali"("smali")
    }
  }
}

so that this configuration is done as soon as that plugin was applied and did add the configuration where you try to add dependencies to.

others were also using afterEvaluate

And if those people are jumping from a skyscraper, you jump along?
Don’t do stupid things, just because others do stupid things, especially if you are asking experts in physics and health that tell you “do not jump from skyscrapers or you will hurt yourself”.

I would intuitively use thatProject.subprojects and filter for the parent == thatProject like this:

And your intuition leads you back to the roof of the skyscraper as I already told you multiple times. How often do I have to repeat it? Do not use that!
Besides that it is non-sense to get all subprojects and then filter for the parent when you could as well just get the child projects.
But that would not be any better.
Do not do that!

So given that thatProject.subProjects is a subset of gradle.allprojects why is the latter suggested over the former?

I already told you multiple times, that it is evil and bad to configure one project from another project.
And that is exactly what you do with things like allprojects { ... }, subproject { ... }, childProjects.values.forEach { ... }, project("...") { ... } and similar approaches from the context of configuring one project. Using gradle.allprojects { ... } from a settings script is not configuring one project from the scope of another, but configuring projects from the settings script which is also questionable, but at least better than cross-project configuration which is a big fat no-go as I explained already multiple times.

Btw, here I also use get() on extension properties, but how else would I be able to use the provider?

Here this is fine, because the configuration phase “is” the execution phase from the PoV of the settings phase. When the content of rootProject { ... } is evaluated, the settings script evaluation is already finalized and cannot be changed any further, so it is ok to query the value by then.

The reason I am doing it from the outside is because otherwise, one would have to apply the plugin every time they create a new subproject.

Yes, and that is exactly the idiomatic, and highly recommended way that makes builds much more readable, much more understandable, and much more maintainable.
Adding one line is not really that much boilerplate, I think people will survive that.
If it really is too much for them to type, then provide a task that creates the build script for a new subproject, so that it is generated automatically for them by calling a task like ./gradlew newExtension --name foo that sets up the structure and the build script with that one line (ok, 3 lines if you format it nicely).

Instead I was imagining that I can setup a specific directory in which projects would be automatically configured.

Yes, I fully understood your intention long ago. Doesn’t change that it is highly discouraged bad practice and that you have to live with some drawbacks for doing so like not having the type-safe accessors generated for example.
Do whatever works best for you, sprinkle afterEvaluates everywhere where it sometimes makes things work, do heavy cross configuration, do whatever you like, but you have to live with the consequences. But then please just decide to do so and do not ask the same over and over, ignoring my responses and explanations, requiring me to repeat over and over in desperation to help you when you don’t really want it but always say “why it works how I do it” when I multiple times say that it does not and at best is flaky.

I am able to use type-safe accessors in my patches and extension projects without needing to add the plugins manually.

Ah, ok, was not aware that injecting from the settings script into the projects generates accessors. It was always said that only the extensions added by plugins within the plugins { ... } block within the same script generate accessors. Gradle Kotlin DSL Primer even explicitly states that any form of cross-project configuration will not make the accessors available. So depending on the accessors you got might be just an unintended side-effect and not supported behavior. :man_shrugging: .

The issue starts to occur only once I merge the plugins.

I don’t think the pure merge should really be relevant. You probably changed anything else that then causes the accessors to vanish. Or you are not tripping the bug anymore that actually made the accessors available when they shouldn’t be. Hard to say without seeing the situation. But whether you have the same code in three projects or in one project really shouldn’t make a difference here.

They are slightly different, so would that still work merging them all to a settings plugin?

Probably, if you do it right I guess.
I prefer doing things right if possible, so I did not do much project logic in settings plugins so far, because that is simply out-of-scope and not the right way to go.
But if you want to follow the evil road and have all logic in the settings plugin, injecting it to the projects from outside, I guess it should work just fine.
Also merging the plugins to one project does not mean you have to merge it to just the settings plugin, you can still have the three plugin classes all in one project.
Or you can also make a plugin that is applicable to both, settings and project by not having Plugin<Settings> or Plugin<Project but Plugin<PluginAware>. But then check the type either way, because it could then also be applied to an init script.

PS: Please reasearch the term “Help Vampire”