I have a custom task which has some inputs, and some outputs: it creates a directory, and then puts some files in it. Some of those files are important for other tasks, so I’d like to create an output property that I can wire into the input of those other tasks.
The names of the important output files are derived from the task’s inputs, and their full path could be found with something like outputDir.files("${input.get()}.dylib").
For the life of me, I can’t figure out how to successfully populate the @OutputFiles annotated property in my custom task. Here’s a simplified version of the task class:
I’ve also tried with setting the property when I define it:
@OutputFiles
abstract final ConfigurableFileCollection libraryFiles = project.objects.fileCollection().from(project.provider {
outputDir.map { it.files("${libName.get()}-${libVersion.get()}-eg.txt") }
} as Provider<FileCollection>)
Neither works, with different errors. The at-exec-time with the error that The value for this file collection is final and cannot be changed., and the at-creation-time with the error that Querying the mapped value of task ':buildThing' property 'outputDir' before task ':buildThing' has completed is not supported.
Why do you need libraryFiles at all?
You can just use the task itself as input for another task which makes all its output files used which seems to be what you want.
In the real case, there are multiple sets of output files, each required by a different task, so having each on its own property makes sense and stops the downstream tasks needing to filter the outputs: there’s a lot of stuff that gets generated in the output directory that should not be considered when checking if the output is up-to-date.
Generating a specific file output based on the specified output directory plus some of the inputs feels like it ought to be a straightforward way to do things, which is why I’m asking here - if my intuition here is wrong, I’d like to understand why, and what the expected way to achieve these kinds of things is…
In the real case, there are multiple sets of output files, each required by a different task, so having each on its own property makes sense and stops the downstream tasks needing to filter the outputs: there’s a lot of stuff that gets generated in the output directory that should not be considered when checking if the output is up-to-date.
Ok, yeah, in that case multiple output file properties are indeed a good idea.
if my intuition here is wrong, I’d like to understand why, and what the expected way to achieve these kinds of things is…
No, I think your intuition is right.
The at-exec-time with the error that
Well, yeah, setting it at execution time is waaay too late.
Gradle needs to know up-front which will be the inputs and outputs.
For example to do the up-to-date check.
And even if that would not be the case, if the task is up-to-date and thus not executed, your property would also not be filled.
the at-creation-time with the error that
This should be the right way if done properly.
I’ve had a look at your MCVE.
having the property abstract does not make much sense as you right away set a value I actually wonder that this even compiles
having a ConfigurableFileCollection is also not the best idea as the file collection should not be configured, but only provide a “view” on the output directory
Something like this is probably what you want to have:
@OutputFiles
final Provider<Set<FileSystemLocation>> libraryFiles = outputDir.flatMap {
it.files("${libName.get()}-${libVersion.get()}-eg.txt").elements
}
I had stumbled onto another way to get the result, which was not nearly as clean as yours, so this is much nicer, thank you. Best of all, I figured out where I was going wrong the first time thanks to your example!
My much less elegant solution came when I found that if I flipped the access order of the providers I was using, I could successfully create a ConfigurableFileCollection at configure time. That is, if I mapped the Directory property inside a map of a Property containing the filenames, it would work:
@Internal
final Provider<List<String>> libraryFilenames = libName.map { name ->
["eg.txt"].collect {suffix ->
"${name}-${libVersion.get()}-${suffix}"
}
}
@OutputFiles
final ConfigurableFileCollection libraryFiles = project.files(libraryFilenames.map { names ->
def dir = outputDir.get()
names.collect { name -> dir.file(name) }
})
What your approach showed me was that .map and .flatMap on the provider returned by Directory.files do different things:
If I use your example @OutputFiles statement, that looks like this (shortened, for clarity):
final Provider<Set<FileSystemLocation>> stuff = outputDir.flatMap { it.files("x").elements }
It all works, but if I swap .flatMap for .map then it throws the Querying the mapped value of task ':buildThing' property 'outputDir' before task ':buildThing' has completed is not supported error.
It had not occurred to me that the two methods were different in that way – I was assuming the difference was as with map and flatMap in Ruby, where flatMap is like map, but it flattens out any nested arrays in the result.
It kind of is like the Ruby one, in that within flatMap you return a Provider<X> while in map you return an X to in both cases result in a Provider<X>.
But it is subtlely more different.
With flatMap, the outer provider is replaced by the returned provider.
So you for example loose task dependencies while with map they are preserved.
You should also be able to do outputDir.locationOnly.map { ... } to loose the task dependency and go on from there.
But why creating a configurable file collection and setting its value from a provider if you can just get a provider with file system locations from the file collection.
Especially as a configurable file collection could be modified by downstream users as it is configurable, unless maybe you prevent further modification and additionally set the public type to FileCollection explicitly.