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)
}
}