Comments (22)
Any update on this? Multiple MRs related to refactoring the Gradle plugin were merged recently.
from binary-compatibility-validator.
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.
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
- the core (essentially everything the
kotlinx.validation.api
package) - the Gradle plugin
- the core (essentially everything the
- 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.
... 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.
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.
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.
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.
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.
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.
- set up buildSrc build & settings scripts
- in
buildSrc/build.gradle.kts
the dependencies, add the Maven coordinates (not the plugin ID) of BCV - 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.
I already provided the answer on how to do it in a settings plugin in my last comment. 😊
from binary-compatibility-validator.
@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.
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.
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.
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.
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.
Settings plugin doesn't get free accessors. You have to create a lambda manually so the consumers can use it
from binary-compatibility-validator.
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.
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.
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.
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.
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.
@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)
- Klib `.api` files should sort members above types HOT 4
- Why use the `.api` extension and not `.abi`? HOT 6
- Klib validation failing on CI but not locally HOT 5
- Klib validation failing with unexpected header line error HOT 1
- Make KLib validation related tasks public HOT 1
- Improve error message when the apiCheck fails due to missing klib dump file
- Klib validation may incorrectly handle projects with generated sources
- Use the Gradle Provider API to allow for lazy configuration HOT 1
- BCV Gradle Plugin should not depend on kotlin-compiler-embeddable HOT 1
- BCV tasks do not work for a project with generated sources when configuration cache is enabled
- org.gradle.api.InvalidUserDataException? HOT 2
- @JvmOverloads doesn't generate different methods for compose functions HOT 2
- Klib validation DSL for Groovy is different HOT 1
- Removal of default parameter is binary compatible but breaks runtime HOT 3
- `public` members of non-API supertype should be visible in the inheritor
- Unknown native target errors with Kotlin 2.0.0-RC1 HOT 4
- `KlibSignatureVersion.LATEST.toString()` is rendered as `KlibSignatureVersion(-2147483648)`
- Add Gradle version compatibility tests HOT 2
- Consider moving BCV to Kotlin repo HOT 3
- Description of a Syncs API task is uninformative HOT 1
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from binary-compatibility-validator.