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:18:41 UTC

(maven-jdeps-plugin) branch agent/auto-detect-multi-release created (now 091bcc8)

This is an automated email from the ASF dual-hosted git repository.

slachiewicz pushed a change to branch agent/auto-detect-multi-release
in repository https://gitbox.apache.org/repos/asf/maven-jdeps-plugin.git


      at 091bcc8  Auto-detect multi-release JARs and default --multi-release to base

This branch includes the following new commits:

     new 091bcc8  Auto-detect multi-release JARs and default --multi-release to base

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



(maven-jdeps-plugin) 01/01: Auto-detect multi-release JARs and default --multi-release to base

Posted by sl...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

slachiewicz pushed a commit to branch agent/auto-detect-multi-release
in repository https://gitbox.apache.org/repos/asf/maven-jdeps-plugin.git

commit 091bcc88f2a9b1174b040ba777265bea3045d858
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"));
+    }
 }