JAVA / PLATFORM, BUILDS AND TESTING
Packaging runnable jars and the manifest detail
Package a jar that runs with java -jar, read and write META-INF/MANIFEST.MF correctly, and know why -cp stops working in -jar mode.
What you will learn
- Build a runnable jar: jar --create --file app.jar --main-class app.Report -C out .
- Write Main-Class as a dotted class name, with no path separators and no .class suffix
- Know that java -jar ignores -cp; dependencies belong in the manifest Class-Path
- End a hand-written manifest with a newline, or its last header is silently dropped
Understanding Packaging runnable jars and the manifest detail
A jar is a zip file plus one convention: an entry at META-INF/MANIFEST.MF holding name: value headers. When you run java -jar app.jar the launcher opens that entry, reads Main-Class, loads that class from inside the archive and calls its main method; if the header is absent it stops with "no main manifest attribute, in app.jar". The value is a binary class name in dotted form, app.Report and not app/Report or app/Report.class, because the launcher passes it straight to a class loader. Let the tool write the header for you with jar --create --file app.jar --main-class app.Report -C out . rather than composing it by hand.
The part that surprises people is that -jar mode replaces the class path instead of adding to it: -cp and the CLASSPATH variable are ignored, and every token after app.jar is handed to your main method as an argument. Dependencies therefore have to be declared inside the archive with a Class-Path header, a space-separated list of relative URLs resolved against the directory that holds the jar rather than the working directory, with no wildcard expansion, so each library is named individually. That header is a class-path feature only; a jar loaded on the module path gets no such expansion. If the bookkeeping is unwelcome, skip -jar and launch with java -cp "app.jar:lib/*" app.Report, or merge the libraries into a single archive.
The file format matters because a manifest is read by a strict parser, not a forgiving one. Lines end with CRLF, no line may exceed 72 bytes so long values are folded onto continuation lines that begin with exactly one space and are rejoined with nothing inserted, the main section ends at the first blank line, and each later section opens with Name: and carries per-entry data such as Sealed or signature digests. A file that does not end with a line terminator loses its final header, which is exactly how a hand-edited manifest produces a jar that refuses to launch.
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
public class ManifestDetail {
public static void main(String[] args) throws Exception {
Manifest mf = new Manifest();
Attributes attr = mf.getMainAttributes();
attr.put(Attributes.Name.MANIFEST_VERSION, "1.0");
attr.put(Attributes.Name.MAIN_CLASS, "com.example.Report");
ByteArrayOutputStream sink = new ByteArrayOutputStream();
mf.write(sink);
byte[] raw = sink.toByteArray();
System.out.println("META-INF/MANIFEST.MF is " + raw.length + " bytes:");
for (byte b : raw) {
if (b == '\r') {
System.out.print("<CR>");
} else if (b == '\n') {
System.out.println("<LF>");
} else {
System.out.print((char) b);
}
}
Manifest parsed = new Manifest(new ByteArrayInputStream(raw));
Attributes back = parsed.getMainAttributes();
System.out.println("Main-Class -> " + back.getValue("Main-Class"));
System.out.println("main-class -> " + back.getValue("main-class"));
System.out.println("Class-Path -> " + back.getValue("Class-Path"));
}
}A jar is runnable only because META-INF/MANIFEST.MF names a Main-Class, and in -jar mode that manifest also decides the entire class path.
Worked examples
Writing a jar and reading its manifest back
Builds a real archive with JarOutputStream and reopens it to show where the manifest lands and what the headers look like on the way out.
import java.io.File;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
public class BuildJar {
public static void main(String[] args) throws Exception {
Manifest mf = new Manifest();
Attributes attr = mf.getMainAttributes();
attr.put(Attributes.Name.MANIFEST_VERSION, "1.0");
attr.put(Attributes.Name.MAIN_CLASS, "app.Report");
attr.put(new Attributes.Name("Class-Path"), "lib/db.jar");
File jar = new File(System.getProperty("java.io.tmpdir"), "demo.jar");
try (JarOutputStream out = new JarOutputStream(new FileOutputStream(jar), mf)) {
out.putNextEntry(new JarEntry("app/messages.properties"));
out.write("greeting=hi\n".getBytes(StandardCharsets.UTF_8));
out.closeEntry();
}
try (JarFile in = new JarFile(jar)) {
in.stream().forEach(e -> System.out.println("entry " + e.getName()));
Attributes got = in.getManifest().getMainAttributes();
System.out.println("Main-Class " + got.getValue("Main-Class"));
System.out.println("Class-Path " + got.getValue("Class-Path"));
}
System.out.println("deleted " + jar.delete());
}
}Example explained
Line 1JarOutputStream(out, manifest) writes META-INF/MANIFEST.MF itself, as the first entry, which is why it heads the listing.
Line 2Entry names use forward slashes, so app.Report would have to sit at app/Report.class; this archive only holds a properties file, so java -jar demo.jar would fail to find the main class.
Line 3Class-Path is stored verbatim: nothing verifies that lib/db.jar exists, and at launch it is resolved beside demo.jar, not beside the working directory.
Line 4delete() succeeds because try-with-resources closed the JarFile first; on Windows an open archive blocks removal.
Parsing manifest text with a folded line and a per-entry section
Feeds hand-written manifest text to the parser to show how continuation lines rejoin and how sections after the blank line are kept apart from the main attributes.
import java.io.ByteArrayInputStream;
import java.nio.charset.StandardCharsets;
import java.util.jar.Manifest;
public class ManifestText {
public static void main(String[] args) throws Exception {
String text = String.join("\r\n",
"Manifest-Version: 1.0",
"Main-Class: com.example.report",
" ing.Report",
"Class-Path: lib/db.jar lib/json.jar",
"",
"Name: com/example/report/",
"Sealed: true",
"",
"");
Manifest mf = new Manifest(
new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8)));
System.out.println("Main-Class = " + mf.getMainAttributes().getValue("Main-Class"));
System.out.println("Class-Path = " + mf.getMainAttributes().getValue("Class-Path"));
System.out.println("sections = " + mf.getEntries().keySet());
System.out.println("Sealed = "
+ mf.getAttributes("com/example/report/").getValue("Sealed"));
}
}Example explained
Line 1The line starting with one space is a continuation, and the parser appends its remainder with no separator, so "com.example.report" and "ing.Report" fuse into one class name.
Line 2The blank line closes the main section, which is why Class-Path is a main attribute and Sealed is not.
Line 3Name: com/example/report/ opens a per-entry section, reachable through getEntries() and getAttributes(name) rather than getMainAttributes().
Line 4An accidental leading space on a real header would be read as structure, silently gluing that header onto the value above it.
Important notes
getResourceAsStream("/META-INF/MANIFEST.MF") returns the first manifest on the class path, often a dependency's; to read your own, open the jar from getProtectionDomain().getCodeSource() or ask Package.getImplementationVersion().
When merging libraries into one jar, same-named META-INF/services files must be concatenated rather than overwritten and the original META-INF/*.SF signature files must be dropped, or the merged jar fails at runtime.
Common mistakes
Writing a file path in the header, Main-Class: app/Report.class, so the launcher looks for a class whose name ends in .class and aborts with "Could not find or load main class".
Saving a hand-written manifest without a final newline: every header except the last is read, Main-Class disappears, and java -jar app.jar prints "no main manifest attribute, in app.jar".
Expecting java -cp lib/json.jar -jar app.jar to supply a library: the -cp is discarded in -jar mode and the program dies with NoClassDefFoundError the first time it touches that library.
Try it yourself
Change, predict, then run
Write a program that builds a Manifest with Main-Class and a Class-Path naming two jars, prints the serialized bytes with <CR> and <LF> marked so you can see the CRLF endings and the trailing blank line, then reparses those bytes and confirms getValue("main-class") still finds the entry point.
Open the Java workspaceCheck your understanding
You run java -cp lib/json.jar -jar app.jar and get NoClassDefFoundError for a class that lives in json.jar. What explains it?
- -cp must be placed after the jar name for the launcher to see it
- The manifest needs a Manifest-Version header before any class path is honoured
- In -jar mode the class path is the jar itself, so -cp is ignored and the library belongs in the manifest Class-Path
- json.jar has to be unpacked into app.jar because a jar on the class path cannot contain another jar
Show answer
The -jar form tells the launcher to use that single archive as the class path and take the entry point from its manifest, so -cp is discarded and only a Class-Path header (or dropping -jar for java -cp) brings the library in. Moving -cp after app.jar changes nothing, because everything following the jar name is passed to main as a program argument rather than interpreted by the launcher.