I'm trying to add a new configuration option when using the gradle ScalaTest plugin:
https://github.com/maiflai/gradle-scalatest
In its source code, the config was injected into the Test class as a dynamic extension:
static void configure(Test test) {
...
Map<String, ?> config = [:]
test.extensions.add(ScalaTestAction.CONFIG, config)
test.extensions.add("config", { String name, value -> config.put(name, value) })
test.extensions.add("configMap", { Map<String, ?> c -> config.putAll(c) })
...
}
If using groovy as the dsl, calling this property is easy:
test {
configMap([
'db.name': 'testdb'
'server': '192.168.1.188'
])
}
unfortunately the kotlin dsl can't use this method due to static typing, when being invoked as a test plugin, it is clearly visible within the test scope, e.g. when using extensions.getByName:
tasks {
test {
val map = extensions.getByName("configMap")
println(map)
}
}
It yields the following output:
...
> Configure project :
com.github.maiflai.ScalaTestPlugin$_configure_closure6#45c21cac
But there is no way to retrieve or assert its type in compile time, and it ends up being useless (unless reflection is used, which is against the design philosophy of kotlin dsl). Is there a easy way for kotlin dsl to achieve the same?
I saw in the Scala test gradle plugin that the dynamic extension is defined like this:
test.extensions.add("configMap", { Map<String, ?> c -> config.putAll(c) })
The com.github.maiflai.ScalaTestPlugin$_configure_closure6#45c21cac you saw should be a closure of type (Map<String, Any>) -> Unit, which means you can do that. We'll have to change the map values so let's assume that it's also mutable.
extensions.getByName("configMap").closureOf<MutableMap<String, Any?>> {
this["db.name"] = "testdb"
this["server"] = "192.168.1.188"
}
This builds fine but I don't have Scala installed and never used Scala test. I have no idea if it actually works, so please tell me.
Related
I'm trying to develop a Gradle plugin for a language I use (SystemVerilog). I'm still experimenting and figuring things out. Before I write the entire thing as a plugin, I thought it would be best to try out the different parts I need inside a build script, to get a feel of how things should work.
I'm trying to define a container of source sets, similar to how the Java plugin does it. I'd like to be able to use a closure when configuring a source set. Concretely, I'd like to be able to do the following:
sourceSets {
main {
sv {
include '*.sv'
}
}
}
I defined my own sourceSet class:
class SourceSet implements Named {
final String name
final ObjectFactory objectFactory
#Inject
SourceSet(String name, ObjectFactory objectFactory) {
this.name = name
this.objectFactory = objectFactory
}
SourceDirectorySet getSv() {
SourceDirectorySet sv = objectFactory.sourceDirectorySet('sv',
'SystemVerilog source')
sv.srcDir("src/${name}/sv")
return sv
}
SourceDirectorySet sv(#Nullable Closure configureClosure) {
configure(configureClosure, getSv());
return this;
}
}
I'm using org.gradle.api.file.SourceDirectorySet because that already implements PatternFilterable, so it should give me access to include, exclude, etc.
If I understand the concept correctly, the sv(#Nullable Closure configureClosure) method is the one that gives me the ability to write sv { ... } to configure via a closure.
To add the sourceSets property to the project, I did the following:
project.extensions.add("sourceSets",
project.objects.domainObjectContainer(SourceSet.class))
As per the Gradle docs, this should give me the possibility to configure sourceSets using a closure. This site, which details using custom types, states that by using NamedDomainObjectContainer, Gradle will provide a DSL that build scripts can use to define and configure elements. This would be the sourceSets { ... } part. This should also be the sourceSets { main { ... } } part.
If I create a sourceSet for main and use it in a task, then everything works fine:
project.sourceSets.create('main')
task compile(type: Task) {
println 'Compiling source files'
println project.sourceSets.main.sv.files
}
If I try to configure the main source set to only include files with the .sv extension, then I get an error:
sourceSets {
main {
sv {
include '*.sv'
}
}
}
I get the following error:
No signature of method: build_47mnuak4y5k86udjcp7v5dkwm.sourceSets() is applicable for argument types: (build_47mnuak4y5k86udjcp7v5dkwm$_run_closure1) values: [build_47mnuak4y5k86udjcp7v5dkwm$_run_closure1#effb286]
I don't know what I'm doing wrong. I'm sure it's just a simple thing that I'm forgetting. Does anyone have an idea of what that might be?
I figured out what was going wrong. It was a combination of poor copy/paste skills and the fact that Groovy is a dynamic language.
First, let's look at the definition of the sv(Closure) function again:
SourceDirectorySet sv(#Nullable Closure configureClosure) {
configure(configureClosure, getSv());
return this;
}
Once I moved this code to an own Groovy file and used the IDE to show me what is getting called, I noticed that it wasn't calling the function I expected. I was expecting a call to org.gradle.util.ConfigureUtil.configure. Since this is part of the public API, I expected it to be imported by default in the build script. As this page states, this is not the case.
To solve the issue, it's enough to add the following import:
import static org.gradle.util.ConfigureUtil.configure
This will get rid of the cryptic closure related error. It is replaced by the following error, though:
Cannot cast object 'SourceSet_Decorated#a6abab9' with class 'SourceSet_Decorated' to class 'org.gradle.api.file.SourceDirectorySet'
This is caused by the copy/paste error I mentioned. When I wrote the SourceSet class, I drew heavily from org.gradle.api.tasks.SourceSet (and org.gradle.api.internal.tasks.DefaultSourceSet). If we look at the java(Closure) method there, we'll see it has the following signature:
SourceSet java(#Nullable Closure configureClosure);
Notice that it returns SourceSet and not SourceDirectorySet like in my code. Using the proper return type fixes the issue:
SourceSet sv(#Nullable Closure configureClosure)
With this new return type, let's look again at the configuration code for the source set:
sourceSets {
main {
sv {
include '*.sv'
}
}
}
Initially, I thought it was supposed to work as follows: pass main { ... } as a Closure to sourceSets, pass sv { ... } as a Closure to main, and handle the include ... part inside sourceDirectorySet. I banged my head against the wall for a while, because I couldn't find any code in that class hierarchy that takes closures like this.
Now, I think the flow is slightly different: pass main { ... } as a Closure to sourceSets (as initially thought), but call the sv(Closure) function on main (of type sourceSet), passing it { include ... } as the argument.
Bonus: There was one more issue that wasn't related to the "compile" errors I was having.
Even after getting the code to run without errors, it still wasn't behaving as expected. I had some files with the *.svh extension that were still getting picked up. This is because, when calling getSv(), it was creating a new SourceDirectorySet each time. Any configuration that was done previously was getting thrown away each time that this function was called.
Making the sourceDirectorySet a class member and moving its creation to the constructor fixed the issue:
private SourceDirectorySet sv
SourceSet(String name, ObjectFactory objectFactory) {
// ...
sv = objectFactory.sourceDirectorySet('sv',
'SystemVerilog source')
sv.srcDir("src/${name}/sv")
}
SourceDirectorySet getSv() {
return sv
}
I'm using Gradle 6.2.2 with this plugin: com.gorylenko.gradle-git-properties (version 2.2.2). I'm trying to "translate" the following snippet into Kotlin DSL:
gitProperties {
extProperty = "gitProps" // git properties will be put in a map at project.ext.gitProps
}
shadowJar {
manifest {
attributes(
"Build-Revision": "${ -> project.ext.gitProps["git.commit.id"]}" // Uses GString lazy evaluation to delay until git properties are populated
)
}
}
...but this what I've come up with so far:
gitProperties {
extProperty = "gitProps"
keys = listOf("git.branch", "git.build.host", "git.build.version", "git.commit.id", "git.commit.id.abbrev",
"git.commit.time", "git.remote.origin.url", "git.tags", "git.total.commit.count")
}
tasks {
withType<ShadowJar> {
manifest.attributes.apply {
put("Build-Revision", "${project.ext.properties["git.commit.id"]}")
}
}
}
I can't figure out to make the "GString lazy evaluation" part working in Kotlin DSL, nor how the gitProps map fits on here; eventually that approach (which I know it's partially wrong) is returning null. Any ideas?
The below Kotlin syntax worked for me:
put("Build-Revision", object {
override fun toString():String = (project.extra["gitProps"] as Map<String, String>)["git.commit.id"]!!
})
I think you have some confusion over where and how the data is being stored, and in particular when it's available.
I just got hold of this plugin and had a look at it: it supplies a project extension, which you're configuring to specify why extras property to populate, and a task: "generateGitProperties". This task is added as a dependency for the "classes" task, so it's already run once you get to "shadowJar"
The issue is that figuring out the git properties and populating the extra properties only happens when that task is executed, so they're not available when the build is configured, hence the need for the lazy GString shenanigans to pass a lazy value down into the shadowJar configuration that will only be evaluated once shadowJar executes.
You can get hold of the extra properties like this:
tasks.register("example") {
dependsOn("generateGitProperties")
doFirst {
val gitProps: Map<String, String> by project.ext
for ((name, value) in gitProps) {
println("GIT: $name -> $value")
}
}
}
That works because it's in a "doFirst" block, so it's happening at task execution time, not configuration time. So essentially, you could emulate the "lazy GString" stuff. Something like this:
withType<Jar>().configureEach {
val lazyCommitId = object {
override fun toString(): String {
val gitProps: Map<String, String> by project.ext
return gitProps["git.commit.id"] ?: ""
}
}
manifest {
attributes["Git-Commit-Id"] = lazyCommitId
}
}
I did this just for "jar", but "shadowJar" is just a subtype of a Jar task anyway.
I'm migrating Gradle build scripts from Groovy to Kotlin DSL and one of the things which is not really documented is how to populate extra properties.
In Groovy, I can write:
ext {
comp = 'node_exporter'
compVersion = '0.16.0'
compProject = 'prometheus'
arch = 'linux-amd64'
tarball = "v${compVersion}/${comp}-${compVersion}.${arch}.tar.gz"
downloadSrc = "https://github.com/${compProject}/${comp}/releases/download/${tarball}"
unzipDir = "${comp}-${compVersion}.${arch}"
}
I figured out that in the Kotlin DSL, I can achive the same functionality with:
val comp by extra { "filebeat" }
val compVersion by extra { "6.4.0" }
val arch by extra { "linux-x86_64" }
val tarball by extra { "${comp}-${compVersion}-${arch}.tar.gz" }
val downloadSrc by extra { "https://artifacts.elastic.co/downloads/beats/${comp}/${tarball}" }
val unzipDir by extra { "${comp}-${compVersion}-${arch}" }
which looks pretty repetitive.
Implementation of ExtraPropertiesExtension in Kotlin is a little bit complex, but at the end, it holds just plain old Map<String, Object>.
So, my question: is it possible to populate extra object with multiple properties more easily, than just repeating val myProp by extra { "myValue"}?
According the current (5.2.1) documentation:
All enhanced objects in Gradle’s domain model can hold extra user-defined properties. This includes, but is not limited to, projects, tasks, and source sets.
Extra properties can be added, read and set via the owning object’s extra property. Alternatively, they can be addressed via Kotlin delegated properties using by extra.
Here is an example of using extra properties on the Project and Task objects:
val kotlinVersion by extra { "1.3.21" }
val kotlinDslVersion by extra("1.1.3")
extra["isKotlinDsl"] = true
tasks.register("printExtProps") {
extra["kotlinPositive"] = true
doLast {
// Extra properties defined on the Project object
println("Kotlin version: $kotlinVersion")
println("Kotlin DSL version: $kotlinDslVersion")
println("Is Kotlin DSL: ${project.extra["isKotlinDsl"]}")
// Extra properties defined on the Task object
// this means the current Task
println("Kotlin positive: ${this.extra["kotlinPositive"]}")
}
}
You don't have to use delegation, just write extra.set("propertyName", "propertyValue"). You can do this with an apply block if you want:
extra.apply {
set("propertyName", "propertyValue")
set("propertyName2", "propertyValue2")
}
I'm using play-application plugin, which in turn use gradle rule based model configuration. build.gradle.kts looks like this:
plugins {
`play-application`
}
/* the snippet does not work
model {
components {
play {
platform play: playVersion, scala: scalaVersion, java: javaVersion
injectedRoutesGenerator = true
}
}
}
*/
// this works instead
apply(from = "play_setup.gradle")
val setup: groovy.lang.Closure<Any?> by extra
setup(project, jVersion, scalaVersion, playVersion)
where play_setup.gradle is:
ext.setup = { project, javaVersion, scalaVersion, playVersion ->
model {
components {
play {
platform play: playVersion, scala: scalaVersion, java: javaVersion
injectedRoutesGenerator = true
}
}
}
}
Is there a way to stop using groovy for plugins made with rule based model and configure them directly via kotlin-DSL?
Not according to the limitations listed in the Gradle Kotlin DSL Primer.
The Kotlin DSL will not support the model {} block, which is part of
the discontinued Gradle Software Model. However, you can apply model
rules from scripts — see the model rules sample for more information
The link to the model rules sample is broken in the documentation, but I fixed it above.
I'm writing a custom Gradle plugin that will create a new Test task and run it. I need to set some configuration on this test task.
The plugin is written in Java and the code to execute the Test task looks like this:
private void runSmokeTests() {
Test test = new Test();
test.useTestNG(new Closure(/* What goes in here? */) {
// and here? How do I get hold of TestNGOptions?
});
test.executeTests();
}
I can't figure out how to use the Closure class from Java.
Simplest option is just to call getOptions() and cast to the appropriate type.
test.useTestNG();
TestNGOptions options = (TestNGOptions) test.getOptions();
// configure options ie...
options.setPreserveOrder(true);