enso/project/JPMSUtils.scala

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

229 lines
8.8 KiB
Scala
Raw Normal View History

Upgrade enso to GraalVM for jdk 21 (#7991) Upgrade to GraalVM JDK 21. ``` > java -version openjdk version "21" 2023-09-19 OpenJDK Runtime Environment GraalVM CE 21+35.1 (build 21+35-jvmci-23.1-b15) OpenJDK 64-Bit Server VM GraalVM CE 21+35.1 (build 21+35-jvmci-23.1-b15, mixed mode, sharing) ``` With SDKMan, download with `sdk install java 21-graalce`. # Important Notes - After this PR, one can theoretically run enso with any JRE with version at least 21. - Removed `sbt bootstrap` hack and all the other build time related hacks related to the handling of GraalVM distribution. - `project-manager` remains backward compatible - it can open older engines with runtimes. New engines now do no longer require a separate runtime to be downloaded. - sbt does not support compilation of `module-info.java` files in mixed projects - https://github.com/sbt/sbt/issues/3368 - Which means that we can have `module-info.java` files only for Java-only projects. - Anyway, we need just a single `module-info.class` in the resulting `runtime.jar` fat jar. - `runtime.jar` is assembled in `runtime-with-instruments` with a custom merge strategy (`sbt-assembly` plugin). Caching is disabled for custom merge strategies, which means that re-assembly of `runtime.jar` will be more frequent. - Engine distribution contains multiple JAR archives (modules) in `component` directory, along with `runner/runner.jar` that is hidden inside a nested directory. - The new entry point to the engine runner is [EngineRunnerBootLoader](https://github.com/enso-org/enso/pull/7991/files#diff-9ab172d0566c18456472aeb95c4345f47e2db3965e77e29c11694d3a9333a2aa) that contains a custom ClassLoader - to make sure that everything that does not have to be loaded from a module is loaded from `runner.jar`, which is not a module. - The new command line for launching the engine runner is in [distribution/bin/enso](https://github.com/enso-org/enso/pull/7991/files#diff-0b66983403b2c329febc7381cd23d45871d4d555ce98dd040d4d1e879c8f3725) - [Newest version of Frgaal](https://repo1.maven.org/maven2/org/frgaal/compiler/20.0.1/) (20.0.1) does not recognize `--source 21` option, only `--source 20`.
2023-11-17 21:02:36 +03:00
import sbt.*
import sbt.Keys.*
import sbtassembly.Assembly.{Dependency, JarEntry, Library, Project}
import sbtassembly.MergeStrategy
import java.io.{File, FilenameFilter}
import sbtassembly.CustomMergeStrategy
import xsbti.compile.IncToolOptionsUtil
import sbt.internal.inc.{CompileOutput, PlainVirtualFile}
import java.nio.file.attribute.BasicFileAttributes
import java.nio.file.{
FileVisitResult,
FileVisitor,
Files,
Path,
SimpleFileVisitor
}
/** Collection of utility methods dealing with JPMS modules.
* The motivation comes from the update of GraalVM to
* [Truffle unchained](https://medium.com/graalvm/truffle-unchained-13887b77b62c) -
* we need to add Truffle and Graal related Jars on module-path.
* We also need to convert our runtime projects to *explicit modules*, and thus,
* all our other projects to *automatic modules*.
* @see
*/
object JPMSUtils {
val slf4jVersion = "2.0.9"
val logbackClassicVersion = "1.3.7"
/** The list of modules that are included in the `component` directory in engine distribution.
* When invoking the `java` command, these modules need to be put on the module-path.
*/
val componentModules: Seq[ModuleID] =
GraalVM.modules ++ GraalVM.langsPkgs ++ GraalVM.toolsPkgs ++ Seq(
Upgrade enso to GraalVM for jdk 21 (#7991) Upgrade to GraalVM JDK 21. ``` > java -version openjdk version "21" 2023-09-19 OpenJDK Runtime Environment GraalVM CE 21+35.1 (build 21+35-jvmci-23.1-b15) OpenJDK 64-Bit Server VM GraalVM CE 21+35.1 (build 21+35-jvmci-23.1-b15, mixed mode, sharing) ``` With SDKMan, download with `sdk install java 21-graalce`. # Important Notes - After this PR, one can theoretically run enso with any JRE with version at least 21. - Removed `sbt bootstrap` hack and all the other build time related hacks related to the handling of GraalVM distribution. - `project-manager` remains backward compatible - it can open older engines with runtimes. New engines now do no longer require a separate runtime to be downloaded. - sbt does not support compilation of `module-info.java` files in mixed projects - https://github.com/sbt/sbt/issues/3368 - Which means that we can have `module-info.java` files only for Java-only projects. - Anyway, we need just a single `module-info.class` in the resulting `runtime.jar` fat jar. - `runtime.jar` is assembled in `runtime-with-instruments` with a custom merge strategy (`sbt-assembly` plugin). Caching is disabled for custom merge strategies, which means that re-assembly of `runtime.jar` will be more frequent. - Engine distribution contains multiple JAR archives (modules) in `component` directory, along with `runner/runner.jar` that is hidden inside a nested directory. - The new entry point to the engine runner is [EngineRunnerBootLoader](https://github.com/enso-org/enso/pull/7991/files#diff-9ab172d0566c18456472aeb95c4345f47e2db3965e77e29c11694d3a9333a2aa) that contains a custom ClassLoader - to make sure that everything that does not have to be loaded from a module is loaded from `runner.jar`, which is not a module. - The new command line for launching the engine runner is in [distribution/bin/enso](https://github.com/enso-org/enso/pull/7991/files#diff-0b66983403b2c329febc7381cd23d45871d4d555ce98dd040d4d1e879c8f3725) - [Newest version of Frgaal](https://repo1.maven.org/maven2/org/frgaal/compiler/20.0.1/) (20.0.1) does not recognize `--source 21` option, only `--source 20`.
2023-11-17 21:02:36 +03:00
"org.slf4j" % "slf4j-api" % slf4jVersion,
"ch.qos.logback" % "logback-classic" % logbackClassicVersion,
"ch.qos.logback" % "logback-core" % logbackClassicVersion
)
/** Filters modules by their IDs from the given classpath.
* @param cp The classpath to filter
* @param modules These modules are looked for in the class path
* @param shouldContainAll If true, the method will throw an exception if not all modules were found
* in the classpath.
* @return The classpath with only the provided modules searched by their IDs.
*/
def filterModulesFromClasspath(
cp: Def.Classpath,
modules: Seq[ModuleID],
log: sbt.util.Logger,
shouldContainAll: Boolean = false
): Def.Classpath = {
def shouldFilterModule(module: ModuleID): Boolean = {
modules.exists(m =>
m.organization == module.organization &&
m.name == module.name &&
m.revision == module.revision
)
}
val ret = cp.filter(dep => {
val moduleID = dep.metadata.get(AttributeKey[ModuleID]("moduleID")).get
shouldFilterModule(moduleID)
})
if (shouldContainAll) {
if (ret.size < modules.size) {
log.error("Not all modules from classpath were found")
log.error(s"Returned (${ret.size}): $ret")
log.error(s"Expected: (${modules.size}): $modules")
}
}
ret
}
def filterTruffleAndGraalArtifacts(
classPath: Def.Classpath
): Def.Classpath = {
val truffleRelatedArtifacts = classPath
.filter(file =>
file.data.getPath.contains("graalvm") || file.data.getPath.contains(
"truffle"
)
)
truffleRelatedArtifacts
}
/** There may be multiple module-info.class files comming from different
* dependencies. We care only about the one from the `runtime` project.
* The following merge strategy ensures that all other module-info.class
* files are discarded from the resulting uber Jar.
*
* @param projName Project name for which the module-info.class is retained.
*/
def removeAllModuleInfoExcept(
projName: String
): MergeStrategy = {
CustomMergeStrategy(
strategyName =
s"Discard all module-info except for module-info from project $projName",
notifyIfGTE = 1
) { conflictingDeps: Vector[Dependency] =>
val runtimeModuleInfoOpt = conflictingDeps.collectFirst {
case project @ Project(name, _, _, stream) if name == projName =>
project
}
runtimeModuleInfoOpt match {
case Some(runtimeModuleInfo) =>
Right(
Vector(
JarEntry(runtimeModuleInfo.target, runtimeModuleInfo.stream)
)
)
case None => Right(Vector())
}
}
}
/** Compiles a single `module-info.java` source file with the default java compiler (
* the one that is defined for the project). Before the module-info is compiled, all the
* class files from `scopeFilter` are copied into the `target` directory of the current project.
* This is because we want the `module-info.java` to be an *Uber module-info* that is defined for
* multiple sbt projects, like `runtime` and `runtime-with-instruments`, so before the `module-info.java`
* is passed to the compiler, we need to copy all the classes from the sbt projects into a single directory
* so that the compiler has an illusion that all these projects are in fact a single project.
*
* Note that sbt is not able to correctly handle `module-info.java` files when
* compilation order is defined to mixed order.
*
* @param copyDepsFilter The filter of scopes of the projects from which the class files are first
* copied into the `target` directory before `module-info.java` is compiled.
* @param modulePath IDs of dependencies that should be put on the module path. The modules
* put into `modulePath` are filtered away from class-path, so that module-path
* and class-path passed to the `javac` are exclusive.
*
* @see https://users.scala-lang.org/t/scala-jdk-11-and-jpms/6102/19
*/
def compileModuleInfo(
copyDepsFilter: ScopeFilter,
modulePath: Seq[ModuleID] = Seq()
): Def.Initialize[Task[Unit]] =
Def
.task {
val moduleInfo = (Compile / javaSource).value / "module-info.java"
val log = streams.value.log
val incToolOpts = IncToolOptionsUtil.defaultIncToolOptions()
val reporter = (Compile / compile / bspReporter).value
val output = CompileOutput((Compile / classDirectory).value.toPath)
// Target directory where all the classes from all the dependencies will be
// copied to.
val outputPath: Path = output.getSingleOutputAsPath
.get()
/** Copy classes into the target directory from all the dependencies */
log.debug(s"Copying classes to $output")
val sourceProducts = products.all(copyDepsFilter).value.flatten
if (!(outputPath.toFile.exists())) {
Files.createDirectory(outputPath)
}
val outputLangProvider =
outputPath / "META-INF" / "services" / "com.oracle.truffle.api.provider.TruffleLanguageProvider"
sourceProducts.foreach { sourceProduct =>
log.debug(s"Copying ${sourceProduct} to ${output}")
val sourceLangProvider =
sourceProduct / "META-INF" / "services" / "com.oracle.truffle.api.provider.TruffleLanguageProvider"
if (
outputLangProvider.toFile.exists() && sourceLangProvider.exists()
) {
log.debug(
s"Merging ${sourceLangProvider} into ${outputLangProvider}"
)
val sourceLines = IO.readLines(sourceLangProvider)
val destLines = IO.readLines(outputLangProvider.toFile)
val outLines = (sourceLines ++ destLines).distinct
IO.writeLines(outputLangProvider.toFile, outLines)
}
// Copy the rest of the directory - don't override META-INF.
IO.copyDirectory(
sourceProduct,
outputPath.toFile,
CopyOptions(
overwrite = false,
preserveLastModified = true,
preserveExecutable = true
)
)
}
log.info("Compiling module-info.java with javac")
val baseJavacOpts = (Compile / javacOptions).value
val fullCp = (Compile / fullClasspath).value
val (mp, cp) = fullCp.partition(file => {
val moduleID =
file.metadata.get(AttributeKey[ModuleID]("moduleID")).get
modulePath.exists(mod => {
mod.organization == moduleID.organization &&
mod.name == moduleID.name &&
mod.revision == moduleID.revision
})
})
val allOpts = baseJavacOpts ++ Seq(
"--class-path",
cp.map(_.data.getAbsolutePath).mkString(File.pathSeparator),
"--module-path",
mp.map(_.data.getAbsolutePath).mkString(File.pathSeparator),
"-d",
outputPath.toAbsolutePath().toString()
)
val javaCompiler =
(Compile / compile / compilers).value.javaTools.javac()
val succ = javaCompiler.run(
Array(PlainVirtualFile(moduleInfo.toPath)),
allOpts.toArray,
output,
incToolOpts,
reporter,
log
)
if (!succ) {
sys.error(s"Compilation of ${moduleInfo} failed")
}
}
.dependsOn(Compile / compile)
}