Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 1 addition & 34 deletions build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -55,38 +55,6 @@ object cli extends CommonScalaModule with NativeImageModule {
def moduleDeps = Seq(lib)
def mainClass = Some("cellar.cli.CellarApp")

def bundledJreJar: T[PathRef] = Task {
import java.net.URI
import java.nio.file.{Files, FileSystems}
import java.util.jar.{JarEntry, JarOutputStream}
import scala.jdk.CollectionConverters.*

val dest = Task.dest / "jre.jar"
val fs = FileSystems.getFileSystem(URI.create("jrt:/"))
val jos = new JarOutputStream(java.io.FileOutputStream(dest.toIO))
val seen = scala.collection.mutable.Set[String]()

Files.list(fs.getPath("modules")).iterator().asScala.toList.foreach { modPath =>
Files.walk(modPath).iterator().asScala
.filterNot(Files.isDirectory(_))
.foreach { p =>
val name = p.getFileName.toString
if (name.endsWith(".class") || name.endsWith(".tasty")) && name != "module-info.class" then
val rel = modPath.relativize(p)
val entryName = (0 until rel.getNameCount).map(i => rel.getName(i).toString).mkString("/")
if !seen(entryName) then
seen += entryName
jos.putNextEntry(new JarEntry(entryName))
jos.write(Files.readAllBytes(p))
jos.closeEntry()
}
}
jos.close()
PathRef(Task.dest)
}

override def resources = super.resources() ++ Seq(bundledJreJar())

def cellarVersion: T[String] = Task.Input {
sys.env.getOrElse("CELLAR_VERSION", "0.1.0-SNAPSHOT").stripPrefix("v")
}
Expand Down Expand Up @@ -124,8 +92,7 @@ object cli extends CommonScalaModule with NativeImageModule {
Assembly.Rule.ExcludePattern("META-INF/.*\\.SF"),
Assembly.Rule.ExcludePattern("META-INF/.*\\.DSA"),
Assembly.Rule.ExcludePattern("META-INF/.*\\.RSA"),
Assembly.Rule.ExcludePattern("module-info.class"),
Assembly.Rule.ExcludePattern("jre.jar")
Assembly.Rule.ExcludePattern("module-info.class")
) ++ Assembly.defaultRules

object test extends ScalaTests with TestModule.Munit {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
"includes": [
{"pattern": "\\Qcoursier/coursier.properties\\E"},
{"pattern": "\\Qcoursier/launcher/coursier.properties\\E"},
{"pattern": "\\Qlibrary.properties\\E"},
{"pattern": "\\Qjre.jar\\E"}
{"pattern": "\\Qlibrary.properties\\E"}
]
}
}
107 changes: 24 additions & 83 deletions lib/src/cellar/JreClasspath.scala
Original file line number Diff line number Diff line change
@@ -1,99 +1,40 @@
package cellar

import cats.effect.IO
import java.io.ByteArrayInputStream
import java.net.{URI, URLClassLoader}
import java.nio.file.{Files, FileSystems, Path}
import java.util.zip.ZipInputStream
import scala.collection.mutable
import scala.jdk.CollectionConverters.*
import tastyquery.Classpaths
import tastyquery.Classpaths.InMemory
import tastyquery.jdk.ClasspathLoaders

object JreClasspath:
// True when running as a GraalVM native image binary
private val isNativeImage: Boolean =
sys.props.get("org.graalvm.nativeimage.imagecode").contains("runtime")

def jrtPath(): IO[Classpaths.Classpath] =
if isNativeImage then loadBundledJre()
else
sys.env.get("JAVA_HOME") match
case Some(h) => jrtPath(Path.of(h))
case None =>
IO.raiseError(new RuntimeException(
"Could not locate JRE classpath. Set JAVA_HOME or pass --java-home pointing to a JDK installation."
))
sys.env.get("JAVA_HOME") match
case Some(h) => jrtPath(Path.of(h))
case None =>
IO.raiseError(new RuntimeException(
"Could not locate JRE classpath. Set JAVA_HOME or pass --java-home pointing to a JDK installation."
))

def jrtPath(javaHome: Path): IO[Classpaths.Classpath] =
if isNativeImage then loadBundledJre()
else
IO.blocking {
val jrtFsJar = javaHome.resolve("lib/jrt-fs.jar")
if !Files.exists(jrtFsJar) then
throw new IllegalArgumentException(
s"Not a valid JDK home (missing lib/jrt-fs.jar): $javaHome"
)
val env = java.util.Map.of("java.home", javaHome.toString)
val fs =
try
// Bypasses GraalVM's FileSystems substitution in native image
val cls = Class.forName("jdk.internal.jrtfs.JrtFileSystemProvider")
val ctor = cls.getDeclaredConstructor()
ctor.setAccessible(true)
val provider = ctor.newInstance().asInstanceOf[java.nio.file.spi.FileSystemProvider]
provider.newFileSystem(URI.create("jrt:/"), env)
catch case _: java.lang.reflect.InaccessibleObjectException =>
// JVM without --add-opens: fall back to URLClassLoader
val cl = new URLClassLoader(Array(jrtFsJar.toUri.toURL))
FileSystems.newFileSystem(URI.create("jrt:/"), env, cl)
ClasspathLoaders.read(Files.list(fs.getPath("modules")).iterator().asScala.toList)
}

private def loadBundledJre(): IO[Classpaths.Classpath] =
IO.blocking {
val stream = Thread.currentThread().getContextClassLoader.getResourceAsStream("jre.jar")
if stream == null then
throw new RuntimeException(
"Bundled JRE not found. This is a build error — please report it."
val jrtFsJar = javaHome.resolve("lib/jrt-fs.jar")
if !Files.exists(jrtFsJar) then
throw new IllegalArgumentException(
s"Not a valid JDK home (missing lib/jrt-fs.jar): $javaHome"
)
try parseJarToClasspath(stream.readAllBytes())
finally stream.close()
}

private def parseJarToClasspath(jarBytes: Array[Byte]): Classpaths.Classpath =
// package dotSeparatedName -> (binaryName -> (classBytes, tastyBytes))
val pkgMap = mutable.LinkedHashMap[String, mutable.LinkedHashMap[String, (Option[IArray[Byte]], Option[IArray[Byte]])]]()

val zis = new ZipInputStream(new ByteArrayInputStream(jarBytes))
var entry = zis.getNextEntry()
while entry != null do
val name = entry.getName
if !entry.isDirectory then
val isClass = name.endsWith(".class")
val isTasty = name.endsWith(".tasty")
if isClass || isTasty then
val bytes = IArray.unsafeFromArray(zis.readAllBytes())
val base = name.stripSuffix(".class").stripSuffix(".tasty")
val lastSlash = base.lastIndexOf('/')
val (pkg, simpleName) =
if lastSlash < 0 then ("", base)
else (base.substring(0, lastSlash).replace('/', '.'), base.substring(lastSlash + 1))
if simpleName != "module-info" then
val classMap = pkgMap.getOrElseUpdate(pkg, mutable.LinkedHashMap())
val (existingClass, existingTasty) = classMap.getOrElse(simpleName, (None, None))
classMap(simpleName) =
if isClass then (Some(bytes), existingTasty)
else (existingClass, Some(bytes))
entry = zis.getNextEntry()
zis.close()

val packageDatas = pkgMap.toList.map { (pkgName, classMap) =>
val classDatas = classMap.toList.map { case (simpleName, (classBytes, tastyBytes)) =>
val debug = if pkgName.isEmpty then simpleName else s"$pkgName.$simpleName"
InMemory.ClassData(debug, simpleName, tastyBytes, classBytes)
}
InMemory.PackageData(pkgName, pkgName, classDatas)
val env = java.util.Map.of("java.home", javaHome.toString)
val fs =
try
// Bypasses GraalVM's FileSystems substitution in native image
val cls = Class.forName("jdk.internal.jrtfs.JrtFileSystemProvider")
val ctor = cls.getDeclaredConstructor()
ctor.setAccessible(true)
val provider = ctor.newInstance().asInstanceOf[java.nio.file.spi.FileSystemProvider]
provider.newFileSystem(URI.create("jrt:/"), env)
catch case _: java.lang.reflect.InaccessibleObjectException =>
// JVM without --add-opens: fall back to URLClassLoader
val cl = new URLClassLoader(Array(jrtFsJar.toUri.toURL))
FileSystems.newFileSystem(URI.create("jrt:/"), env, cl)
ClasspathLoaders.read(Files.list(fs.getPath("modules")).iterator().asScala.toList)
}
List(InMemory.ClasspathEntry("bundled-jre", packageDatas))