Stylianos Gakis
11/06/2024, 9:57 AMmyPlugin {
// Pick if it's an android or jvm module, if it's android or not, have options to configure compose compiler etc.
androidLibrary {
withCompose()
}
jvmLibrary() // this OR androidLibrary etc.
// plus other optional configuration
kotlinxSerialization()
ksp()
}
I was reading “Gradle plugins and extensions: A primer for the bemused” to get an idea of how to structure this along with trying to follow the docs.
I feel like I am having a problem of not properly understanding the order in which I should be doing things and I am doing some very critical mistake in how I am structuring everything. This is the first time I try to do this, so I will post my current approach in the thread and any help would be greatly appreciated!Stylianos Gakis
11/06/2024, 9:57 AMabstract class HedvigGradlePluginExtension @Inject constructor(objectFactory: ObjectFactory) {
internal val library: LibraryHandler = objectFactory.newInstance()
fun library(action: Action<in LibraryHandler>) {
library.isConfigured.set(true)
library.isConfigured.disallowChanges()
action.execute(library)
}
companion object {
internal fun Project.hedvig(): HedvigGradlePluginExtension {
return extensions.create<HedvigGradlePluginExtension>("hedvig", project.objects)
}
}
}
abstract class LibraryHandler @Inject constructor(
objectFactory: ObjectFactory,
) {
internal val isConfigured: Property<Boolean> = objectFactory.property<Boolean>()
internal val jvm: Property<Boolean> = objectFactory.property<Boolean>().convention(false)
internal val android: Property<Boolean> = objectFactory.property<Boolean>().convention(false)
internal val compose: Property<Boolean> = objectFactory.property<Boolean>().convention(false)
fun jvm() {
jvm.set(true)
jvm.disallowChanges()
}
fun android() {
android.set(true)
android.disallowChanges()
}
fun compose() {
compose.set(true)
compose.disallowChanges()
}
}
and on the plugin side:
class HedvigGradlePlugin : Plugin<Project> {
override fun apply(project: Project) {
val libs = with(project) { the<LibrariesForLibs>() }
val pluginManager = project.pluginManager
val hedvig = project.hedvig()
if (hedvig.library.isConfigured.get()) {
if (hedvig.library.android.get() && hedvig.library.jvm.get()) {
error("Module: [${project.name}] can not both be an android and a jvm library.")
}
if (hedvig.library.android.get()) {
pluginManager.apply(LibraryConventionPlugin::class.java) // LibraryConventionPlugin etc. are other custom plugins I have which apply AGP and so on inside their `apply` function
}
if (hedvig.library.jvm.get()) {
pluginManager.apply(LibraryJvmConventionPlugin::class.java)
}
if (hedvig.library.compose.get()) {
pluginManager.apply(LibraryComposeConventionPlugin::class.java)
}
}
}
}
And so far the DSL seems to work well, I get autocompletion etc.
However it seems like the properties that I am setting are not evaluated yet at this point, so they always default to whatever I’ve put as convention() for them.
Googling some more, I figured that I do get the real values if I wrap everything starting with if (hedvig.library.isConfigured.get()) { in a project.afterEvaluate { then all the properties are in fact set to what they should be.
However the problem then becomes that I am configuring the other plugins too late. In particular I am getting an exception from the android gradle plugin which says:
It is too late to set compileSdk It has already been read to configure this project. Consider either moving this call to be during evaluation, or using the variant API.
Which is a result of me trying to configure it inside afterEvaluate.
Am I taking a completely wrong approach to this? Is there a way to be able to have such a dsl set some properties, and still read them before evaluation so that I can conditionally apply other plugins too?Martin
11/06/2024, 10:12 AMapply logic to your extension.Martin
11/06/2024, 10:13 AMMartin
11/06/2024, 10:15 AMplugins {} block
if (hedvig.library.android.get()) {
pluginManager.apply(LibraryConventionPlugin::class.java) // LibraryConventionPlugin etc. are other custom plugins I have which apply AGP and so on inside their `apply` function
}
So you lose the autogenerated accessorsMartin
11/06/2024, 10:16 AMMartin
11/06/2024, 10:17 AMplugins {} block and have your plugin react with pluginManager.withId{}
2. Or switch to completely imperative code and you lose the generated accessors
I like 1. better because it also allows users of your plugin to control the version they are applying (unless you're in a convention plugin where you control all the classpath and this is less of an issue)Stylianos Gakis
11/06/2024, 10:44 AMfun compose() for example? Without setting some property to "mark" it to do it later?Martin
11/06/2024, 10:52 AMInside my extension I would need to apply the plugin directly where I have theExactlyfor example?fun compose()
Martin
11/06/2024, 10:53 AMandroid { } blocks with stuff like:
project.extensions.getByName("android") {
this as BaseExtension<*,*,*,*,*>
// usual stuff here
defaultConfig { ... }
}Martin
11/06/2024, 10:54 AMStylianos Gakis
11/06/2024, 12:33 PMclass HedvigGradlePlugin : Plugin<Project> {
override fun apply(project: Project) {
project.hedvig()
}
}
abstract class HedvigGradlePluginExtension @Inject constructor(
private val pluginManager: PluginManager,
) {
fun library() {
pluginManager.apply(LibraryConventionPlugin::class.java)
pluginManager.apply(LibraryComposeConventionPlugin::class.java)
}
companion object {
internal fun Project.hedvig(): HedvigGradlePluginExtension {
return extensions.create<HedvigGradlePluginExtension>("hedvig", project.pluginManager)
}
}
}
And later applying it in my module like:
plugins {
id("hedvig.gradle.plugin")
}
hedvig {
library()
}
(Which plugin I've created by doing this in the place I have the rest of my convention plugins)
gradlePlugin {
fun createPlugin(id: String, className: String) {
plugins.create(id) {
this.id = id
implementationClass = className
}
}
createPlugin("hedvig.gradle.plugin", "HedvigGradlePlugin")
}
Where LibraryConventionPlugin and LibraryComposeConventionPlugin (here and here is what they call internally) are pre-existing plugins.
These are what I was previously calling individually on each of my modules, but I was hoping to re-use here.
It seems like I am still not setting up everything properly. Not sure if I am again somehow doing something "too late". The error I get when I try to sync in my IDE is that I get Unresolved reference: implementation, making me understand that those two plugins are just not applied in time properly
Does this by itself feel like it should just work already as-is or not really?Vampire
11/06/2024, 1:02 PMlibrary() call, then yes.
Such type-safe accessors are only available for things Gradle knows will be there at runtime.
That means for anything that plugins create unconditionally when applied using the plugins { ... } block.
But the plugin you apply does not do that.
You only apply the plugins that add the implementation configuration from your extension's function, so no type-safe accessors get generated.
You would need to do
val implementation by configurations.existing
dependencies {
implementation(...)
}
If you want the type-safe accessors, the configuration needs to be created by something you apply in your plugins block without condition.
You could e. g. have a plugin hedvig.library.gradle.plugin that then applies those plugins directly in its apply method, then you would have the accessors you miss.Stylianos Gakis
11/06/2024, 1:08 PMplugins {
id("hedvig.gradle.plugin") // <- This is applying my plugin I am trying to create
// here come other plugins that are applied to this project. *not* including kotlin or android gradle plugins
}
hedvig {
library()
}
dependencies {
// Here is what crashes when I mean `implementation` is not found
implementation(libs...)
api(libs...)
}
So if I understand you correctly, due to the fact that this gradle module does not actually know about the fact that "hedvig.gradle.plugin" will internally apply the kotlin gradle plugin, the android gradle plugin etc, it can't understand where dependencies is even coming from, right?
Since the kotlin gradle plugin is only conditionally applied, therefore the dependencies block is not always there.
This is what we mean here by "losing access to the autogen accessors" then? Am I closer to understanding this now?Stylianos Gakis
11/06/2024, 1:25 PMorg.jetbrains.kotlin.android or the org.jetbrains.kotlin.jvm or in the future the one for multiplatform depending on what I put in my hedvig {} dsl.
Doing val implementation by configurations.existing actually does seem to work, that's very interesting!
Is there a way for me to perhaps expose these from inside my plugin so that I can still have my call site omit adding
val implementation by configurations.existing
val api by configurations.existing
val debugImplementation by configurations.existing
val debugApi by configurations.existing
val testImplementation by configurations.existing
etc. in all of my modules?
Or if not, am I perhaps trying to take down a path I should not be taking here?
I am even considering going down this path in the first place as I want to make it easier to let some of my modules start supporting KMP, and I want us to more eagerly make more of our modules be jvm only when they don't need to be android ones.
If I continue adding yet more stringly-typed convention plugins which I must then remember to call in each module, like:
plugins {
// in each module, I need to be careful to call the right combination of these. typically hedvig.[WHATPLATFORM].[compose | omit this entirely], depending on what it needs to target, and what other plugins need to be applied in there, like the compose gradle plugin
id("hedvig.android.library")
id("hedvig.android.library.compose")
id("hedvig.jvm.library")
id("hedvig.jvm.library.compose")
id("hedvig.multiplatform.library")
id("hedvig.multiplatform.library.compose")
// and so on
}
It starts getting a bit uncomfortable, and prone to mistakes since it's all just not type-safe strings.
And what we end up doing instead is just keep things as just
plugins{
id("hedvig.android.library")
id("hedvig.android.library.compose")
}
Since it's the easiest to do. So we keep many modules as android modules since "it already works" even if flipping to a jvm module might be trivial.Martin
11/06/2024, 1:37 PMIs there a way for me to perhaps expose these from inside my plugin so that I can still have my call site omit addingYou can create configurations in
Plugin.apply() and expose them in your extension:
abstract class HedvigGradlePluginExtension @Inject constructor(
private val pluginManager: PluginManager,
private val debugImplementation: Configuration,
private val implementation: Configuration,
...
)Martin
11/06/2024, 1:39 PMckass HedvigGradlePluginExtension {
fun library(action: Action<LibrarySpec>) {
pluginManager.apply("someOtherPlugin")
val librarySpec = LibrarySpec(
project.configurations.getByName("debugImplementation")
project.configurations.getByName("implementation")
// ...
)
action.execute(librarySpec)
}
}Vampire
11/06/2024, 1:55 PMIs there a way for me to perhaps expose these from inside my plugin so that I can still have my call site omit addingYou can, but you definitely shouldn't. If you create such from your plugin code, then they will always be available even if no
library() or similar is called.
This will then work when the consumers script is compiled, but fail at runtime.
Which is exactly why those accessors are only generated if Gradle is pretty sure it will be there at runtime, so that it already fails at script compilation time, not at runtime and only if that code-path is actually hit.Vampire
11/06/2024, 1:56 PMhedvig {
library {
dependencies {
implementation(...)
}
}
}
or similar could for example workVampire
11/06/2024, 1:57 PMStylianos Gakis
11/06/2024, 1:58 PMStylianos Gakis
11/11/2024, 10:54 AMplugins {
id("hedvig.android.feature")
id("hedvig.android.ktlint")
id("hedvig.android.library")
id("hedvig.android.library.compose")
alias(libs.plugins.apollo)
alias(libs.plugins.serialization)
alias(libs.plugins.dependencyAnalysis)
alias(libs.plugins.squareSortDependencies)
}
dependencies {
...
}
apollo {
service("octopus") {
packageName = "octopus"
dependsOn(projects.apolloOctopusPublic, true)
}
}
to this:
plugins {
id("hedvig.gradle.plugin")
id("hedvig.android.library")
}
hedvig {
apollo("octopus")
serialization()
compose() // bonus points that this works regardless of it it's an android or jvm module
}
dependencies {
...
}
I did after all do the tradeoff where my plugin will not conditionally apply the necessary android or jvm configuration, each module makes their own decision for that as a separate plugin. That way I still get all the autogenerated accessors for android related stuff, but I moved the configuration for the rest of the things (like apollo-kotlin) inside my extension as I was suggested to do here.
And I even got a good idea of how I could improve all of this moving forward in various ways, since I now do at least somewhat understand what I am doing with all of this gradle stuff 😄
There’s no way I could’ve figured all of this out without both your help, I really really appreciate it!