· 3 min read

Gradle brainteasers 2/2: relocatable input files

There is no such thing as a "File" input...

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
This behaviour of using the path as package name is optional and probably questionable but provides a very good use case for us diving into file inputs APIs.

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 Configurations but most importantly may contain “roots”. In other words, a FileCollection contains `File`s but also their relative path to the root(s).

💡
UsingPathSensitivity.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.Fileinputs. 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 FileTrees 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

    Share:
    Back to Blog