I know that it’s a chicken-egg-problem when you want to use the actual version of a plugin in the plugin’s project.
But why can I not use the previous version?
If I try to use the previous version I get:
Could not apply requested plugin [id: 'my-hello-world', version: '1'] as it does not provide a plugin with id 'my-hello-world'. This is caused by an incorrect plugin implementation. Please contact the plugin author(s).
> Plugin with id 'my-hello-world' not found.
If I use the version that leads to the error message above in any other project (also composite builds) it works. E.g.
If you add apply false and execute the buildEnvironment task, you’ll see that the dependency on the plugin code artifact is substituted by / conflict-resolved to the root project, but at plugin application time this is not yet available of course and thus correctly complains that the plugin was not found in the dependency.
I guess it is a bug that this happens at all, as it does not make much sense to have this in the build environment, so you probably should report this and post the link here.
But you can actually relatively easily work-around it. Remove the version in the plugins block and instead add the plugin to the settings script plugins block with version and apply false. This way it is added to the settings script classpath at a time where the root project is not at all available yet, so the biggest dependency is used and then in the project build script the plugin simply is applied.
Sadly the workaround only works if I do not use a sub-project, e.g. include(“:my-plugin”).
If I add the previous version of the plugin to the settings.gradle.kts of the root project and then only use the plugin id in the my-plugin sub-project I get a bunch of errors:
FAILURE: Build failed with an exception.
* Where:
Build file 'D:\repos\git\github\link-intersystems\lis-gradle-plugins\publication-utils-plugin\build.gradle.kts' line: 34
* What went wrong:
Script compilation errors:
Line 34: publications.withType<MavenPublication> {
^ Unresolved reference. None of the following candidates is applicable because of receiver type mismatch:
public inline fun <S : TypeVariable(T), T : Any> DomainObjectCollection<TypeVariable(T)>.withType(type: KClass<TypeVariable(S)>): DomainObjectCollection<TypeVariable(S)> defined in org.gradle.kotlin.dsl
public inline fun <S : TypeVariable(T), T : Any> DomainObjectCollection<TypeVariable(T)>.withType(type: KClass<TypeVariable(S)>, configureAction: Action<in TypeVariable(S)>): DomainObjectCollection<TypeVariable(S)> defined in org.gradle.kotlin.dsl
public inline fun <reified S : Any> DomainObjectCollection<in TypeVariable(S)>.withType(): DomainObjectCollection<TypeVariable(S)!> defined in org.gradle.kotlin.dsl
public inline fun <reified S : Any> DomainObjectCollection<in TypeVariable(S)>.withType(noinline configuration: TypeVariable(S).() -> Unit): DomainObjectCollection<TypeVariable(S)!> defined in org.gradle.kotlin.dsl
public inline fun <S : TypeVariable(T), T : Any> DomainObjectSet<TypeVariable(T)>.withType(type: KClass<TypeVariable(S)>): DomainObjectSet<TypeVariable(S)> defined in org.gradle.kotlin.dsl
public inline fun <S : TypeVariable(T), T : Any> NamedDomainObjectCollection<TypeVariable(T)>.withType(type: KClass<TypeVariable(S)>): NamedDomainObjectCollection<TypeVariable(S)> defined in org.gradle.kotlin.dsl
public inline fun <reified S : Any> NamedDomainObjectCollection<in TypeVariable(S)>.withType(): NamedDomainObjectCollection<TypeVariable(S)> defined in org.gradle.kotlin.dsl
........
It’s weird that this change leads to issues in a complete other area - the maven publication plugin.
Well, that’s quite easy.
If you comment out the bad-practice afterEvaluate { ... }, sync the project and then comment it in, you see pretty well that due to the context switch by afterEvaluate you do not call withType on publishing.publications but on project.publications, an extension added by your plugin.
But why is afterEvaluate a bad-practice? When I write a plugin I often use afterEvaluate, because the plugins code gets applied first in the build script. That means that the rest of the build script has not been executed yet, has it? If it has not executed yet any plugin extension configuration that occurs later in the script has not been executed. So I will not get the configured values if I try to access them in the apply method of the plugin.
Am I missing something? Or do you mean that it is ‘only’ bad-practice in a build script?
Because it’s main effects are, that you add ordering problems, timing problems, and race conditions. Using afterEvaluate - no matter where - is almost always the wrong thing to do. There are only rare cases where you really need to use it, most often when plugins are using it too and you have to adapt to that, or when you need to bridge legacy primitive extensions with properly Property-ized extensions. When writing a plugin there is almost always a better alternative.
When I write a plugin I often use afterEvaluate
That’s a really bad habit you should get rid of quickly. Using afterEvaluate to “fix” a build problem is like using SwingUtilities.runLater or Platform.runLater to “fix” a GUI problem. It just does symptom treatment without fixing the actual problem and just shifts the problem to a harder to reproduce, harder to debug, and harder to fix point in time. You really don’t do you a favor if you use afterEvaluate ligthly without actual real need.
So I will not get the configured values if I try to access them in the apply method of the plugin.
That’s right, and you shouldn’t.
What for example if a consumer of your plugin or another plugin your consumer applies after your plugin uses afterEvaluate to configure the value you query? Then you still do not get the final value.
To resolve this big f***-up, the Property / Provider APIs were introduce.
With those you wire the properites in your extensions to properties in your tasks and so on and only query them at execution time, so that all configuration time code has already finished when you retrieve the value.
In the rare cases where you need a configured value at configuration time, because you need to do some actual configuration base on it like registering additional tasks, there are other patterns to avoid afterEvaluate like not having a property for the configuration value, but a function in your extension where you give the configuration values as arguments and do the configuration in the function body, eventually preventing the method to be called multiple times if a second call with other parameters cannot undo what the first call did but would need to. Or a function call that takes an Action<MyConfig> that can from consumer side then still be used DSL-like just with one more nesting level and then you react in that method to the done configuration and so on. But how to concretely do it “properly” highly depends on the concrete situation in each case.
Thanks for the clarification. You pointed out a lot of things that I also worried about. Unfortunately the web is full of bad examples about Gradle and you will find even more when it comes to plugin development. I will adapt my plugins as soon as poosible so that I’m not another bad example
I will also open a bug for the initial issue as you proposed.