autonomousapps / dependency-analysis-gradle-plugin

Gradle plugin for JVM projects written in Java, Kotlin, Groovy, or Scala; and Android projects written in Java or Kotlin. Provides advice for managing dependencies and other applied plugins
Apache License 2.0
1.82k stars 120 forks source link

Plugin fails because of use of `afterEvaluate` #630

Open melix opened 2 years ago

melix commented 2 years ago

Build scan link

https://ge.micronaut.io/s/slhg65invj4t2

Plugin version

1.0.0-rc-06

Gradle version

7.4.1

Describe the bug

Applying the plugin to the root project fails with:

Caused by: org.gradle.api.InvalidUserCodeException: Cannot run Project.afterEvaluate(Action) when the project is already evaluated.
        at org.gradle.api.internal.project.DefaultProject.failAfterProjectIsEvaluated(DefaultProject.java:1067)
        at org.gradle.api.internal.project.DefaultProject.afterEvaluate(DefaultProject.java:1048)
        at com.autonomousapps.subplugin.ProjectPlugin.configureJavaLibProject(ProjectPlugin.kt:328)
        at com.autonomousapps.subplugin.ProjectPlugin.access$configureJavaLibProject(ProjectPlugin.kt:46)
        at com.autonomousapps.subplugin.ProjectPlugin$apply$$inlined$run$lambda$4.execute(ProjectPlugin.kt:108)
        at com.autonomousapps.subplugin.ProjectPlugin$apply$$inlined$run$lambda$4.execute(ProjectPlugin.kt:46)
        at org.gradle.api.internal.plugins.DefaultPluginManager$2.execute(DefaultPluginManager.java:258)
        at org.gradle.api.internal.plugins.DefaultPluginManager$2.execute(DefaultPluginManager.java:255)

Steps to reproduce the behavior:

  1. git clone https://github.com/micronaut-projects/micronaut-core.git
  2. cd micronaut-core
  3. edit the root build.gradle to add the plugin:
    plugins {
    id "io.micronaut.build.internal.docs"
    id "io.micronaut.build.internal.dependency-updates"
    id "io.micronaut.build.internal.version-catalog-updates"
    id "io.micronaut.build.internal.convention-quality"
    id 'com.autonomousapps.dependency-analysis' version '1.0.0-rc06'
    }
  4. run ./gradlew buildHealth

Expected behavior

Build shouldn't fail.

Additional context

I'm investigating the use of this plugin (which I wish was a core feature of Gradle, I wanted this for a looooooong time) for integration in our Micronaut Build Plugin. This project provides convention plugins which are applied on more than 40 distinct Micronaut projects (including Micronaut Core which I used in this bug report). Our convention plugins require the use of evaluationDependsOn, which triggers evaluation of subprojects before the root project is evaluated.

I am aware that there's a dependency.analysis.autoapply=false flag that I can use which would not apply the plugin to subprojects automatically. However, there are a number of issues:

I looked into the sources and I've found that internally it does ProjectPlugin(this).apply(), but the ProjectPlugin type is internal. Would it make sense to expose a plugin which can be indidivually applied to a single project instead? That's basically what ProjectPlugin does, it seems. In general I'm not super fond of plugins which automagically apply plugins on subprojects. Like here, it can easily break if the expectations aren't met. In a typical Micronaut project we have a root plugin which performs some aggregation (javadocs, code quality, ...), different module types plugins (a Micronaut Library, a Micronaut Component) and a BOM type which requires evaluationDependsOn so that we can automatically determine dependencies based on the application of a plugin or not.

I'm happy to discuss options and/or provide PRs, I just need a bit more guidance on how I can individually run the plugin on a project to make progress: unfortunately I couldn't find answers in the wiki.

Thanks!

autonomousapps commented 2 years ago

Thanks for the report.

To start, here's what you'd do to have full control over how the plugin gets applied to individual projects:

# gradle.properties
dependency.analysis.autoapply=false
// root build.gradle
plugins {
  id 'com.autonomousapps.dependency-analysis' version '1.0.0-rc06'
}
// every/other/build.gradle
plugins {
  id 'com.autonomousapps.dependency-analysis' version '1.0.0-rc06'
}

As you can see, it's the same plugin in both cases. The plugin takes care of determining if it's in the root project or not, and does the right thing. Note that the plugin must be applied to the root, or it will fail. Here's the check it does:

  /** Plugin _must_ be applied to the root for it to work. */
  private fun Project.checkPluginWasAppliedToRoot() {
    // "test" is the name of the dummy project that Kotlin DSL applies a plugin to when generating
    // script accessors
    if (getExtensionOrNull() == null && rootProject.name != "test") {
      throw GradleException("You must apply the plugin to the root project. Current project is $path")
    }
  }

  /** Used for validity check. */
  internal fun Project.getExtensionOrNull(): DependencyAnalysisExtension? = rootProject.extensions.findByType()

I am not certain how the above requirement will work out in the presence of your requirement that subprojects be evaluated before the root project.

You asked

Would it make sense to expose a plugin which can be individually applied to a single project instead?

I have considered this but haven't taken any steps to investigate this yet; in fact, that idea was one of the reasons I split up the enormous DependencyAnalysisPlugin.kt into sub-classes that are pseudo-plugins. I've also wondered about publishing a settings plugin that might be able to safely apply plugins to subprojects, but I haven't looked at that either.

Happy to continue this discussion and see what kinds of solutions we can come up with. Let me know how far you get with the advice at the top of this comment.

autonomousapps commented 2 years ago

hey @melix just curious if you've had a chance to try this out again? No rush of course, just triaging my issues.

melix commented 2 years ago

I still plan to do it, just didn't have time!

melix commented 2 years ago

I'm still having troubles even applying what you described. The build fails with:

An exception occurred applying plugin request [id: 'io.micronaut.build.internal.convention-core-library']
> Failed to apply plugin 'io.micronaut.build.internal.convention-core-library'.
   > Failed to apply plugin 'io.micronaut.build.internal.convention-library'.
      > Failed to apply plugin 'io.micronaut.build.internal.convention-base'.
         > Failed to apply plugin 'com.autonomousapps.dependency-analysis'.
            > You must apply the plugin to the root project. Current project is :aop

Which I'm pretty sure is the same underlying cause, an evaluation order issue (because of evaluationDependsOn).

melix commented 2 years ago

I'm not saying that this is what we should do, but I made progress:https://ge.micronaut.io/s/z5lzhxadcje5m

with this diff:

diff --git a/build-logic/convention/src/main/kotlin/com/autonomousapps/convention/ConventionExtension.kt b/build-logic/convention/src/main/kotlin/com/autonomousapps/convention/ConventionExtension.kt
index fd4872a4..7a53627e 100644
--- a/build-logic/convention/src/main/kotlin/com/autonomousapps/convention/ConventionExtension.kt
+++ b/build-logic/convention/src/main/kotlin/com/autonomousapps/convention/ConventionExtension.kt
@@ -24,7 +24,7 @@ abstract class ConventionExtension(
   internal var pomConfiguration: Action<MavenPom>? = null
   internal val publishedVersion: Property<String> = project.objects.property(String::class.java)
   internal val isSnapshot: Provider<Boolean> = publishedVersion.map {
-    it.endsWith("SNAPSHOT")
+    true
   }

   fun version(version: Any?) {
diff --git a/src/main/kotlin/com/autonomousapps/DependencyAnalysisPlugin.kt b/src/main/kotlin/com/autonomousapps/DependencyAnalysisPlugin.kt
index 5026158c..6ba803a6 100644
--- a/src/main/kotlin/com/autonomousapps/DependencyAnalysisPlugin.kt
+++ b/src/main/kotlin/com/autonomousapps/DependencyAnalysisPlugin.kt
@@ -53,7 +53,7 @@ class DependencyAnalysisPlugin : Plugin<Project> {
     // "test" is the name of the dummy project that Kotlin DSL applies a plugin to when generating
     // script accessors
     if (getExtensionOrNull() == null && rootProject.name != "test") {
-      throw GradleException("You must apply the plugin to the root project. Current project is $path")
+      rootProject.pluginManager.apply(DependencyAnalysisPlugin::class.java)
     }
   }

diff --git a/src/main/kotlin/com/autonomousapps/subplugin/ProjectPlugin.kt b/src/main/kotlin/com/autonomousapps/subplugin/ProjectPlugin.kt
index 686ea071..356ca1f9 100644
--- a/src/main/kotlin/com/autonomousapps/subplugin/ProjectPlugin.kt
+++ b/src/main/kotlin/com/autonomousapps/subplugin/ProjectPlugin.kt
@@ -103,7 +103,7 @@ internal class ProjectPlugin(private val project: Project) {
     }
     pluginManager.withPlugin(KOTLIN_JVM_PLUGIN) {
       logger.log("Adding Kotlin-JVM tasks to ${project.path}")
-      configureKotlinJvmProject()
+//      configureKotlinJvmProject()
     }
     pluginManager.withPlugin(JAVA_PLUGIN) {
       configureJavaAppProject(maybeAppProject = true)
autonomousapps commented 2 years ago

This is exactly the kind of thing I thought would trip you up when I thought about how your root project depends on your subprojects:

-      throw GradleException("You must apply the plugin to the root project. Current project is $path")
+      rootProject.pluginManager.apply(DependencyAnalysisPlugin::class.java)

The whole deal with the root extension and subproject extensions and how they communicate needs to be cleaned up. I am very open to improvements in this area.

melix commented 2 years ago

I wonder if you could use a BuildService instead for communication. But this bug may be annoying.

waliahimanshu commented 2 years ago

I faced the same issue in my project; the workaround to add the plugin to every/other/build.gradle worked for me now.

TWiStErRob commented 1 year ago

Same as Himanshu, I added the flag and included the plugin in every module, and it started working.

@melix you might be lucky:

https://github.com/autonomousapps/dependency-analysis-android-gradle-plugin/blob/83f17c1af310e21c10d7f2072b639855a82ab570/src/main/kotlin/com/autonomousapps/Flags.kt#L28

This means that doing System.setProperty("dependency.analysis.autoapply", false) at any point before the plugin is applied might be a viable alternative to changing all your gradle.properties files. If you have a settings plugin in for micronaut I think that would be the place, because otherwise it's quite tricky to do something before a root plugin is applied. Although it looks like you wrap every plugin, which means you set this before calling project.plugins.apply().

autonomousapps commented 1 year ago

PRs welcome!