· 3 min read
Gradle brainteasers 2/2: relocatable input files
There is no such thing as a "File" input...
This is a follow up to this other post about having fun with the Gradle APIs.
In this post, I’m talking about how I spent a shameful amount of time understanding how Gradle handles input files.
The problem
The Apollo Gradle Plugin is generating Kotlin code from GraphQL files.
If you have GraphQL files like this:
.
└── src
└── main
└── graphql
└── com
└── example
├── GetUser.graphql
├── GetProduct.graphql
└── GetReview.graphql
The Apollo Compiler generates Kotlin classes like this:
com.example.GetUser
com.example.GetProduct
com.example.GetReview
In a nutshell, the compiler looks at the relative path of the file and uses that as the package name.
Build cache relocation
A naive implementation could use the “base” directory as input so that it can compute the package names:
@get:PathSensitive(PathSensitivity.RELATIVE)
@get:InputFiles
abstract val inputFiles: Set<File>
// The base directory to compute the package name
// This isn't great
@get:Input
abstract var baseDir: String
@TaskAction
fun taskAction() {
inputFiles.forEach {
val packageName = it.relativeTo(File(baseDir))
.canonicalPath
.replace(File.separatorChar, '.')
// ...
}
}
That works but it also completely breaks build cache relocation. If we were to copy our repository to another directory on our machine (or in CI), then no cache results could ever be reused.
Entering FileCollection
The solution is to use FileCollection
and its mutable cousin ConfigurableFileCollection
.
FileCollection
is a lazy API that can resolve Configuration
s but most importantly may contain “roots”. In other words, a FileCollection
contains `File`s but also their relative path to the root(s).
PathSensitivity.RELATIVE
on a java.io.File
property is the same as using PathSensitivity.NAME_ONLY
since it doesn't not contain any root.At a high level, Gradle doesn’t havejava.io.File
inputs. It’s always java.io.File
+ "fileIdentifier"
.
fileIdentifier
being a name, a relative or absolute path, or even empty if only the contents of the file matter and you’re using PathSensitivity.NONE
.
So how do we get that identifier? Well, for NAME_ONLY
we can use file.name
, for NONE
, we can use ""
and for ABSOLUTE
, we can use file.asbolutePath
. But what about RELATIVE
?
The secret to getting the relative path from a FileCollection
is using asFileTree
which returns the underlying FileTree
if any (which is really a FileForest
because FileTree
s can contain multiple roots):
@get:PathSensitive(PathSensitivity.RELATIVE)
@get:InputFiles
abstract val inputFiles: ConfigurableFileCollection
@TaskAction
fun taskAction() {
inputFiles.asFileTree.visit {
// Yay, we have the File here alongside its relative path 🎉
val packageName = path.replace(File.separatorChar, '.')
// do codegen
}
}
Using ConfigurableFileCollection
, we get all the information about our files, including their relative path.
It’s a very convenient API that non only allows resolving dependencies (Configurations
are FileCollections
) and filtering (FileTree.include()
) and do so in a lazy way but most importantly, they make it possible to model input files without breaking cache relocation! SUCCESS!
Looking ahead, Gratatouille will not allow simple `File` inputs to model the fact that task inputs also have a name (even if empty). Hopefully that will make the task of writing Gradle tasks easier!
Brainteasers pictures from mtairymd on Instructables