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_...} &rarr; {@code 8}) and the current one
+     * ({@code 17.0.20.1} &rarr; {@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"));
+    }
 }