Taco Steemers

A personal blog.
☼ / ☾

Solving jlink Error: module not found, required by

How to make it possible to use and old jar or automatic module with jlink.

Related error messages:

  • Error: module-info.class not found for module
  • Error: module not found, required by
  • Error: automatic module cannot be used with jlink: from

Problem and solution

The dependency is not ready to be integrated in a project that uses the module system. It does not have a module-info.

This can be solved in three steps:

  • generating the module-info.java with jdeps
  • compiling module-info.java into module-info.class with javac
  • injecting the module-info.class in to the jar file

Solution on the commandline

Make sure that the directory that contains the jar file is actually in jlink's --module-path parameter.

The commands we need to turn the non-modular dependency in to a module look like the following:

jdeps --ignore-missing-deps --module-path <jar_dir_path> --add-modules <module_name --generate-module-info <out_dir_path> <jar_path>
javac --patch-module <module_name>=<jar_path> <module-info.java>
jar uf <jar_path> -C <module_name> <module-info.class>

Below we have the commands using jsoup as an example. "libs" is the directory containing the jar file, relative to the working directory.

jdeps --ignore-missing-deps --module-path libs --add-modules org.jsoup --generate-module-info libs/tmpOut libs/jsoup-1.15.4.jar
javac --patch-module org.jsoup=libs/jsoup-1.15.4.jar libs/tmpOut/module-info.java
jar uf libs/jsoup-1.15.4.jar -C libs/tmpOut/org.jsoup module-info.class

Note that jlink will not be able to see the newly created module if we don't have it in the --module path parameter.

As a gradle task

If this needs to be done as part of a proper build process it will take a bit more work, because it needs to be automated. I show my solution for gradle version 7.1 below. It starts by copying the resolved dependencies we need, based on a list. I ended up only needing to do this for jsoup, and the code below will need additional work to handle multiple dependencies.

task copySomeResolvedLibs() {
    doFirst {
        List<String> requestedLibs = Arrays.asList("jsoup-1.15.4.jar");
        var outDirPath = Paths.get(project.projectDir.toPath().toAbsolutePath().toString(), 'build'+File.separator+'resolvedLibs'+File.separator)
        String outDir = outDirPath.toString()
        Files.createDirectories(outDirPath)
        configurations.resolvableImpl.resolvedConfiguration.resolvedArtifacts.each {
            if (requestedLibs.contains(it.getFile().getName())) {
                var source = it.getFile().toPath().toAbsolutePath()
                var target = Paths.get(outDir, it.getFile().getName()).toAbsolutePath()
                Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING)
            }
        }
    }
}

task createModuleInfoForResolvedLibJsoup(type: Exec) {
    dependsOn "copySomeResolvedLibs"

    var dirPath = Paths.get(project.projectDir.toPath().toAbsolutePath().toString(), "build"+File.separator+"resolvedLibs"+File.separator);
    var modInfoDirPathStr = Paths.get(dirPath.toString(), "tmpOut").toString()
    var jarPathStr = Paths.get(dirPath.toString(), "jsoup-1.15.4.jar").toString();

    group = "Execution"
    description = "Will generate module.info for jsoup."
    commandLine "jdeps", "--ignore-missing-deps", "--module-path", dirPath.toString(), "--add-modules", "org.jsoup", "--generate-module-info", modInfoDirPathStr, jarPathStr
}

task compileJsoupModuleInfo(type: Exec) {
    dependsOn "createModuleInfoForResolvedLibJsoup"

    var dirPath = Paths.get(project.projectDir.toPath().toAbsolutePath().toString(), "build"+File.separator+"resolvedLibs"+File.separator);
    var modInfoPathStr = Paths.get(dirPath.toString(), "tmpOut"+File.separator+"org.jsoup"+File.separator+"module-info.java").toString()
    var jarPathStr = Paths.get(dirPath.toString(), "jsoup-1.15.4.jar").toString();

    group = "Execution"
    description = "Will compile the module.info for jsoup."
    commandLine "javac", "--patch-module", "org.jsoup="+jarPathStr, modInfoPathStr
}

task finalizeJsoupByInjectingModuleInfo(type: Exec) {
    dependsOn "compileJsoupModuleInfo"

    var dirPath = Paths.get(project.projectDir.toPath().toAbsolutePath().toString(), "build"+File.separator+"resolvedLibs"+File.separator);
    var modInfoDirPathStr = Paths.get(dirPath.toString(), "tmpOut"+ File.separator+"org.jsoup").toString()
    var jarPathStr = Paths.get(dirPath.toString(), "jsoup-1.15.4.jar").toString();

    group = "Execution"
    description = "Will inject module.info in to jsoup jar."
    commandLine "jar", "uf", jarPathStr, "-C", modInfoDirPathStr, "module-info.class"
}

Make sure to actually call the final task, finalizeJsoupByInjectingModuleInfo, as part of the task that needs it. For example, using a dependsOn action:

task preparePackaging(type: Exec) {
    dependsOn 'clean'
    dependsOn 'build'
    dependsOn 'finalizeJsoupByInjectingModuleInfo'
    tasks.findByName('build').mustRunAfter 'clean'
    tasks.findByName('finalizeJsoupByInjectingModuleInfo').mustRunAfter 'build'

    ...