How do I add new type-safe accessors to a Gradle Project? - kotlin

We have a plugin which defines additional properties and adds them as extension properties, like:
project.extra["copyright"] = "Copyright ..."
Then in the build scripts, I can access this like:
project.extra["copyright"]
I'd like to just write:
project.copyright
Some Gradle plugins seem to do something like this. I can access project.sourceSets or project.kotlin even though those certainly aren't in the Project interface.
Using an IDE, I can drill into those convenience methods, which then lands me in some autogenerated code, so I know it's being autogenerated somewhere, but I haven't been able to find any clues to how to get this to happen for our own plugin. The Gradle docs mention type-safe accessors which is ultimately what allowed me to phrase the question, but the docs don't say how to add new ones.
How do we get this treatment for our own plugin?

Creating DSL-like APIS is documented here: https://docs.gradle.org/current/userguide/implementing_gradle_plugins.html#modeling_dsl_like_apis.
It's pretty easy to do. Here's a quick guide, and some tips.
Define an extension.
import org.gradle.api.provider.Property
interface MyExtension {
val copyright: Property<String>
}
It looks pretty boring! What's important is that it's either be an abstract class, or an interface - this is so Gradle can create a new instance (see 'Managed types'), and this is where the Gradle magic begins.
Aside: I've used Property<String> instead of String, although both will work. I recommend using types compatible with Lazy Configuration.
Register the extension.
import org.gradle.api.*
abstract class MyPlugin : Plugin<Project> {
override fun apply(project: Project) {
val myExtension: MyExtension =
project.extensions.create("myPlugin", MyExtension::class.java)
}
}
This is the magic part. Simply by registering the extension against the project, Gradle will make the extension available and automatically generate Kotlin-DSL convenience methods.
Apply the plugin
// build.gradle.kts
plugins {
id("my-plugin")
}
myPlugin {
copyright.set("blah blah 2022")
}
Just like that, Gradle will generate Kotlin DSL accessors. Here's one of them:
// Accessorsajp3oxzka99ro52ctxwv0petb.kt
/**
* Configures the [myPlugin][MyExtension] extension.
*/
fun org.gradle.api.Project.`myPlugin`(configure: Action<MyExtension>): Unit =
(this as org.gradle.api.plugins.ExtensionAware).extensions.configure("myPlugin", configure)
Use the extension values
Going back to the Plugin definition, lets say you want to register your own task:
// a demo task
abstract class MyTask : DefaultTask() {
#get:Input
abstract val copyright: Property<String>
#TaskAction
fun run() {
println("Copyright is ${copyright.get()}")
}
}
(Note that this task, like the extension, is a 'managed type').
Now the custom task can be registered, and a default value for copyright set.
abstract class MyPlugin : Plugin<Project> {
override fun apply(project: Project) {
val myExtension: MyExtension = project.extensions.create("myPlugin", MyExtension::class.java)
project.tasks.register("myCustomTask", MyTask::class.java) {
copyright.set(myExtension.copyright)
}
}
}
It's good that both MyExtension and MyTask use Property<String> - the actual value will be evaluated lazily, and only if required.
Now if I run ./gradlew :myCustomTask, I see:
> Task :myCustomTask
Copyright is blah blah 2022
Further reading
What if you want to have multiple copyrights? Then you can create a configuration container
What if MyExtension has lots of properties and you want to provide them all to MyTask? Then you can use #Nested inputs
The extension properties can have default values.

Related

How do you set up a property in a custom gradle task?

I want to write a task that takes a directory from , does something with the files in it and writes the result into some other directory to.
I've been led to believe this was the way to define such a task (kotlin dsl):
package my.app
abstract class FooBarTask : DefaultTask() {
#get:InputDirectory
abstract val from: Property<Directory>
#get:OutputDirectory
abstract val to: Property<Directory>
#TaskAction
fun doSomething() {
println("Hakuna Matata")
}
}
now how do I set the from and to value in a groovy-based build.gradle?
def myTask = tasks.register('myTask', FooBarTask) {
from = layout.projectDirectory.dir("foo")
to = layout.buildDirectory.dir("bar")
}
this results in
Could not create task ':my-subproject:myTask'.
> Please use the ObjectFactory.directoryProperty() method to create a property of type Directory.
and it shouldn't.
How do you correctly define a directory property in a custom task?
Gradle has the specialized DirectoryProperty, that offers some additional functionality, compared to the plain Property<Directory> which is one of the implemented interfaces. So this specialized type should be used when declaring directory inputs/outputs.
I'm actually not a 100% sure what caused the error you saw.

Null property provided by Gradle when using custom plugin

I'm trying to follow the Gradle custom plugin documentation to create a plugin that can be configured.
My plugin code:
interface MyExtension {
var myValue: Property<String>
}
class MyPlugin : Plugin<Project> {
override fun apply(project: Project) {
val extension = project.extensions.create<MyExtension>("myExt")
}
}
in build.gradle.kts:
plugins {
`java-library`
}
apply<MyPlugin>()
the<MyExtension>().myValue.set("some-value")
Running this will give
Build file '<snip>/build.gradle.kts' line: 6
java.lang.NullPointerException (no error message)
Turns out the the<MyExtension>().myValue is null, so the set call fails. How do I do this correctly? Did I miss something in the documentation, or is it just wrong?
The documentation is not wrong. Properties can be managed by either you or by Gradle. For the latter, certain conditions have to be met.
Without managed properties
If you want to be completely in charge, you can instantiate any variables you declare yourself. For example, to declare a property on an extension that is an interface, it could look like this:
override fun apply(project: Project) {
val extension = project.extensions.create("myExt", MyExtension::class.java)
extension.myValue = project.objects.property(String::class.java)
}
Or you could instantiate it directly in the extension by making it a class instead:
open class MessageExtension(objects: ObjectFactory) {
val myValue: Property<String> = objects.property(String::class.java)
}
However, a property field is not really supposed to have a setter as the property itself has both a setter and a getter. So you should generally avoid the first approach and remove the setter on the second.
See here for more examples on managing the properties yourself.
With managed properties
To help you reduce boilerplate code, Gradle can instantiate the properties for you with what is called managed properties. To do use these, the property must not have a setter, and the getter should be abstract (which it implicitly is on an interface). So you could go back to your first example and fix it by changing var to val:
interface MyExtension {
val myValue: Property<String> // val (getter only)
}
Now Gradle will instantiate the field for you. The same thing works for abstract classes.
Read more about managed properties in the documentation here.

How to access `JavaToolchainSpec` from within custom Gradle task

According to "Toolchains for plugin authors" it should be possible to access the configured JavaToolchainSpec from within a custom task. I try to use this approach within a custom plugin which creates a task based on the presence of the JavaPlugin and queries the configured languageVersion property. Here is a minimal example.
build.gradle
plugins {
id 'application'
id 'com.example.myplugin'
}
...
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
}
MyPlugin.kt
class MyPlugin : Plugin<Project> {
override fun apply(target: Project) {
target.plugins.withType<JavaPlugin> {
target.tasks.create<MyTask>("mytask")
}
}
}
MyTask.kt
abstract class MyTask : DefaultTask() {
init {
val extension = project.extensions.getByType<JavaPluginExtension>();
val languageVersion = extension.toolchain.languageVersion.get();
...
}
}
Once Gradle creates MyTask and the languageVersion property is queried, the build fails with the following error.
Cannot query the value of property 'languageVersion' because it has no value available.
My guess is that I am accessing the extension too early and it has not set its values at this time. My question now is if there is a way to wire up the configured properties (ideally with lazy mechanisms) with the task.
apparently you have to configure the toolchain object in the java plugin extension to be able to use JavaToolchainService, using something like:
java {
toolchain {
languageVersion = JavaLanguageVersion.of(11)
}
}
if you don't do that Gradle will defaults to the current JVM and the whole toolchain API will be unavailable (leaving you with the error you reported)

Create a Gradle function for dependencies block in Kotlin

Currently, I'm creating a function, which is available for the dependencies block in Groovy with:
project.dependencies.ext.foo = { String value ->
project.files(extension.getFooDependency(project).jarFiles).asFileTree
}
Thanks to that, I'm able to do:
afterEvaluate {
dependencies {
compileOnly foo('junit')
}
}
I'm converting the Groovy code to Kotlin, and I'm wondering how to rewrite this foo extension.
I've ended up with:
project.dependencies.extensions.extraProperties.set("foo", Action { value: String ->
project.files(extension.getIdeaDependency(project).jarFiles).asFileTree
})
After calling foo('junit'), I get the following exception:
> Could not find method foo() for arguments [junit] on object of type org.gradle.api.internal.artifacts.dsl.dependencies.DefaultDependencyHandler.
I do not think that would work the same way in Kotlin DSL. Instead, you may declare a Kotlin extension function somewhere in the project. Then calling it would include all necessary receivers to you.
For multiple projects, I would recommend using a buildSrc project. Declarations there are visible to all project files below.
Speaking about Groovy and Kotlin support, I would do something like that:
private fun getFooImpl(scope: getFooImpl, name: String) { /*here is the implementation */ }
fun DependencyHandlerScope.getFoo(name:String) = getFooImpl(this, name)
//in Groovy
project.dependencies.extensions.extraProperties.set("foo", {getFooImpl(..)})
The same code could fit into a plugin as well. A more generic way could be to register a custom DLS extension, so to allow a custom block-like thisIsMyPlugin { .. } in the Gradle DSL and define all necessary helper functions in the extension class. Here the downside is in forcing users to wrap their code into the thisIsMyPlugin block.

how to configure build.gradle.kts to fix error "Duplicate JVM class name generated from: package-fragment"

I'm trying to follow this tutorial https://dev.to/tagmg/step-by-step-guide-to-building-web-api-with-kotlin-and-dropwizard and am instead writing my gradle.build file in Kotlin's DSL and am finding there is no direct mapping from Groovy to Kotlin and I'm now getting this error when running ./gradlew run:
(4, 1): Duplicate JVM class name 'dropwizard/tut/AppKt' generated from: package-fragment dropwizard.tut, package-fragment dropwizard.tut
plugins {
// Apply the Kotlin JVM plugin to add support for Kotlin on the JVM.
id("org.jetbrains.kotlin.jvm").version("1.3.31")
// Apply the application plugin to add support for building a CLI application.
application
}
repositories {
// Use jcenter for resolving dependencies.
// You can declare any Maven/Ivy/file repository here.
mavenCentral()
jcenter()
}
dependencies {
// Use the Kotlin JDK 8 standard library.
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
// Use the Kotlin test library.
testImplementation("org.jetbrains.kotlin:kotlin-test")
// Use the Kotlin JUnit integration.
testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
compile("io.dropwizard:dropwizard-core:1.3.14")
}
application {
// Define the main class for the application
mainClassName = "dropwizard.tut.AppKt"
}
tasks.withType<Jar> {
manifest {
attributes["Main-Class"] = application.mainClassName
}
from({
configurations.runtimeClasspath.get().filter { it.name.endsWith("jar") }.map { zipTree(it) }
})
}
tasks.named<JavaExec>("run") {
args("server", "config/local.yaml")
}
I cannot tell (yet) why this happens but to work around it add #file:JvmName("SomethingUnique") to your JVM file. Note that renaming the file will not help and lead to the same error. Only changing the output name will resolve it.
The JVM only knows how to load classes, so the Kotlin-to-JVM compiler generates classes to hold top-level val or fun declarations.
When you have two similarly named files
// src/commonMain/kotlin/com/example/Foo.kt
package com.example
val a = 1
and
// src/jvmMain/kotlin/com/example/Foo.kt
package com.example
val b = 2
the kotlin-to-JVM compiler generates
package com.example;
public class FooKt {
public static final int a = 1;
}
and
public com.example;
public class FooKt {
public static final int b = 2;
}
Obviously, these two files can't coexist in the same JVM ClassLoader, hence the error message.
Solutions involve:
As #Fleshgrinder noted, adding a file-level JvmName annotation to at least one to override the derived name, FooKt.
Renaming files to be different where possible.
Moving top-level val and fun declarations from those files into other files so Kotlin does not need to create the FooKt class.
Moving top-level val and fun declarations into objects or companion objects.