When native image is built, there is no MANIFEST.MF for the application being built. Running "MyApp.class.getPackage().getImplementationVersion()" will return "null" in such scenario.
This is expected, as MANIFEST.MF is written by maven-jar-plugin, but native image build operates on target/classes dir itself.
How would one proceed with "get version of the currently running app" when running as native executable?
Consider the following example: https://github.com/krezovic/native-image-demo
@SpringBootApplication
public class DemoApplication {
private static final String VERSION;
static {
VERSION = DemoApplication.class.getPackage().getImplementationVersion();
}
public static void main(String[] args) {
log.info("Running version: {}", VERSION);
SpringApplication.run(DemoApplication.class, args);
}
}
./mvnw clean package -P native
./mvnw native:compile
will produce
$ ls -l target
total 104128
drwxr-xr-x 5 armin armin 4096 Sep 22 13:45 classes
-rwxr-xr-x 1 armin armin 85846072 Sep 22 13:46 demo
-rw-r--r-- 1 armin armin 20596296 Sep 22 13:45 demo-0.0.1-SNAPSHOT.jar
-rw-r--r-- 1 armin armin 137454 Sep 22 13:45 demo-0.0.1-SNAPSHOT.jar.original
drwxr-xr-x 3 armin armin 4096 Sep 22 13:45 generated-sources
drwxr-xr-x 3 armin armin 4096 Sep 22 13:45 generated-test-sources
drwxr-xr-x 3 armin armin 4096 Sep 22 13:45 graalvm-reachability-metadata
drwxr-xr-x 2 armin armin 4096 Sep 22 13:45 maven-archiver
drwxr-xr-x 3 armin armin 4096 Sep 22 13:45 maven-status
drwxr-xr-x 3 armin armin 4096 Sep 22 13:45 spring-aot
drwxr-xr-x 2 armin armin 4096 Sep 22 13:45 surefire-reports
drwxr-xr-x 3 armin armin 4096 Sep 22 13:45 test-classes
drwxr-xr-x 2 armin armin 4096 Sep 22 13:45 test-ids
Running
$ java -jar target/demo-0.0.1-SNAPSHOT.jar
13:47:41.896 [main] INFO com.example.demo.DemoApplication -- Running version: 0.0.1-SNAPSHOT
Running native executable, however
$ target/demo
13:48:04.685 [main] INFO com.example.demo.DemoApplication -- Running version: null
As expected, there's no MANIFEST.MF inside target/classes/META-INF
$ ls -l target/classes/META-INF
total 4
drwxr-xr-x 6 armin armin 4096 Sep 22 13:45 native-image
It is only present inside final JAR file
$ jar -tf target/demo-0.0.1-SNAPSHOT.jar | grep META-INF
META-INF/
META-INF/MANIFEST.MF
META-INF/services/
META-INF/services/java.nio.file.spi.FileSystemProvider
META-INF/native-image/
META-INF/native-image/ch.qos.logback/
META-INF/native-image/ch.qos.logback/logback-classic/
META-INF/native-image/ch.qos.logback/logback-classic/1.5.8/
META-INF/native-image/com.example/
META-INF/native-image/com.example/demo/
META-INF/native-image/com.fasterxml.jackson.core/
META-INF/native-image/com.fasterxml.jackson.core/jackson-databind/
META-INF/native-image/com.fasterxml.jackson.core/jackson-databind/2.17.2/
META-INF/native-image/org.apache.tomcat.embed/
META-INF/native-image/org.apache.tomcat.embed/tomcat-embed-core/
META-INF/native-image/org.apache.tomcat.embed/tomcat-embed-core/10.1.30/
META-INF/maven/
META-INF/maven/com.example/
META-INF/maven/com.example/demo/
META-INF/native-image/ch.qos.logback/logback-classic/1.5.8/reflect-config.json
META-INF/native-image/ch.qos.logback/logback-classic/1.5.8/resource-config.json
META-INF/native-image/com.example/demo/reflect-config.json
META-INF/native-image/com.example/demo/resource-config.json
META-INF/native-image/com.example/demo/native-image.properties
META-INF/native-image/com.fasterxml.jackson.core/jackson-databind/2.17.2/reflect-config.json
META-INF/native-image/org.apache.tomcat.embed/tomcat-embed-core/10.1.30/reflect-config.json
META-INF/native-image/org.apache.tomcat.embed/tomcat-embed-core/10.1.30/resource-config.json
META-INF/maven/com.example/demo/pom.xml
META-INF/maven/com.example/demo/pom.properties
Comment From: krezovic
More details ... The default classes directory is set to "target/classes" by native-maven-plugin
If I run it like this
./mvnw native:compile-no-fork -DclassesDirectory=target/demo-0.0.1-SNAPSHOT.jar.original
Then the app behaves as expected
$ target/demo
14:06:19.307 [main] INFO com.example.demo.DemoApplication -- Running version: 0.0.1-SNAPSHOT
I wonder if this is something worthy of a documentation, or a pre-configuration to native-maven-plugin - since it comes from spring boot parent in this case.
native-image invocation before
Executing: /home/armin/.sdkman/candidates/java/current/bin/native-image -cp /home/armin/projects/native-image-demo/target/classes:/home/armin/.m2/repository/org/springframework/boot/spring-boot-starter-web/3.4.0-M3/spring-boot-starter-web-3.4.0-M3.jar...
native-image invocation after
[INFO] Executing: /home/armin/.sdkman/candidates/java/current/bin/native-image -cp /home/armin/projects/native-image-demo/target/demo-0.0.1-SNAPSHOT.jar.original:/home/armin/.m2/repository/org/springframework/boot/spring-boot-starter-web/3.4.0-M3/spring-boot-starter-web-3.4.0-M3.jar...
Using JAR without .original suffix (spring boot fatjar) obviously fails
[INFO] Executing: /home/armin/.sdkman/candidates/java/current/bin/native-image -cp /home/armin/projects/native-image-demo/target/demo-0.0.1-SNAPSHOT.jar:/home/armin/.m2/repository/org/springframework/boot/spring-boot-starter-web/3.4.0-M3/spring-boot-starter-web-3.4.0-M3.jar:...
...
Error: Main entry point class 'com.example.demo.DemoApplication' neither found on
classpath: '/home/armin/projects/native-image-demo/target/demo-0.0.1-SNAPSHOT.jar:/home/armin/.m2/repository/org/springframework/boot/spring-boot-starter-web/3.4.0-M3/spring-boot-starter-web-3.4.0-M3.jar:... nor modulepath: '/home/armin/.sdkman/candidates/java/23.1.4.r21-mandrel/lib/svm/library-support.jar'.
Comment From: snicoll
@krezovic thanks for the report.
Using JAR without .original suffix (spring boot fatjar) obviously fails
Yes. And that is why our parent configures the native image maven plugin to use the classesDirectory option. It would be better if we didn't have to do this but we can't know upfront if the build is configured to create a repackaged archive or not, and if a classifier is set. We can't configure it to hardcode .original as it may or may not be accurate based on how the app is configured.
Our best course of action is some documentation here.
Comment From: ZIRAKrezovic
Leaving as a reference
Another hint is required if you dynamically use manifest via ResourceLoader
static class ApplicationRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
hints.resources().registerPattern("META-INF/MANIFEST.MF");
}
}
Example code that works after adding the hint, but not before
var resource =
new ClassPathResource(
"META-INF/MANIFEST.MF", this.getClass().getClassLoader());
log.info("Manifest resource: " + resource);
log.info("Manifest class loader: " + this.getClass().getClassLoader());
log.info("Manifest exists: " + resource.exists());
Manifest manifest = null;
if (resource.exists()) {
try {
manifest = new Manifest(resource.getInputStream());
log.info(
"Manifest attributes: "
+ manifest.getMainAttributes().entrySet().stream()
.map(
kv ->
String.join(
": ",
kv.getKey().toString(),
kv.getValue().toString()))
.collect(Collectors.joining("\n")));
} catch (IOException e) {
log.warn("Could not read manifest", e);
}
}