Skip to content

Commit 1ae4cec

Browse files
maxandersenclaude
andcommitted
feat: add support for Add-Reads and SplashScreen-Image manifest attributes
Extends manifest attribute support for jars beyond PR jbangdev#2439 to handle Add-Reads module dependencies and SplashScreen-Image extraction. Changes: - Add Project.ATTR_ADD_READS and Project.ATTR_SPLASH_SCREEN_IMAGE constants - Import Add-Reads and SplashScreen-Image attributes from jar manifests - Process Add-Reads: converts to --add-reads JVM flags (Java 9+) - Process SplashScreen-Image: extracts image and passes -splash flag - Fix KeyValue.of() to support values containing '=' signs Add-Reads implementation: - Parses space-separated module dependencies (e.g., "mod1=mod2 mod3=mod4") - Generates --add-reads flags for each dependency pair - Version-gated for Java 9+ with runAsModule check SplashScreen-Image implementation: - Extracts splash image from jar to <jarPath>.splash.<ext> in cache - Smart caching: only re-extracts if jar is newer than cached image - Fails gracefully with warnings (never breaks the build) - Adds -splash:<path> flag before other JVM options KeyValue parser fix: - Changed split("=") to split("=", 2) to handle values with '=' - Enables manifest directives like //MANIFEST Add-Reads=mod1=mod2 This builds on PR jbangdev#2439's copyManifestAttribute() and addAllUnnamedManifestOptions() patterns. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent fb47430 commit 1ae4cec

File tree

4 files changed

+79
-21
lines changed

4 files changed

+79
-21
lines changed

src/main/java/dev/jbang/source/Project.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ public class Project {
5454

5555
public static final String ATTR_PREMAIN_CLASS = "Premain-Class";
5656
public static final String ATTR_AGENT_CLASS = "Agent-Class";
57+
public static final String ATTR_ADD_EXPORTS = "Add-Exports";
58+
public static final String ATTR_ADD_OPENS = "Add-Opens";
59+
public static final String ATTR_ENABLE_NATIVE_ACCESS = "Enable-Native-Access";
60+
public static final String ATTR_ADD_READS = "Add-Reads";
61+
public static final String ATTR_SPLASH_SCREEN_IMAGE = "SplashScreen-Image";
5762

5863
public enum BuildFile {
5964
jbang("build.jbang");

src/main/java/dev/jbang/source/ProjectBuilder.java

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -444,14 +444,11 @@ private Project importJarMetadata(Project prj, boolean importModuleName) {
444444
// TODO: this does mean we can't separate from user specified options and jar
445445
// origined ones, but not sure if needed?
446446
// https://openjdk.org/jeps/261#Breaking-encapsulation
447-
String exports = attrs.getValue("Add-Exports");
448-
if (exports != null) {
449-
prj.getManifestAttributes().put("Add-Exports", exports);
450-
}
451-
String opens = attrs.getValue("Add-Opens");
452-
if (opens != null) {
453-
prj.getManifestAttributes().put("Add-Opens", exports);
454-
}
447+
copyManifestAttribute(attrs, prj, Project.ATTR_ADD_EXPORTS);
448+
copyManifestAttribute(attrs, prj, Project.ATTR_ADD_OPENS);
449+
copyManifestAttribute(attrs, prj, Project.ATTR_ENABLE_NATIVE_ACCESS);
450+
copyManifestAttribute(attrs, prj, Project.ATTR_ADD_READS);
451+
copyManifestAttribute(attrs, prj, Project.ATTR_SPLASH_SCREEN_IMAGE);
455452

456453
}
457454

src/main/java/dev/jbang/source/generators/JarCmdGenerator.java

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import java.net.ServerSocket;
66
import java.nio.file.Files;
77
import java.nio.file.Path;
8+
import java.nio.file.StandardCopyOption;
89
import java.util.ArrayList;
910
import java.util.Arrays;
1011
import java.util.Collection;
@@ -85,6 +86,10 @@ public JarCmdGenerator(BuildContext ctx) {
8586
super(ctx);
8687
}
8788

89+
private boolean isRunAsModule() {
90+
return moduleName != null && ctx.getProject().getModuleName().isPresent();
91+
}
92+
8893
@Override
8994
protected List<String> generateCommandLineList() throws IOException {
9095
List<String> fullArgs = new ArrayList<>();
@@ -97,19 +102,31 @@ protected List<String> generateCommandLineList() throws IOException {
97102
Jdk jdk = project.projectJdk();
98103
String javacmd = JavaUtil.resolveInJavaHome("java", jdk);
99104

100-
if (jdk.majorVersion() > 9) {
101-
String opens = ctx.getProject().getManifestAttributes().get("Add-Opens");
102-
if (opens != null) {
103-
for (String val : opens.split(" ")) {
104-
optionalArgs.add("--add-opens=" + val + "=ALL-UNNAMED");
105+
// Handle splash screen - extract image from jar and pass -splash:path
106+
String splashImage = project.getManifestAttributes().get(Project.ATTR_SPLASH_SCREEN_IMAGE);
107+
if (splashImage != null) {
108+
Path jarPath = ctx.getJarFile();
109+
if (jarPath != null && Files.exists(jarPath)) {
110+
Path splashPath = extractSplashImage(jarPath, splashImage);
111+
if (splashPath != null) {
112+
optionalArgs.add("-splash:" + splashPath.toAbsolutePath());
105113
}
106114
}
115+
}
107116

108-
String exports = ctx.getProject().getManifestAttributes().get("Add-Exports");
109-
if (exports != null) {
110-
for (String val : exports.split(" ")) {
111-
optionalArgs.add("--add-exports=" + val + "=ALL-UNNAMED");
112-
}
117+
if (!isRunAsModule() && jdk.majorVersion() >= 9) {
118+
addAllUnnamedManifestOptions(optionalArgs, project.getManifestAttributes().get(Project.ATTR_ADD_OPENS),
119+
"--add-opens=");
120+
addAllUnnamedManifestOptions(optionalArgs, project.getManifestAttributes().get(Project.ATTR_ADD_EXPORTS),
121+
"--add-exports=");
122+
123+
// Add-Reads: module1=module2 module3=module4 → --add-reads=module1=module2
124+
// --add-reads=module3=module4
125+
String addReads = project.getManifestAttributes().get(Project.ATTR_ADD_READS);
126+
if (addReads != null) {
127+
Arrays.stream(addReads.trim().split("\\s+"))
128+
.filter(val -> !val.isEmpty())
129+
.forEach(val -> optionalArgs.add("--add-reads=" + val));
113130
}
114131
}
115132

@@ -197,7 +214,7 @@ protected List<String> generateCommandLineList() throws IOException {
197214
}
198215
}
199216
if (!Util.isBlankString(classpath)) {
200-
if (moduleName != null && project.getModuleName().isPresent()) {
217+
if (isRunAsModule()) {
201218
optionalArgs.addAll(Arrays.asList("-p", classpath));
202219
} else {
203220
optionalArgs.addAll(Arrays.asList("-classpath", classpath));
@@ -230,7 +247,7 @@ protected List<String> generateCommandLineList() throws IOException {
230247

231248
String main = Optional.ofNullable(mainClass).orElse(project.getMainClass());
232249
if (main != null && !Glob.isGlob(main)) {
233-
if (moduleName != null && project.getModuleName().isPresent()) {
250+
if (isRunAsModule()) {
234251
String modName = moduleName.isEmpty() ? ModuleUtil.getModuleName(project) : moduleName;
235252
fullArgs.add("-m");
236253
fullArgs.add(modName + "/" + main);
@@ -316,4 +333,43 @@ private static void addPropertyFlags(Map<String, String> properties, String def,
316333
properties.forEach((k, e) -> result.add(def + k + "=" + e));
317334
}
318335

336+
/**
337+
* Extract splash screen image from jar to cache directory for use with -splash
338+
* flag.
339+
*
340+
* @param jarPath Path to the jar file
341+
* @param imagePath Path to image inside jar (from manifest SplashScreen-Image
342+
* attribute)
343+
* @return Path to extracted image, or null if extraction failed
344+
*/
345+
private static Path extractSplashImage(Path jarPath, String imagePath) {
346+
try (java.util.jar.JarFile jar = new java.util.jar.JarFile(jarPath.toFile())) {
347+
java.util.jar.JarEntry entry = jar.getJarEntry(imagePath);
348+
if (entry == null) {
349+
Util.warnMsg("Splash screen image not found in jar: " + imagePath);
350+
return null;
351+
}
352+
353+
// Extract to jar's directory with unique name
354+
String jarName = jarPath.getFileName().toString();
355+
int extIndex = imagePath.lastIndexOf('.');
356+
String imageExt = (extIndex > 0) ? imagePath.substring(extIndex) : "";
357+
Path targetPath = jarPath.getParent().resolve(jarName + ".splash" + imageExt);
358+
359+
// Extract if not cached or jar is newer
360+
if (!Files.exists(targetPath) ||
361+
Files.getLastModifiedTime(jarPath).compareTo(Files.getLastModifiedTime(targetPath)) > 0) {
362+
try (InputStream is = jar.getInputStream(entry)) {
363+
Files.copy(is, targetPath, StandardCopyOption.REPLACE_EXISTING);
364+
}
365+
Util.verboseMsg("Extracted splash screen to: " + targetPath);
366+
}
367+
368+
return targetPath;
369+
} catch (IOException e) {
370+
Util.warnMsg("Failed to extract splash screen: " + e.getMessage());
371+
return null;
372+
}
373+
}
374+
319375
}

src/main/java/dev/jbang/source/parser/KeyValue.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public String getValue() {
1818
}
1919

2020
public static KeyValue of(String line) {
21-
String[] split = line.split("=");
21+
String[] split = line.split("=", 2);
2222
String key;
2323
String value = null;
2424

0 commit comments

Comments
 (0)