From 52f14a2ad141a683c02db9ff889680c059c2e33c Mon Sep 17 00:00:00 2001 From: Abdeslam Yassine Agmar Date: Fri, 27 Mar 2026 22:55:10 +0100 Subject: [PATCH 1/3] fix(run): honor manifest launcher flags --- src/main/java/dev/jbang/source/Project.java | 3 + .../java/dev/jbang/source/ProjectBuilder.java | 22 +++--- .../source/generators/JarCmdGenerator.java | 40 +++++++---- src/test/java/dev/jbang/cli/TestRun.java | 70 +++++++++++++++++++ 4 files changed, 112 insertions(+), 23 deletions(-) diff --git a/src/main/java/dev/jbang/source/Project.java b/src/main/java/dev/jbang/source/Project.java index 5d583fbdd..035f21f30 100644 --- a/src/main/java/dev/jbang/source/Project.java +++ b/src/main/java/dev/jbang/source/Project.java @@ -54,6 +54,9 @@ public class Project { public static final String ATTR_PREMAIN_CLASS = "Premain-Class"; public static final String ATTR_AGENT_CLASS = "Agent-Class"; + public static final String ATTR_ADD_EXPORTS = "Add-Exports"; + public static final String ATTR_ADD_OPENS = "Add-Opens"; + public static final String ATTR_ENABLE_NATIVE_ACCESS = "Enable-Native-Access"; public enum BuildFile { jbang("build.jbang"); diff --git a/src/main/java/dev/jbang/source/ProjectBuilder.java b/src/main/java/dev/jbang/source/ProjectBuilder.java index 17bb60b14..76d0fa15b 100644 --- a/src/main/java/dev/jbang/source/ProjectBuilder.java +++ b/src/main/java/dev/jbang/source/ProjectBuilder.java @@ -440,18 +440,13 @@ private Project importJarMetadata(Project prj, boolean importModuleName) { prj.setJavaVersion(JavaUtil.parseJavaVersion(ver) + "+"); } - // we pass exports/opens into the project... + // we pass exports/opens/native access into the project... // TODO: this does mean we can't separate from user specified options and jar - // origined ones, but not sure if needed? + // originated ones, but not sure if needed? // https://openjdk.org/jeps/261#Breaking-encapsulation - String exports = attrs.getValue("Add-Exports"); - if (exports != null) { - prj.getManifestAttributes().put("Add-Exports", exports); - } - String opens = attrs.getValue("Add-Opens"); - if (opens != null) { - prj.getManifestAttributes().put("Add-Opens", exports); - } + copyManifestAttribute(attrs, prj, Project.ATTR_ADD_EXPORTS); + copyManifestAttribute(attrs, prj, Project.ATTR_ADD_OPENS); + copyManifestAttribute(attrs, prj, Project.ATTR_ENABLE_NATIVE_ACCESS); } @@ -482,6 +477,13 @@ private Project importJarMetadata(Project prj, boolean importModuleName) { return prj; } + private static void copyManifestAttribute(Attributes attrs, Project prj, String name) { + String value = attrs.getValue(name); + if (value != null) { + prj.getManifestAttributes().put(name, value); + } + } + private Project updateProject(Project prj) { SourceSet ss = prj.getMainSourceSet(); prj.addRepositories(allToMavenRepo(replaceAllProps(additionalRepos))); diff --git a/src/main/java/dev/jbang/source/generators/JarCmdGenerator.java b/src/main/java/dev/jbang/source/generators/JarCmdGenerator.java index 576cff8bf..90d9b13a0 100644 --- a/src/main/java/dev/jbang/source/generators/JarCmdGenerator.java +++ b/src/main/java/dev/jbang/source/generators/JarCmdGenerator.java @@ -90,6 +90,7 @@ protected List generateCommandLineList() throws IOException { List fullArgs = new ArrayList<>(); Project project = ctx.getProject(); + boolean runAsModule = moduleName != null && project.getModuleName().isPresent(); String classpath = ctx.resolveClassPath().getClassPath(); List optionalArgs = new ArrayList<>(); @@ -97,18 +98,22 @@ protected List generateCommandLineList() throws IOException { Jdk jdk = project.projectJdk(); String javacmd = JavaUtil.resolveInJavaHome("java", jdk); - if (jdk.majorVersion() > 9) { - String opens = ctx.getProject().getManifestAttributes().get("Add-Opens"); - if (opens != null) { - for (String val : opens.split(" ")) { - optionalArgs.add("--add-opens=" + val + "=ALL-UNNAMED"); - } - } + if (!runAsModule && jdk.majorVersion() >= 9) { + addAllUnnamedManifestOptions(optionalArgs, project.getManifestAttributes().get(Project.ATTR_ADD_OPENS), + "--add-opens="); + addAllUnnamedManifestOptions(optionalArgs, project.getManifestAttributes().get(Project.ATTR_ADD_EXPORTS), + "--add-exports="); + } - String exports = ctx.getProject().getManifestAttributes().get("Add-Exports"); - if (exports != null) { - for (String val : exports.split(" ")) { - optionalArgs.add("--add-exports=" + val + "=ALL-UNNAMED"); + if (!runAsModule && jdk.majorVersion() >= 22) { + String nativeAccess = project.getManifestAttributes().get(Project.ATTR_ENABLE_NATIVE_ACCESS); + if (nativeAccess != null) { + nativeAccess = nativeAccess.trim(); + if ("ALL-UNNAMED".equals(nativeAccess)) { + optionalArgs.add("--enable-native-access=" + nativeAccess); + } else { + throw new ExitException(BaseCommand.EXIT_INVALID_INPUT, + "Invalid value for manifest attribute Enable-Native-Access: " + nativeAccess); } } } @@ -197,7 +202,7 @@ protected List generateCommandLineList() throws IOException { } } if (!Util.isBlankString(classpath)) { - if (moduleName != null && project.getModuleName().isPresent()) { + if (runAsModule) { optionalArgs.addAll(Arrays.asList("-p", classpath)); } else { optionalArgs.addAll(Arrays.asList("-classpath", classpath)); @@ -230,7 +235,7 @@ protected List generateCommandLineList() throws IOException { String main = Optional.ofNullable(mainClass).orElse(project.getMainClass()); if (main != null && !Glob.isGlob(main)) { - if (moduleName != null && project.getModuleName().isPresent()) { + if (runAsModule) { String modName = moduleName.isEmpty() ? ModuleUtil.getModuleName(project) : moduleName; fullArgs.add("-m"); fullArgs.add(modName + "/" + main); @@ -312,6 +317,15 @@ protected String generateCommandLineString(List fullArgs) throws IOExcep .asCommandLine(); } + private static void addAllUnnamedManifestOptions(List result, String manifestValue, String optionPrefix) { + if (manifestValue == null) { + return; + } + Arrays.stream(manifestValue.trim().split("\\s+")) + .filter(val -> !val.isEmpty()) + .forEach(val -> result.add(optionPrefix + val + "=ALL-UNNAMED")); + } + private static void addPropertyFlags(Map properties, String def, List result) { properties.forEach((k, e) -> result.add(def + k + "=" + e)); } diff --git a/src/test/java/dev/jbang/cli/TestRun.java b/src/test/java/dev/jbang/cli/TestRun.java index 83b50145a..bd37f45f3 100644 --- a/src/test/java/dev/jbang/cli/TestRun.java +++ b/src/test/java/dev/jbang/cli/TestRun.java @@ -15,6 +15,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -36,6 +37,8 @@ import java.util.Map; import java.util.jar.Attributes; import java.util.jar.JarFile; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -2604,6 +2607,58 @@ void testReadingAddExports() throws IOException { "--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED")); } + @Test + void testReadingAddOpens(@TempDir Path output) throws IOException { + assumeTrue(Runtime.version().feature() >= 9, "requires Java 9+"); + String opens = "java.base/java.lang java.base/java.nio"; + Path jar = createJar(output, Integer.toString(Runtime.version().feature()), + Collections.singletonMap(Project.ATTR_ADD_OPENS, opens)); + + CommandLine.ParseResult pr = JBang.getCommandLine().parseArgs("run", jar.toString()); + Run run = (Run) pr.subcommand().commandSpec().userObject(); + + ProjectBuilder pb = run.createProjectBuilderForRun(); + Project code = pb.build(jar.toString()); + String cmd = CmdGenerator.builder(code).build().generate(); + + assertThat(code.getManifestAttributes(), hasEntry(Project.ATTR_ADD_OPENS, opens)); + assertThat(cmd, containsString("--add-opens=java.base/java.lang=ALL-UNNAMED")); + assertThat(cmd, containsString("--add-opens=java.base/java.nio=ALL-UNNAMED")); + } + + @Test + void testReadingEnableNativeAccess(@TempDir Path output) throws IOException { + assumeTrue(Runtime.version().feature() >= 22, "requires Java 22+"); + Path jar = createJar(output, Integer.toString(Runtime.version().feature()), + Collections.singletonMap(Project.ATTR_ENABLE_NATIVE_ACCESS, "ALL-UNNAMED")); + + CommandLine.ParseResult pr = JBang.getCommandLine().parseArgs("run", jar.toString()); + Run run = (Run) pr.subcommand().commandSpec().userObject(); + + ProjectBuilder pb = run.createProjectBuilderForRun(); + Project code = pb.build(jar.toString()); + + assertThat(code.getManifestAttributes(), hasEntry(Project.ATTR_ENABLE_NATIVE_ACCESS, "ALL-UNNAMED")); + assertThat(CmdGenerator.builder(code).build().generate(), + containsString("--enable-native-access=ALL-UNNAMED")); + } + + @Test + void testRejectsInvalidEnableNativeAccess(@TempDir Path output) throws IOException { + assumeTrue(Runtime.version().feature() >= 22, "requires Java 22+"); + Path jar = createJar(output, Integer.toString(Runtime.version().feature()), + Collections.singletonMap(Project.ATTR_ENABLE_NATIVE_ACCESS, "SOME-MODULE")); + + CommandLine.ParseResult pr = JBang.getCommandLine().parseArgs("run", jar.toString()); + Run run = (Run) pr.subcommand().commandSpec().userObject(); + + ProjectBuilder pb = run.createProjectBuilderForRun(); + Project code = pb.build(jar.toString()); + + ExitException ex = assertThrows(ExitException.class, () -> CmdGenerator.builder(code).build().generate()); + assertThat(ex.getMessage(), containsString("Enable-Native-Access")); + } + @Test @Disabled("java 8 is not installing reliably on github action") void testReadingNoAddExportsOnJava8() throws IOException { @@ -2692,4 +2747,19 @@ void testRunLocalWarFile() throws IOException { Files.deleteIfExists(warPath); } } + + private Path createJar(Path outputDir, String buildJdk, Map manifestAttributes) throws IOException { + Path jar = outputDir.resolve("manifest-test.jar"); + Manifest manifest = new Manifest(); + Attributes attrs = manifest.getMainAttributes(); + attrs.put(Attributes.Name.MANIFEST_VERSION, "1.0"); + attrs.put(Attributes.Name.MAIN_CLASS, "test.Main"); + attrs.putValue(JarBuildStep.ATTR_BUILD_JDK, buildJdk); + manifestAttributes.forEach(attrs::putValue); + try (JarOutputStream ignored = new JarOutputStream(Files.newOutputStream(jar), manifest)) { + // Manifest-only JAR is enough for command generation tests. + } + return jar; + } } + From e35dd5188b9f9a1d2498096bd601dc875e34043e Mon Sep 17 00:00:00 2001 From: Abdeslam Yassine Agmar Date: Sat, 28 Mar 2026 11:57:24 +0100 Subject: [PATCH 2/3] fix(run): remove module guard and match java -jar native-access errors --- .../java/dev/jbang/source/generators/JarCmdGenerator.java | 7 +++---- src/test/java/dev/jbang/cli/TestRun.java | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/java/dev/jbang/source/generators/JarCmdGenerator.java b/src/main/java/dev/jbang/source/generators/JarCmdGenerator.java index 90d9b13a0..83329409c 100644 --- a/src/main/java/dev/jbang/source/generators/JarCmdGenerator.java +++ b/src/main/java/dev/jbang/source/generators/JarCmdGenerator.java @@ -98,21 +98,20 @@ protected List generateCommandLineList() throws IOException { Jdk jdk = project.projectJdk(); String javacmd = JavaUtil.resolveInJavaHome("java", jdk); - if (!runAsModule && jdk.majorVersion() >= 9) { + if (jdk.majorVersion() >= 9) { addAllUnnamedManifestOptions(optionalArgs, project.getManifestAttributes().get(Project.ATTR_ADD_OPENS), "--add-opens="); addAllUnnamedManifestOptions(optionalArgs, project.getManifestAttributes().get(Project.ATTR_ADD_EXPORTS), "--add-exports="); } - if (!runAsModule && jdk.majorVersion() >= 22) { + if (jdk.majorVersion() >= 22) { String nativeAccess = project.getManifestAttributes().get(Project.ATTR_ENABLE_NATIVE_ACCESS); if (nativeAccess != null) { - nativeAccess = nativeAccess.trim(); if ("ALL-UNNAMED".equals(nativeAccess)) { optionalArgs.add("--enable-native-access=" + nativeAccess); } else { - throw new ExitException(BaseCommand.EXIT_INVALID_INPUT, + throw new ExitException(1, "Invalid value for manifest attribute Enable-Native-Access: " + nativeAccess); } } diff --git a/src/test/java/dev/jbang/cli/TestRun.java b/src/test/java/dev/jbang/cli/TestRun.java index bd37f45f3..2a169aafc 100644 --- a/src/test/java/dev/jbang/cli/TestRun.java +++ b/src/test/java/dev/jbang/cli/TestRun.java @@ -2656,6 +2656,7 @@ void testRejectsInvalidEnableNativeAccess(@TempDir Path output) throws IOExcepti Project code = pb.build(jar.toString()); ExitException ex = assertThrows(ExitException.class, () -> CmdGenerator.builder(code).build().generate()); + assertThat(ex.getStatus(), equalTo(1)); assertThat(ex.getMessage(), containsString("Enable-Native-Access")); } @@ -2762,4 +2763,3 @@ private Path createJar(Path outputDir, String buildJdk, Map mani return jar; } } - From 332ae536c62ef6e66813c200e8ad35ee8a0186ac Mon Sep 17 00:00:00 2001 From: Max Rydahl Andersen Date: Sat, 28 Mar 2026 22:52:58 +0100 Subject: [PATCH 3/3] Pass through Enable-Native-Access and add comprehensive tests Changes: - Replace validation with pass-through for Enable-Native-Access - Let JVM validate values instead of JBang - Consistent with how we handle Add-Opens/Add-Exports - Simpler code, future-proof if spec changes - Add addManifestOptions() helper - Similar to addAllUnnamedManifestOptions but without =ALL-UNNAMED suffix - Used for Enable-Native-Access which passes values directly - Update tests: - Replace testRejectsInvalidEnableNativeAccess with testPassesThroughEnableNativeAccessModuleName - Add testReadingAddExportsWithHelper (using new createJar() helper) - Add edge case tests: * testEmptyManifestAttributeIgnored (whitespace-only values) * testMultipleSpacesBetweenValues (multiple spaces between values) * testMultipleModulesForEnableNativeAccess (space-separated module list) - Add documentation: - New "JAR Manifest Attributes" section in running.adoc - Documents Add-Opens, Add-Exports, Enable-Native-Access - Explains how values are passed to JVM - Links to JAR spec and JEP 472 - References issue #2441 for --ignore-manifest flag Co-Authored-By: Claude Sonnet 4.5 --- docs/modules/ROOT/pages/running.adoc | 33 ++++++++ .../source/generators/JarCmdGenerator.java | 21 +++-- src/test/java/dev/jbang/cli/TestRun.java | 82 +++++++++++++++++-- 3 files changed, 122 insertions(+), 14 deletions(-) diff --git a/docs/modules/ROOT/pages/running.adoc b/docs/modules/ROOT/pages/running.adoc index 3ec56b141..965831e85 100644 --- a/docs/modules/ROOT/pages/running.adoc +++ b/docs/modules/ROOT/pages/running.adoc @@ -184,6 +184,39 @@ If an item has no equals sign and no value than the value is taken to be the str ---- +== JAR Manifest Attributes + +When running JAR files, JBang honors certain manifest attributes and converts them to equivalent JVM flags. +This allows JAR files to declare their requirements for module access and native functionality. + +=== Java Module System Attributes (Java 9+) + +JBang reads `Add-Opens` and `Add-Exports` from the JAR manifest and passes them to the JVM with `=ALL-UNNAMED` appended: + +* `Add-Opens: java.base/java.lang java.base/java.nio` → `--add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED` +* `Add-Exports: jdk.compiler/com.sun.tools.javac.api` → `--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED` + +These attributes are specified in the https://docs.oracle.com/en/java/javase/25/docs/specs/jar/jar.html[JAR File Specification] and allow JARs to declare which module internals they need access to. + +=== Native Access Attribute (Java 22+) + +`Enable-Native-Access` is passed through directly to the JVM: + +* `Enable-Native-Access: ALL-UNNAMED` → `--enable-native-access=ALL-UNNAMED` +* `Enable-Native-Access: com.example.module` → `--enable-native-access=com.example.module` + +This attribute allows code to use restricted methods from the Foreign Function & Memory API (https://openjdk.org/jeps/472[JEP 472]). + +NOTE: For executable JARs run with `java -jar`, the JAR specification restricts `Enable-Native-Access` to only `ALL-UNNAMED` in Java 22 and later. The JVM will reject other values. JBang passes through whatever value is in the manifest, allowing the JVM to perform validation. + +=== Other Manifest Attributes + +JBang also reads and honors: + +* `Main-Class` - The main class to execute +* `Build-Jdk` - Minimum Java version required (JBang will use this version or higher) +* `Premain-Class`, `Agent-Class` - For Java agents + === (Experimental) Application Class Data Sharing If your scripts uses a lot of classes Class Data Sharing might help on your startup. The following requires Java 13+. diff --git a/src/main/java/dev/jbang/source/generators/JarCmdGenerator.java b/src/main/java/dev/jbang/source/generators/JarCmdGenerator.java index 83329409c..cc5473106 100644 --- a/src/main/java/dev/jbang/source/generators/JarCmdGenerator.java +++ b/src/main/java/dev/jbang/source/generators/JarCmdGenerator.java @@ -106,15 +106,9 @@ protected List generateCommandLineList() throws IOException { } if (jdk.majorVersion() >= 22) { - String nativeAccess = project.getManifestAttributes().get(Project.ATTR_ENABLE_NATIVE_ACCESS); - if (nativeAccess != null) { - if ("ALL-UNNAMED".equals(nativeAccess)) { - optionalArgs.add("--enable-native-access=" + nativeAccess); - } else { - throw new ExitException(1, - "Invalid value for manifest attribute Enable-Native-Access: " + nativeAccess); - } - } + addManifestOptions(optionalArgs, + project.getManifestAttributes().get(Project.ATTR_ENABLE_NATIVE_ACCESS), + "--enable-native-access="); } addPropertyFlags(project.getProperties(), "-D", optionalArgs); @@ -325,6 +319,15 @@ private static void addAllUnnamedManifestOptions(List result, String man .forEach(val -> result.add(optionPrefix + val + "=ALL-UNNAMED")); } + private static void addManifestOptions(List result, String manifestValue, String optionPrefix) { + if (manifestValue == null) { + return; + } + Arrays.stream(manifestValue.trim().split("\\s+")) + .filter(val -> !val.isEmpty()) + .forEach(val -> result.add(optionPrefix + val)); + } + private static void addPropertyFlags(Map properties, String def, List result) { properties.forEach((k, e) -> result.add(def + k + "=" + e)); } diff --git a/src/test/java/dev/jbang/cli/TestRun.java b/src/test/java/dev/jbang/cli/TestRun.java index 2a169aafc..5c5505d72 100644 --- a/src/test/java/dev/jbang/cli/TestRun.java +++ b/src/test/java/dev/jbang/cli/TestRun.java @@ -2644,20 +2644,92 @@ void testReadingEnableNativeAccess(@TempDir Path output) throws IOException { } @Test - void testRejectsInvalidEnableNativeAccess(@TempDir Path output) throws IOException { + void testPassesThroughEnableNativeAccessModuleName(@TempDir Path output) throws IOException { assumeTrue(Runtime.version().feature() >= 22, "requires Java 22+"); Path jar = createJar(output, Integer.toString(Runtime.version().feature()), - Collections.singletonMap(Project.ATTR_ENABLE_NATIVE_ACCESS, "SOME-MODULE")); + Collections.singletonMap(Project.ATTR_ENABLE_NATIVE_ACCESS, "com.example.module")); CommandLine.ParseResult pr = JBang.getCommandLine().parseArgs("run", jar.toString()); Run run = (Run) pr.subcommand().commandSpec().userObject(); ProjectBuilder pb = run.createProjectBuilderForRun(); Project code = pb.build(jar.toString()); + String cmd = CmdGenerator.builder(code).build().generate(); + + assertThat(code.getManifestAttributes(), hasEntry(Project.ATTR_ENABLE_NATIVE_ACCESS, "com.example.module")); + assertThat(cmd, containsString("--enable-native-access=com.example.module")); + } + + @Test + void testReadingAddExportsWithHelper(@TempDir Path output) throws IOException { + assumeTrue(Runtime.version().feature() >= 9, "requires Java 9+"); + String exports = "jdk.compiler/com.sun.tools.javac.api jdk.compiler/com.sun.tools.javac.tree"; + Path jar = createJar(output, Integer.toString(Runtime.version().feature()), + Collections.singletonMap(Project.ATTR_ADD_EXPORTS, exports)); + + CommandLine.ParseResult pr = JBang.getCommandLine().parseArgs("run", jar.toString()); + Run run = (Run) pr.subcommand().commandSpec().userObject(); + + ProjectBuilder pb = run.createProjectBuilderForRun(); + Project code = pb.build(jar.toString()); + String cmd = CmdGenerator.builder(code).build().generate(); + + assertThat(code.getManifestAttributes(), hasEntry(Project.ATTR_ADD_EXPORTS, exports)); + assertThat(cmd, containsString("--add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED")); + assertThat(cmd, containsString("--add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED")); + } + + @Test + void testEmptyManifestAttributeIgnored(@TempDir Path output) throws IOException { + assumeTrue(Runtime.version().feature() >= 9, "requires Java 9+"); + Path jar = createJar(output, Integer.toString(Runtime.version().feature()), + Collections.singletonMap(Project.ATTR_ADD_OPENS, " ")); + + CommandLine.ParseResult pr = JBang.getCommandLine().parseArgs("run", jar.toString()); + Run run = (Run) pr.subcommand().commandSpec().userObject(); + + ProjectBuilder pb = run.createProjectBuilderForRun(); + Project code = pb.build(jar.toString()); + String cmd = CmdGenerator.builder(code).build().generate(); + + // Should not add any --add-opens flags for empty/whitespace-only values + assertThat(cmd, not(containsString("--add-opens="))); + } + + @Test + void testMultipleSpacesBetweenValues(@TempDir Path output) throws IOException { + assumeTrue(Runtime.version().feature() >= 9, "requires Java 9+"); + String opens = "java.base/java.lang java.base/java.nio"; + Path jar = createJar(output, Integer.toString(Runtime.version().feature()), + Collections.singletonMap(Project.ATTR_ADD_OPENS, opens)); + + CommandLine.ParseResult pr = JBang.getCommandLine().parseArgs("run", jar.toString()); + Run run = (Run) pr.subcommand().commandSpec().userObject(); + + ProjectBuilder pb = run.createProjectBuilderForRun(); + Project code = pb.build(jar.toString()); + String cmd = CmdGenerator.builder(code).build().generate(); + + // Should handle multiple spaces correctly + assertThat(cmd, containsString("--add-opens=java.base/java.lang=ALL-UNNAMED")); + assertThat(cmd, containsString("--add-opens=java.base/java.nio=ALL-UNNAMED")); + } + + @Test + void testMultipleModulesForEnableNativeAccess(@TempDir Path output) throws IOException { + assumeTrue(Runtime.version().feature() >= 22, "requires Java 22+"); + Path jar = createJar(output, Integer.toString(Runtime.version().feature()), + Collections.singletonMap(Project.ATTR_ENABLE_NATIVE_ACCESS, "module1 module2")); + + CommandLine.ParseResult pr = JBang.getCommandLine().parseArgs("run", jar.toString()); + Run run = (Run) pr.subcommand().commandSpec().userObject(); + + ProjectBuilder pb = run.createProjectBuilderForRun(); + Project code = pb.build(jar.toString()); + String cmd = CmdGenerator.builder(code).build().generate(); - ExitException ex = assertThrows(ExitException.class, () -> CmdGenerator.builder(code).build().generate()); - assertThat(ex.getStatus(), equalTo(1)); - assertThat(ex.getMessage(), containsString("Enable-Native-Access")); + assertThat(cmd, containsString("--enable-native-access=module1")); + assertThat(cmd, containsString("--enable-native-access=module2")); } @Test