This is for a JVM app.
I am creating a properties file with the version to use in the app runtime. I wrote this task in the base project build.gradle.kts
:
allprojects {
//...
tasks.register("createVersionPropertiesFile") {
group = "build"
doLast {
val resourcesDir = File("$buildDir/resources/main").apply { mkdirs() }
val resource = File(resourcesDir, "version.properties")
resource.writeText("version=${project.version}")
}
}
}
If I run this task independently, it creates the file as I expect.
To run the task during the build, I put this in the app module's build.gradle.kts
:
tasks.processResources {
dependsOn += tasks["createVersionPropertiesFile"]
}
However, during the build process, the resources directory is actually removed and not created (or if it is, it gets deleted again before the build is complete). So, the resource isn't found when the app runs.
How can I set up this task to run at the appropriate stage of the build process?
I know an alternate strategy would be to create the resource file in the actual resources directory of the source code, but I don't want to dirty the checked in source code with this auto-generated file. I want it to be only a part of the build.
The build/resources/
dir is an output directory of the processResources
task, so (as per the Javadoc) any other files will be removed automatically.
There's a better way to hook up a task so Gradle will recognise it as source file, and make Gradle automatically run the file.
For simplicity I'll demonstrate how to set up createVersionPropertiesFile
in a single build.gradle.kts
, without the allprojects {}
config.
First, make sure in your generation task you register an output directory, as well as the inputs. This can be done using the runtime API. Tasks have a default tempDirectory
that's perfect for using as an output directory, but you can use a custom directory if you like.
The project version should be set as an input property of the task, so that Gradle knows it needs to re-run the task if the value changes. To be compatible with Gradle's Configuration Cache (project
mustn't be used during task execution), it should be defined as a val
inside the task configuration block.
val createVersionPropertiesFileTask = tasks.register("createVersionPropertiesFile") {
group = "build"
// define a local val, to be compatible with Configuration Cache
val projectVersion = project.version
// Register the project version as a task input, so Gradle can cache the task
inputs.property("projectVersion", projectVersion)
// tell Gradle about the output directory
outputs.dir(temporaryDir)
doLast {
val resource = File(temporaryDir, "version.properties")
resource.writeText("version=${projectVersion}")
}
}
The docs explain more about why registering inputs and outputs is beneficial. What's important here is that, thanks to Gradle's Provider API, a task can be converted into a file-provider.
Java projects are organised using SourceSets. These come preconfigured with the src/main/java
and src/test/resources
that we know and love, and it's possible to add more:
sourceSets {
main {
java {
// add an additional source directory
srcDir("$buildDir/some-extra-sources/java")
}
resources {
// add an additional resource directory
srcDir("$buildDir/some-extra-sources/resources")
}
}
}
While you could hardcode the output directory of createVersionPropertiesFile
to be an additional resource directory, Gradle wouldn't know that it needs to run the task to generate the files. We can have a two-for-one deal: add a new resource directory, and tell Gradle it needs to run the task.
sourceSets {
main {
resources {
// add a new resource dir that is produced by the task
srcDir(createVersionPropertiesFileTask.map { it.temporaryDir })
}
}
}
Note that there's no need to add any dependsOn(createVersionPropertiesFileTask)
- so you can remove the configuration for tasks.processResources
.
Now when you run ./gradlew build
, you'll see your custom file will be generated into build/resources
by the processResources
task.
Another little bonus if you're using IntelliJ: it will highlight the new resource directory in the Project explorer sidebar.
Something you might find problematic is using allprojects {}
to configure this task for all projects. (There are a few other reasons why allprojects {}
and subprojects {}
are discouraged.)
Instead, consider creating a convention plugin that creates and configures createVersionPropertiesFileTask
in a few easy steps using buildSrc:
create buildSrc/build.gradle.kts
and, since buildSrc is effectively a stand-alone project, buildSrc/settings.gradle.kts
Apply the kotlin-dsl
plugin in buildSrc/build.gradle.kts
, so we can make pre-compiled script plugins.
Create a file for your convention, using the config provided above.
// ./buildSrc/src/main/kotlin/my-java-conventions.gradle.kts
plugins {
java
}
val createVersionPropertiesFileTask = tasks.register("createVersionPropertiesFile") {
group = "build"
val projectVersion = project.version
inputs.property("projectVersion", projectVersion)
outputs.dir(temporaryDir)
doLast {
val resource = File(temporaryDir, "version.properties")
resource.writeText("version=${projectVersion}")
}
}
sourceSets {
main {
resources {
srcDir(createVersionPropertiesFileTask.map { it.temporaryDir })
}
}
}
Now, in all subprojects, you can specifically apply the convention plugin where it is required - no more need for allprojects {}
// ./app-subproject/build.gradle.kts
plugins {
`my-java-conventions`
}
Since you're generating a .properties
file, you might also want to consider using the built-in task type for generating properties - WriteProperties. It has some utilities for adding multiple properties, and making sure the generated file is re-producible (so that Gradle's caching works well.)