Declaring Gradle task inputs and outputs is essential for your build to work properly. By telling Gradle what files or properties your task consumes and produces, the incremental build feature kicks in, improving the performance of your build. You’ll also be following best practices, allowing you to easily create more complex projects.
In this article, you’ll learn how Gradle task inputs and outputs work, why they’re important, as well as practically how to implement them in your own projects.
Tasks 30,000 foot view
A task represents a unit of work to get done in your Gradle project. That could be compiling code, copying files, publishing an artifact, or whatever action it is your task needs to accomplish.
We run tasks on the command line. For example, running ./gradlew compileJava
will take your project’s .java files and compile them into .class files.
Task inputs and outputs
For you task to actually do anything useful, it needs some stuff to work on. This is called the task inputs.
Your task normally produces something. This is called the task outputs.
Overview of task inputs and outputs
You’ll find that most official Gradle tasks have inputs and outputs. Can you guess what the compileJava
task inputs and outputs are?
It’s actually quite straightforward. The inputs of compileJava
are the source .java files and the Java version, and the outputs are the compiled .class files.
Task inputs and outputs for the compileJava
task
The relationship between task inputs and outputs is normally that a change in the inputs creates a change in the outputs. In the case of compileJava
, if we change the .java files, it makes sense that the compiled .class files would also change.
The importance of task inputs and outputs
So you know what task inputs and outputs are now, but so what? Well it turns out that they play a crucial role in at least 3 key areas of Gradle functionality.
1. Up-to-date checks
Gradle’s incremental build feature helps your build avoid doing the same work more than once. For example, does Gradle really need to recompile your code if nothing’s changed?
Well thankfully not. The way Gradle knows if a task should be executed again is using inputs and outputs. Every time a task is executed it takes a fingerprint of the inputs and outputs, and stores the fingerprints ready for next time the task is executed. If an input or output hasn’t changed, then the calculated fingerprint will be the same.
If all inputs and all outputs of a task have the same fingerprint as the last execution, the task can be skipped. Or in Gradle terminology the task is marked as UP-TO-DATE
.
How up-to-date checks work with task inputs and outputs
An example
Let’s take any Java Gradle project as an example. If we run the compileJava
task on a clean project (without a build directory), we get this output.
$ ./gradlew compileJava
> Task :compileJava
BUILD SUCCESSFUL in 2s
1 actionable task: 1 executed
If we run the same task again, the output is this.
$ ./gradlew compileJava
> Task :compileJava UP-TO-DATE
BUILD SUCCESSFUL in 1s
1 actionable task: 1 up-to-date
You can clearly see that the compileJava
task is marked as UP-TO-DATE
, meaning Gradle skips running it completely. It knows that neither the inputs nor the outputs have changed.
For this small project, it makes a tiny 1s difference in performance. For a large project, this incremental build feature can be a game-changer, saving developers a lot of time.
2. Linking task inputs and outputs
Another important use is to link the output of one task to the input of another. One way to think about this is using the producer/consumer analogy. A producer task creates some output that’s used as an input to a consumer task.
Linking tasks through inputs and outputs
Outputs can only be files or directories. This effectively makes the input of the consumer task the same file, directory, or file collection as the producer task.
This has some important benefits:
- Gradle automatically adds a task dependency from the consumer to the producer. This means when you run the consumer task it first runs the producer task.
- when the outputs of the producer task change, the consumer task will get executed again since it’s no longer up-to-date
An example
Imagine we have a Gradle project which does some very simple string manipulation on movie quotes. 🎬
There are two tasks
a) addQuotationMarks
- input is a file containing a movie quote e.g. quote.txt containing Bond. James Bond.
- output is a file containing the quote in quotation marks e.g. quote-with-quotation-marks.txt containing “Bond. James Bond.”
b) addQuoteSource
- input a file containing a movie quote in quotation marks i.e. the output of
addQuotationMarks
- output is a file containing the movie quote in quotation marks along with the source movie the quote came from e.g. quote-with-source.txt containing “Bond. James Bond.” Dr. No (1963)
When we run the addQuoteSource
task you can see that both tasks are executed, since the inputs of addQuoteSource
are linked to the outputs of addQuotationMarks
$ ./gradlew addQuoteSource
> Task :addQuotationMarks
> Task :addQuoteSource
BUILD SUCCESSFUL in 1s
2 actionable tasks: 2 executed
Linking the output of addQuoationMarks
to the input of addQuoteSource
is as simple as this.
tasks.register('addQuoteSource', AddQuoteSource) {
// any other task configuration
inputFile = addQuotationMarks.get().outputFile
}
You can see the full example build.gradle is this GitHub repository.
3. Using dependency configurations
In Gradle a dependency configuration (or just configuration) is a way of grouping together dependencies to define their scope. For example, the java
plugin adds the implementation
configuration which is used to generate the compile and runtime classpaths.
One way configurations can be used is as a producer e.g. I want to add an artifact built by this project to the implementation
configuration of any consuming projects.
Task outputs can be used to easily tell Gradle that an artifact produced by a task should be added to a specific configuration. This configuration can then be used to share the artifact between projects.
An example
Let’s reuse the movie quote project idea. 🎥
This time we’ll have two subprojects:
- produce-quote containing the addQuotationMarks task
- consume-quote containing the addQuoteSource task
In produce-quote we can create a custom configuration called quote. We can then add the output text file of the addQuotationMarks task to this configuration very simply.
configurations {
quote
}
artifacts {
quote(addQuotationMarks)
}
This means that if the consume-quote subproject needs to consume the output file quote-with-quotation-marks.txt it could do that like this.
configurations {
quote
}
dependencies {
quote(project(path: ":produce-quote", configuration: 'quote'))
}
This is in fact Gradle’s recommended way for sharing inputs and outputs across subprojects.
Assuming the consuming task addQuoteSource has an input inputsFiles
of type ConfigurableFileCollection
, you can wire in the artifact dependency like this.
tasks.register('addQuoteSource', AddQuoteSource) {
inputFiles.from(configurations.quote)
// any other task configuration
}
To see the full example, including how to get the expected file from the ConfigurableFileCollection
, check out the GitHub repository.
How to declare task inputs and outputs
Task inputs and outputs are highly configurable. You can create inputs and outputs that always apply to a task inside the task class, or add them dynamically on a case-by-case basis.
Before moving onto full implementation details, let’s quickly explore the full options for what types can be declared as inputs and outputs.
Type | Inputs | Outputs |
---|---|---|
String | ✓ | |
File | ✓ | ✓ |
Iterable of files (Iterable<File> e.g. FileTree or FileCollection ) |
✓ | ✓ |
Map of files (Map<String, File> ) |
✓ | |
Directory | ✓ | ✓ |
Iterable of directories (Iterable<File> ) |
✓ | |
Map of directories (Map<String, File> ) |
✓ | |
Java classpath | ✓ |
Type support for Gradle task inputs & outputs
Note that:
- strings are only supported for task inputs, not outputs. These are normally used for configuration options e.g.
sourceCompatibility
of the compileJava task type. - task outputs can only be files or directories. If you think about a task creating some kind of artifact, this makes sense.
Task lazy configuration
Gradle has the concept of lazy configuration, which allows task inputs and outputs to be referenced before they are actually set. This is done via a Property
class type.
One advantages of this mechanism is that you can link the output file of one task to the input file of another, all before the filename has even been decided. The Property
class also knows about which task it’s linked to, so using inputs and outputs in this way enables Gradle to automatically add the required task dependency.
To understand how this works, here are some input types and their equivalent property-based type.
Simple type | Property-based types |
---|---|
String |
Property<String> |
File |
RegularFileProperty |
(extends Property<File> ) |
|
Iterable<File> |
ConfigurableFileCollection ConfigurableFileTree (both extend Property<Iterable<File>> ) |
It’s normally preferable to use the property-based type, due to the flexibility mentioned above. You’ll see these types used frequently if you read Gradle task code.
Learn more about these different types in the documentation linked at the end of the article.
1. Task class inputs & outputs
A good practice is to create a task class for your custom task. The class encapsulates the task action logic, but should also declare any inputs and outputs the task expects. To do this, we use annotations.
For task inputs we can use @Input
, @InputFile
, @InputDirectory
, @InputFiles
, @Classpath
, and @CompileClasspath
.
For task outputs we have @OutputFile
, @OutputDirectory
, @OutputFiles
, @OutputDirectories
.
An example
Here’s a simple example of a task class that takes as input two quote files. The output is another file containing the result of joining the values from the input files.
abstract class JoinQuote extends DefaultTask {
@InputFile
final abstract RegularFileProperty firstInputFile = project.objects.fileProperty().convention(project.layout.projectDirectory.file('quote-part-1.txt'))
@InputFile
final abstract RegularFileProperty secondInputFile = project.objects.fileProperty().convention(project.layout.projectDirectory.file('quote-part-2.txt'))
@OutputFile
final abstract RegularFileProperty outputFile = project.objects.fileProperty().convention(project.layout.buildDirectory.file('full-quote.txt'))
@TaskAction
void join() {
outputFile.get().asFile.text = firstInputFile.get().asFile.text + secondInputFile.get().asFile.text
}
}
- the
@InputFile
annotation means that Gradle knows these properties are inputs, and it treats them accordingly. - the inputs are of type
RegularFileProperty
, which extendsProperty<File>
. We create such a property using theObjectFactory
class, which contains helper methods for creating different types of properties. - on the returned property from
project.objects.fileProperty()
we callconvention
which just allows us to set a default value - finally, when we need the actual property value in the task action, we call
get()
on the property
Let’s define an instance of the class with tasks.register('joinQuote', JoinQuote)
.
After running ./gradlew joinQuote
multiple times, Gradle knows it’s up to date.
$ ./gradlew joinQuote
> Task :joinQuote UP-TO-DATE
BUILD SUCCESSFUL in 1s
1 actionable task: 1 up-to-date
Let’s change the quote file quote-part-1.txt, or in other words change the inputs.
Now we’ll see that the joinQuote
task is no longer up-to-date and Gradle executes it again.
$ ./gradlew joinQuote
> Task :joinQuote
BUILD SUCCESSFUL in 3s
1 actionable task: 1 executed
Finally, if we execute the clean
task before joinQuote
, or in other words change the outputs, Gradle also knows to execute the task again.
$ ./gradlew clean joinQuote
> Task :clean
> Task :joinQuote
BUILD SUCCESSFUL in 1s
2 actionable tasks: 2 executed
To try it out for yourself see the example in this GitHub repository.
Working with other input and output types
So you’ve seen how to use the @InputFile
and @OutputFile
annotations, create property values, and set defaults, but what about the other input and output types?
Well, here’s a task which uses all the types, for your reference.
abstract class AllTypes extends DefaultTask {
//inputs
@Input
final abstract Property<String> inputString = project.objects.property(String).convention("default value")
@InputFile
final abstract RegularFileProperty inputFile = project.objects.fileProperty().convention(project.layout.projectDirectory.file('default-file.txt'))
@InputDirectory
final abstract DirectoryProperty inputDirectory = project.objects.directoryProperty().convention(project.layout.projectDirectory.dir('default-dir'))
@InputFiles
final abstract ConfigurableFileCollection inputFileCollection = project.objects.fileCollection().from(project.layout.projectDirectory.file('default-file-1.txt'), project.layout.projectDirectory.file('default-file-2.txt'))
@Classpath
final abstract ConfigurableFileCollection inputClasspath = project.objects.fileCollection().from(project.layout.projectDirectory.file('MyClass.class'))
// outputs
@OutputFile
final abstract RegularFileProperty outputFile = project.objects.fileProperty().convention(project.layout.buildDirectory.file('default-file.txt'))
@OutputDirectory
final abstract DirectoryProperty outputDirectory = project.objects.directoryProperty().convention(project.layout.projectDirectory.dir('default-dir'))
@OutputFiles
final abstract ConfigurableFileCollection outputFiles = project.objects.fileCollection().from(project.layout.buildDirectory.file('default-file-1.txt'), project.layout.buildDirectory.file('default-file-2.txt'))
@OutputDirectories
final abstract ConfigurableFileCollection outputDirectories = project.objects.fileCollection().from(project.layout.projectDirectory.dir('default-dir-1'), project.layout.projectDirectory.dir('default-dir-2'))
}
As with all the examples in this article, you can find the code in this GitHub repository.
2. Ad-hoc task inputs & outputs
When we can, it’s preferable to declare task inputs and outputs in a task class using annotations. That might not always be possible or desirable, for example if we’re working with a 3rd party task class or we just want to define everything inline without creating a class. In this case, we can dynamically assign a task’s inputs and outputs within the build.gradle itself.
To illustrate this, here’s a task definition for emphasiseQuote which takes as inputs a quote file and emphasis character, and outputs a file containing the quote with the emphasis character appended on the end.
tasks.register('emphasiseQuote') {
it.inputs.file('quote.txt')
it.inputs.property('emphasisCharacter', '!')
it.outputs.file(layout.buildDirectory.file('emphasised-quote.txt'))
it.doLast {
outputs.files.singleFile.text = inputs.files.singleFile.text + inputs.properties.get('emphasisCharacter')
}
}
For example, if quote.txt contains You’re gonna need a bigger boat, once we’ve run ./gradlew emphasiseQuote
the output file would contain You’re gonna need a bigger boat!.
Some notes on this implementation:
- define inputs by calling the appropriate function on the task’s
inputs
(seeTaskInputs
) - define outputs by calling the appropriate function on the task’s
outputs
(seeTaskOutputs
) - retrieve the inputs or outputs at execution time with
getFiles()
orgetProperties()
- if retrieving inputs by name, declare the input with a name using
property(String name, Object value)
Feel free to try the full example for yourself.
Some more examples
There are many possible use cases involving inputs and outputs, so check out this list of Gradle example projects to see if one covers your scenario.
- custom-task: creates a task class declaring inputs and outputs with annotations
- custom-task-define-inputs-and-outputs-externally: similar to custom-task, but this time we don’t rely on defaults and define the values of inputs and outputs outside the task class
- pre-packaged-task: uses an existing Gradle task class (in this case
Copy
) and demonstrates the up-to-date checks working - ad-hoc-task: doesn’t use a task class, but instead defines an ad-hoc task, dynamically creating inputs and outputs
- linking-tasks: demonstrates how to link inputs & outputs between tasks in the same project
- sharing-outputs-between-projects: a similar outcome to linking-tasks, but this time we share task outputs between subprojects using dependency configurations
- all-types-custom-task: defines a dummy task to illustrate how to use all the different input and output types
If you think there’s something missing, please leave a comment below and I’ll try to fill in the gap!
Final thoughts
You should now have an understanding of what inputs and outputs are, why they’re important, and how you can start using them in tasks in your own project. There’s plenty more to learn on this topic, so I recommend the following Gradle documentation.
- Authoring tasks: contains the full list of annotations to use with inputs & outputs
- Lazy Configuration: goes into a lot more detail on the
Property
class discussed in this article