Skip to content

Emit clear error when running JDK cannot compile source level - #12633

Draft
gnodet wants to merge 12 commits into
masterfrom
implement-automatic-jdk-toolchain-selection-when-t
Draft

Emit clear error when running JDK cannot compile source level#12633
gnodet wants to merge 12 commits into
masterfrom
implement-automatic-jdk-toolchain-selection-when-t

Conversation

@gnodet

@gnodet gnodet commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

When the running JDK does not support the project's required --source/--release level (per JEP 182 retirement schedule), Maven currently lets the build fail with a cryptic javac error. This PR adds two complementary features:

  1. mvnup — a new ToolchainPluginStrategy that automatically adds the maven-toolchains-plugin with the select-jdk-toolchain goal when the project's source level requires an older JDK
  2. Core error message — when the running JDK cannot compile the project's source level and no toolchain is configured, emit a clear, actionable error instead of letting javac fail cryptically

Example error output (at build time, if mvnup was not run)

[ERROR] Project requires --source 6 which needs JDK <= 11, but the running JDK 21 no longer supports it.
[ERROR] To fix: run 'mvnup' to add the maven-toolchains-plugin with automatic JDK discovery,
[ERROR] or install JDK 11 and configure it in toolchains.xml or via the maven-toolchains-plugin.

Design

mvnup: ToolchainPluginStrategy

A new UpgradeStrategy (@Priority(15)) that runs by default (same activation as CompatibilityFixStrategy):

  1. Detects the project's source level from maven.compiler.release / maven.compiler.source properties, or compiler plugin <configuration> (<release> / <source>)
  2. Checks if the running JDK supports it (using JdkSourceLevelSupport)
  3. Skips if the maven-toolchains-plugin with select-jdk-toolchain goal is already configured
  4. Adds the plugin with the goal to the build > plugins section if not present

This leverages the plugin's existing JDK discovery mechanism (ToolchainDiscoverer) rather than duplicating filesystem scanning in core.

Core: DefaultToolchainManager.checkJdkSourceLevelCompatibility()

Called from getToolchainFromBuildContext("jdk") when no explicit toolchain is configured:

  1. Reads the source level from Model 4.1.0 source > targetVersion, compiler properties, or compiler plugin config
  2. If the running JDK is incompatible, emits the error message above with the exact JDK version needed

Shared: JdkSourceLevelSupport (public)

Maps JDK versions to supported source levels per the JEP 182 retirement schedule. Provides:

  • supportsSourceLevel(jdkMajor, sourceLevel)
  • latestJdkForSourceLevel(sourceLevel)
  • normalizeSourceLevel(version) — handles 1.8, 1.8.0_392, 21.0.1, etc.

Test plan

  • Unit tests for JdkSourceLevelSupport — normalization, compatibility, latestJdkForSourceLevel() (19 tests)
  • Unit tests for DefaultToolchainManager — error emitted/suppressed for various source levels and detection paths (16 tests)
  • Unit tests for ToolchainPluginStrategy — applicability, source level detection from properties/plugin config, plugin detection, plugin addition, full apply scenarios (20 tests)
  • maven-impl module: 569 tests pass
  • maven-compat module: 160 tests pass
  • IT suite compiles clean

gnodet and others added 2 commits July 30, 2026 16:31
…'s source level

When the running JDK does not support the project's --source/--release level
(e.g., JDK 17 cannot compile --source 6), Maven now automatically searches
configured toolchains for a compatible JDK and selects it for compilation.

The auto-selection hooks into getToolchainFromBuildContext() — when no
explicit toolchain has been set via maven-toolchains-plugin, the manager
checks the project's targetVersion (Model 4.1.0) or legacy properties
(maven.compiler.release, maven.compiler.source) against the running JDK's
supported source levels. If the running JDK cannot handle the required
level, it picks the newest compatible JDK from configured toolchains,
caches the selection, and emits a warning.

Key changes:
- New JdkSourceLevelSupport utility mapping JDK versions to supported
  --source levels based on the javac retirement schedule (JEP 182)
- DefaultToolchainManager.getToolchainFromBuildContext() now falls back
  to auto-selection for "jdk" type when no explicit toolchain is stored
- Reads source level from Model 4.1.0 <source><targetVersion> elements
  and legacy maven.compiler.release/source properties
- Prefers the newest compatible JDK from configured toolchains

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two ITs verify the auto-selection behavior end-to-end through Maven's
compat bridge:
- testAutoSelectToolchainWhenSourceLevelUnsupported: project requires
  source 6 (unsupported by JDK 12+), verifies Maven auto-selects a
  compatible JDK 11 toolchain and logs a warning
- testNoAutoSelectWhenSourceLevelSupported: project requires source 11
  (supported by JDK 12+), verifies no auto-selection occurs

Also adds debug logging to the auto-selection path for diagnostics.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@elharo elharo added the enhancement New feature or request label Jul 30, 2026

@elharo elharo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. does this need an issue to get picked up in the release notes?

@gnodet

gnodet commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

Good idea. does this need an issue to get picked up in the release notes?

No, an issue and a PR are similar in all points, but the fact that an issue does not have code.

gnodet and others added 7 commits July 31, 2026 07:44
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When no compatible JDK is found in toolchains.xml, Maven now lazily
discovers JDKs from the filesystem by scanning well-known locations:
JAVA*_HOME env vars, SDKMan, IntelliJ .jdks/, Gradle, jEnv, JBang,
asdf, mise, and OS-specific paths (/usr/lib/jvm, etc.).

Discovery only runs on the failure path (running JDK incompatible
AND no configured toolchain matches), so normal builds pay zero cost.
JDK version is read from the release file (no java process spawned).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace direct System.getProperty/System.getenv calls with the
properties map from Session.getSystemProperties(), where env vars
are available as "env.VAR_NAME" entries. This follows Maven's
convention of accessing environment through the Session and makes
the discoverer fully testable without relying on JVM global state.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nd JDK discovery

Three fixes for automatic JDK toolchain selection:

1. Read source level from compiler plugin configuration: many projects set
   <source> and <release> directly in the maven-compiler-plugin <configuration>
   block rather than as properties. Added getSourceLevelFromCompilerPlugin()
   to check both <release> and <source> elements.

2. Pass JdkToolchainDiscoverer to compat layer: ToolchainManagerFactory was
   creating DefaultToolchainManager without the discoverer, so old plugins
   going through the compat layer could detect incompatibility but had no
   discoverer to find alternative JDKs.

3. Scan JAVA_HOME parent directory for sibling JDKs: in CI and container
   environments, multiple JDKs are often installed as siblings (e.g.
   /toolchain/jdk-8, /toolchain/jdk-11, /toolchain/jdk-21). Added parent
   directory scanning from each JAVA*_HOME env var.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The gh-11055 DI service injection IT calls new DefaultToolchainManager(Map.of())
from outside the package, so the single-arg Map constructor must be public.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…nd IT harness

The gh-11055 DI service injection IT calls new DefaultToolchainManager(Map.of())
from outside the package, and the compat layer ToolchainManagerFactory calls
the 3-arg constructor (factories, discoverer, logger). All constructors must be
accessible from outside the package.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove the JdkToolchainDiscoverer and automatic JDK toolchain selection.
Instead, when the running JDK cannot honour the project's --source/--release
level, emit a clear, actionable error message telling the user which JDK
version they need and suggesting 'mvnup' to add the maven-toolchains-plugin
with automatic discovery.

This avoids duplicating the maven-toolchains-plugin's discovery mechanism
in core and follows a simpler "detect and error" approach.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gnodet gnodet changed the title Auto-select JDK toolchain when running JDK cannot compile source level Emit clear error when running JDK cannot compile source level Aug 5, 2026
gnodet and others added 3 commits August 5, 2026 15:33
When mvnup detects a project's --source/--release level is no longer
supported by the running JDK (per JEP 182), automatically add the
maven-toolchains-plugin with the select-jdk-toolchain goal. This
leverages the plugin's built-in JDK discovery mechanism instead of
duplicating it in core.

Also makes JdkSourceLevelSupport methods public so they can be reused
across modules (maven-impl → maven-cli).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The select-jdk-toolchain goal needs a <version> constraint to know
which JDK to select. Without it, it picks the latest LTS (e.g. 21)
which defeats the purpose. Now generates e.g. <version>(,8]</version>
based on latestJdkForSourceLevel() so the plugin matches a JDK that
actually supports the project's source level.

Verified end-to-end: mvnup on servicemix-utils (source 1.5) adds
the plugin with (,8], and with JAVA8_HOME set the build succeeds
with forked compilation via JDK 8.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Tests the full end-to-end flow: mvnup apply on a POM with
maven.compiler.source=1.5 adds the maven-toolchains-plugin with
select-jdk-toolchain goal and version constraint (,8], then verifies
a second run is idempotent.

Also adds mvnup to the chmod list in the IT suite's antrun config
so it gets execute permission when extracted from the zip.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants