Skip to content

Commit

Permalink
JlinkPlugin: Add and document a workaround for automatic modules (#1295)
Browse files Browse the repository at this point in the history
* Remove .metals from the repository

* Move Jlink plugin description to a separate chapter

* Make sure there is a workaround for the automatic module issue

* Document the workaround for the automatic module issue

Co-authored-by: Nepomuk Seiler <muuki88@users.noreply.github.com>
  • Loading branch information
nigredo-tori and muuki88 committed Jan 13, 2020
1 parent 1768d2a commit 2851155
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ object JlinkPlugin extends AutoPlugin {
val javaHome0 = javaHome.in(jlinkBuildImage).value.getOrElse(defaultJavaHome)
val run = runJavaTool(javaHome0, log) _
val paths = fullClasspath.in(jlinkBuildImage).value.map(_.data.getPath)
val modulePath = jlinkModulePath.in(jlinkModules).value
val shouldIgnore = jlinkIgnoreMissingDependency.value

// We can find the java toolchain version by parsing the `release` file. This
Expand All @@ -69,9 +70,13 @@ object JlinkPlugin extends AutoPlugin {
}
.getOrElse(sys.error("JAVA_VERSION not found in ${releaseFile.getAbsolutePath}"))

val modulePathOpts = if (modulePath.nonEmpty) {
Vector("--module-path", modulePath.mkString(":"))
} else Vector.empty

// Jdeps has a few convenient options (like --print-module-deps), but those
// are not flexible enough - we need to parse the full output.
val jdepsOutput = run("jdeps", "--multi-release" +: javaVersion +: "-R" +: paths)
val jdepsOutput = run("jdeps", "--multi-release" +: javaVersion +: modulePathOpts ++: "-R" +: paths)

val deps = jdepsOutput.linesIterator
// There are headers in some of the lines - ignore those.
Expand Down Expand Up @@ -135,7 +140,7 @@ object JlinkPlugin extends AutoPlugin {
JlinkOptions(
addModules = modules,
output = Some(target.in(jlinkBuildImage).value),
modulePath = jlinkModulePath.value
modulePath = jlinkModulePath.in(jlinkBuildImage).value
)
},
jlinkBuildImage := {
Expand Down
33 changes: 33 additions & 0 deletions src/sbt-test/jlink/test-jlink-misc/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,36 @@ val issue1284 = project
jlinkModules := List("no-such-module"),
runFailingChecks := jlinkBuildImage.value
)

// We should be able to make the whole thing work for modules that depend
// on automatic modules - at least by manually setting `jlinkModulePath`.
val issue1293 = project
.enablePlugins(JlinkPlugin)
.settings(
libraryDependencies ++= Seq(
// This has a module dependency on `paranamer`, which is not an explicit module.
"com.fasterxml.jackson.module" % "jackson-module-paranamer" % "2.10.1",
// Make sure that JARs with "bad" names are excluded.
"org.typelevel" % "cats-core_2.12" % "2.0.0",

// Two dependencies with overlapping packages - just to make sure we don't inadvertedly
// put them into the module path.
"org.openoffice" % "unoil" % "4.1.2",
"org.openoffice" % "ridl" % "4.1.2"
),
jlinkIgnoreMissingDependency := JlinkIgnore.everything,
// Use `paramaner` (and only it) as an automatic module
jlinkModulePath := {
// Get the full classpath with all the resolved dependencies.
fullClasspath.in(jlinkBuildImage).value
// Find the ones that have `paranamer` as their artifact names.
.filter { item =>
item.get(moduleID.key).exists { modId =>
modId.name == "paranamer"
}
}
// Get raw `File` objects.
.map(_.data)
},
runChecks := jlinkBuildImage.value
)
3 changes: 2 additions & 1 deletion src/sbt-test/jlink/test-jlink-misc/test
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
> issue1247ExternalModule/runChecks
> issue1247JakartaJavaModules/runChecks
> issue1266/runChecks
-> issue1284/runFailingChecks
-> issue1284/runFailingChecks
> issue1293/runChecks
1 change: 1 addition & 0 deletions src/sphinx/archetypes/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ of them to choose from.
Java Server Application <java_server/index.rst>
Systemloaders <systemloaders.rst>
Configuration Archetypes <misc_archetypes.rst>
Jlink Plugin <jlink_plugin.rst>
An archetype cheatsheet <cheatsheet.rst>
81 changes: 81 additions & 0 deletions src/sphinx/archetypes/jlink_plugin.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
.. _jlink-plugin:

Jlink Plugin
============

This plugin builds on Java's `jlink`_ tool to embed a JVM image (a stripped-down JRE)
into your package. It produces a JVM image containing only the modules that are referenced
from the dependency classpath.

Note: Current implementation only detects the platform modules (that is, the ones present in
the JDK used to build the image). Modular JARs and directories are packaged as specified
by the `UniversalPlugin`.

.. code-block:: scala
enablePlugins(JlinkPlugin)
The plugin requires Oracle JDK 11 or OpenJDK 11. Although `jlink` and `jdeps` are also
a part of the older JDK versions, those lack some of the newer features, which was not
addressed in the current plugin version.

This plugin must be run on the platform of the target installer. The tooling does *not*
provide a means of creating, say, Windows installers on MacOS, or MacOS on Linux, etc.

The plugin analyzes the dependencies between packages using `jdeps`, and raises an error in case of a missing dependency (e.g. for a provided transitive dependency). The missing dependencies can be suppressed on a case-by-case basis (e.g. if you are sure the missing dependency is properly handled):

.. code-block:: scala
jlinkIgnoreMissingDependency := JlinkIgnore.only(
"foo.bar" -> "bar.baz",
"foo.bar" -> "bar.qux"
)
For large projects with a lot of dependencies this can get unwieldy. You can use a more flexible ignore strategy:

.. code-block:: scala
jlinkIgnoreMissingDependency := JlinkIgnore.byPackagePrefix(
"foo.bar" -> "bar"
)
Otherwise you may opt out of the check altogether (which is not recommended):

.. code-block:: scala
jlinkIgnoreMissingDependency := JlinkIgnore.everything
Known issues
------------

Adding some library dependencies can lead to errors like this:

::

java.lang.module.FindException: Module paranamer not found, required by com.fasterxml.jackson.module.paranamer

This is often caused by depending on automatic modules. In the example above, `com.faterxml.jackson.module.paranamer` is an explicit module (as in, it is a JAR with a module descriptor) that defines a dependency on the `paranamer` module. However, there is no explicit `paranamer` module - instead, Jackson expects us to use the `paranamer` JAR file as an automatic module. To do this, the JAR has to be on the module path. At the moment `JlinkPlugin` does not put it there automatically, so we have to do that ourselves:

.. code-block:: scala
jlinkModulePath := {
// Get the full classpath with all the resolved dependencies.
fullClasspath.in(jlinkBuildImage).value
// Find the ones that have `paranamer` as their names.
.filter { item =>
item.get(moduleID.key).exists { modId =>
modId.name == "paranamer"
}
}
// Get raw `File` objects.
.map(_.data)
}
Further reading
---------------

For further details on the capabilities of `jlink`, see the
`jlink <https://docs.oracle.com/en/java/javase/11/tools/jlink.html>`_ and
`jdeps <https://docs.oracle.com/en/java/javase/11/tools/jdeps.html>`_ references.
(Note: only some of the possible settings are exposed through this plugin. Please submit a
`Github <https://github.com/sbt/sbt-native-packager/issues>`_ issue or pull request if something specific is desired.)
52 changes: 0 additions & 52 deletions src/sphinx/archetypes/misc_archetypes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,55 +22,3 @@ ClasspathJar & LauncherJar Plugin
---------------------------------

See the :ref:`long-classpaths` section for usage of these plugins.

Jlink Plugin
------------

This plugin builds on Java's `jlink`_ tool to embed a JVM image (a stripped-down JRE)
into your package. It produces a JVM image containing only the modules that are referenced
from the dependency classpath.

Note: Current implementation only detects the platform modules (that is, the ones present in
the JDK used to build the image). Modular JARs and directories are packaged as specified
by the `UniversalPlugin`.

.. code-block:: scala
enablePlugins(JlinkPlugin)
The plugin requires Oracle JDK 11 or OpenJDK 11. Although `jlink` and `jdeps` are also
a part of the older JDK versions, those lack some of the newer features, which was not
addressed in the current plugin version.

This plugin must be run on the platform of the target installer. The tooling does *not*
provide a means of creating, say, Windows installers on MacOS, or MacOS on Linux, etc.

The plugin analyzes the dependencies between packages using `jdeps`, and raises an error in case of a missing dependency (e.g. for a provided transitive dependency). The missing dependencies can be suppressed on a case-by-case basis (e.g. if you are sure the missing dependency is properly handled):

.. code-block:: scala
jlinkIgnoreMissingDependency := JlinkIgnore.only(
"foo.bar" -> "bar.baz",
"foo.bar" -> "bar.qux"
)
For large projects with a lot of dependencies this can get unwieldy. You can use a more flexible ignore strategy:

.. code-block:: scala
jlinkIgnoreMissingDependency := JlinkIgnore.byPackagePrefix(
"foo.bar" -> "bar"
)
Otherwise you may opt out of the check altogether (which is not recommended):

.. code-block:: scala
jlinkIgnoreMissingDependency := JlinkIgnore.everything
For further details on the capabilities of `jlink`, see the
`jlink <https://docs.oracle.com/en/java/javase/11/tools/jlink.html>`_ and
`jdeps <https://docs.oracle.com/en/java/javase/11/tools/jdeps.html>`_ references.
(Note: only some of the possible settings are exposed through this plugin. Please submit a
`Github <https://github.com/sbt/sbt-native-packager/issues>`_ issue or pull request if something specific is desired.)

0 comments on commit 2851155

Please sign in to comment.