GithubHelp home page GithubHelp logo

Comments (22)

JavierSegoviaCordoba avatar JavierSegoviaCordoba commented on June 5, 2024 3

Any update on this? Multiple MRs related to refactoring the Gradle plugin were merged recently.

from binary-compatibility-validator.

Fleshgrinder avatar Fleshgrinder commented on June 5, 2024 2

Changing the plugin from a project plugin to a settings plugin would allow safe access to all projects without any hacks. It would also make it trivial to apply the plugin.

public abstract class SettingsPlugin : Plugin<Settings> {
    override fun apply(target: Settings) {
        target.gradle.apply {
            rootProject {
                // special handling for root project
            }

            beforeProject {
                // handling for ALL projects
            }
        }
    }
}

Obviously it is possible to introduce this in a backward compatible way:

public abstract class SettingsAndBuildPlugin : Plugin<Any> {
    override fun apply(target: Any) {
        when (target) {
            is Project -> {
                logger.error("{} should not be applied in build scripts anymore, apply it in the settings script of the build.", this::class.java.canonicalName)
                // apply
            }

            is Settings -> {
                // apply
            }

            else -> error("${this::class.java.canonicalName} cannot be applied to ${target::class.java.canonicalName}; it can only be applied to settings and build scripts.")
        }
    }
}

from binary-compatibility-validator.

aSemy avatar aSemy commented on June 5, 2024 1

I've done some experimenting, and I can pick this up. I'd first need to tidy up the build logic and doing some restructuring though.

  • Update Gradle config to use buildSrc convention plugins
  • Split up the current project into two projects
  • Finally, create a separate Settings plugin, and appropriate tests

@qwwdfsad I can make PRs for the above, if you approve? Would you like me to make separate issues for each step? The steps are also quite broad, and I will try and make the PRs smaller, but are there any steps that should be made more granular?

from binary-compatibility-validator.

aSemy avatar aSemy commented on June 5, 2024 1

... BCV will get rid its own kotlinx-metadata dependency and will always use one provided by KGP itself

Yeah that makes sense. Perhaps the 'isolated' naming is misleading in this sense - it doesn't mean that the worker classpath must be isolated, but instead BCV can control what the classpath contains.

So, until KGP also supplies kotlinx-metadata, the BCV worker classpath can add a default dependency on kotlinx-metadata - independent of KGP. And once KGP does provide kotlinx-metadata, the BCV worker classpath can extend/inherit (whatever the right terminology is) the buildscript classpath, and so it'll be synced.

This achieves the goal of decoupling a specific KGP from BCV, so long as the classes that BCV uses are stable.

... I would gladly accept the contribution

Great! I'll try breaking the steps here up into smaller PRs - I think it would help to make a separate branch 'dev' branch to merge these PRs into?

I actually already made a start on refactoring the plugin if you want to take a look: https://github.com/adamko-dev/kotlin-binary-compatibility-validator-mu. It's a bit of a fresh start, and includes some other refactorings. Under the hood it still uses BCV (since the signature-generator and the plugin are combined), but as a dependency.

I also implemented the proposed settings plugin (thanks @Fleshgrinder), and I while I haven't tested it much I found a couple of unexpected things:

  • it requires that KGP is present on the classpath

    // settings.gradle.kts
    
    buildscript {
      dependencies {
        // BCV-MU requires the Kotlin Gradle Plugin classes are present 
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10")
      }
    }
    
    plugins {
      id("dev.adamko.kotlin.binary-compatibility-validator") version "$bcvMuVersion"
    }

    BCV-MU can't add a runtime dependency on KGP, because then it's too opinionated and it will clash with any KGP applied in subprojects. Without adding a buildscript dependency, BCV-MU fails because it can't fetch the KotlinMultiplatformExtension class. But this isn't great, because there are at least 4 different ways of applying Gradle plugins, or specifying plugin versions, so it might force users into this specific way - which is annoying.

    Maybe there's another way - this is why I made BCV-MU so if anyone (@Fleshgrinder?) wants to pick it up and experiment, please do!

  • I couldn't get Gradle to generate a nice Kotlin DSL accessor for the BCV-MU extension, so it requires this:

    extensions
      .getByType<dev.adamko.kotlin.binary_compatibility_validator.BCVSettingsPlugin.Extension>()
      .apply {
      }

    which is a little ugly. Again, maybe there's another way.

from binary-compatibility-validator.

Fleshgrinder avatar Fleshgrinder commented on June 5, 2024 1

Gradle uses classpath isolation with inheritance. The classpath of settings is isolated from the projects, and projects inherit the classpath of their settings. But, an init script for instance is entirely isolated from everything else.

Defining a dependency as compileOnly means that our classpath at runtime does not contain the classes we used for compilation. Since settings is isolated from projects we never see their classes, hence, we cannot find the classes we defined as compileOnly since nobody added them to our runtime classpath.

Us adding the classes to our runtime classpath (either by using implementation or combining compileOnly with runtimeOnly which is implementation) would force the dependency upon our consumers. Since projects inherit from settings they would inherit our version of whatever we added to our runtime classpath. This is obviously not what we want.

This is a classic problem with Gradle plugins. A good way to solve it is by shadowing the dependencies. We actually do not require a dependency on the Gradle Kotlin DSL, we only require a dependency on its API (org.jetbrains.kotlin:kotlin-gradle-plugin-api) (reducing the amount of code to shadow). By using the API we lose the KotlinMultiplatformExtension class, but we could rewrite the code to use an alternative instead that works just as well:

  private fun createKotlinMultiplatformTargets(project: Project, extension: BCVProjectExtension) {
    project.pluginManager.withPlugin("kotlin-multiplatform") {
      project.extensions.getByType<KotlinTargetsContainer>().targets.forEach {
        val platformType = it.platformType
        if (platformType == KotlinPlatformType.jvm || platformType == KotlinPlatformType.androidJvm) {
          extension.targets.register(it.targetName) {
            enabled.convention(true)
            it.compilations.configureEach {
              inputClasses.from(output.classesDirs)
            }
          }
        }
      }
    }
  }

With that it should work. Maybe there is also a way to change the current classloader, since the current thread within the project.pluginManager.withPlugin("kotlin-multiplatform") block can load the class. But, I do not know how or if this is at all possible (sounds dangerous, hopefully it is not allowed).

from binary-compatibility-validator.

qwwdfsad avatar qwwdfsad commented on June 5, 2024 1

Hi @aSemy, thank you!

I've pushed bcv-gradle-rework, it would be nice to have all your chained PRs targeting this branch that we can use as staging for further changes (and for intermediate dev releases for internal validation). Meanwhile I'll start reviewing your PRs one by one

from binary-compatibility-validator.

qwwdfsad avatar qwwdfsad commented on June 5, 2024

The request itself is quite reasonable, though unfortunately, it's out of my scope right now.
I'm ready to accept a high-quality PR (with tests and docs) in a timely manner tho

from binary-compatibility-validator.

qwwdfsad avatar qwwdfsad commented on June 5, 2024

I would really like the contribution here, esp. in the face of Gradle 8.
I'm no expert in Gradle, so I'd very much like to figure out technicalities though.

Split up the current project into two projects

Could you please elaborate on why this might be necessary?

Finally, create a separate Settings plugin

Could you please elaborate on the approach, is it a recommended Gradle's approach for such plugins?
I was thinking that for project isolation there is no need for a separate plugin, rather a more careful and isolated configuration phase

from binary-compatibility-validator.

aSemy avatar aSemy commented on June 5, 2024

Split up the current project into two projects

Could you please elaborate on why this might be necessary?

Sure! It's not strictly necessary, after all it currently works fine, but it helps in a number of ways.

Gradle Plugins should only use the Kotlin that's embedded into Gradle, while BCV currently does not. Splitting up the code means that the actual validation code can use a separate Kotlin plugin, while the Gradle Plugin will only use the embedded version.

Additionally the classpath could be isolated. Currently the BCV Gradle Plugin's dependencies might class with those of other plugins. However, if BCV was a separate project then the classpath could be isolated using the Worker API.

In practice it would work similarly to how the Gradle uses JaCoCo. Gradle sets a default JaCoCo version, but the specific version can be overridden if desired. It's also how I'm proposing the Dokka Gradle plugin should be re-written.

Finally, it opens up the possibility of creating a command-line tool, Maven plugin, etc in the future.

Finally, create a separate Settings plugin

Could you please elaborate on the approach, is it a recommended Gradle's approach for such plugins? I was thinking that for project isolation there is no need for a separate plugin, rather a more careful and isolated configuration phase

Using allprojects {} and subprojects {} is discouraged. So, let's say that BCV doesn't use them any more. All projects that should have a .api generated have to have the plugin manually applied.

// ./components/foo-subproject/build.gradle.kts

plugins {
  id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.13.0"
}

This is a good solution - you can't get simpler. Subprojects are decoupled. It's explicit, so to see what the configuration of a subproject is you only ned to check it's build.gradle.kts. And you don't get plugins randomly applied from some 'parent' build.

The problem is when you want to add some custom config. Let's say you want to add some common BCV config into every subproject

// ./components/foo-subproject/build.gradle.kts

plugins {
  id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.13.0"
}

// add some custom config to all subprojects
apiValidation {
  nonPublicMarkers += ["my.package.MyInternalApiAnnotation"]
}

I don't want to copy and paste that config, it's going to be the same everywhere. It's noisy, and it'll be easy to make a mistake or have a merge conflict.

The recommended Gradle way of fixing this is to define a convention-plugin. The easiest way to set this up is to use buildSrc.

  1. set up buildSrc build & settings scripts
  2. in buildSrc/build.gradle.kts the dependencies, add the Maven coordinates (not the plugin ID) of BCV
  3. Define a convention plugin:
    // ./buildSrc/src/main/kotlin/bcv-convention.kts
    
    plugins {
      id("org.jetbrains.kotlinx.binary-compatibility-validator") // no version needed - it's set in buildSrc/build.gradle.kts`
    }
    
    apiValidation {
      nonPublicMarkers += ["my.package.MyInternalApiAnnotation"]
      
      // we could even apply some conditional logic based on the subproject
      enabled.set(project.path.endsWith("-api") // only enable BCV in API projects
    }

Now in any subprojects we can apply the convention instead of the plugin

// ./components/foo-subproject/build.gradle.kts

plugins {
  `bcv-convention`
}

Job done. This is the most practical solution. And because convention plugins just set up the conventions, individual subprojects could apply some additional config, or could even opt-out entirely.

// ./components/foo-subproject/build.gradle.kts

plugins {
  `bcv-convention`
}

apiValidation {
  // nonPublicMarkers will contain both MyInternalApiAnnotation and SPApiAnnotation
  nonPublicMarkers += listOf("subproject.package.SPApiAnnotation")
  
  // or we could just opt-out
  enabled.set(false)
}

However, this still requires that all subprojects apply the convention plugin. I can certainly understand that when it comes to a tool like BCV it should be applied by default. Additionally, some users might find setting up buildSrc convention plugins a little bit too much work. I think that's where a settings-plugin comes in.

Settings plugins can be added just like project plugins, and can also define a configuration DSL.

// ./settings.gradle.kts

plugins {
  id("org.jetbrains.kotlinx.binary-compatibility-validator.settings")
}

apiValidation {
  nonPublicMarkers += listOf("subproject.package.SPApiAnnotation")
}

The benefit is that settings plugins have access to the Gradle instance, which can execute logic on all projects after they are loaded but before they are evaluated.

abstract class BCVSettingsPlugin : Plugin<Settings> {

  override fun apply(target: Settings) {

    val extension = target.extensions.create("apiValidation", Extension::class)

    target.gradle.projectsLoaded {
      allprojects {
        if (org.gradle.util.Path.path(project.path) !in extension.disabledProjects.get()) {
          logger.lifecycle("applying binary-compatibility-validator to subproject ${project.path}")
          pluginManager.apply(BCVProjectPlugin::class)
        }
      }
    }
  }

  interface Extension {
    val disabledProjects: SetProperty<org.gradle.util.Path>
  }
}

The end result would be a central place (settings.gradle.kts) where the BCV logic for all subprojects could be defined. The downside is that they don't work so well with convention plugins - it's not that they're incompatible, it's just a bit awkward. You can write convention-plugins for settings-plugins too, but it's a little bit more work, and they're not as easy to share.

However, I am not very familiar with settings plugins! I do not know how well supported they are, and whether the use of allproject {} in settings plugins is also discouraged. I think it is worth investigating though.

from binary-compatibility-validator.

Fleshgrinder avatar Fleshgrinder commented on June 5, 2024

I already provided the answer on how to do it in a settings plugin in my last comment. 😊

from binary-compatibility-validator.

aSemy avatar aSemy commented on June 5, 2024

@Fleshgrinder Yeah I think it's a good idea. What I would want to do is still create specific Project/Settings plugins, so logic is kept separated. And what you could do is only register one plugin ID for the base BCVPlugin, or define 3 plugin IDs - one for each.

abstract class BCVPlugin : Plugin<PluginAware> {
  override fun apply(target: PluginAware) {
    when (target) {
      is Project    -> target.pluginManager.apply(BCVProjectPlugin::class)
      is Settings -> target.pluginManager.apply(BCVSettingsPlugin::class)
      else      -> error("cannot apply BCV to ${target::class}")
    }
  }
}

abstract class BCVProjectPlugin : Plugin<Project> {
  override fun apply(target: Project) = TODO()
}

abstract class BCVSettingsPlugin : Plugin<Settings> {
  override fun apply(target: Settings) = TODO()
} 

from binary-compatibility-validator.

Fleshgrinder avatar Fleshgrinder commented on June 5, 2024

Splitting the Gradle plugin from the rest of the code makes sense. Splitting the plugin does not. It only creates complexity for people who want to apply it. Having a single plugin that can be applied to both settings and build scripts is much easier to communicate and understand. The only difference between the settings and the build script plugin would be the target type, everything else stays the same. In turn it increases code complexity and harms efficiency if we split them, because we need to work with multiple JARs and multiple Gradle plugins.

However, I am not very familiar with settings plugins! I do not know how well supported they are, and whether the use of allproject {} in settings plugins is also discouraged. I think it is worth investigating though.

The part that you are missing in my comment is this:

public abstract class SettingsPlugin : Plugin<Settings> {
    override fun apply(target: Settings) {
        target.gradle.apply {
            rootProject {
                // special handling for root project
            }

            beforeProject {
                // handling for ALL projects
            }
        }
    }
}

A settings plugin does not use allprojects or subprojects or whatever, it can register hooks in a safe way to manipulate projects. The only important thing is to ensure that no bad things happen if someone applies the settings plugin and the project plugin (well, a warning would be nice).

from binary-compatibility-validator.

aSemy avatar aSemy commented on June 5, 2024

because we need to work with multiple JARs and multiple Gradle plugins

I'm not sure what you mean by multiple JARs, or it being more complex? If there were multiple plugins they would be defined in a single project, that would produce a single JAR that contains them all. And then optionally the plugins could have a nice plugin ID for use in the plugins {} block, but you can have as many or few plugin IDs per project as you'd like. The plugin IDs aren't mandatory, so it's okay if abstract class BCVProjectPlugin : Plugin<Project> doesn't have a plugin ID.

The Project/Settings config has got to be separated in some way, and specific plugins is just a nice, Gradle idiomatic way of doing it.

Personally I probably wouldn't want to use a Settings plugins in my projects, as I'd want to apply a Project plugin via a convention-plugin, so I want separate plugin ID to be explicit.

from binary-compatibility-validator.

Fleshgrinder avatar Fleshgrinder commented on June 5, 2024

Ah, sorry, I misunderstood. I would still stick with a single plugin ID, it's only going to confuse users, and they won't know when to apply which. It needs to be explained everywhere when to use which, etc. Much easier to have a single ID and handle it for the user automatically. This does not take away the possibility to not apply it in settings but instead on a per project basis.

Also, it is not mandatory that the settings plugin applies the project plugin to all suitable projects. It could very well just provide the default settings, and only if the plugin is actually applied to a project it is enabled. Basically what currently would require people to write convention plugins. This would result in a great user experience, since nobody needs to write custom code, learn about convention plugins, etc.

from binary-compatibility-validator.

qwwdfsad avatar qwwdfsad commented on June 5, 2024

Thanks a lot for the explanation!

Gradle Plugins should only use the Kotlin that's embedded into Gradle, while BCV currently does not. Splitting up the code means that the actual validation code can use a separate Kotlin plugin, while the Gradle Plugin will only use the embedded version.

I see, thanks! Such approach is much more reasonable, but in BCV we would very much like to avoid that.
We are going to stabilize kotlinx-metadata (that BCV is based upon) soon and ship it with Kotlin provided by KGP. After that, BCV will get rid its own kotlinx-metadata dependency and will always use one provided by KGP itself. It means that when new Kotlin (say, 1.15) is released, we won't be forced to release BCV with the version of metadata that support 1.15, we'll get one from the classpath instead! We plan to do the same trick with Kotlin/Native (not yet ready) counterpart of BCV.
Because otherwise having a trailing BCV release after each Kotlin release is quite cumbersome.
Does it make sense to you?

id("org.jetbrains.kotlinx.binary-compatibility-validator.settings")
The benefit is that settings plugins have access to the Gradle instance, which can execute logic on all projects after they are loaded but before they are evaluated.

Neat! It also seems to address my concern regarding "disabled projects" validation.

If we are on the same page about non-isolated classpath (I'm open to any other solution that addresses "have kotlinx-metadata version from KGP". This requirement is also not quite thought out so it may have some logical holes like "Since Gradle 8 nobody is allowed to use other's plugin dependencies"), I would gladly accept the contribution

from binary-compatibility-validator.

JavierSegoviaCordoba avatar JavierSegoviaCordoba commented on June 5, 2024

Settings plugin doesn't get free accessors. You have to create a lambda manually so the consumers can use it

from binary-compatibility-validator.

Fleshgrinder avatar Fleshgrinder commented on June 5, 2024

Can you elaborate the problem with KotlinProjectExtension in more detail? I could not find any reference to this class in your code. 🤔

What @JavierSegoviaCordoba wrote, Gradle does not yet generate code for them, but we can just write it ourselves. For that we create a Kotlin file in the global package, providing the functions that Gradle would generate:

// no package!

val Settings.bcv: Extension get() = extensions.getByType<Extension>()

inline fun Settings.bcv(action: Extension.() -> Unit) = bcv.apply(action)

Written from the top of my head, might need to look slightly different.

from binary-compatibility-validator.

aSemy avatar aSemy commented on June 5, 2024

Thanks for the custom DSL function - I will give it a go

I misremembered the class - it was failing on fetching the KotlinMultiplatformExtension here: https://github.com/adamko-dev/kotlin-binary-compatibility-validator-mu/blob/22228d17a5da2c6dcdd4a9f0a4ea8947ed0c1b36/modules/bcv-gradle-plugin/src/main/kotlin/BCVProjectPlugin.kt#L159

from binary-compatibility-validator.

JavierSegoviaCordoba avatar JavierSegoviaCordoba commented on June 5, 2024

Currently the plugin needs the KGP source sets, so it needs to get them from KotlinProjectExtension which is a parent class of all kotlin projects (KotlinJvmProjectExtension, and so on).

from binary-compatibility-validator.

JavierSegoviaCordoba avatar JavierSegoviaCordoba commented on June 5, 2024

And that is unavoidable in order to get the registered source sets, as Kotlin Multiplatform doesn't use default source sets at all, they are only used by Kotlin JVM.

from binary-compatibility-validator.

Fleshgrinder avatar Fleshgrinder commented on June 5, 2024

How do I reproduce this problem? I created a simple Gradle project with a single library at lib as a subproject with the Kotlin JVM Gradle plugin, published your plugin locally, and added the following to my settings. Everything seems to work.

pluginManagement {
    repositories {
        mavenCentral()
        gradlePluginPortal()
        mavenLocal()
    }
}

plugins {
    id("dev.adamko.kotlin.binary-compatibility-validator") version "0.0.1"
}

Edit I see, it happens only with the multiplatform plugin, interesting. 🤔

from binary-compatibility-validator.

JavierSegoviaCordoba avatar JavierSegoviaCordoba commented on June 5, 2024

@Fleshgrinder it is probably happening on Kotlin JS too. And it is perhaps that the support goes in that direction for JVM instead of Multiplatform supporting "normal" source sets.

There is another solution: the consumer passes the source sets manually, but it isn't a good solution IMO.

I want to understand why it can't use KGP as compileOnly dependency on the Gradle plugin side.

from binary-compatibility-validator.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.