I have a task that downloads a large file (basical...
# plugin-development
r
I have a task that downloads a large file (basically required assets for a subsequent JavaExec task). During development, I test by publishing my plugin to my local Maven repo and pulling it in my test project, but I don't change the version. Despite setting path sensitivity for the large file task input (as a regularfileproperty), every plugin change results in the task cache being busted and that large file being re-downloaded, despite it not moving or changing at all. Is there a method of development I'm missing that avoids this?
m
Wait, isn't your path a task output?
Your task should have an input, probably the url
r
Yes, it is - the download task outputs a file property which is wired to the file property of the next task
m
What are the inputs of your download task?
r
https://github.com/hygradle/hygradle/blob/trunk/src/main/kotlin/dev/hygradle/internal/task/DownloadAssets.kt#L26 , the only inputs with any cache bearing are version and patchline and they don't change between runs
m
every plugin change
what do you mean by taht?
m
It's probably the plugin change
If your buildscript classpath changes, it invalidates all the tasks
This is a major pita
r
what do you mean by taht?
When I make a change to the plugin source, publish to local maven, and then sync again in my consumer project
👍 1
Yeah, that'd explain it - is there any way around that?
m
No real way that I know about. You can track https://github.com/gradle/gradle/issues/31482
Best you can is do your own caching to avoid having to re-download
It's not really pretty but I don't have another way sadly
r
Yikes, fair enough. Do you do any further checking beyond if the file exists, or is the file path/name pinned to something deterministic from the R8 version?
Technically my assets filename is determined solely from the properties that determine if it's cached (or should be) so I could just do that as well
m
The R8 version is the git sha so I'm relying on no collision there
Also this doesn't show progress during the download. I looked at alternatives at that time and then gave up to this simple (but working) task
The task should probably have an outputDir and remove the stale files or else the directory will grow forever but oh well...
v
The task implementation classpath is always an input to the task too, because if the classpath changed, the implementation and thus behavior could have changed and thus the result could be different, so the task needs to be rerun. If you know your output is in place and up-to-date, you could for example use
-x yourTask
to skip the task explicitly.
r
So if I made any change to the plugin source, even if it doesn't touch the task, it'll bust the cache? So even between release versions of the plugin this will happen for users
m
The task implementation classpath is always an input to the task too, because if the classpath changed, the implementation and thus behavior could have changed and thus the result could be different
I'd love to be able to opt-out of this. Most of my tasks implementations delegate to another classloader. The build script classpath has no importance at all in the final outcome.
r
The task should probably have an outputDir and remove the stale files or else the directory will grow forever but oh well...
This doesn't mean I actually have to consume that directory in any dependent tasks, right? I can just clean the dir of any not read in x days or whatever archives
v
So if I made any change to the plugin source, even if it doesn't touch the task, it'll bust the cache? So even between release versions of the plugin this will happen for users
Even if some totally unrelated plugin changes, or the user changes some convention plugin. Another plugin or a user convention plugin could for example also add a
doLast
action to your task and so on. If the classpath changed, Gradle simply cannot know whether the output might eventually be up-to-date.
m
This doesn't mean I actually have to consume that directory
If you want to be pedantic, you can probably transform the directory to a
Provider<File>
before passing it to the next task. But I would probably just keep growing the directory ^^
If the classpath changed, Gradle simply cannot know whether the output might eventually be up-to-date.
Agreed. Still, would be cool to have a way to opt-out. Any call to
doLast {}
could crash and then it's on the plugin author to ensure the task action is not sensitive to the build script classpath.
r
If you want to be pedantic, you can probably transform the directory to a
Provider<File>
before passing it to the next task. But I would probably just keep growing the directory ^^
Oh, you mean you'd use an output dir instead of a file?
As in output the cache directory of the files vs the cached file property itself?
m
Yes, the output of the download task could be a directory containing a single file and removing all the stale ones
When the input changes from
v1
to
v2
, it deletes
v1.foo
and downloads
v2.foo
r
I suppose I could just use
filecollection.singleFile
to assert that in my consumer task too
m
Yep
But it's nitpicking at this point. Unless your file is terabytes, no one will probably notice ^^
👆 1
Especially if the version doesn't change too often
r
Fair 😄 I might just do naive download avoidance for now then. If the release/patchine hasn't changed, and an archive exists in that dir named
<release-<patchline>.zip
, avoid downloading
👍 1
What is the most idiomatic way to pass that directory through multiple tasks @Martin? I tried passing output directories to configurablefilecollections, but oddly enough trying to add a directory to one adds the actual directory itself as a file path, not the files it contains
m
It's complicated. I like to keep directories for outputs and always use
InputFiles
for inputs
trying to add a directory to one adds the actual directory itself as a file path, not the files it contains
You're probably looking for something like
files(fileTree(dir))
fileTree
will "expand" your dir
v
Be aware that you loose task dependencies if you are not careful. Besides that, if you have a configurable file collection and want the actual files, use
asFileTree
on it when you use it, then it resolves to the actual files.
plus1 1
m
+1 I mostly use
asFileTree
Just beware it loses the normalized path across workers boundaries: https://github.com/gradle/gradle/issues/28147
r
Makes sense, thank you both! Is
asSingleFile
fine as well if I'm expecting there to only be one file, or should I be using filetree filter methods?
v
If you know it is exactly one file and also want a hard error if your assumption is wrong, the methods seems a fit
r
Is there a way to provide lazy providers to filetree filter specs? i.e. if I want to match/include on the result of 2 properties in a task combined into a filename for example
or is it perfectly valid to do
stringProp.map { fileCollection.asFileTree.matching { include(it)} }
I've actually adjusted this, and am no longer doing this during configuration. I'm noticing that with the output directory approach, deleting a file in the output dir of a task doesn't bust that task's cache Figured it out :D
🙌 1
a
Does the http server support etags? You could store the etag when downloading the file, and attach it to the http request. Then it matters less if the task re-runs - the server will respond with "up to date" https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag
r
No, it doesn't sadly. Naive avoidance is the best I've got :( @Martin Quick question, you mentioned early I could be pedantic and transform the output directory into a file provider when I pass it on. What's the best way to do that? Should I be passing it into a RegularFileProperty?
I'm also trying to use output directories instead of directly files, but oddly it seems Gradle is... forgetting the contents of output directories? For example in one pass I create
.gradle\caches\bundles\release-2026.02.19-1a311a592.zip
, with the bundles dir being the output dir - and then in a subsequent call,
Gradle does not know how file '.gradle\caches\bundles\release-2026.02.19-1a311a592.zip' was created (output property 'assetBundleCacheDirectory')
. Am I missing a step to get Gradle to realize that it's also responsible for the contents of the output directory?
m
@Remi Gelinas I did it there: https://github.com/GradleUp/gr8/pull/45
Copy code
taskProvider.map {
  it.outputDir.get().asFile.listFiles()?.singleOrNull() ?: error("foo")
}
r
Does Gradle recognize the output file between runs? I've got it implemented almost exactly the same, but as I mentioned ^ it doesn't realize that it was responsible for the single file in that dir
m
@Remi Gelinas I think so. But don't trust me on this, try it out yourself in the gr8 repo:
Copy code
./gradlew -p test-plugin :gr8DefaultDownload 

[...]

./gradlew -p test-plugin :gr8DefaultDownload -i

Skipping task ':gr8DefaultDownload' as it is up-to-date.