Skip to content

Instantly share code, notes, and snippets.

@nigredo-tori
Last active August 23, 2021 04:04
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nigredo-tori/3b168da8e3cdcc7766ace3f23e999ec5 to your computer and use it in GitHub Desktop.
Save nigredo-tori/3b168da8e3cdcc7766ace3f23e999ec5 to your computer and use it in GitHub Desktop.
JlinkPlugin settings to make dealing with jdeps less of a pain
import java.lang.{Runtime => JRuntime}
import java.lang.module.ModuleDescriptor
import java.net.URI
import java.nio.file.FileSystems
import java.util.jar.JarFile
import java.util.zip.ZipFile
import scala.collection.JavaConverters._
import com.typesafe.sbt.packager.archetypes.jlink.JlinkPlugin
import com.typesafe.sbt.packager.archetypes.jlink.JlinkPlugin.autoImport._
import sbt._
import sbt.Keys._
import sbt.io.Using
/** Additional settings to make `JlinkPlugin` behave.
*
* The issue we have is as follows. `JlinkPlugin` uses `jdeps` utility to
* locate the JVM modules in includes in the image. `jdeps`, however, is very
* particular when it comes to its inputs, and especially when it comes to
* dependencies between them. It has issues with automatic modules, and
* requires ''all'' the referenced modules to be present. Together with
* the library authors either not adding module descriptors, or making them
* more strict than they need to, this makes choosing proper settings for
* `JlinkPlugin` an exercise in frustration.
*
* To combat this, this plugin splits the classpath entries into two groups.
* 1. ''Explicit'' modules are handled separately. We parse their descriptors,
* and get their requirements from there. Since these modules don't make it
* into `jdeps`, we don't have to deal with missing dependencies on other
* non-platform modules they might have.
* 2. Other JARs, class directories etc. are still handled by `jdeps`, which
* scans their bytecode and gives us a list of platform modules they depend
* upon.
*/
object JlinkPluginFix extends AutoPlugin {
object autoImport {
val jlinkPartitionedClasspath = taskKey[PartitionedClasspath](
"Classpath partitioned based on module descriptor presence")
val jlinkJdkVersion = taskKey[JRuntime.Version](
"Version of the JDK used to build the JVM image")
}
import autoImport._
override def requires = JlinkPlugin
override def trigger = AllRequirements
override def projectSettings = Seq(
// Hardcoded for now.
jlinkBuildImage / jlinkJdkVersion := JRuntime.Version.parse("11"),
jlinkBuildImage / jlinkPartitionedClasspath := {
val jdkVersion = (jlinkBuildImage / jlinkJdkVersion).value
val full = (Compile / fullClasspath).value
val eithers = full.map { entry =>
getExplicitModule(entry.data, jdkVersion).toRight(entry)
}
PartitionedClasspath(
eithers.collect { case Right(m) => m },
eithers.collect { case Left(e) => e }
)
},
// Exclude explicit modules from the `jdeps` run.
jlinkBuildImage / fullClasspath :=
(jlinkBuildImage / jlinkPartitionedClasspath).value.other,
jlinkModules ++= {
val modules = (jlinkBuildImage / jlinkPartitionedClasspath).value.explicitModules
modules.flatMap(_.requires().asScala)
.map(_.name)
.distinct
.filter(isPlatformModule)
},
// Ignore broken package dependencies except for platform packages.
jlinkIgnoreMissingDependency := {
// Assume that platform packages correspond to platform modules.
case (_, dependee) if isPlatformModule(dependee) => false
case _ => true
}
)
// Some JakartaEE artifacts use `java.*` module names, even though
// they are not a part of the platform anymore.
// https://github.com/eclipse-ee4j/ee4j/issues/34
// This requires special handling on our part when deciding if the module
// is a part of the platform or not.
// At least the new modules shouldn't be doing this...
private val knownJakartaJavaModules = Set("java.xml.bind", "java.xml.soap", "java.ws.rs")
private def isPlatformModule(
name: String
): Boolean =
(name.startsWith("jdk.") || name.startsWith("java.")) &&
!knownJakartaJavaModules.contains(name)
private def getExplicitModule(
file: File,
version: JRuntime.Version
): Option[ModuleDescriptor] = {
if (!file.isFile) None
else {
Using.file(new JarFile(_, false, ZipFile.OPEN_READ, version))(file) { jar =>
Option(jar.getEntry("module-info.class"))
.map { entry =>
Using.zipEntry(jar)(entry)(ModuleDescriptor.read)
}
}
}
}
}
final case class PartitionedClasspath(
explicitModules: Seq[ModuleDescriptor],
other: Classpath
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment