How to create a new file and add to classpath in Gradle?

I’ve been reading the documentation and they talk a lot about how to copy a file, but suppose I want to generate a fresh file and include it in my jar’s classpath like so:

val file = File("my-resource.txt")
file.writeText("Some example content")
// how do I now add `file` to my jar's classpath, e.g. adding it to src/main/resources?

I’m struggling to figure out what the best way is to achieve this.

My goal is to be able to communicate both my app version (stored in a VERSION file in the root project) and the app name (equal to the name of the root project) dynamically down to my code. I tried adding system properties/environment variables in the JavaExec task but this doesn’t work when creating and running a distribution.

My ideal would be to have my application start with both name and version provided via default system properties and/or environment variables. Then I don’t have to write any additional code and I can just grab that information with System.getProperty("app.name"). I currently don’t think that is feasible so my next idea is to create a .properties file and add it to resources, which I can then load first thing in my main function with a one-liner. Using properties like this is nice because it would scale well, and once I can pass one property down like this, I can easily pass down more data should I want to going forward.

My current method is to simply set the application name manually in a constant (it wont change often), and then to copy the VERSION file into my jar’s classpath via the processResources task. This is adequate for now, but I’m wondering if there is a better way that would scale up better? I know I could write a custom task for this but my gut is telling me there is a better way.

Let me start with "don’t use File("some/relative/path") ever anywhere.
There is almost not use-case where this is appropriate.
The only case I’m aware where this is appropriate is, if you coded a CLI program where that relative path is given on the commandline are argument, or if you absolutely control the execution environment.

File constructor with a relative path - as well as practically all other places accepting a relative path like Path.of(...) or FileInputStream(...) and so on - all resolve the path relative to the current working directory of the user. So unless you process a relative path given by the user where you can assume that it is relative to his working directory or if you absolutely control the CWD like by using cd to the script directory in a start script or similar, you should never use a relative path like that as it can work fine while you test and happen to have the right working directory, but will fail as soon as someone calls it that has an unexpected CWD.

If you do this in build-logic, your build is inherently broken.
Currently, often the root project directory is the CWD, but this is not guaranteed, and sometimes it is the daemon log directory, sometimes it is the IDE installation directory, and so on.
In the future it will maybe be consistent, but then consistent the daemon log directory, not the project directory as changing the CWD of the Gradle daemon process is a flaky operation that can work or can not work and is not reliable.

In build logic, always use file(...) for example on project or settings (implicitly or explicitly) or use something like layout.projectDirectory.file(...).


Also you said you don’t want to use a custom task.
If you just meant you don’t want to use a full custom task class in an included build-logic build or similar but you still use an ad-hoc task and do it in its execution phase, that’s fine.
But you should not do this in the configuration phase of your build, as you would then write that file everytime your project is configured, even if the file is not needed or up-to-date, and if you use the configuration cache (you really should) then maybe the file writing is not done at all or you cannot reuse the configuration cache entry but reconfigure the whole project which wastes your time.


If you really want to create a fresh file out of the blue, you should really use a task even if it is an ad-hoc task. And you should always properly declare the inputs and outputs of the task like for each and every task, so that it can also be up-to-date.

With the inputs and outputs properly configured, you can then register the task as (re)source directory, so that its outputs are automatically used for each consumer of (re)sources like for example also the sources jar task, and all such tasks will automatically have the necessary task dependencies automatically.


If you want to write a properties file, you should neither just use writeText nor use a Properties instance and save it, but you should simply use the existing WriteProperties task type which does everything automatically properly, including producing sorted and thus reliable reproducible content, escpacing special characters, and not writing a timestamp comment.

For example something like

val generatePropertes = tasks.register<WriteProperties>("generatePropertes") {
    property("foo", project.version)
    destinationFile = layout.buildDirectory.file("my.properites")
}
sourceSets.main {
    resources.srcDir(generatePropertes)
}

What I usually do - especially if I know there are no special characters in the values - I just add a template file like src/main/resources/my.properties with content

foo = $version

and then just configure the processResources task to fill in the placeholders like

tasks.processResources {
    inputs.property("version", project.version)
    filteringCharset = ISO_8859_1.name()
    filesMatching("my.properties") {
        expand("version" to project.version)
    }
}