Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions docs/modules/ROOT/pages/running.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,61 @@ If an item has no equals sign and no value than the value is taken to be the str
//MANIFEST Built-By=Shadowman Sealed
----

=== Standard Manifest Attributes

JBang automatically processes certain standard JAR manifest attributes and translates them into appropriate JVM flags when running your code. This allows jars to work correctly even though jbang runs them using classpath mode instead of `-jar` mode.

==== Module System Attributes (Java 9+)

When your jar requires module system flags, jbang will read these from the manifest and apply them automatically:

[source, java]
----
//MANIFEST Add-Opens=java.base/java.lang java.base/java.nio
//MANIFEST Add-Exports=java.base/sun.nio.ch
//MANIFEST Add-Reads=java.base=java.logging
----

These translate to JVM flags:
- `Add-Opens` → `--add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED`
- `Add-Exports` → `--add-exports=java.base/sun.nio.ch=ALL-UNNAMED`
- `Add-Reads` → `--add-reads=java.base=java.logging`

NOTE: Module attributes are only applied when running on Java 9 or later and when not running as a module.

==== Native Access (Java 22+)

For code using the Foreign Function & Memory API:

[source, java]
----
//MANIFEST Enable-Native-Access=ALL-UNNAMED
----

This translates to: `--enable-native-access=ALL-UNNAMED`

==== Splash Screen

For GUI applications with splash screens:

[source, java]
----
//MANIFEST SplashScreen-Image=images/splash.png
----

JBang will extract the image from your jar and pass it via the `-splash` flag. The image is cached to avoid re-extraction on subsequent runs.

==== Using in Plain JARs

These manifest attributes also work when running plain jar files with jbang:

[source, bash]
----
$ jbang run myapp.jar
----

If `myapp.jar` contains `Add-Opens` or other supported attributes in its manifest, jbang will automatically apply them.


=== (Experimental) Application Class Data Sharing

Expand Down
15 changes: 14 additions & 1 deletion docs/modules/ROOT/pages/script-directives.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ public class ModularApp {

=== //MANIFEST

Let you specify the manifest file key-values in the generated jar file xref:running.adoc#adding-entries-to-manifest-mf[].
Let you specify the manifest file key-values in the generated jar file. See xref:running.adoc#adding-entries-to-manifest-mf[] for full details.

**Syntax**: `//MANIFEST key=value key2=value2 ...`

Expand All @@ -347,6 +347,17 @@ Let you specify the manifest file key-values in the generated jar file xref:runn
// Version information
//MANIFEST Implementation-Version=1.0.0 Implementation-Vendor=MyCompany

// Module system flags (Java 9+)
//MANIFEST Add-Opens=java.base/java.lang
//MANIFEST Add-Exports=java.base/sun.nio.ch
//MANIFEST Add-Reads=java.base=java.logging

// Native access (Java 22+)
//MANIFEST Enable-Native-Access=ALL-UNNAMED

// Splash screen for GUI apps
//MANIFEST SplashScreen-Image=images/splash.png

// Boolean flag (no value = true)
//MANIFEST Custom-Flag Multi-Release
----
Expand All @@ -355,6 +366,8 @@ Let you specify the manifest file key-values in the generated jar file xref:runn

- Entries without `=value` default to `true`
- Useful for application metadata
- Special attributes like `Add-Opens`, `Add-Exports`, `Add-Reads`, `Enable-Native-Access`, and `SplashScreen-Image` are automatically processed by jbang and converted to appropriate JVM flags
- Values can contain `=` signs (e.g., `Add-Reads=mod1=mod2`)

== Performance Optimization

Expand Down
5 changes: 5 additions & 0 deletions src/main/java/dev/jbang/source/Project.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ 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 static final String ATTR_ADD_READS = "Add-Reads";
public static final String ATTR_SPLASH_SCREEN_IMAGE = "SplashScreen-Image";

public enum BuildFile {
jbang("build.jbang");
Expand Down
20 changes: 16 additions & 4 deletions src/main/java/dev/jbang/source/ProjectBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -444,13 +444,25 @@ private Project importJarMetadata(Project prj, boolean importModuleName) {
// TODO: this does mean we can't separate from user specified options and jar
// origined ones, but not sure if needed?
// https://openjdk.org/jeps/261#Breaking-encapsulation
String exports = attrs.getValue("Add-Exports");
String exports = attrs.getValue(Project.ATTR_ADD_EXPORTS);
if (exports != null) {
prj.getManifestAttributes().put("Add-Exports", exports);
prj.getManifestAttributes().put(Project.ATTR_ADD_EXPORTS, exports);
}
String opens = attrs.getValue("Add-Opens");
String opens = attrs.getValue(Project.ATTR_ADD_OPENS);
if (opens != null) {
prj.getManifestAttributes().put("Add-Opens", exports);
prj.getManifestAttributes().put(Project.ATTR_ADD_OPENS, opens);
}
String enableNativeAccess = attrs.getValue(Project.ATTR_ENABLE_NATIVE_ACCESS);
if (enableNativeAccess != null) {
prj.getManifestAttributes().put(Project.ATTR_ENABLE_NATIVE_ACCESS, enableNativeAccess);
}
String addReads = attrs.getValue(Project.ATTR_ADD_READS);
if (addReads != null) {
prj.getManifestAttributes().put(Project.ATTR_ADD_READS, addReads);
}
String splashScreenImage = attrs.getValue(Project.ATTR_SPLASH_SCREEN_IMAGE);
if (splashScreenImage != null) {
prj.getManifestAttributes().put(Project.ATTR_SPLASH_SCREEN_IMAGE, splashScreenImage);
}

}
Expand Down
123 changes: 114 additions & 9 deletions src/main/java/dev/jbang/source/generators/JarCmdGenerator.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.net.ServerSocket;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
Expand All @@ -13,6 +14,8 @@
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand Down Expand Up @@ -85,6 +88,10 @@ public JarCmdGenerator(BuildContext ctx) {
super(ctx);
}

private boolean isRunAsModule() {
return moduleName != null && ctx.getProject().getModuleName().isPresent();
}

@Override
protected List<String> generateCommandLineList() throws IOException {
List<String> fullArgs = new ArrayList<>();
Expand All @@ -97,18 +104,58 @@ protected List<String> 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");
// Handle splash screen - extract image from jar and pass -splash:path
String splashImage = project.getManifestAttributes().get(Project.ATTR_SPLASH_SCREEN_IMAGE);
if (splashImage != null) {
Path jarPath = ctx.getJarFile();
if (jarPath != null && Files.exists(jarPath)) {
Path splashPath = extractSplashImage(jarPath, splashImage);
if (splashPath != null) {
optionalArgs.add("-splash:" + splashPath.toAbsolutePath());
}
}
}

if (!isRunAsModule() && jdk.majorVersion() >= 9) {
String opens = project.getManifestAttributes().get(Project.ATTR_ADD_OPENS);
if (opens != null) {
for (String val : opens.split(" ")) {
optionalArgs.add("--add-opens=" + val + "=ALL-UNNAMED");
for (String val : opens.trim().split("\\s+")) {
if (!val.isEmpty()) {
optionalArgs.add("--add-opens=" + val + "=ALL-UNNAMED");
}
}
}

String exports = ctx.getProject().getManifestAttributes().get("Add-Exports");
String exports = project.getManifestAttributes().get(Project.ATTR_ADD_EXPORTS);
if (exports != null) {
for (String val : exports.split(" ")) {
optionalArgs.add("--add-exports=" + val + "=ALL-UNNAMED");
for (String val : exports.trim().split("\\s+")) {
if (!val.isEmpty()) {
optionalArgs.add("--add-exports=" + val + "=ALL-UNNAMED");
}
}
}

// Add-Reads: module1=module2 module3=module4 → --add-reads=module1=module2
// --add-reads=module3=module4
String addReads = project.getManifestAttributes().get(Project.ATTR_ADD_READS);
if (addReads != null) {
for (String val : addReads.trim().split("\\s+")) {
if (!val.isEmpty()) {
optionalArgs.add("--add-reads=" + val);
}
}
}
}

if (!isRunAsModule() && 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);
}
}
}
Expand Down Expand Up @@ -197,7 +244,7 @@ protected List<String> generateCommandLineList() throws IOException {
}
}
if (!Util.isBlankString(classpath)) {
if (moduleName != null && project.getModuleName().isPresent()) {
if (isRunAsModule()) {
optionalArgs.addAll(Arrays.asList("-p", classpath));
} else {
optionalArgs.addAll(Arrays.asList("-classpath", classpath));
Expand Down Expand Up @@ -230,7 +277,7 @@ protected List<String> generateCommandLineList() throws IOException {

String main = Optional.ofNullable(mainClass).orElse(project.getMainClass());
if (main != null && !Glob.isGlob(main)) {
if (moduleName != null && project.getModuleName().isPresent()) {
if (isRunAsModule()) {
String modName = moduleName.isEmpty() ? ModuleUtil.getModuleName(project) : moduleName;
fullArgs.add("-m");
fullArgs.add(modName + "/" + main);
Expand Down Expand Up @@ -316,4 +363,62 @@ private static void addPropertyFlags(Map<String, String> properties, String def,
properties.forEach((k, e) -> result.add(def + k + "=" + e));
}

/**
* Extract splash screen image from jar to cache directory for use with -splash
* flag.
*
* @param jarPath Path to the jar file
* @param imagePath Path to image inside jar (from manifest SplashScreen-Image
* attribute)
* @return Path to extracted image, or null if extraction failed
*/
private static Path extractSplashImage(Path jarPath, String imagePath) {
// Validate image path for security
if (imagePath == null || imagePath.trim().isEmpty()) {
Util.warnMsg("Splash screen image path is empty");
return null;
}

// Normalize and validate path to prevent directory traversal attacks
String normalizedPath = imagePath.replace('\\', '/');
if (normalizedPath.startsWith("/") || normalizedPath.contains("../") || normalizedPath.contains("..\\")) {
Util.warnMsg("Invalid splash screen image path (absolute or contains '..'): " + imagePath);
return null;
}

try (JarFile jar = new JarFile(jarPath.toFile())) {
JarEntry entry = jar.getJarEntry(normalizedPath);
if (entry == null) {
Util.warnMsg("Splash screen image not found in jar: " + imagePath);
return null;
}

// Reject directory entries
if (entry.isDirectory()) {
Util.warnMsg("Splash screen image path is a directory: " + imagePath);
return null;
}

// Extract to jar's directory with unique name
String jarName = jarPath.getFileName().toString();
int extIndex = normalizedPath.lastIndexOf('.');
String imageExt = (extIndex > 0) ? normalizedPath.substring(extIndex) : "";
Path targetPath = jarPath.getParent().resolve(jarName + ".splash" + imageExt);

// Extract if not cached or jar is newer
if (!Files.exists(targetPath) ||
Files.getLastModifiedTime(jarPath).compareTo(Files.getLastModifiedTime(targetPath)) > 0) {
try (InputStream is = jar.getInputStream(entry)) {
Files.copy(is, targetPath, StandardCopyOption.REPLACE_EXISTING);
}
Util.verboseMsg("Extracted splash screen to: " + targetPath);
}

return targetPath;
} catch (IOException e) {
Util.warnMsg("Failed to extract splash screen: " + e.getMessage());
return null;
}
}

}
2 changes: 1 addition & 1 deletion src/main/java/dev/jbang/source/parser/KeyValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public String getValue() {
}

public static KeyValue of(String line) {
String[] split = line.split("=");
String[] split = line.split("=", 2);
String key;
String value = null;

Expand Down
Loading
Loading