Skip to content

Gradle 9 + parallel builds: apt/init.gradle fails with IllegalResolutionException, aborting classpath refresh (generated source folders lost) #3807

Description

@petrmac

Summary

gradle/apt/init.gradle (the bundled annotation-processor discovery script) fails on Gradle 9 multi-project builds that use org.gradle.parallel=true. The Map tooling-model fetch throws IllegalResolutionException, which aborts the classpath refresh for every project in the build. The most visible casualty: source folders for generated code (e.g. codegen output registered in sourceSets.main.java.srcDirs under build/generated/...) never reach the JDT classpath, so all generated types are reported as unresolved across the workspace — even though the same build compiles cleanly from the CLI and goToDefinition/hover work for regular sources.

This was previously reported as #3505, but that issue was closed by the reporter after finding the org.gradle.parallel=false workaround. Disabling parallel execution project-wide is not a viable fix for larger builds (it taxes every developer and CI run to work around a language-server script), so I'd like to propose an actual fix. The same crash has also been independently hit by other jdt.ls embedders (e.g. oraios/serena#1510, which resorted to making annotation processing disableable as an escape hatch). Notably, clients that cannot forward java.import.gradle.jvmArguments (headless/agent launchers using plain jdtls) have no configuration-level workaround at all.

Environment

  • jdt.ls 1.58.0 (also reproduced against current master's org.eclipse.jdt.ls.core/gradle/apt/init.gradle, which is identical)
  • Gradle 9.3.1 (first broken in 9.0.0 per init.gradle exist one error since gradle 9.0.0 #3505)
  • Multi-project build (~10 subprojects), org.gradle.parallel=true in gradle.properties
  • JDK 25 toolchain, annotation processors present (Lombok, MapStruct)

Error

!MESSAGE Could not fetch model of type 'Map' using connection to Gradle distribution 'https://services.gradle.org/distributions/gradle-9.3.1-bin.zip'.
org.gradle.tooling.BuildException: Could not fetch model of type 'Map' ...
Caused by: org.gradle.internal.exceptions.LocationAwareException:
  Initialization script '<config>/org.eclipse.osgi/59/0/.cp/gradle/apt/init.gradle' line: 88
Caused by: org.gradle.api.internal.artifacts.configurations.DefaultConfiguration$IllegalResolutionException:
  Resolution of the configuration ':someproject:annotationProcessor' was attempted without an exclusive lock.
  This is unsafe and not allowed.

Root cause

AnnotationProcessorModelBuilder.buildAll(...) iterates rootProject.getAllprojects() and, for each project, calls options.getAnnotationProcessorPath().getFiles() (line 88). That resolves the annotationProcessor configuration of other projects from the root project's model-builder thread, without holding those projects' state locks. Gradle 9 with parallel execution enforces exclusive locking on configuration resolution and throws.

Because the exception propagates out of buildAll, the entire model fetch fails, and GradleBuildSupport never gets to update project classpaths — so the breakage radius is much larger than annotation-processor metadata: any source-folder change coming from the Gradle model (including generated-source dirs) is silently lost. Diagnosing this from the user side is hard because the visible symptom ("generated types unresolved") is three steps removed from the cause.

Minimal mitigation (tested)

Wrapping the per-project collection in a try/catch makes the model builder degrade to "no AP info for that project" instead of failing the entire fetch:

private void collectApConfiguration(project, compileTaskName, processors, compilerArgs) {
    try {
        JavaCompile javaCompile = project.getTasks().findByName(compileTaskName)
        if (javaCompile != null) {
            CompileOptions options = javaCompile.getOptions()
            if (!options.compilerArgs.contains("-proc:none")) {
                FileCollection apPath = options.getAnnotationProcessorPath()
                if (apPath != null) {
                    processors.addAll(apPath.getFiles())
                }
                compilerArgs.addAll(options.getCompilerArgs())
            }
        }
    } catch (Throwable t) {
        // Gradle 9 parallel builds: resolving another project's configuration
        // without its state lock throws IllegalResolutionException. Degrade to
        // "no AP info" for this project rather than failing the whole model.
    }
}

With this patch applied locally, the sync completes, classpath refresh runs, generated source folders are registered, and generated types resolve again. The trade-off is that AP metadata may be incomplete for projects whose configuration couldn't be resolved.

A proper fix probably wants the resolution to happen under the owning project's lock — e.g. via project.getOwner().fromMutableState { ... } semantics or by restructuring the builder so each project contributes its own AP info instead of the root walking all projects (which is also the direction Gradle's isolated-projects effort enforces).

Happy to turn either variant into a PR if maintainers have a preference.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions