Is the iteration order of a `FileCollection` predi...
# community-support
m
Is the iteration order of a
FileCollection
predictable?
v
It's JavaDoc says
A file collection is can be used to define a classpath
and as classpaths are ordering sensitive, I'd very much hope it is predictable / consistent. If it were not, I guess that would also be pretty bad for up-to-dateness and cacheability.
m
Right 👍 I’m guessing the order is implementation details, though, right?
v
Could be considered like that I guess, yes. But if you for example have a configurable file collection and add things to it, it should hopefully preserve insertion order, or you can hardly construct a classpath that is order-sensitive (even if it is more or less bad practice). But if you just have a
FileCollection
and don't know how it is built-up / implemented, I'd expect consistent ordering on iteration, but if you don't know how it was built / implemented, you can hardly predict the order, yes.
👍 1
m
I see, thanks!
👌 1
Looks like it’s not stable accross OSes. I made a test here
I’ll file a bug
v
Yeah, listing files on the filesystem could indeed different. Please link the issue here, I'm curious whether they agree it is a bug or works-as-designed. 🙂
m
Is the order even relevant to input snapshotting?
v
I guess it depends on the normalization. If it is
@Classpath
or
@CompileClasspath
, order should definitely be considered.
👍 1
In other cases, probably not
But maybe test it in your example. Enable build cache debugging and check whether the build keys are different with different order if you make it
@InputFiles
👍 1
m
Copy code
Build cache key for task ':doStuff' is 577c34d43bca13bcb1fef643dac7d885
same cache key with
PathSensistivity.RELATIVE
Looks like it’s the same with
@Classpath
even if the order is different 🤔 (ci run) macos
Copy code
Build cache key for task ':doStuff' is abdad9a112189e9a43f70ec6a3d2f366
Task ':doStuff' is not up-to-date because:
  No history is available.
/Users/runner/work/test-filecollection-order/test-filecollection-order/jars/netty-codec-4.1.34.Final.jar
/Users/runner/work/test-filecollection-order/test-filecollection-order/jars/analytics-jvm-0.6.4.jar
/Users/runner/work/test-filecollection-order/test-filecollection-order/jars/configuration-annotations-jvm-0.6.2.jar
/Users/runner/work/test-filecollection-order/test-filecollection-order/jars/okio-jvm-3.3.0.jar
/Users/runner/work/test-filecollection-order/test-filecollection-order/jars/okio-jvm-3.6.0.jar
/Users/runner/work/test-filecollection-order/test-filecollection-order/jars/okio-jvm-3.2.0.jar
linux
Copy code
Build cache key for task ':doStuff' is abdad9a112189e9a43f70ec6a3d2f366
Task ':doStuff' is not up-to-date because:
  No history is available.
/home/runner/work/test-filecollection-order/test-filecollection-order/jars/netty-codec-4.1.34.Final.jar
/home/runner/work/test-filecollection-order/test-filecollection-order/jars/okio-jvm-3.3.0.jar
/home/runner/work/test-filecollection-order/test-filecollection-order/jars/okio-jvm-3.2.0.jar
/home/runner/work/test-filecollection-order/test-filecollection-order/jars/analytics-jvm-0.6.4.jar
/home/runner/work/test-filecollection-order/test-filecollection-order/jars/configuration-annotations-jvm-0.6.2.jar
/home/runner/work/test-filecollection-order/test-filecollection-order/jars/okio-jvm-3.6.0.jar
I guess I’ll just sort my inputs always to be on the safe side
v
You mean you get a cache-hit even if the order in a
@Classpath
property is different? That sounds wrong to me
m
I didn’t go as far as testing end to end but the cache key is the same and the iterating order is not
t
Fwiw, looks like you're iterating a FileTree actually, not just a FileCollection (and even more specifically, a SourceDirectoryTree). AFAICT those are indeed platform-dependent: the srcDirs are iterated in the order they're added, but then each is transformed to a FileTree whose iteration order is platform-dependent. And AFAICT this would still be compatible with
@Classpath
in the same way that
-classpath "somedir/*"
passed to the JVM is patform-dependent too.
v
I did a quick test with this:
Copy code
val a by configurations.registering { isTransitive = false }
val b by configurations.registering { isTransitive = false }
dependencies {
    a("commons-io:commons-io:2.4")
    b("commons-lang:commons-lang:2.4")
}
@CacheableTask
abstract class Foo : DefaultTask() {
    @get:Classpath
    abstract val classpath: ConfigurableFileCollection

    init {
        outputs.upToDateWhen { true }
    }

    @TaskAction
    fun foo() {
        classpath.forEach { println(it) }
    }
}
val foo by tasks.registering(Foo::class) {
    classpath.from(a)
    classpath.from(b)
}
val bar by tasks.registering(Foo::class) {
    classpath.from(b)
    classpath.from(a)
}
which results in
Copy code
Resolve mutations for :foo (Thread[included builds,5,main]) started.
:foo (Thread[included builds,5,main]) started.

> Task :foo
Appending implementation to build cache key: Build_gradle$Foo_Decorated@b553d57856188a37e9c153a99e17afac
Appending additional implementation to build cache key: Build_gradle$Foo_Decorated@b553d57856188a37e9c153a99e17afac
Appending input file fingerprints for 'classpath' to build cache key: 80b864fcce8ee35d1fbbd1c341205786 - CLASSPATH{~\.gradle\caches\modules-2\files-2.1\commons-io\commons-io\2.4\b1b6ea3b7e4aa4f492509a4952029cd8e48019ad\commons-io-2.4.jar=IGNORED / d472b1d1d088ae048cd96c7c0939fbf5, ~\.gradle\caches\modules-2\files-2.1\commons-lang\commons-lang\2.4\16313e02a793435009f1e458fa4af5d879f6fb11\commons-lang-2.4.jar=IGNORED / aece18b33613844514d59ddfe00c2f1c}
Non-cacheable because No outputs declared [NO_OUTPUTS_DECLARED]
Caching disabled for task ':foo' because:
  No outputs declared
Task ':foo' is not up-to-date because:
  No history is available.
~\.gradle\caches\modules-2\files-2.1\commons-io\commons-io\2.4\b1b6ea3b7e4aa4f492509a4952029cd8e48019ad\commons-io-2.4.jar
~\.gradle\caches\modules-2\files-2.1\commons-lang\commons-lang\2.4\16313e02a793435009f1e458fa4af5d879f6fb11\commons-lang-2.4.jar
Resolve mutations for :bar (Thread[included builds,5,main]) started.
:bar (Thread[included builds,5,main]) started.

> Task :bar
Appending implementation to build cache key: Build_gradle$Foo_Decorated@b553d57856188a37e9c153a99e17afac
Appending additional implementation to build cache key: Build_gradle$Foo_Decorated@b553d57856188a37e9c153a99e17afac
Appending input file fingerprints for 'classpath' to build cache key: 45a7937557734f93d73fa600c2e4cb16 - CLASSPATH{~\.gradle\caches\modules-2\files-2.1\commons-lang\commons-lang\2.4\16313e02a793435009f1e458fa4af5d879f6fb11\commons-lang-2.4.jar=IGNORED / aece18b33613844514d59ddfe00c2f1c, ~\.gradle\caches\modules-2\files-2.1\commons-io\commons-io\2.4\b1b6ea3b7e4aa4f492509a4952029cd8e48019ad\commons-io-2.4.jar=IGNORED / d472b1d1d088ae048cd96c7c0939fbf5}
Non-cacheable because No outputs declared [NO_OUTPUTS_DECLARED]
Caching disabled for task ':bar' because:
  No outputs declared
Task ':bar' is not up-to-date because:
  No history is available.
~\.gradle\caches\modules-2\files-2.1\commons-lang\commons-lang\2.4\16313e02a793435009f1e458fa4af5d879f6fb11\commons-lang-2.4.jar
~\.gradle\caches\modules-2\files-2.1\commons-io\commons-io\2.4\b1b6ea3b7e4aa4f492509a4952029cd8e48019ad\commons-io-2.4.jar
So different order here result in different cache keys. Maybe it is handled differently or has a bug when it is coming from one source with different order, so if you list files in an OS directory and it then mitigates the different order. But even if so, if later the iteration order is different, it should probably not mitigate the different order there.
-classpath "somedir/*"
passed to the JVM is patform-dependent too.
Might be platform dependent, but if it is, then still the result can be different if the classpath is ordered differently and thus the cache-key / up-to-date-key has to be different too
a
The docs here say "The order of the files in a FileTree is not stable, even on a single computer"
👌 1
🎯 1
v
Ah, yeah, right. FileTree indeed do not have a guaranteed order.
So hopefully only your showcase was "wrong" 🙂
m
Means I have to sort my
FileCollection
because I can’t make any assumptions how it was created
👍 1
t
Fwiw, JavaCompile task redeclares its inputs to make sure the sources (inherited from SourceTask, as a FileTree) are _stable_: https://github.com/gradle/gradle/blob/master/platforms/jvm/language-java/src/main/java/org/gradle/api/tasks/compile/JavaCompile.java (same goes for GroovyCompile, but not e.g. Javadoc 🤷)
👀 1
v
Interesting. I wonder why. The sources should not be order-sensitive and are
@InputFiles
. Do you know why that was necessary?
Ah, seems that the incremental file processing would not work properly without stable sorting. At least for that it was introduced. And
Javadoc
probably does not support incremental building due to the overview pages and so on so there it is not necessary.
m
Copy code
private final FileCollection stableSources = getProject().files((Callable<FileTree>) this::getSource);
Wait, how does that ensure stability? Is
files()
going to sort by path or something like this?
PS: this is also important for intermediate results for us (not incremental processing but somewhat similar). This is the PR if you’re curious
v
I think the
stable
does not refer to the order of the files, but about the
FileCollection
instance.
The docs say:
IMPORTANT: To query incremental changes for an input file property, that property must always return the same instance.
👀 1
m
Oh I see
v
But
JavaCompile#getSource()
calls
super.getSource()
m
So much stability
v
And that calls
sourceFiles.getAsFileTree().matching(patternSet)
And
getAsFileTree()
returns a new instance on each call
Du to that
getSource()
cannot be used directly for the incremental processing, but the
stableSources
always has the same instance that then has the result of
getSource
and thus is stable for the matter of the incremental task logic
👌 2