0

Context: Linux Mint 21.1, Java Adoptium 17.0.7 JDK, Maven 3.6.3.

My modular application uses LWJGL (and then its native libraries); using the Maven jlink plug-in I generate a Java image. During generation, the jlink plug-in prints the following:

[INFO] --- maven-jlink-plugin:3.1.0:jlink (default-cli) @ treni ---
INFO]  -> module: org.lwjgl.stb ( /home/mmg/.m2/repository/org/lwjgl/lwjgl-stb/3.3.2/lwjgl-stb-3.3.2.jar )
[INFO]  -> module: org.slf4j ( /home/mmg/.m2/repository/org/slf4j/slf4j-api/2.0.7/slf4j-api-2.0.7.jar )
[INFO]  -> module: imgui.natives.linux ( /home/mmg/.m2/repository/io/github/spair/imgui-java-natives-linux/1.86.10/imgui-java-natives-linux-1.86.10.jar )
[INFO]  -> module: org.lwjgl ( /home/mmg/.m2/repository/org/lwjgl/lwjgl/3.3.2/lwjgl-3.3.2.jar )
[INFO]  -> module: imgui.binding ( /home/mmg/.m2/repository/io/github/spair/imgui-java-binding/1.86.10/imgui-java-binding-1.86.10.jar )
[INFO]  -> module: org.lwjgl.stb.natives ( /home/mmg/.m2/repository/org/lwjgl/lwjgl-stb/3.3.2/lwjgl-stb-3.3.2-natives-linux.jar )
[INFO]  -> module: org.lwjgl.glfw ( /home/mmg/.m2/repository/org/lwjgl/lwjgl-glfw/3.3.2/lwjgl-glfw-3.3.2.jar )
[INFO]  -> module: org.joml ( /home/mmg/.m2/repository/org/joml/joml/1.10.5/joml-1.10.5.jar )
[INFO]  -> module: org.lwjgl.opengl ( /home/mmg/.m2/repository/org/lwjgl/lwjgl-opengl/3.3.2/lwjgl-opengl-3.3.2.jar )
[INFO]  -> module: org.slf4j.jul ( /home/mmg/.m2/repository/org/slf4j/slf4j-jdk14/2.0.7/slf4j-jdk14-2.0.7.jar )
[INFO]  -> module: org.lwjgl.opengl.natives ( /home/mmg/.m2/repository/org/lwjgl/lwjgl-opengl/3.3.2/lwjgl-opengl-3.3.2-natives-linux.jar )
[INFO]  -> module: org.lwjgl.natives ( /home/mmg/.m2/repository/org/lwjgl/lwjgl/3.3.2/lwjgl-3.3.2-natives-linux.jar )
[INFO]  -> module: com.vistamaresoft.treni ( /home/mmg/Documents/projects/Eclipse_workspaces/Treni/treni/target/classes )
[INFO]  -> module: org.lwjgl.glfw.natives ( /home/mmg/.m2/repository/org/lwjgl/lwjgl-glfw/3.3.2/lwjgl-glfw-3.3.2-natives-linux.jar )
[INFO] Building zip: /home/mmg/Documents/projects/Eclipse_workspaces/Treni/treni/target/treni-0.0.1.zip

which shows that the plug-in knows about all the required native library JAR's.

I expected the resulting application to be able to locate the (evidently included, see below) native libraries, but it seems it is not, at leat without additional hits I have no idea how to give it (and I could not find described anywhere). In fact, when I run the generate shell script, I receive the error:

Exception in thread "main" java.lang.UnsatisfiedLinkError: Failed to locate library: liblwjgl.so

Note that the required library is contained in the lwjgl-3.3.2-natives-linux.jar referenced by the pom.xml dependencies via the appropriate profile (see below for the pom.xml contents).

The shell script is the default one generated by the jlink plug-in (the same for all my jlink-ed apps, only the module/main_class changes from one to another); anyway for completeness, this is its contents:

#!/bin/sh
JLINK_VM_OPTIONS=
DIR=`dirname $0`
$DIR/java $JLINK_VM_OPTIONS -m com.vistamaresoft.treni/com.vistamaresoft.treni.Main "$@"

The generated image DOES contain the native libraries: I tried generating an image WITHOUT the native dependencies and the result is shorter roughly of the same size of those JAR's. The difference, as expected, is in the resulting lib/modules file. So, this file presumably does contain the needed library/ies, but the executable(s) are not able to retrieve them?

I have Googled and 'stackoverflow-ed' for a whole day and I found nothing: any suggestion, help, hint about what it is happening is welcome.

This is the (slightly shortened for brevity) pom.xml;

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

<!-- Several descriptive tags removed for brevity -->
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <target.name>treni</target.name>
        <!-- Versions -->
        <project.version>0.0.1</project.version>
        <java.version>17</java.version>
        <joml.version>1.10.5</joml.version>
        <joml-primitives.version>1.10.4</joml-primitives.version>
        <lwjgl.version>3.3.2</lwjgl.version>
        <imgui-java.version>1.86.10</imgui-java.version>
        <exec-maven-plugin.version>3.0.0</exec-maven-plugin.version>
        <maven-compiler-plugin.version>3.11.0</maven-compiler-plugin.version>
        <maven-dependency-plugin.version>3.6.0</maven-dependency-plugin.version>
        <maven-jar-plugin.version>3.3.0</maven-jar-plugin.version>
        <maven-jlink-plugin.version>3.1.0</maven-jlink-plugin.version>
        <maven-resources-plugin.version>3.3.1</maven-resources-plugin.version>
    </properties>

    <profiles>
        <profile>
            <id>lwjgl-natives-linux-amd64</id>
            <activation>
                <os>
                    <family>unix</family>
                    <arch>amd64</arch>
                </os>
            </activation>
            <properties>
                <lwjgl.natives>natives-linux</lwjgl.natives>
                <imgui.native>natives-linux</imgui.native>
                <imgui.native.module>linux</imgui.native.module>
            </properties>
        </profile>
        <!-- Windows and MacOs profiles removed for brevity -->
    </profiles>

    <dependencies>
        <dependency>
            <groupId>org.lwjgl</groupId>
            <artifactId>lwjgl</artifactId>
            <version>${lwjgl.version}</version>
        </dependency>
        <dependency>
            <groupId>org.lwjgl</groupId>
            <artifactId>lwjgl-glfw</artifactId>
            <version>${lwjgl.version}</version>
        </dependency>
        <dependency>
            <groupId>org.lwjgl</groupId>
            <artifactId>lwjgl-opengl</artifactId>
            <version>${lwjgl.version}</version>
        </dependency>
        <dependency>
            <groupId>org.lwjgl</groupId>
            <artifactId>lwjgl-stb</artifactId>
            <version>${lwjgl.version}</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>2.0.7</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-jdk14</artifactId>
            <version>2.0.7</version>
        </dependency>
        <dependency>
            <groupId>org.joml</groupId>
            <artifactId>joml</artifactId>
            <version>${joml.version}</version>
        </dependency>
        <dependency>
            <groupId>io.github.spair</groupId>
            <artifactId>imgui-java-binding</artifactId>
            <version>${imgui-java.version}</version>
        </dependency>

<!-- Natives -->

        <dependency>
            <groupId>org.lwjgl</groupId>
            <artifactId>lwjgl</artifactId>
            <version>${lwjgl.version}</version>
            <classifier>${lwjgl.natives}</classifier>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.lwjgl</groupId>
            <artifactId>lwjgl-glfw</artifactId>
            <version>${lwjgl.version}</version>
            <classifier>${lwjgl.natives}</classifier>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.lwjgl</groupId>
            <artifactId>lwjgl-opengl</artifactId>
            <version>${lwjgl.version}</version>
            <classifier>${lwjgl.natives}</classifier>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.lwjgl</groupId>
            <artifactId>lwjgl-stb</artifactId>
            <version>${lwjgl.version}</version>
            <classifier>${lwjgl.natives}</classifier>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.github.spair</groupId>
            <artifactId>imgui-java-${imgui.native}</artifactId>
            <version>${imgui-java.version}</version>
            <scope>runtime</scope>
        </dependency>

    </dependencies>

    <build>
        <pluginManagement>
            <plugins>

                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>${maven-compiler-plugin.version}</version>
                    <configuration> <!--
                        <release>S${java.version}</release> -->
                        <source>${java.version}</source>
                        <target>${java.version}</target>
                        <showDeprecation>true</showDeprecation>
                    </configuration>
                </plugin>

                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-jar-plugin</artifactId>
                    <configuration>
                        <archive>
                            <manifest>
                                <addClasspath>true</addClasspath>
                                <mainClass>com.vistamaresoft.treni.Main</mainClass>
                            </manifest>
                        </archive>
                        <outputDirectory>${project.build.directory}/${target.name}</outputDirectory>
                    </configuration>
                </plugin>

                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-javadoc-plugin</artifactId>
                    <version>3.5.0</version>
                    <!-- run with `mvn javadoc:javadoc` -->
                    <configuration>
                        <reportOutputDirectory>${project.basedir}/doc</reportOutputDirectory>
                        <show>public</show>
                    </configuration>
                </plugin>

                <plugin>
                    <groupId>org.moditect</groupId>
                    <artifactId>moditect-maven-plugin</artifactId>
                    <version>1.0.0.Final</version>
                        <configuration>
                            <outputDirectory>${project.build.directory}/modules</outputDirectory>
                            <overwriteExistingFiles>true</overwriteExistingFiles>
                            <modules>
                                <module>
                                    <artifact>
                                        <groupId>io.github.spair</groupId>
                                        <artifactId>imgui-java-binding</artifactId>
                                        <version>${imgui-java.version}</version>
                                    </artifact>
                                    <moduleInfoFile>${project.basedir}/src/main/moduleInfos/imgui.binding.module-info.java</moduleInfoFile>
                                </module>

                                <module>
                                    <artifact>
                                        <groupId>io.github.spair</groupId>
                                        <artifactId>imgui-java-${imgui.native}</artifactId>
                                        <version>${imgui-java.version}</version>
                                    </artifact>
                                    <moduleInfoFile>${project.basedir}/src/main/moduleInfos/imgui.natives.${imgui.native.module}.module-info.java</moduleInfoFile>
                                </module>
                            </modules>
                        </configuration>
                </plugin>

                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-jlink-plugin</artifactId>
                    <version>${maven-jlink-plugin.version}</version>
                    <!-- run with `mvn jlink:jlink` separately from `mvn package` as combining both into `mvn package jlink:jlink` raises an error -->
                    <extensions>true</extensions>
                    <configuration>
                        <!-- Module paths overriding the Mavem local repo path for non-modular JAR, modularised with moditect-maven-plugin -->
                        <modulePaths>
                            <modulePath>${project.build.directory}/modules/imgui-java-binding-${imgui-java.version}.jar</modulePath>
                            <modulePath>${project.build.directory}/modules/imgui-java-natives-${imgui.native.module}-${imgui-java.version}.jar</modulePath>
                        </modulePaths>
                        <compress>2</compress>
                        <noHeaderFiles>true</noHeaderFiles>
                        <noManPages>true</noManPages>
                        <stripDebug>true</stripDebug>
                        <launcher>treni=com.vistamaresoft.treni/com.vistamaresoft.treni.Main</launcher>
                    </configuration>
                </plugin>

            </plugins>
        </pluginManagement>

        <!-- PLUG-IN EXECUTIONS -->

        <plugins>
            <plugin>
                <groupId>org.moditect</groupId>
                <artifactId>moditect-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <id>add-module-infos</id>
                        <phase>generate-resources</phase>
                        <goals>
                        <goal>add-module-info</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

And this is the module-info.java of the app (currently in a single module):

module com.vistamaresoft.treni
{
    exports             com.vistamaresoft.treni;
    exports             com.vistamaresoft.treni.engine;
    exports             com.vistamaresoft.treni.engine.elements;
    exports             com.vistamaresoft.treni.objectviewer;
    exports             com.vistamaresoft.treni.sim;

    requires            imgui.binding;
    requires transitive org.joml;
    requires            org.lwjgl;
    requires            org.lwjgl.glfw;
    requires            org.lwjgl.opengl;
    requires            org.lwjgl.stb;
    requires transitive org.slf4j;
    requires java.prefs;
    requires java.base;
    requires java.logging;
}
  • Please post the shell script you mentioned – g00se Jul 17 '23 at 16:02
  • @g00se : thanks for the reply; the script is the default one, generated by the pug-in; it is mostly the same for all the apps I have jlink-ed. Anyway, for completeness, I have added it to the question. – Maurizio M. Gavioli Jul 17 '23 at 16:52
  • But how is that script getting invoked? What parameters are being passed? – g00se Jul 17 '23 at 16:59
  • None; the application (at least in its current state) needs (or knows) no parameter. Note that any parameter passed to the JVM after the `-m ..." parameter (the "$@" at the end of the last script line) is ignored by the JVM itself and passed directly to the application. – Maurizio M. Gavioli Jul 17 '23 at 19:47
  • I think you might try to intepolate `-Djava.library.path=foo` – g00se Jul 17 '23 at 19:53
  • Well, there is no physical path to the these libraries: they are within the `bin/modules` binary file, which is packed by jlink and added to the module and to the class path. This is the whole point of the jlink (and of the Maven jlink plug-in which is essentially a higher-level interface to it): to prepare a bunch of files containing EVERYTHING (and as far as possible ONLY) what it is needed to run the application (including a minimal Java executable), so that copying the pack to another machine (of a compatible platform, of course) you get a runnable application with no other prerequisites. – Maurizio M. Gavioli Jul 18 '23 at 10:51
  • Yes, I know that's the general theory. Clearly that's not happened in this case, as what would normally be a shared library has *not* been linked properly, so I was suggesting that as a temporary workaround. Is it possible for us to get to the whole project? – g00se Jul 18 '23 at 12:09
  • The whole project is quite large; I'll try to put together a minimal project and rephrase the question. – Maurizio M. Gavioli Jul 18 '23 at 13:07

1 Answers1

0

I am far from sure this is the best solution, as it is rather cumbersome, but it is the only one I found so far, it works (mostly) and may be useful to someone else too. I want to thank SPASI in the LWJGL forum, who greatly helped me in finding it.

  1. Add native libraries modules to module-info.java: this ensures that the libraries will be found within the runnable image generated by the jlink pug-in. In practice, add to module-info.java one line like
requires transitive org.lwjgl.natives;

for each native library.

  1. Unfortunately, both Eclipse and the Maven compiler seem unable to locate these modules within the local Maven repo .m2. The solution is three-fold:

2a) Copy the dependencies from the repo to a local folder, for instance target/modules using the copy-dependencies goal of the Maven dependency plug-in; this must be done before the Maven compile phase (I use the generate-resources phase), for instance:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-dependency-plugin</artifactId>
    <version>3.6.0</version>
    <configuration>
        <outputDirectory>${project.build.directory}/modules</outputDirectory>
        <overWriteReleases>false</overWriteReleases>
        <overWriteSnapshots>false</overWriteSnapshots>
        <overWriteIfNewer>true</overWriteIfNewer>
    </configuration>
    <executions>
        <execution>
            <id>copy-dependencies</id>
            <phase>generate-resources</phase>
            <goals>
                <goal>copy-dependencies</goal>
            </goals>
        </execution>
    </executions>
</plugin>

2b) Maven compiler plug-in: point this plug-in to the folder where the dependencies have been copied, adding this directive to the plug-in configuration:

<compilerArgs>
    <arg>--module-path</arg>
    <arg>${project.build.directory}/modules</arg>
</compilerArgs>

2c) Maven jlink plug-in: point this plug-in also to the same folder, adding the following directive to the plug-in configuration:

<modulePaths>
    <modulePath>${project.build.directory}/modules</modulePath>
</modulePaths>

Now, Maven should correctly compile the project and jlink a runnable application.

  1. Eclipse (and perhaps other Java IDE's too): this leaves unsolved the issue with Eclipse not resolving the native library modules added to the module-info.java in step 1.

This precludes building the project from within Eclipse, which I find very useful at least for debugging. Maybe this can be solved for the libraries whose JAR's are already modular (but I do not know how), but surely it cannot be solved for the JAR's which are NOT modular and have to be modularised (for instance with the org.moditect:moditect-maven-plugin): their modules simply do not exist in any well-known location and are generated out-of-source (this happen to me with the io.github.spair:imgui-java-natives-<platform> which is not modular and is often used with openGL / Vulkan projects).

As I use Maven only via command line, I work-around this preparing (our of the Java source and parallel to the main project pom.xml) 2 versions of the module-info.java, one without and one with the added modules.

Normally the project module-info.java is kept equal to the first, shorter, version; when I have to launch a Maven build, a small shell script overwrites module-info with the extended version, run the required mvn command(s) and finally overwrites module-info.java back with the 'normal' version. Not really fool-proof, but usable.