Gradle extensions compatible with configuration cache

Hi,
Some time ago I wrote Gradle plugin that uses extension and registers it.

public class OpenApiPlugin implements Plugin<Project> {
public void apply(Project project) {
 var openApiExtension = project.getExtensions().create("openApi", OpenApiExtension.class);
 project.getTasks().register("openapi", OpenApiGenerateTask.class); 
 project.afterEvaluate(x -> addSourceSets(x, openApiExtension, project.getLayout().getBuildDirectory().get().getAsFile()));
}

Everything works as expected as long as I do not have configuration cache enabled in properties file.
I recently tried to apply my plugins to a project that uses configuration cache. Unfortunately running openApi task yields:

Task :openapi FAILED
[Incubating] Problems report is available at: file:///*/build/reports/problems/problems-report.html
FAILURE: Build completed with 2 failures.

1: Task failed with an exception.

-----------

What went wrong:

Execution failed for task ':openapi'.
 Extension of type 'OpenApiExtension' does not exist. Currently registered extension types: [ExtraPropertiesExtension]

2: Task failed with an exception.
-----------

* What went wrong:
Configuration cache problems found in this build.
1 problem was found storing the configuration cache.
- Task `:openapi` of type `openapi.OpenApiGenerateTask`: invocation of 'Task.project' at execution time is unsupported.

See https://docs.gradle.org/8.13/userguide/configuration_cache.html#config_cache:requirements:use_project_during_execution

See the complete report at file:///Users/*/build/reports/configuration-cache/bjual5udy3l26hkfaui666twe/2exkfognnzyxz01u9gz0c7e0h/configuration-cache-report.html

Invocation of 'Task.project' by task ':openapi' at execution time is unsupported.

Problems report direct me to: Configuration cache which provides a number of replacement methods for Project methods.

Unfortunately there is nothing for extensions.

Could you please provide me with an example how to rewrite my code or point me to the documentation that could help me do this?

You missed to provide all relevant code, so it is hard to give concrete advice. :smiley:

Failure 1 says you want to access an extension of type OpenApiExtension without that extension being registered. But the code you showed does not access it but registers it, so it is impossible to say where you hit that as you neither provided the --stacktrace nor a build --scan URL.
My best guess that is the same location as failure 2 and you try to do at executiont time project.extensions.getByType(OpenApiExtension.class), but with configuration cache you must not access the project model at execution time.
Usually your extension has properties of type Property and friends and your task also has properties of type Property and friends, and your plugin wires those together.

Failure 2 says that you try to access project at execution time of the OpenApiGenerateTask which is deprecated and with configuration cache enabled forbidden, causing that failure. As I said, I assume you try to use it to get hold of the extension.

Besides being bad practice to do it like that, it also most probably means you do not have the inputs and outputs of your task declared properly, as they are not defined as input and output properties of the task but you try to get them at execution time from the extension. That means your task can never properly be up-to-date (or is up-to-date when it shouldn’t be) and can also not properly be wired to other tasks or ever made properly cacheable without poisoning the cache.

Also, you should find an alternative to using afterEvaluate under almost all circumstances. Using afterEvaluate is highly discouraged bad practice. The main benefit you gain from it are timing problems, ordering problems, and race conditions.

Also getting a property at configuration time (like you do with your get() call you showed) is highly discouraged and should be avoided wherever possible. Whenever you get() a property or similar lazy types at configuration time you again introduce race conditions, for example someone or something could set the layout.buildDirectory after you have read it there and so you do not get the final value. Due to that you should optimally always wire Property and friends together and only ever query their value at execution time.

Thanks you for swift response!

The source code for Extension:

public abstract class OpenApiExtension {
  @Input
  public abstract ListProperty<String> getProducer();
  public void producer(String... producedContracts) {
    getProducer().set(Arrays.asList(producedContracts));
  }

  @Input
  public abstract ListProperty<String> getConsumer();
  public void consumer(String... consumedContracts) {
    getConsumer().set(Arrays.asList(consumedContracts));
  }
}

Beside registration there is another place were I do hit Project object:

@CacheableTask
public class OpenApiGenerateTask extends DefaultTask {
   ...

  @TaskAction
  public void doWork() {
    var project = getProject();
    var openApiExtension = project.getExtensions().getByType(OpenApiExtension.class);
    gitContractsAdapter = openApiExtension.getContractsHost().orElse(DEFAULT_URL).get();
    var gradleBuildDir = project.getLayout().getBuildDirectory().get().getAsFile();

    generateContractStubs(gradleBuildDir, CONSUMER, openApiExtension.getConsumer());
    generateContractStubs(gradleBuildDir, PRODUCER, openApiExtension.getProducer());
  }
…
}

Exception being shown in report is:

[error] invocation of `Task.project` at execution time is unsupported.
    - task `:openapi` of type `ph.maya.isse.openapi.OpenApiGenerateTask`
        - Exception at `ph.maya.isse.openapi.OpenApiGenerateTask.doWork(OpenApiGenerateTask.java:55)`
org.gradle.api.InvalidUserCodeException: Invocation of 'Task.project' by task ':openapi' at execution time is unsupported.
	at org.gradle.api.DefaultTask.getProject(DefaultTask.java:59)(26 internal lines hidden)
	at ph.maya.isse.openapi.OpenApiGenerateTask.doWork(OpenApiGenerateTask.java:55)
	at java.base/java.lang.Thread.run(Thread.java:1583)(125 internal lines hidden)

Line 55 in OpenApiGenerateTask.java is:
var project = getProject();

So what I understand I should

  1. Find different way of registering Extension → could you point me the documentation that describes Extension registration without a call to Project object?
  2. Use @Input for OpenApiGenerateTask instead of getting properties directly from extension.
  3. Get rid of project.afterEvaluate().
  4. Not use project.getLayout().getBuildDirectory().get().getAsFile() and wire Property to get build directory.

many thanks,
Maciej

edit:
I checked Implementing Binary Plugins documentation but it uses Project object to register extensions as well.

The source code for Extension:

Having @Input annotations on the extension is little helpful unless you use it somewhere as @Nested property.
But as I said, usually your plugin should wire the single properties from the extension to single properties from the task.
The task properties need to have the input- and output-property annotations for being effective for up-to-date checks and cache-key calculation.

Beside registration there is another place were I do hit Project object:

Yes, that is exactly what I said is most probably the problem.
You access getProject() which is deprecated without configuration cache and a hard error with configuration cache.
And you try to get the extension and its values at execution time which is bad practice without configuration cache and a hard error with configuration cache.
And just as I assumed, your task is declared cacheable, but it does not properly declare its inputs and outputs, as the extension values influence the outcome of the task but are not declared as inputs, which means the task will be up-to-date when it should not be and also results are written to and reused from the build cache even if the inputs are different as they are not properly declared.

Find different way of registering Extension

Again, the registering is fine, but you must not get it at execution time in the task, but wire its properties to input properties on the task in your plugin.

Use @Input for OpenApiGenerateTask

On respective input properties on it that you wire to the extension properties, yes.

Get rid of project.afterEvaluate().

Optimally, yes.

Not use project.getLayout().getBuildDirectory().get().getAsFile() and wire Property to get build directory.

Correct. Wiring project.getLayout().getBuildDirectory() or project.getLayout().getBuildDirectory().map { ... } or project.getLayout().getBuildDirectory().flatMap { ... } or similar to some property is fine, calling get() at configuration time is bad. I cannot give more concrete advice as you didn’t show how it is used.

Hi,
I made changes mentioned by you and the configuration cache issue is gone.

For anyone reading it in the future:

  • I added @Input properties in my Task
  • In the class that extends Plugin I mapped properties from Extension instance to the task
  • Configured buildDir property as: @InputDirectory @PathSensitive(PathSensitivity.RELATIVE) public DirectoryProperty gradleBuildDir;

Now the only missing part is removal of project.afterEvaluate(). Everything else works as expected!

Thank you for your help!

1 Like

Do you really want the contents of all files in the build directory to be an input to your task?
That sounds like a very bad idea and like your task will never be up-to-date or served from cache.

Do you really want the contents of all files in the build directory to be an input to your task?

No, not really. It’s just a way to inform my custom task where the build directory is. But I assume there’s a better way to do it, right? I assume I could add the following line to my task and use projectLayout moving forward :

@Inject public ProjectLayout projectLayout;

What my task does is:

  1. Get configuration from Extension configuration in build.gradle.kts
  2. Download files based on aforementioned configuration
  3. Generate Java code based on downloaded files
  4. Add newly generated files/directories to sourceSets

But I assume there’s a better way to do it, right?

“Informing” it is totally unnecessary.
Just let a ProjectLayout instance be injected by Gradle then you could ask that.
But also that is most probably not what you should do.
Assuming you want to know where it is to generate the files to somewhere, instead you should declare an @OutputDirectory property that you set to something like layout.buildDirectory.dir("generated/sources/whatever") in the plugin that registers that task.

Download files based on aforementioned configuration

Can these files change on the serverside without change in configuration in your buildscript? If so, how can the task ever be up-to-date or cacheable if its outcome depends on that external mutable state? If it is the case, you should probably split your task into two, one that does the download and is an @UntrackedTask so that is always executed and then the task processing the downloaded files which can be up-to-date or cached if the downloaded input files are the same as previously.

Add newly generated files/directories to sourceSets

Hopefully you were unclear in your words and your task does not try to change the source set configuration, that is really not something a task should do.

Once your task properly declares its inputs and outputs correctly, you should just configure the task itself (or a provider for it) as srcDir for the source set, then the outputs of the task are automatically considered source files and all consumers of source files that properly request them automatically have the necessary task dependency on the generation task implicitly.

Hi,
I think I finally have working Java version. I’m sharing this if anyone needs it in the future.

Thank you @Vampire for all your help!

Key points:

  • Read and understand well Gradle manual: Understanding Properties and Providers
  • Read excellent blog post: CĂ©dric Champeau's blog: Understanding Gradle plugins: the provider API
  • Task class
    • Make your task class abstract
    • @Inject ProjectLayout via constructor injection
    • Provide @OutputDirectories and populate them in Plugin class.
    • Generate your source classes to those paths in @TaskAction marked method.
  • Plugin class
    • In Plugin.apply(), during task registration provide OutputDirectories values so your task can be registered as source for SourceSet
    • Register you task as SourceSet dir

I hope it helps anyone struggling with the same problems as I had.

Why would you inject ProjectLayout to the task class if you write to the @OutputDirectory that you configured from your plugin? :slight_smile:

Hi,
source code is generated as separate director with /src/main/java, /src/main/resources and some additional files here and there. Therefore I’m passing projectLayout.getBuildDir()+“/generated” to the generator. This is going to generate directory to: generated/producer/$GROUP/$CONTRACT/

Source set should set to: generated/producer/$GROUP/$CONTRACT/src/main/java

Injecting ProjectLayout allows me separating the two paths.

Sounds like you still do it wrongly.
You must not configure the path for the srcDir manually or you miss the implicit task dependency.
Sounds like you should have two @OutputDirectory properties, one for sources, one for resources.
Then you can configure those properties as srcDir for java and resources of the source set, properly configured output properties also inherit the task dependency.

I declare @OutputDirectorier property that I set to something like layout.buildDirectory.dir("generated/sources/whatever") in the plugin that registers that task.

Then I just configure the task provider as srcDir for the source set - also in Plugin class.

I hope that this is correct :slight_smile:

@OutputDirectory not @OutputDirectorier, but I guess that was just a typo. :smiley:

Well, if the task only generates sources that is fine.
But you said it generates sources and resources, so you would need it separated for main.java.srcDir and main.resources.srcDir, wouldn’t you?

Also with that setup you mean you do not longer inject the project layout into the task, do you?
If you do, I did not yet understand why.