Configuration cache
Introduction
The configuration cache is a feature that significantly improves build performance by caching the result of the configuration phase and reusing this for subsequent builds. Using the configuration cache, Gradle can skip the configuration phase entirely when nothing that affects the build configuration, such as build scripts, has changed. Gradle also applies performance improvements to task execution as well.
The configuration cache is conceptually similar to the build cache, but caches different information. The build cache takes care of caching the outputs and intermediate files of the build, such as task outputs or artifact transform outputs. The configuration cache takes care of caching the build configuration for a particular set of tasks. In other words, the configuration cache saves the output of the configuration phase, and the build cache saves the outputs of the execution phase.
This feature is currently not enabled by default. This feature has the following limitations:
|
How does it work?
When the configuration cache is enabled and you run Gradle for a particular set of tasks, for example by running gradlew check
, Gradle checks whether a configuration cache entry is available for the requested set of tasks.
If available, Gradle uses this entry instead of running the configuration phase.
The cache entry contains information about the set of tasks to run, along with their configuration and dependency information.
The first time you run a particular set of tasks, there will be no entry in the configuration cache for these tasks and so Gradle will run the configuration phase as normal:
-
Run init scripts.
-
Run the settings script for the build, applying any requested settings plugins.
-
Configure and build the
buildSrc
project, if present. -
Run the build scripts for the build, applying any requested project plugins.
-
Calculate the task graph for the requested tasks, running any deferred configuration actions.
Following the configuration phase, Gradle writes a snapshot of the task graph to a new configuration cache entry, for later Gradle invocations. Gradle then loads the task graph from the configuration cache, so that it can apply optimizations to the tasks, and then runs the execution phase as normal. Configuration time will still be spent the first time you run a particular set of tasks. However, you should see build performance improvement immediately because tasks will run in parallel.
When you subsequently run Gradle with this same set of tasks, for example by running gradlew check
again, Gradle will load the tasks and their configuration directly from the configuration cache and skip the configuration phase entirely.
Before using a configuration cache entry, Gradle checks that none of the "build configuration inputs", such as build scripts, for the entry have changed.
If a build configuration input has changed, Gradle will not use the entry and will run the configuration phase again as above, saving the result for later reuse.
Build configuration inputs include:
-
Init scripts
-
Settings scripts
-
Build scripts
-
System properties used during the configuration phase
-
Gradle properties used during the configuration phase
-
Environment variables used during the configuration phase
-
Configuration files accessed using value suppliers such as providers
-
buildSrc
and plugin included build inputs, including build configuration inputs and source files.
Gradle uses its own optimized serialization mechanism and format to store the configuration cache entries. It automatically serializes the state of arbitrary object graphs. If your tasks hold references to objects with simple state or of supported types you don’t have anything to do to support the serialization.
As a fallback and to provide some aid in migrating existing tasks, some semantics of Java Serialization are supported. But it is not recommended relying on it, mostly for performance reasons.
Performance improvements
Apart from skipping the configuration phase, the configuration cache provides some additional performance improvements:
-
All tasks run in parallel by default, subject to dependency constraints.
-
Dependency resolution is cached.
-
Configuration state and dependency resolution state is discarded from heap after writing the task graph. This reduces the peak heap usage required for a given set of tasks.
Using the configuration cache
It is recommended to get started with the simplest task invocation possible.
Running help
with the configuration cache enabled is a good first step:
❯ gradle --configuration-cache help Calculating task graph as no cached configuration is available for tasks: help ... BUILD SUCCESSFUL in 4s 1 actionable task: 1 executed Configuration cache entry stored.
Running this for the first time, the configuration phase executes, calculating the task graph.
Then, run the same command again. This reuses the cached configuration:
❯ gradle --configuration-cache help Reusing configuration cache. ... BUILD SUCCESSFUL in 500ms 1 actionable task: 1 executed Configuration cache entry reused.
If it succeeds on your build, congratulations, you can now try with more useful tasks. You should target your development loop. A good example is running tests after making incremental changes.
If any problem is found caching or reusing the configuration, an HTML report is generated to help you diagnose and fix the issues. The report also shows detected build configuration inputs like system properties, environment variables and value suppliers read during the configuration phase. See the Troubleshooting section below for more information.
Keep reading to learn how to tweak the configuration cache, manually invalidate the state if something goes wrong and use the configuration cache from an IDE.
Enabling the configuration cache
By default, Gradle does not use the configuration cache.
To enable the cache at build time, use the configuration-cache
flag:
❯ gradle --configuration-cache
You can also enable the cache persistently in a gradle.properties
file using the org.gradle.configuration-cache
property:
org.gradle.configuration-cache=true
If enabled in a gradle.properties
file, you can override that setting and disable the cache at build time with the no-configuration-cache
flag:
❯ gradle --no-configuration-cache
Ignoring problems
By default, Gradle will fail the build if any configuration cache problems are encountered. When gradually improving your plugin or build logic to support the configuration cache it can be useful to temporarily turn problems into warnings, with no guarantee that the build will work.
This can be done from the command line:
❯ gradle --configuration-cache-problems=warn
or in a gradle.properties
file:
org.gradle.configuration-cache.problems=warn
Allowing a maximum number of problems
When configuration cache problems are turned into warnings, Gradle will fail the build if 512
problems are found by default.
This can be adjusted by specifying an allowed maximum number of problems on the command line:
❯ gradle -Dorg.gradle.configuration-cache.max-problems=5
or in a gradle.properties
file:
org.gradle.configuration-cache.max-problems=5
Enabling parallel configuration caching
Configuration cache storing and loading are done sequentially by default. Parallel storing and loading provide better performance, however not all builds are compatible with it.
To enable parallel configuration caching on the command line:
❯ gradle -Dorg.gradle.configuration-cache.parallel=true
or in a gradle.properties
file:
org.gradle.configuration-cache.parallel=true
The parallel configuration caching feature is incubating, as not all builds are guaranteed to work correctly.
Common symptoms are ConcurrentModificationException
exceptions during the configuration phase.
The feature should work well for decoupled multi-project builds.
Invalidating the cache
The configuration cache is automatically invalidated when inputs to the configuration phase change. However, certain inputs are not tracked yet, so you may have to manually invalidate the configuration cache when untracked inputs to the configuration phase change. This can happen if you ignored problems. See the Requirements and Not yet implemented sections below for more information.
The configuration cache state is stored on disk in a directory named .gradle/configuration-cache
in the root directory of the Gradle build in use.
If you need to invalidate the cache, simply delete that directory:
❯ rm -rf .gradle/configuration-cache
Configuration cache entries are checked periodically (at most every 24 hours) for whether they are still in use. They are deleted if they haven’t been used for 7 days.
Stable configuration cache
Working towards the stabilization of configuration caching we implemented some strictness behind a feature flag when it was considered too disruptive for early adopters.
You can enable that feature flag as follows:
enableFeaturePreview("STABLE_CONFIGURATION_CACHE")
enableFeaturePreview "STABLE_CONFIGURATION_CACHE"
The STABLE_CONFIGURATION_CACHE
feature flag enables the following:
- Undeclared shared build service usage
-
When enabled, tasks using a shared build service without declaring the requirement via the
Task.usesService
method will emit a deprecation warning.
In addition, when the configuration cache is not enabled but the feature flag is present, deprecations for the following configuration cache requirements are also enabled:
It is recommended to enable it as soon as possible in order to be ready for when we remove the flag and make the linked features the default.
IDE support
If you enable and configure the configuration cache from your gradle.properties
file, then the configuration cache will be enabled when your IDE delegates to Gradle.
There’s nothing more to do.
gradle.properties
is usually checked in to source control.
If you don’t want to enable the configuration cache for your whole team yet you can also enable the configuration cache from your IDE only as explained below.
Note that syncing a build from an IDE doesn’t benefit from the configuration cache, only running tasks does.
IntelliJ based IDEs
In IntelliJ IDEA or Android Studio this can be done in two ways, either globally or per run configuration.
To enable it for the whole build, go to Run > Edit configurations…
.
This will open the IntelliJ IDEA or Android Studio dialog to configure Run/Debug configurations.
Select Templates > Gradle
and add the necessary system properties to the VM options
field.
For example to enable the configuration cache, turning problems into warnings, add the following:
-Dorg.gradle.configuration-cache=true -Dorg.gradle.configuration-cache.problems=warn
You can also choose to only enable it for a given run configuration.
In this case, leave the Templates > Gradle
configuration untouched and edit each run configuration as you see fit.
Combining these two ways you can enable globally and disable for certain run configurations, or the opposite.
You can use the gradle-idea-ext-plugin to configure IntelliJ run configurations from your build. This is a good way to enable the configuration cache only for the IDE. |
Eclipse IDEs
In Eclipse IDEs you can enable and configure the configuration cache through Buildship in two ways, either globally or per run configuration.
To enable it globally, go to Preferences > Gradle
.
You can use the properties described above as system properties.
For example to enable the configuration cache, turning problems into warnings, add the following JVM arguments:
-
-Dorg.gradle.configuration-cache=true
-
-Dorg.gradle.configuration-cache.problems=warn
To enable it for a given run configuration, go to Run configurations…
, find the one you want to change, go to Project Settings
, tick the Override project settings
checkbox and add the same system properties as a JVM argument
.
Combining these two ways you can enable globally and disable for certain run configurations, or the opposite.
Supported plugins
The configuration cache is brand new and introduces new requirements for plugin implementations. As a result, both core Gradle plugins, and community plugins need to be adjusted. This section provides information about the current support in core Gradle plugins and community plugins.
Core Gradle plugins
Not all core Gradle plugins support configuration caching yet.
JVM languages and frameworks |
Native languages |
Packaging and distribution |
---|---|---|
Code analysis |
IDE project files generation |
Utility |
✓ |
Supported plugin |
⚠ |
Partially supported plugin |
✖ |
Unsupported plugin |
Community plugins
Please refer to issue gradle/gradle#13490 to learn about the status of community plugins.
Troubleshooting
The following sections will go through some general guidelines on dealing with problems with the configuration cache. This applies to both your build logic and to your Gradle plugins.
Upon failure to serialize the state required to run the tasks, an HTML report of detected problems is generated. The Gradle failure output includes a clickable link to the report. This report is useful and allows you to drill down into problems, understand what is causing them.
Let’s look at a simple example build script that contains a couple problems:
tasks.register("someTask") {
val destination = System.getProperty("someDestination") (1)
inputs.dir("source")
outputs.dir(destination)
doLast {
project.copy { (2)
from("source")
into(destination)
}
}
}
tasks.register('someTask') {
def destination = System.getProperty('someDestination') (1)
inputs.dir('source')
outputs.dir(destination)
doLast {
project.copy { (2)
from 'source'
into destination
}
}
}
Running that task fails and print the following in the console:
❯ gradle --configuration-cache someTask -DsomeDestination=dest ... * What went wrong: Configuration cache problems found in this build. 1 problem was found storing the configuration cache. - Build file 'build.gradle': line 6: invocation of 'Task.project' at execution time is unsupported. See https://docs.gradle.org/0.0.0/userguide/configuration_cache.html#config_cache:requirements:use_project_during_execution See the complete report at file:///home/user/gradle/samples/build/reports/configuration-cache/<hash>/configuration-cache-report.html > Invocation of 'Task.project' by task ':someTask' at execution time is unsupported. * Try: > Run with --stacktrace option to get the stack trace. > Run with --info or --debug option to get more log output. > Run with --scan to get full insights. > Get more help at https://help.gradle.org. BUILD FAILED in 0s 1 actionable task: 1 executed Configuration cache entry discarded with 1 problem.
The configuration cache entry was discarded because of the found problem failing the build.
Details can be found in the linked HTML report:
The report displays the set of problems twice. First grouped by problem message, then grouped by task. The former allows you to quickly see what classes of problems your build is facing. The latter allows you to quickly see which tasks are problematic. In both cases you can expand the tree in order to discover where the culprit is in the object graph.
The report also includes a list of detected build configuration inputs, such as environment variables, system properties and value suppliers that were read at configuration phase:
Problems displayed in the report have links to the corresponding requirement where you can find guidance on how to fix the problem or to the corresponding not yet implemented feature. When changing your build or plugin to fix the problems you should consider testing your build logic with TestKit. |
At this stage, you can decide to either turn the problems into warnings and continue exploring how your build reacts to the configuration cache, or fix the problems at hand.
Let’s ignore the reported problem, and run the same build again twice to see what happens when reusing the cached problematic configuration:
❯ gradle --configuration-cache --configuration-cache-problems=warn someTask -DsomeDestination=dest Calculating task graph as no cached configuration is available for tasks: someTask > Task :someTask 1 problem was found storing the configuration cache. - Build file 'build.gradle': line 6: invocation of 'Task.project' at execution time is unsupported. See https://docs.gradle.org/0.0.0/userguide/configuration_cache.html#config_cache:requirements:use_project_during_execution See the complete report at file:///home/user/gradle/samples/build/reports/configuration-cache/<hash>/configuration-cache-report.html BUILD SUCCESSFUL in 0s 1 actionable task: 1 executed Configuration cache entry stored with 1 problem. ❯ gradle --configuration-cache --configuration-cache-problems=warn someTask -DsomeDestination=dest Reusing configuration cache. > Task :someTask 1 problem was found reusing the configuration cache. - Build file 'build.gradle': line 6: invocation of 'Task.project' at execution time is unsupported. See https://docs.gradle.org/0.0.0/userguide/configuration_cache.html#config_cache:requirements:use_project_during_execution See the complete report at file:///home/user/gradle/samples/build/reports/configuration-cache/<hash>/configuration-cache-report.html BUILD SUCCESSFUL in 0s 1 actionable task: 1 executed Configuration cache entry reused with 1 problem.
The two builds succeed reporting the observed problem, storing then reusing the configuration cache.
With the help of the links present in the console problem summary and in the HTML report we can fix our problems. Here’s a fixed version of the build script:
abstract class MyCopyTask : DefaultTask() { (1)
@get:InputDirectory abstract val source: DirectoryProperty (2)
@get:OutputDirectory abstract val destination: DirectoryProperty (2)
@get:Inject abstract val fs: FileSystemOperations (3)
@TaskAction
fun action() {
fs.copy { (3)
from(source)
into(destination)
}
}
}
tasks.register<MyCopyTask>("someTask") {
val projectDir = layout.projectDirectory
source = projectDir.dir("source")
destination = projectDir.dir(System.getProperty("someDestination"))
}
abstract class MyCopyTask extends DefaultTask { (1)
@InputDirectory abstract DirectoryProperty getSource() (2)
@OutputDirectory abstract DirectoryProperty getDestination() (2)
@Inject abstract FileSystemOperations getFs() (3)
@TaskAction
void action() {
fs.copy { (3)
from source
into destination
}
}
}
tasks.register('someTask', MyCopyTask) {
def projectDir = layout.projectDirectory
source = projectDir.dir('source')
destination = projectDir.dir(System.getProperty('someDestination'))
}
1 | We turned our ad-hoc task into a proper task class, |
2 | with inputs and outputs declaration, |
3 | and injected with the FileSystemOperations service, a supported replacement for project.copy {} . |
Running the task twice now succeeds without reporting any problem and reuses the configuration cache on the second run:
❯ gradle --configuration-cache someTask -DsomeDestination=dest Calculating task graph as no cached configuration is available for tasks: someTask > Task :someTask BUILD SUCCESSFUL in 0s 1 actionable task: 1 executed Configuration cache entry stored. ❯ gradle --configuration-cache someTask -DsomeDestination=dest Reusing configuration cache. > Task :someTask BUILD SUCCESSFUL in 0s 1 actionable task: 1 executed Configuration cache entry reused.
But, what if we change the value of the system property?
❯ gradle --configuration-cache someTask -DsomeDestination=another Calculating task graph as configuration cache cannot be reused because system property 'someDestination' has changed. > Task :someTask BUILD SUCCESSFUL in 0s 1 actionable task: 1 executed Configuration cache entry stored.
The previous configuration cache entry could not be reused, and the task graph had to be calculated and stored again. This is because we read the system property at configuration time, hence requiring Gradle to run the configuration phase again when the value of that property changes. Fixing that is as simple as obtaining the provider of the system property and wiring it to the task input, without reading it at configuration time.
tasks.register<MyCopyTask>("someTask") {
val projectDir = layout.projectDirectory
source = projectDir.dir("source")
destination = projectDir.dir(providers.systemProperty("someDestination")) (1)
}
tasks.register('someTask', MyCopyTask) {
def projectDir = layout.projectDirectory
source = projectDir.dir('source')
destination = projectDir.dir(providers.systemProperty('someDestination')) (1)
}
1 | We wired the system property provider directly, without reading it at configuration time. |
With this simple change in place we can run the task any number of times, change the system property value, and reuse the configuration cache:
❯ gradle --configuration-cache someTask -DsomeDestination=dest Calculating task graph as no cached configuration is available for tasks: someTask > Task :someTask BUILD SUCCESSFUL in 0s 1 actionable task: 1 executed Configuration cache entry stored. ❯ gradle --configuration-cache someTask -DsomeDestination=another Reusing configuration cache. > Task :someTask BUILD SUCCESSFUL in 0s 1 actionable task: 1 executed Configuration cache entry reused.
We’re now done with fixing the problems with this simple task.
Keep reading to learn how to adopt the configuration cache for your build or your plugins.
Declare a task incompatible with the configuration cache
It is possible to declare that a particular task is not compatible with the configuration cache via the Task.notCompatibleWithConfigurationCache() method.
Configuration cache problems found in tasks marked incompatible will no longer cause the build to fail.
And, when an incompatible task is scheduled to run, Gradle discards the configuration state at the end of the build. You can use this to help with migration, by temporarily opting out certain tasks that are difficult to change to work with the configuration cache.
Check the method documentation for more details.
Adoption steps
An important prerequisite is to keep your Gradle and plugins versions up to date. The following explores the recommended steps for a successful adoption. It applies both to builds and plugins. While going through these steps, keep in mind the HTML report and the solutions explained in the requirements chapter below.
- Start with
:help
-
Always start by trying your build or plugin with the simplest task
:help
. This will exercise the minimal configuration phase of your build or plugin. - Progressively target useful tasks
-
Don’t go with running
build
right away. You can also use--dry-run
to discover more configuration time problems first.When working on a build, progressively target your development feedback loop. For example, running tests after making some changes to the source code.
When working on a plugin, progressively target the contributed or configured tasks.
- Explore by turning problems into warnings
-
Don’t stop at the first build failure; turn problems into warnings to discover how your build and plugins behave. If a build fails, use the HTML report to reason about the reported problems related to the failure. Continue running more useful tasks.
This will give you a good overview of the nature of the problems your build and plugins are facing. Remember that when turning problems into warnings you might need to manually invalidate the cache in case of troubles.
- Step back and fix problems iteratively
-
When you feel you know enough about what needs to be fixed, take a step back and start iteratively fixing the most important problems. Use the HTML report and this documentation to help you in this journey.
Start with problems reported when storing the configuration cache. Once fixed, you can rely on a valid cached configuration phase and move on to fixing problems reported when loading the configuration cache if any.
- Report encountered issues
-
If you face a problem with a Gradle feature or with a Gradle core plugin that is not covered by this documentation, please report an issue on
gradle/gradle
.If you face a problem with a community Gradle plugin, see if it is already listed at gradle/gradle#13490 and consider reporting the issue to the plugin’s issue tracker.
A good way to report such issues is by providing information such as:
-
a link to this very documentation,
-
the plugin version you tried,
-
the custom configuration of the plugin if any, or ideally a reproducer build,
-
a description of what fails, for example problems with a given task
-
a copy of the build failure,
-
the self-contained
configuration-cache-report.html
file.
-
- Test, test, test
-
Consider adding tests for your build logic. See the below section on testing your build logic for the configuration cache. This will help you while iterating on the required changes and prevent future regressions.
- Roll it out to your team
-
Once you have your developer workflow working, for example running tests from the IDE, you can consider enabling it for your team. A faster turnaround when changing code and running tests could be worth it. You’ll probably want to do this as an opt-in first.
If needed, turn problems into warnings and set the maximum number of allowed problems in your build
gradle.properties
file. Keep the configuration cache disabled by default. Let your team know they can opt-in by, for example, enabling the configuration cache on their IDE run configurations for the supported workflow.Later on, when more workflows are working, you can flip this around. Enable the configuration cache by default, configure CI to disable it, and if required communicate the unsupported workflow(s) for which the configuration cache needs to be disabled.
Reacting to the configuration cache in the build
Build logic or plugin implementations can detect if the configuration cache is enabled for a given build, and react to it accordingly.
The active status of the configuration cache is provided in the corresponding build feature.
You can access it by injecting the BuildFeatures
service into your code.
You can use this information to configure features of your plugin differently or to disable an optional feature that is not yet compatible. Another example involves providing additional guidance for your users, should they need to adjust their setup or be informed of temporary limitations.
Adopting changes in the configuration cache behavior
Gradle releases bring enhancements to the configuration cache, making it detect more cases of configuration logic interacting with the environment. Those changes improve the correctness of the cache by eliminating potential false cache hits. On the other hand, they impose stricter rules that plugins and build logic need to follow to be cached as often as possible.
Some of those configuration inputs may be considered "benign" if their results do not affect the configured tasks. Having new configuration misses because of them may be undesirable for the build users, and the suggested strategy for eliminating them is:
-
Identify the configuration inputs causing the invalidation of the configuration cache with the help of the configuration cache report.
-
Fix undeclared configuration inputs accessed by the build logic of the project.
-
Report issues caused by third-party plugins to the plugin maintainers, and update the plugins once they get fixed.
-
-
For some kinds of configuration inputs, it is possible to use the opt-out options that make Gradle fall back to the earlier behavior, omitting the inputs from detection. This temporary workaround is aimed to mitigate performance issues coming from out-of-date plugins.
It is possible to temporarily opt out of configuration input detection in the following cases:
-
Since Gradle 8.1, using many APIs related to the file system is correctly tracked as configuration inputs, including the file system checks, such as
File.exists()
orFile.isFile()
.For the input tracking to ignore these file system checks on the specific paths, the Gradle property
org.gradle.configuration-cache.inputs.unsafe.ignore.file-system-checks
, with the list of the paths, relative to the root project directory and separated by;
, can be used. To ignore multiple paths, use*
to match arbitrary strings within one segment, or**
across segments. Paths starting with~/
are based on the user home directory. For example:gradle.propertiesorg.gradle.configuration-cache.inputs.unsafe.ignore.file-system-checks=\ ~/.third-party-plugin/*.lock;\ ../../externalOutputDirectory/**;\ build/analytics.json
-
Before Gradle 8.4, some undeclared configuration inputs that were never used in the configuration logic could still be read when the task graph was serialized by the configuration cache. However, their changes would not invalidate the configuration cache afterward. Starting with Gradle 8.4, such undeclared configuration inputs are correctly tracked.
To temporarily revert to the earlier behavior, set the Gradle property
org.gradle.configuration-cache.inputs.unsafe.ignore.in-serialization
totrue
.
Ignore configuration inputs sparingly, and only if they do not affect the tasks produced by the configuration logic. The support for these options will be removed in future releases.
Testing your build logic
The Gradle TestKit (a.k.a. just TestKit) is a library that aids in testing Gradle plugins and build logic generally. For general guidance on how to use TestKit, see the dedicated chapter.
To enable configuration caching in your tests, you can pass the --configuration-cache
argument to GradleRunner or use one of the other methods described in Enabling the configuration cache.
You need to run your tasks twice. Once to prime the configuration cache. Once to reuse the configuration cache.
@Test
fun `my task can be loaded from the configuration cache`() {
buildFile.writeText("""
plugins {
id 'org.example.my-plugin'
}
""")
runner()
.withArguments("--configuration-cache", "myTask") (1)
.build()
val result = runner()
.withArguments("--configuration-cache", "myTask") (2)
.build()
require(result.output.contains("Reusing configuration cache.")) (3)
// ... more assertions on your task behavior
}
def "my task can be loaded from the configuration cache"() {
given:
buildFile << """
plugins {
id 'org.example.my-plugin'
}
"""
when:
runner()
.withArguments('--configuration-cache', 'myTask') (1)
.build()
and:
def result = runner()
.withArguments('--configuration-cache', 'myTask') (2)
.build()
then:
result.output.contains('Reusing configuration cache.') (3)
// ... more assertions on your task behavior
}
1 | First run primes the configuration cache. |
2 | Second run reuses the configuration cache. |
3 | Assert that the configuration cache gets reused. |
If problems with the configuration cache are found then Gradle will fail the build reporting the problems, and the test will fail.
A good testing strategy for a Gradle plugin is to run its whole test suite with the configuration cache enabled. This requires testing the plugin with a supported Gradle version. If the plugin already supports a range of Gradle versions it might already have tests for multiple Gradle versions. In that case we recommend enabling the configuration cache starting with the Gradle version that supports it. If this can’t be done right away, using tests that run all tasks contributed by the plugin several times, for e.g. asserting the |
Requirements
In order to capture the state of the task graph to the configuration cache and reload it again in a later build, Gradle applies certain requirements to tasks and other build logic. Each of these requirements is treated as a configuration cache "problem" and fails the build if violations are present.
For the most part these requirements are actually surfacing some undeclared inputs. In other words, using the configuration cache is an opt-in to more strictness, correctness and reliability for all builds.
The following sections describe each of the requirements and how to change your build to fix the problems.
Certain types must not be referenced by tasks
There are a number of types that task instances must not reference from their fields.
The same applies to task actions as closures such as doFirst {}
or doLast {}
.
These types fall into some categories as follows:
-
Live JVM state types
-
Gradle model types
-
Dependency management types
In all cases the reason these types are disallowed is that their state cannot easily be stored or recreated by the configuration cache.
Live JVM state types (e.g. ClassLoader
, Thread
, OutputStream
, Socket
etc…) are simply disallowed.
These types almost never represent a task input or output.
The only exceptions are the standard streams: System.in
, System.out
, and System.err
.
These streams can be used, for example, as parameters to Exec
and JavaExec
tasks.
Gradle model types (e.g. Gradle
, Settings
, Project
, SourceSet
, Configuration
etc…) are usually used to carry some task input that should be explicitly and precisely declared instead.
For example, if you reference a Project
in order to get the project.version
at execution time, you should instead directly declare the project version as an input to your task using a Property<String>
.
Another example would be to reference a SourceSet
to later get the source files, the compilation classpath or the outputs of the source set.
You should instead declare these as a FileCollection
input and reference just that.
The same requirement applies to dependency management types with some nuances.
Some types, such as Configuration
or SourceDirectorySet
, don’t make good task input parameters, as they hold a lot of irrelevant state, and it is better to model these inputs as something more precise.
We don’t intend to make these types serializable at all.
For example, if you reference a Configuration
to later get the resolved files, you should instead declare a FileCollection
as an input to your task.
In the same vein, if you reference a SourceDirectorySet
you should instead declare a FileTree
as an input to your task.
Referencing dependency resolution results is also disallowed (e.g. ArtifactResolutionQuery
, ResolvedArtifact
, ArtifactResult
etc…).
For example, if you reference some ResolvedComponentResult
instances, you should instead declare a Provider<ResolvedComponentResult>
as an input to your task.
Such a provider can be obtained by invoking ResolutionResult.getRootComponent()
.
In the same vein, if you reference some ResolvedArtifactResult
instances, you should instead use ArtifactCollection.getResolvedArtifacts()
that returns a Provider<Set<ResolvedArtifactResult>>
that can be mapped as an input to your task.
The rule of thumb is that tasks must not reference resolved results, but lazy specifications instead, in order to do the dependency resolution at execution time.
Some types, such as Publication
or Dependency
are not serializable, but could be.
We may, if necessary, allow these to be used as task inputs directly.
Here’s an example of a problematic task type referencing a SourceSet
:
abstract class SomeTask : DefaultTask() {
@get:Input lateinit var sourceSet: SourceSet (1)
@TaskAction
fun action() {
val classpathFiles = sourceSet.compileClasspath.files
// ...
}
}
abstract class SomeTask extends DefaultTask {
@Input SourceSet sourceSet (1)
@TaskAction
void action() {
def classpathFiles = sourceSet.compileClasspath.files
// ...
}
}
1 | this will be reported as a problem because referencing SourceSet is not allowed |
The following is how it should be done instead:
abstract class SomeTask : DefaultTask() {
@get:InputFiles @get:Classpath
abstract val classpath: ConfigurableFileCollection (1)
@TaskAction
fun action() {
val classpathFiles = classpath.files
// ...
}
}
abstract class SomeTask extends DefaultTask {
@InputFiles @Classpath
abstract ConfigurableFileCollection getClasspath() (1)
@TaskAction
void action() {
def classpathFiles = classpath.files
// ...
}
}
1 | no more problems reported, we now reference the supported type FileCollection |
In the same vein, if you encounter the same problem with an ad-hoc task declared in a script as follows:
tasks.register("someTask") {
doLast {
val classpathFiles = sourceSets.main.get().compileClasspath.files (1)
}
}
tasks.register('someTask') {
doLast {
def classpathFiles = sourceSets.main.compileClasspath.files (1)
}
}
1 | this will be reported as a problem because the doLast {} closure is capturing a reference to the SourceSet |
You still need to fulfil the same requirement, that is not referencing a disallowed type. Here’s how the task declaration above can be fixed:
tasks.register("someTask") {
val classpath = sourceSets.main.get().compileClasspath (1)
doLast {
val classpathFiles = classpath.files
}
}
tasks.register('someTask') {
def classpath = sourceSets.main.compileClasspath (1)
doLast {
def classpathFiles = classpath.files
}
}
1 | no more problems reported, the doLast {} closure now only captures classpath which is of the supported FileCollection type |
Note that sometimes the disallowed type is indirectly referenced. For example, you could have a task reference some type from a plugin that is allowed. That type could reference another allowed type that in turn references a disallowed type. The hierarchical view of the object graph provided in the HTML reports for problems should help you pinpoint the offender.
Using the Project
object
A task must not use any Project
objects at execution time.
This includes calling Task.getProject()
while the task is running.
Some cases can be fixed in the same way as for disallowed types.
Often, similar things are available on both Project
and Task
.
For example if you need a Logger
in your task actions you should use Task.logger
instead of Project.logger
.
Otherwise, you can use injected services instead of the methods of Project
.
Here’s an example of a problematic task type using the Project
object at execution time:
abstract class SomeTask : DefaultTask() {
@TaskAction
fun action() {
project.copy { (1)
from("source")
into("destination")
}
}
}
abstract class SomeTask extends DefaultTask {
@TaskAction
void action() {
project.copy { (1)
from 'source'
into 'destination'
}
}
}
1 | this will be reported as a problem because the task action is using the Project object at execution time |
The following is how it should be done instead:
abstract class SomeTask : DefaultTask() {
@get:Inject abstract val fs: FileSystemOperations (1)
@TaskAction
fun action() {
fs.copy {
from("source")
into("destination")
}
}
}
abstract class SomeTask extends DefaultTask {
@Inject abstract FileSystemOperations getFs() (1)
@TaskAction
void action() {
fs.copy {
from 'source'
into 'destination'
}
}
}
1 | no more problem reported, the injected FileSystemOperations service is supported as a replacement for project.copy {} |
In the same vein, if you encounter the same problem with an ad-hoc task declared in a script as follows:
tasks.register("someTask") {
doLast {
project.copy { (1)
from("source")
into("destination")
}
}
}
tasks.register('someTask') {
doLast {
project.copy { (1)
from 'source'
into 'destination'
}
}
}
1 | this will be reported as a problem because the task action is using the Project object at execution time |
Here’s how the task declaration above can be fixed:
interface Injected {
@get:Inject val fs: FileSystemOperations (1)
}
tasks.register("someTask") {
val injected = project.objects.newInstance<Injected>() (2)
doLast {
injected.fs.copy { (3)
from("source")
into("destination")
}
}
}
interface Injected {
@Inject FileSystemOperations getFs() (1)
}
tasks.register('someTask') {
def injected = project.objects.newInstance(Injected) (2)
doLast {
injected.fs.copy { (3)
from 'source'
into 'destination'
}
}
}
1 | services can’t be injected directly in scripts, we need an extra type to convey the injection point |
2 | create an instance of the extra type using project.object outside the task action |
3 | no more problem reported, the task action references injected that provides the FileSystemOperations service, supported as a replacement for project.copy {} |
As you can see above, fixing ad-hoc tasks declared in scripts requires quite a bit of ceremony. It is a good time to think about extracting your task declaration as a proper task class as shown previously.
The following table shows what APIs or injected service should be used as a replacement for each of the Project
methods.
Instead of: | Use: |
---|---|
|
A task input or output property or a script variable to capture the result of using |
|
A task input or output property or a script variable to capture the result of using |
|
A task input or output property or a script variable to capture the result of using |
|
A task input or output property or a script variable to capture the result of using |
|
A task input or output property or a script variable to capture the result of using |
|
A task input or output property or a script variable to capture the result of using |
|
A task input or output property or a script variable to capture the result of using |
|
|
|
|
|
|
|
A task input or output property or a script variable to capture the result of using |
|
A task input or output property or a script variable to capture the result of using |
|
|
|
|
|
|
|
|
|
|
|
A task input or output property or a script variable to capture the result of using |
|
|
|
|
|
|
|
|
|
The Kotlin, Groovy or Java API available to your build logic. |
|
|
|
|
|
|
|
Accessing a task instance from another instance
Tasks should not directly access the state of another task instance. Instead, tasks should be connected using inputs and outputs relationships.
Note that this requirement makes it unsupported to write tasks that configure other tasks at execution time.
Sharing mutable objects
When storing a task to the configuration cache, all objects directly or indirectly referenced through the task’s fields are serialized.
In most cases, deserialization preserves reference equality: if two fields a
and b
reference the same instance at configuration time, then upon deserialization they will reference the same instance again, so a == b
(or a === b
in Groovy and Kotlin syntax) still holds.
However, for performance reasons, some classes, in particular java.lang.String
, java.io.File
, and many implementations of java.util.Collection
interface, are serialized without preserving the reference equality.
Upon deserialization, fields that referred to the object of such a class can refer to different but equal objects.
Let’s look at a task that stores a user-defined object and an ArrayList
in task fields.
class StateObject {
// ...
}
abstract class StatefulTask : DefaultTask() {
@get:Internal
var stateObject: StateObject? = null
@get:Internal
var strings: List<String>? = null
}
tasks.register<StatefulTask>("checkEquality") {
val objectValue = StateObject()
val stringsValue = arrayListOf("a", "b")
stateObject = objectValue
strings = stringsValue
doLast { (1)
println("POJO reference equality: ${stateObject === objectValue}") (2)
println("Collection reference equality: ${strings === stringsValue}") (3)
println("Collection equality: ${strings == stringsValue}") (4)
}
}
class StateObject {
// ...
}
abstract class StatefulTask extends DefaultTask {
@Internal
StateObject stateObject
@Internal
List<String> strings
}
tasks.register("checkEquality", StatefulTask) {
def objectValue = new StateObject()
def stringsValue = ["a", "b"] as ArrayList<String>
stateObject = objectValue
strings = stringsValue
doLast { (1)
println("POJO reference equality: ${stateObject === objectValue}") (2)
println("Collection reference equality: ${strings === stringsValue}") (3)
println("Collection equality: ${strings == stringsValue}") (4)
}
}
1 | doLast action captures the references from the enclosing scope. These captured references are also serialized to the configuration cache. |
2 | Compare the reference to an object of user-defined class stored in the task field and the reference captured in the doLast action. |
3 | Compare the reference to ArrayList instance stored in the task field and the reference captured in the doLast action. |
4 | Check the equality of stored and captured lists. |
Running the build without the configuration cache shows that reference equality is preserved in both cases.
❯ gradle --no-configuration-cache checkEquality > Task :checkEquality POJO reference equality: true Collection reference equality: true Collection equality: true
However, with the configuration cache enabled, only the user-defined object references are the same. List references are different, though the referenced lists are equal.
❯ gradle --configuration-cache checkEquality > Task :checkEquality POJO reference equality: true Collection reference equality: false Collection equality: true
In general, it isn’t recommended to share mutable objects between configuration and execution phases. If you need to do this, you should always wrap the state in a class you define. There is no guarantee that the reference equality is preserved for standard Java, Groovy, and Kotlin types, or for Gradle-defined types.
Note that no reference equality is preserved between tasks: each task is its own "realm", so it is not possible to share objects between tasks. Instead, you can use a build service to wrap the shared state.
Accessing task extensions or conventions
Tasks should not access conventions and extensions, including extra properties, at execution time. Instead, any value that’s relevant for the execution of the task should be modeled as a task property.
Using build listeners
Plugins and build scripts must not register any build listeners.
That is listeners registered at configuration time that get notified at execution time.
For example a BuildListener
or a TaskExecutionListener
.
These should be replaced by build services, registered to receive information about task execution if needed.
Use dataflow actions to handle the build result instead of buildFinished
listeners.
Running external processes
Plugin and build scripts should avoid running external processes at configuration time.
In general, it is preferred to run external processes in tasks with properly declared inputs and outputs to avoid unnecessary work when the task is up-to-date.
However, if needed, you should only use the configuration-cache-compatible APIs described below, instead of Java and Groovy standard APIs, or
Gradle-provided methods Project.exec
, Project.javaexec
, ExecOperations.exec
, and ExecOperations.javaexec
.
The flexibility of these methods prevents Gradle from determining how the calls impact the build configuration, making it difficult to ensure that the configuration cache entry can be safely reused.
For simpler cases, when grabbing the output of the process is enough, providers.exec() and providers.javaexec() can be used:
val gitVersion = providers.exec {
commandLine("git", "--version")
}.standardOutput.asText.get()
def gitVersion = providers.exec {
commandLine("git", "--version")
}.standardOutput.asText.get()
For more complex cases a custom ValueSource implementation with injected ExecOperations
can be used.
This ExecOperations
instance can be used at configuration time without restrictions.
abstract class GitVersionValueSource : ValueSource<String, ValueSourceParameters.None> {
@get:Inject
abstract val execOperations: ExecOperations
override fun obtain(): String {
val output = ByteArrayOutputStream()
execOperations.exec {
commandLine("git", "--version")
standardOutput = output
}
return String(output.toByteArray(), Charset.defaultCharset())
}
}
abstract class GitVersionValueSource implements ValueSource<String, ValueSourceParameters.None> {
@Inject
abstract ExecOperations getExecOperations()
String obtain() {
ByteArrayOutputStream output = new ByteArrayOutputStream()
execOperations.exec {
it.commandLine "git", "--version"
it.standardOutput = output
}
return new String(output.toByteArray(), Charset.defaultCharset())
}
}
You can also use standard Java/Kotlin/Groovy process APIs like java.lang.ProcessBuilder
in the ValueSource
.
The ValueSource
implementation can then be used to create a provider with providers.of:
val gitVersionProvider = providers.of(GitVersionValueSource::class) {}
val gitVersion = gitVersionProvider.get()
def gitVersionProvider = providers.of(GitVersionValueSource.class) {}
def gitVersion = gitVersionProvider.get()
In both approaches, if the value of the provider is used at configuration time then it will become a build configuration input. The external process will be executed for every build to determine if the configuration cache is up-to-date, so it is recommended to only call fast-running processes at configuration time. If the value changes then the cache is invalidated and the process will be run again during this build as part of the configuration phase.
Reading system properties and environment variables
Plugins and build scripts may read system properties and environment variables directly at configuration time with standard Java, Groovy, or Kotlin APIs or with the value supplier APIs. Doing so makes such variable or property a build configuration input, so changing the value invalidates the configuration cache. The configuration cache report includes a list of these build configuration inputs to help track them.
In general, you should avoid reading the value of system properties and environment variables at configuration time, to avoid cache misses when value changes.
Instead, you can connect the Provider
returned by providers.systemProperty() or
providers.environmentVariable() to task properties.
Some access patterns that potentially enumerate all environment variables or system properties (for example, calling System.getenv().forEach()
or using the iterator of its keySet()
) are
discouraged.
In this case, Gradle cannot find out what properties are actual build configuration inputs, so every available property becomes one.
Even adding a new property will invalidate the cache if this pattern is used.
Using a custom predicate to filter environment variables is an example of this discouraged pattern:
val jdkLocations = System.getenv().filterKeys {
it.startsWith("JDK_")
}
def jdkLocations = System.getenv().findAll {
key, _ -> key.startsWith("JDK_")
}
The logic in the predicate is opaque to the configuration cache, so all environment variables are considered inputs.
One way to reduce the number of inputs is to always use methods that query a concrete variable name, such as getenv(String)
, or getenv().get()
:
val jdkVariables = listOf("JDK_8", "JDK_11", "JDK_17")
val jdkLocations = jdkVariables.filter { v ->
System.getenv(v) != null
}.associate { v ->
v to System.getenv(v)
}
def jdkVariables = ["JDK_8", "JDK_11", "JDK_17"]
def jdkLocations = jdkVariables.findAll { v ->
System.getenv(v) != null
}.collectEntries { v ->
[v, System.getenv(v)]
}
The fixed code above, however, is not exactly equivalent to the original as only an explicit list of variables is supported. Prefix-based filtering is a common scenario, so there are provider-based APIs to access system properties and environment variables:
val jdkLocationsProvider = providers.environmentVariablesPrefixedBy("JDK_")
def jdkLocationsProvider = providers.environmentVariablesPrefixedBy("JDK_")
Note that the configuration cache would be invalidated not only when the value of the variable changes or the variable is removed but also when another variable with the matching prefix is added to the environment.
For more complex use cases a custom ValueSource implementation can be used.
System properties and environment variables referenced in the code of the ValueSource
do not become build configuration inputs, so any processing can be applied.
Instead, the value of the ValueSource
is recomputed each time the build runs and only if the value changes the configuration cache is invalidated.
For example, a ValueSource
can be used to get all environment variables with names containing the substring JDK
:
abstract class EnvVarsWithSubstringValueSource : ValueSource<Map<String, String>, EnvVarsWithSubstringValueSource.Parameters> {
interface Parameters : ValueSourceParameters {
val substring: Property<String>
}
override fun obtain(): Map<String, String> {
return System.getenv().filterKeys { key ->
key.contains(parameters.substring.get())
}
}
}
val jdkLocationsProvider = providers.of(EnvVarsWithSubstringValueSource::class) {
parameters {
substring = "JDK"
}
}
abstract class EnvVarsWithSubstringValueSource implements ValueSource<Map<String, String>, Parameters> {
interface Parameters extends ValueSourceParameters {
Property<String> getSubstring()
}
Map<String, String> obtain() {
return System.getenv().findAll { key, _ ->
key.contains(parameters.substring.get())
}
}
}
def jdkLocationsProvider = providers.of(EnvVarsWithSubstringValueSource.class) {
parameters {
substring = "JDK"
}
}
Undeclared reading of files
Plugins and build scripts should not read files directly using the Java, Groovy or Kotlin APIs at configuration time. Instead, declare files as potential build configuration inputs using the value supplier APIs.
This problem is caused by build logic similar to this:
val config = file("some.conf").readText()
def config = file('some.conf').text
To fix this problem, read files using providers.fileContents() instead:
val config = providers.fileContents(layout.projectDirectory.file("some.conf"))
.asText
def config = providers.fileContents(layout.projectDirectory.file('some.conf'))
.asText
In general, you should avoid reading files at configuration time, to avoid invalidating configuration cache entries when the file content changes.
Instead, you can connect the Provider
returned by providers.fileContents() to task properties.
Bytecode modifications and Java agent
To detect the configuration inputs, Gradle modifies the bytecode of classes on the build script classpath, like plugins and their dependencies. Gradle uses a Java agent to modify the bytecode. Integrity self-checks of some libraries may fail because of the changed bytecode or the agent’s presence.
To work around this, you can use the Worker API with classloader or process isolation to encapsulate the library code. The bytecode of the worker’s classpath is not modified, so the self-checks should pass. When process isolation is used, the worker action is executed in a separate worker process that doesn’t have the Gradle Java agent installed.
In simple cases, when the libraries also provide command-line entry points (public static void main()
method), you can also use the JavaExec task to isolate the library.
Handling of credentials and secrets
The configuration cache has currently no option to prevent storing secrets that are used as inputs, and so they might end up in the serialized configuration cache entry which, by default, is stored under .gradle/configuration-cache
in your project directory.
To mitigate the risk of accidental exposure, Gradle encrypts the configuration cache.
Gradle transparently generates a machine-specific secret key as required, caches it under the
GRADLE_USER_HOME
directory and uses it to encrypt the data in the project specific caches.
To enhance security further, make sure to:
-
secure access to configuration cache entries;
-
leverage
GRADLE_USER_HOME/gradle.properties
for storing secrets. The content of that file is not part of the configuration cache, only its fingerprint. If you store secrets in that file, care must be taken to protect access to the file content.
See gradle/gradle#22618.
Providing an encryption key via GRADLE_ENCRYPTION_KEY
environment variable
By default, Gradle automatically generates and manages the encryption key as a Java keystore stored under the GRADLE_USER_HOME
directory.
For environments where this is undesirable (for instance, when the GRADLE_USER_HOME
directory is shared across machines),
you may provide Gradle with the exact encryption key to use when
reading or writing the cached configuration data via the GRADLE_ENCRYPTION_KEY
environment variable.
You must ensure that the same encryption key is consistently provided across multiple Gradle runs, or else Gradle will not be able to reuse existing cached configurations. |
Generating an encryption key that is compatible with GRADLE_ENCRYPTION_KEY
For Gradle to encrypt the configuration cache using a user-specified encryption key, you must run Gradle while having the GRADLE_ENCRYPTION_KEY environment variable set with a valid AES key, encoded as a Base64 string.
One way of generating a Base64-encoded AES-compatible key is by using a command like this:
❯ openssl rand -base64 16
This command should work on Linux, Mac OS, or on Windows, if using a tool like Cygwin.
You can then use the Base64-encoded key produced by that command and set it as the value of the
GRADLE_ENCRYPTION_KEY
environment variable.
Not yet implemented
Support for using configuration caching with certain Gradle features is not yet implemented. Support for these features will be added in later Gradle releases.
Sharing the configuration cache
The configuration cache is currently stored locally only. It can be reused by hot or cold local Gradle daemons. But it can’t be shared between developers or CI machines.
See gradle/gradle#13510.
Source dependencies
Support for source dependencies is not yet implemented. With the configuration cache enabled, no problem will be reported and the build will fail.
See gradle/gradle#13506.
Using a Java agent with builds run using TestKit
When running builds using TestKit, the configuration cache can interfere with Java agents, such as the Jacoco agent, that are applied to these builds.
See gradle/gradle#25979.
Fine-grained tracking of Gradle properties as build configuration inputs
Currently, all external sources of Gradle properties (gradle.properties
in project directories and in the GRADLE_USER_HOME
, environment variables and system properties that set properties, and properties specified with
command-line flags) are considered build configuration inputs regardless of what properties are actually used at configuration time. These sources, however, are not included in the configuration cache report.
See gradle/gradle#20969.
Java Object Serialization
Gradle allows objects that support the Java Object Serialization protocol to be stored in the configuration cache.
The implementation is currently limited to serializable classes that
either implement the java.io.Externalizable
interface, or implement the java.io.Serializable
interface and define one of the following combination of methods:
-
a
writeObject
method combined with areadObject
method to control exactly which information to store; -
a
writeObject
method with no correspondingreadObject
;writeObject
must eventually callObjectOutputStream.defaultWriteObject
; -
a
readObject
method with no correspondingwriteObject
;readObject
must eventually callObjectInputStream.defaultReadObject
; -
a
writeReplace
method to allow the class to nominate a replacement to be written; -
a
readResolve
method to allow the class to nominate a replacement for the object just read;
The following Java Object Serialization features are not supported:
-
the
serialPersistentFields
member to explicitly declare which fields are serializable; the member, if present, is ignored; the configuration cache considers all buttransient
fields serializable; -
the following methods of
ObjectOutputStream
are not supported and will throwUnsupportedOperationException
:-
reset()
,writeFields()
,putFields()
,writeChars(String)
,writeBytes(String)
andwriteUnshared(Any?)
.
-
-
the following methods of
ObjectInputStream
are not supported and will throwUnsupportedOperationException
:-
readLine()
,readFully(ByteArray)
,readFully(ByteArray, Int, Int)
,readUnshared()
,readFields()
,transferTo(OutputStream)
andreadAllBytes()
.
-
-
validations registered via
ObjectInputStream.registerValidation
are simply ignored; -
the
readObjectNoData
method, if present, is never invoked;
See gradle/gradle#13588.
Accessing top-level methods and variables of a build script at execution time
A common approach to reuse logic and data in a build script is to extract repeating bits into top-level methods and variables. However, calling such methods at execution time is not currently supported if the configuration cache is enabled.
For builds scripts written in Groovy, the task fails because the method cannot be found.
The following snippet uses a top-level method in the listFiles
task:
def dir = file('data')
def listFiles(File dir) {
dir.listFiles({ file -> file.isFile() } as FileFilter).name.sort()
}
tasks.register('listFiles') {
doLast {
println listFiles(dir)
}
}
Running the task with the configuration cache enabled produces the following error:
Execution failed for task ':listFiles'. > Could not find method listFiles() for arguments [/home/user/gradle/samples/data] on task ':listFiles' of type org.gradle.api.DefaultTask.
To prevent the task from failing, convert the referenced top-level method to a static method within a class:
def dir = file('data')
class Files {
static def listFiles(File dir) {
dir.listFiles({ file -> file.isFile() } as FileFilter).name.sort()
}
}
tasks.register('listFilesFixed') {
doLast {
println Files.listFiles(dir)
}
}
Build scripts written in Kotlin cannot store tasks that reference top-level methods or variables at execution time in the configuration cache at all.
This limitation exists because the captured script object references cannot be serialized.
The first run of the Kotlin version of the listFiles
task fails with the configuration cache problem.
val dir = file("data")
fun listFiles(dir: File): List<String> =
dir.listFiles { file: File -> file.isFile }.map { it.name }.sorted()
tasks.register("listFiles") {
doLast {
println(listFiles(dir))
}
}
To make the Kotlin version of this task compatible with the configuration cache, make the following changes:
object Files { (1)
fun listFiles(dir: File): List<String> =
dir.listFiles { file: File -> file.isFile }.map { it.name }.sorted()
}
tasks.register("listFilesFixed") {
val dir = file("data") (2)
doLast {
println(Files.listFiles(dir))
}
}
1 | Define the method inside an object. |
2 | Define the variable in a smaller scope. |
See gradle/gradle#22879.
Using build services to invalidate the configuration cache
Currently, it is impossible to use a BuildServiceProvider
or provider derived from it with map
or flatMap
as a parameter for the ValueSource
, if the value of the ValueSource
is accessed at configuration time.
The same applies when such a ValueSource
is obtained in a task that executes as part of the configuration phase, for example tasks of the buildSrc
build or included builds contributing plugins.
Note that using a @ServiceReference
or storing BuildServiceProvider
in an @Internal
-annotated property of a task is safe.
Generally speaking, this limitation makes it impossible to use a BuildService
to invalidate the configuration cache.
See gradle/gradle#24085.