spring-projects / spring-boot

Spring Boot
https://spring.io/projects/spring-boot
Apache License 2.0
74.96k stars 40.65k forks source link

Document how to handle MANIFEST.MF in native image with Maven #42412

Closed krezovic closed 3 weeks ago

krezovic commented 4 weeks ago

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
krezovic commented 4 weeks ago

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'.
snicoll commented 3 weeks ago

@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.

ZIRAKrezovic commented 2 weeks ago

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);
            }
        }