You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by sl...@apache.org on 2026/09/11 19:29:38 UTC
(maven-jdeps-plugin) branch master updated: Auto-detect multi-release JARs and default --multi-release to base
This is an automated email from the ASF dual-hosted git repository.
slachiewicz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-jdeps-plugin.git
The following commit(s) were added to refs/heads/master by this push:
new 8015e8b Auto-detect multi-release JARs and default --multi-release to base
8015e8b is described below
commit 8015e8b40170522961354e2d398b536e59a1ad93
Author: Sylwester Lachiewicz <sl...@apache.org>
AuthorDate: Fri Sep 11 21:18:31 2026 +0200
Auto-detect multi-release JARs and default --multi-release to base
jdeps refuses to analyze a classpath containing a multi-release JAR
unless --multi-release is set, but the plugin only set it when a
user explicitly configured <multiRelease>. Any dependency bump that
introduces a multi-release JAR (e.g. plexus-utils since 3.5.0) broke
the build with "is a multi-release jar file but --multi-release
option is not set", closed as MJDEPS-15/wontfix in 2019 on the
mistaken assumption that a newer JDK point release made the flag
unnecessary — verified that assumption no longer holds on JDK 17.
Verified empirically that --multi-release base is always safe to
pass, even on an all-non-multi-release classpath or a mixed one, so
this now defaults to it automatically whenever any analyzed JAR is
a multi-release JAR and the resolved jdeps executable is JDK 9+
(detected via `jdeps -version`, since the plugin still targets and
is tested against JDK 8). A user-configured <multiRelease> value is
left untouched.
Fixes #105.
---
.../maven/plugins/jdeps/AbstractJDepsMojo.java | 116 ++++++++++++++++++++-
.../maven/plugins/jdeps/AbstractJDepsMojoTest.java | 55 ++++++++++
2 files changed, 169 insertions(+), 2 deletions(-)
diff --git a/src/main/java/org/apache/maven/plugins/jdeps/AbstractJDepsMojo.java b/src/main/java/org/apache/maven/plugins/jdeps/AbstractJDepsMojo.java
index 3f219ad..695f5e5 100644
--- a/src/main/java/org/apache/maven/plugins/jdeps/AbstractJDepsMojo.java
+++ b/src/main/java/org/apache/maven/plugins/jdeps/AbstractJDepsMojo.java
@@ -33,6 +33,8 @@ import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
@@ -77,12 +79,23 @@ public abstract class AbstractJDepsMojo extends AbstractMojo {
/**
* Specifies the version when processing multi-release JAR files version should be an integer >=9 or base.
+ * <p>
+ * When left unset, and any analyzed JAR turns out to be a multi-release JAR, {@code base} is used
+ * automatically (provided the resolved {@code jdeps} executable supports the option) so the build doesn't
+ * fail with {@code is a multi-release jar file but --multi-release option is not set} the moment a
+ * dependency ships one.
*
* @since 3.1.1
*/
@Parameter(property = "jdeps.multiRelease")
private String multiRelease;
+ /**
+ * The effective value of {@link #multiRelease}: either the user-configured value, or the value
+ * auto-detected by {@link #resolveMultiRelease(String, Set)}.
+ */
+ private String effectiveMultiRelease;
+
/**
* Whether only the sources need to be compatible or also every dependency on the classpath.
*
@@ -225,6 +238,13 @@ public abstract class AbstractJDepsMojo extends AbstractMojo {
} catch (DependencyResolutionRequiredException e) {
throw new MojoExecutionException(e.getMessage(), e);
}
+
+ try {
+ effectiveMultiRelease = resolveMultiRelease(jExecutable, dependenciesToAnalyze);
+ } catch (DependencyResolutionRequiredException e) {
+ throw new MojoExecutionException(e.getMessage(), e);
+ }
+
addJDepsOptions(cmd, dependenciesToAnalyze);
addJDepsClasses(cmd, dependenciesToAnalyze);
@@ -308,9 +328,9 @@ public abstract class AbstractJDepsMojo extends AbstractMojo {
cmd.createArg().setValue(module);
}
- if (multiRelease != null) {
+ if (effectiveMultiRelease != null) {
cmd.createArg().setValue("--multi-release");
- cmd.createArg().setValue(multiRelease);
+ cmd.createArg().setValue(effectiveMultiRelease);
}
if (apiOnly) {
@@ -366,6 +386,98 @@ public abstract class AbstractJDepsMojo extends AbstractMojo {
}
}
+ /**
+ * Determines the value to pass as {@code --multi-release}: the user-configured {@link #multiRelease}
+ * if set, otherwise {@code "base"} if any analyzed JAR is itself a multi-release JAR and the resolved
+ * {@code jdeps} executable understands the option (JDK 9+), otherwise {@code null} (option omitted).
+ */
+ private String resolveMultiRelease(String jdepsExecutable, Set<Path> dependenciesToAnalyze)
+ throws DependencyResolutionRequiredException {
+ if (multiRelease != null) {
+ return multiRelease;
+ }
+
+ boolean hasMultiReleaseJar = false;
+ for (Path path : dependenciesToAnalyze) {
+ if (isMultiReleaseJar(path)) {
+ hasMultiReleaseJar = true;
+ break;
+ }
+ }
+ if (!hasMultiReleaseJar) {
+ for (Path path : getClassPath()) {
+ if (isMultiReleaseJar(path)) {
+ hasMultiReleaseJar = true;
+ break;
+ }
+ }
+ }
+
+ if (hasMultiReleaseJar && jdepsSupportsMultiRelease(jdepsExecutable)) {
+ getLog().debug("Detected a multi-release JAR on the classpath; defaulting to --multi-release base");
+ return "base";
+ }
+
+ return null;
+ }
+
+ private static boolean isMultiReleaseJar(Path path) {
+ File file = path.toFile();
+ if (!file.isFile()) {
+ return false;
+ }
+ try (JarFile jarFile = new JarFile(file)) {
+ Manifest manifest = jarFile.getManifest();
+ return manifest != null
+ && Boolean.parseBoolean(manifest.getMainAttributes().getValue("Multi-Release"));
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ private boolean jdepsSupportsMultiRelease(String jdepsExecutable) {
+ Commandline versionCmd = new Commandline();
+ versionCmd.setExecutable(jdepsExecutable);
+ versionCmd.createArg().setValue("-version");
+
+ CommandLineUtils.StringStreamConsumer out = new CommandLineUtils.StringStreamConsumer();
+ CommandLineUtils.StringStreamConsumer err = new CommandLineUtils.StringStreamConsumer();
+ try {
+ int exitCode = CommandLineUtils.executeCommandLine(versionCmd, out, err);
+ return exitCode == 0 && featureVersion(out.getOutput()) >= 9;
+ } catch (CommandLineException e) {
+ getLog().debug("Unable to determine the jdeps version; assuming no multi-release JAR support", e);
+ return false;
+ }
+ }
+
+ /**
+ * Extracts the JDK feature version from a {@code jdeps -version}/{@code java -version} style string,
+ * handling both the pre-JEP 223 scheme ({@code 1.8.0_...} → {@code 8}) and the current one
+ * ({@code 17.0.20.1} → {@code 17}).
+ */
+ static int featureVersion(String versionOutput) {
+ String v = versionOutput == null ? "" : versionOutput.trim();
+ if (v.startsWith("1.")) {
+ v = v.substring(2);
+ }
+ int dot = v.indexOf('.');
+ String major = dot >= 0 ? v.substring(0, dot) : v;
+
+ int end = 0;
+ while (end < major.length() && Character.isDigit(major.charAt(end))) {
+ end++;
+ }
+ if (end == 0) {
+ return -1;
+ }
+ try {
+ return Integer.parseInt(major.substring(0, end));
+ } catch (NumberFormatException e) {
+ return -1;
+ }
+ }
+
private String getJDepsExecutable() throws IOException {
Toolchain tc = getToolchain();
diff --git a/src/test/java/org/apache/maven/plugins/jdeps/AbstractJDepsMojoTest.java b/src/test/java/org/apache/maven/plugins/jdeps/AbstractJDepsMojoTest.java
index 776d18a..fa1f9e3 100644
--- a/src/test/java/org/apache/maven/plugins/jdeps/AbstractJDepsMojoTest.java
+++ b/src/test/java/org/apache/maven/plugins/jdeps/AbstractJDepsMojoTest.java
@@ -29,6 +29,7 @@ import org.apache.maven.toolchain.ToolchainManager;
import org.codehaus.plexus.util.cli.Commandline;
import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -88,4 +89,58 @@ class AbstractJDepsMojoTest {
assertFalse(
cmdLine.contains("-jdkinternals"), "Command line should not contain -jdkinternals flag when disabled");
}
+
+ @Test
+ void testMultiReleaseOptionIsAddedWhenExplicitlyConfigured() throws Exception {
+ TestJDepsMojo mojo = new TestJDepsMojo(null);
+
+ Field effectiveMultiReleaseField = AbstractJDepsMojo.class.getDeclaredField("effectiveMultiRelease");
+ effectiveMultiReleaseField.setAccessible(true);
+ effectiveMultiReleaseField.set(mojo, "base");
+
+ Commandline cmd = new Commandline();
+ Set<Path> dependenciesToAnalyze = new HashSet<>();
+ dependenciesToAnalyze.add(Paths.get("/path/to/classes"));
+
+ mojo.addJDepsOptions(cmd, dependenciesToAnalyze);
+
+ String cmdLine = cmd.toString();
+ assertTrue(cmdLine.contains("--multi-release"), "Command line should contain --multi-release flag");
+ assertTrue(cmdLine.contains("base"), "Command line should contain the configured multi-release value");
+ }
+
+ @Test
+ void testMultiReleaseOptionNotAddedByDefault() throws Exception {
+ TestJDepsMojo mojo = new TestJDepsMojo(null);
+
+ Commandline cmd = new Commandline();
+ Set<Path> dependenciesToAnalyze = new HashSet<>();
+ dependenciesToAnalyze.add(Paths.get("/path/to/classes"));
+
+ mojo.addJDepsOptions(cmd, dependenciesToAnalyze);
+
+ String cmdLine = cmd.toString();
+ assertFalse(
+ cmdLine.contains("--multi-release"),
+ "Command line should not contain --multi-release flag unless resolved");
+ }
+
+ @Test
+ void testFeatureVersionParsesCurrentScheme() {
+ assertEquals(17, AbstractJDepsMojo.featureVersion("17.0.20.1"));
+ assertEquals(9, AbstractJDepsMojo.featureVersion("9-ea"));
+ assertEquals(21, AbstractJDepsMojo.featureVersion("21"));
+ }
+
+ @Test
+ void testFeatureVersionParsesLegacyScheme() {
+ assertEquals(8, AbstractJDepsMojo.featureVersion("1.8.0_292"));
+ }
+
+ @Test
+ void testFeatureVersionReturnsNegativeOneOnGarbage() {
+ assertEquals(-1, AbstractJDepsMojo.featureVersion(""));
+ assertEquals(-1, AbstractJDepsMojo.featureVersion(null));
+ assertEquals(-1, AbstractJDepsMojo.featureVersion("not a version"));
+ }
}