Last active
August 23, 2021 04:04
-
-
Save nigredo-tori/3b168da8e3cdcc7766ace3f23e999ec5 to your computer and use it in GitHub Desktop.
JlinkPlugin settings to make dealing with jdeps less of a pain
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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