Gradle. Custom function in block plugins{} - kotlin

Can i write in my custom plugin some function like kotlin("jvm")?
plugins {
java
kotlin("jvm") version "1.3.71"
}
I want to write function myplugin("foo") in my custom plugin and then use it like
plugins {
java
kotlin("jvm") version "1.3.71"
custom.plugin
myplugin("foo")
}
How i can do it?

I think that plugins block is some kind of a macro expression. It is parsed and precompiled using a very limited context. Probably, the magic happens somewhere in kotlin-dsl. This is probably the only way to get static accessors and extension functions from plugins to work in Kotlin. I've never seen a mention of this process in Gradle's documentation, but let me explain my thought. Probably, some smart guys from Gradle will correct me.
Let's take a look at some third-party plugin, like Liquibase. It allows you to write something like this in your build.gradle.kts:
liquibase {
activities {
register("name") {
// Configure the activity here
}
}
}
Think about it: in a statically compiled language like Kotlin, in order for this syntaxt to work, there should be an extension named liquibase on a Project type (as it is the type of this object in every build.gradle.kts) available in the classpath of a Gradle's VM that executes the build script.
Indeed, if you click on it, you'll see something like:
fun org.gradle.api.Project.`liquibase`(configure: org.liquibase.gradle.LiquibaseExtension.() -> Unit): Unit =
(this as org.gradle.api.plugins.ExtensionAware).extensions.configure("liquibase", configure)
But take a look at the file where it is defined. In my case it is ~/.gradle/caches/6.3/gradle-kotlin-dsl-accessors/cmljl3ridzazieb8fzn553oa8/cache/src/org/gradle/kotlin/dsl/Accessors39qcxru7gldpadn6lvh8lqs7b.kt. It is definitelly an auto-generated file. A few levels upper in a file tree — at ~/.gradle/caches/6.3/gradle-kotlin-dsl-accessors/ in my case — there are dozens of similar directories. I guess, one by every plugin/version I've ever used with Gradle 6.3. Here is another one for the Detekt plugin:
fun org.gradle.api.Project.`detekt`(configure: io.gitlab.arturbosch.detekt.extensions.DetektExtension.() -> Unit): Unit =
(this as org.gradle.api.plugins.ExtensionAware).extensions.configure("detekt", configure)
So, we have a bunch of .kt files defining all that extensions for different plugins applied to the project. That files are obviously pre-cached and precompiled and their content is available in build.gradle.kts. Indeed, you can find classes directories beside those sources.
The sources are generated based on the content of the applied plugins. It is probably a tricky task that includes some magic, reflection and introspection. Sometimes this magic doesn't work (due too chatic Groovy nature) and then you need to use some crappy DSL from this package.
How are they generated? I see no other way, but to
Parse the build.script.kts with an embedded Kotlin compiler / lexer
Extract all the plugins sections
Compile them, probably against some mocks (remember that Project is not yet available: we're not executing the build.gradle.kts itself yet!)
Resolve the declared plugins from Gradle Plugin repository (with some nuances coming from settngs.gradle.kts)
Introspect plugin's artifacts
Generate the sources
Compile the sources
Add the resulting classes to the script's classpath
And here is the gotcha: there is a very limited context (classpath, classes, methods — call it whatever) available when compiling the plugins block. Actually, no plugins are yet applied! Because, you know, you're parsing the block that applies plugins. Chickens, eggs, and their problems, huh…
So, and we're getting closer to the answer on your question, to provide custom DSL in plugins block, you need to modify that classpath. It's not a classpath of your build.gradle.kts, it's the classpath of the VM that parses build.gradle.kts. Basically, it's Gradle's own classpath — all the classes bundled in a Gradle distribution.
So, probably the only way to provide really custom DSLs in plugins block is to create a custom Gradle distribution.
EDIT:
Indeed, totally forgot to test the buildSrc. I've created a file PluginExtensions.kt in it, with a content
inline val org.gradle.plugin.use.PluginDependenciesSpec.`jawa`: org.gradle.plugin.use.PluginDependencySpec
get() = id("org.gradle.war") // Randomly picked
inline fun org.gradle.plugin.use.PluginDependenciesSpec.`jawa`(): org.gradle.plugin.use.PluginDependencySpec {
return id("org.gradle.cunit") // Randomly picked
}
And it seems to be working:
plugins {
jawa
jawa()
}
However, this is only working when PluginExtensions.kt is in the default package. Whenever I put it into a sub-package, the extensions are not recognized, even with an import:
Magic!

The kotlin function is just a simple extension function wrapping the traditional id method, not hard to define:
fun PluginDependenciesSpec.kotlin(module: String): PluginDependencySpec =
id("org.jetbrains.kotlin.$module")
However, this extension function is part of the standard gradle kotlin DSL API, which means it's available without any plugin. If you want to make a custom function like this available, you would need a plugin. A plugin to load your plugin. Not very practical.
I also tried using the buildSrc module to make an extension function like the above. But it turns out that buildSrc definitions aren't even available from the plugins DSL block, which has a very constrained syntax. That wouldn't have been very practical anyway, you would have needed to make a buildSrc folder for every project in which you have wanted to use the extension.
I'm not sure if this is possible at all. Try asking on https://discuss.gradle.org/.

Related

How can I refer to libraries defined in a shared Gradle build plugin from another build script?

I'm trying to define libraries in a common location. So in an our.libraries.gradle.kts script in a shared build plugin, I have this:
inner class Libraries {
val junit get() = ...
val junitParams get() = ...
}
val libraries = Libraries()
project.extra["libraries"] = libraries
In one of the Groovy build scripts elsewhere in the project, this is referred to like this:
allprojects {
dependencies {
testImplementation libraries.junit
}
}
This works fine.
So I try converting that script to Kotlin:
allprojects {
dependencies {
"testImplementation"(libraries.junit)
}
}
And now this fails, because it can't see the libraries property on the project, so I try explicitly pulling it out at the start:
val libraries: Libraries by project.extra
allprojects {
dependencies {
"testImplementation"(libraries.junit)
}
}
But this doesn't work either, because the script can't find the Libraries class.
I also tried putting Libraries in Libraries.kt, but then I can't seem to call methods like exclude using named parameters because for whatever reason Gradle doesn't support using the DSL when it's moved to a top-level class.
This is sort of similar to this question, but in the case of wanting to put in simple types, everything works fine. Which is to say, I can put the libraries in as a Map, but then any time I want to reference one, I have to write this:
"testImplementation"(libraries["junit"]!!)
This is obviously ugly, so I have been trying to avoid it.
So I'm stumped again.
This is part of a long saga trying to get this to work in many different ways, so the essential question is still the same: how can we define all our libraries in one location, and then refer to those in a type-safe way from other build scripts?
Recently, Gradle added shared dependencies via a TOML file, but that method only supports the version numbers, whereas our library definitions also include the excluded dependencies.
It was hard to put a completely self-contained example in the question because multiple files are involved, so here's a test repo.

How to implement custom platform logic using Kotlin Multiplatform feature?

Kotlin Multiplatform is a good feature to build multiplatform applications, but currently it is (likely) restricted to be intrinsic in Kotlin Multiplatform ecosystem. Can I implement custom build logic to extend the resolution strategy of expect, actual and the like? Or to say treat these features as a general concept of multiplatform, but have different behaviors during build process. Gradle work is welcomed.
For example, if the related extension points were available, one could write a Kotlin compiler plugin to resolve those expect/actual endpoints and maybe compose them into actually platform-specific runtime logic, and then write a Gradle plugin to ultimately process these artifacts.
So if there were two "multiplatform" scenes where both use jvm as "backend", but provide different api with the same or similar logic as "frontend", one could do as above to provide benefits which Kotlin Multiplatform does - write once, run anywhere.
I'd prefer to call this "api-layer multiplatform", to differ that Kotlin Multiplatform is "system-layer multiplatform". "Platform" could be a more abstract one.
So here is what the producer does, just like Kotlin Multiplatform:
build.gradle.kts:
plugins {
kotlin("jvm")
id("<multiplatform-plugin-id>") // Comes with Kotlin compiler plugin too
}
dependencies {
api("<common-dependency-notation>") // Another multiplatform library
}
common module:
fun hello() {
val logger = serviceLogger // Using api from that another multiplatform library
logger.info("Hello")
}
expect fun hookOnStart(block: () -> Unit) // Needs to provide platform-specific implementations
platform module:
actual fun hookOnStart(block: () -> Unit) { // Imaginary
ClientEvents.START.register(block)
}
anotherPlatform module:
actual fun hookOnStart(block: () -> Unit) { // Imaginary
val event = EventFactory.once(ClientStartEvent::class.java, block)
GlobalEventHandler.register(event)
}
As said before, after build, each platform will have its own artifact prepared for runtime or provided as library. He benefits from that another multiplatform library because he could provide each platform with same features through sharing code.
And the following is what the consumer does: (Let's say he's on platform)
build.gradle.kts
plugins {
kotlin("jvm")
}
dependencies {
implementation("<previous-common-dependency-notation>") // From the previous author, mapped to `platform` version
}
Bussiness logic:
fun runBussiness() {
hello()
hookOnStart { serviceLogger.info("world!") }
}
This is pretty uncharted territory and without any documentation.
I'd investigate the source code of the kotlin-multiplatform gradle plugin more in-depth and see if you can extend the existing target palette and expect/actual behaviour.
I'd guess that the plugin isn't really built for this kind of extension, but if you have solid reasons, you could probably submit feature requests and work on a local fork in the meantime.
Update:
If I understood your use-case correctly, you'd like to extend the expect/actual mechanism, which is currently a target/platform based abstraction?
I believe a more general way of making abstractions, such as using interfaces, could serve you. However, I can see the added compile-time safety benefits you seek 🤔, not sure what changes that'd need in the kotlin-multiplatform plugin and if JetBrains team would like that direction. Maybe something Artyom Degtyarev or someone from the JetBrains team could answer?

Using GroovyDSL with #TypeChecked in IntelliJ IDEA: Build Project fails

I have a jenkins.gdsl file defining some bindings I'm using in my Groovy script. In addition, I'd like to use the #TypeChecked annotation on my methods to get some guarantees about built code.
My jenkins.gdsl file looks like:
contributor(context(scope: scriptScope())) {
// some definitions
}
And then my script.groovy looks like:
#TypeChecked(extensions='jenkins.gdsl')
void doStuff() {
// ...
}
IntelliJ IDEA autocomplete works, but when building my project I get an error in my jenkins.gdsl file:
Error:Groovyc: groovy.lang.MissingMethodException: No signature of method: org.codehaus.groovy.transform.stc.GroovyTypeCheckingExtensionSupport.scriptScope() is applicable for argument types: () values: []
Removing (extensions='jenkins.gdsl') gets rid of this error, but then I lose my GDSL definitions when building, so that's a no-go.
It feels like the solution would involve bringing in IntelliJ's standardDsls. I am not at all sure how to do this, or whether it is in fact the correct approach.
#TypeChecked is a Groovy compiler annotation that can run some code during compilation.
But gdsl is an IntelliJ IDEA-specific script that's used only by the IDE to provide some completion and other coding assistance. It doesn't have anything in common with the compiler, and neither of those know anything of each other. So you can remove the extensions value, as it won't provide any typechecking during compilation.

Gradle: automatically add a test dependency on a project

In a multiproject build, some projects depend on others, and the latter provide not only compile/runtime libraries, but also useful test libs (such as, for instance, "mock" implementations of the components they provide).
I know of a couple of ways to make the test sources of one project available to another. They are discussed, for instance as answers to this question.
What I am looking for is some magical way to make this happen automatically, so that if a subproject adds a dependency on another subproject, it automatically gets that projects test sources added to the testCompile config.
I tried the naive approach:
configure(rootProject.subprojects.findAll {
it.configurations.getByName("compile")
.dependencies.any {
it.name == project.name
}
}) {
dependencies {
testCompile project.sourceSets.test.output
}
}
But this does not work ... presumably, because this code is evaluated "at the wrong stage" (or whatever the correct lingo is), and the other projects
don't "know" yet that they depend on this one.
I also tried putting (an equivalent of) this at the end of root build file (hoping, that everything else would already be configured by then), but that did not work either.
Is there a way to do what I am looking for here?
I also tried putting (an equivalent of) this at the end of root build file
The order of declaration in a build.gradle does not matter. Gradle build lifecycle has two phases, configuration and execution. In configuration, the various build.gradle files are read and a graph of execution order is created based on implicit and explicit dependencies among the various tasks.
Normally the root build.gradle is evaluated first, but it is possible to force the child projects to be evaluated first using evaluationDependsOnChildren()
Instead of position in the buildscripts, you can listen for various events of the buildcycle, to run something at certain points. In your case, you want to run your snippet once all projects are evaluated, using an afterEvaluate() block. See example here.
Some possible alternatives to your overall approach:
Add testCompile dependency from the downstream project instead of injecting from the upstream project. Add this to root build.gradle:
subprojects {
project.configurations.getByName("compile").dependencies.each {
if (it.name == foo){ // could be expanded to if it.name in collection
//inherit testCompile
project.configurations.dependencies.testCompile += it.sourceSets.test.output
}
}
}
(pseudo-code, haven't tested)
Separate out the shareable test/mock components into a separate mock-project that you can add as testCompile dependency to both upstream and downstream projects

How to generate a kotlin file from an annotation processor?

I have a java annotation processor which generates a bunch of java files during compilation. I'd like to make the generated classes nicer to use in kotlin by adding extension methods. I've been told on the kotlin forums that something I could try would be to write a kotlin file that contains my extension functions. I've tried this, I used the Filer object to create this file outputting it to the StandardLocations.SOURCE_OUTPUT directory. Intellij can see my generated class, and I can use the extension functions as intended, but the app won't compile because the compiler can't find the new kotlin file. Is there any way I can write a new kotlin file that'll get picked up by the kotlin compiler?
For kapt you can get source folder via.
Map<String, String> options = processingEnv.getOptions();
String generatedPath = options.get("kapt.kotlin.generated");
String path = generatedPath
.replaceAll("(.*)tmp(/kapt/debug/)kotlinGenerated",
"$1generated/source$2");
Unfortunately it doesn't work for kapt2 (see issue KT-14070)
You also can create .kt files via resource writer
Writer w = processingEnv.getFiler().createResource(SOURCE_OUTPUT, "package_name", "Sample.kt")
But for now you need to invoke compiler twice cause compileDebugKotlin task runs before invoking javax annotation processor by compileDebugJavaWithJavac task)
Output your files (with proper package names) into a directory like src/build/generated-src/kotlin/your/package/File.kt
and add this to your build.gradle:
sourceSets {
main.java.srcDirs += 'build/generated-src/kotlin'
}