Skip to content
Draft
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
3 changes: 3 additions & 0 deletions build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,8 @@ trait Core extends ScalaCliCrossSbtModule
| def mavenAppVersion = "${Deps.Versions.mavenAppVersion}"
|
| def scalafixVersion = "${Deps.Versions.scalafix}"
|
| def slothVersion = "${Deps.Versions.sloth}"
|
| def alpineVersion = "$alpineVersion"
|}
Expand Down Expand Up @@ -744,6 +746,7 @@ trait Build extends ScalaCliCrossSbtModule
Deps.nativeTestRunner,
Deps.osLib,
Deps.pprint,
Deps.slothCore,
Deps.scalaJsEnvNodeJs,
Deps.scalaJsTestAdapter,
Deps.swoval,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package scala.build.postprocessing

import os.Path
import sloth.jar.JarProcessor

import java.io.{ByteArrayOutputStream, PrintStream}
import java.math.BigInteger
import java.security.MessageDigest

import scala.build.errors.BuildException
import scala.build.internal.Constants
import scala.build.options.BuildOptions
import scala.build.{Directories, Logger}
import scala.util.control.NonFatal

object SlothPatcher {

private val cacheDir = Directories.directories.cacheDir / "sloth"

def transformClassPath(
classPath: Seq[Path],
options: BuildOptions,
logger: Logger
): Either[BuildException, Seq[Path]] =
if options.notForBloopOptions.sloth then
Right {
// sloth logs to stdout/stderr via its own bundled logging; capture it so it
// doesn't pollute the user program's output, routing anything it prints to debug.
captureStdio(logger) {
classPath.map { entry =>
if entry.ext == "jar" then patchJar(entry, logger)
else entry
}
}
}
else Right(classPath)

private def captureStdio[T](logger: Logger)(f: => T): T = {
val outBuffer = new ByteArrayOutputStream
val errBuffer = new ByteArrayOutputStream
val originalOut = System.out
val originalErr = System.err
System.setOut(new PrintStream(outBuffer, true))
System.setErr(new PrintStream(errBuffer, true))
try f
finally {
System.setOut(originalOut)
System.setErr(originalErr)
val captured = (outBuffer.toString ++ errBuffer.toString).trim
if captured.nonEmpty then logger.debug(captured)
}
}

private def patchJar(jar: Path, logger: Logger): Path =
val cachedDir = cacheDir / Constants.slothVersion / sha1(jar)
val cached = cachedDir / jar.last
if os.exists(cached) then cached
else
os.makeDir.all(cachedDir)
runJarProcessor(jar, cached) match
case Right(()) =>
logger.debug(s"Patched lazy vals in $jar -> $cached")
cached
case Left(message) =>
os.remove.all(cachedDir)
logger.message(s"Could not patch lazy vals in $jar, using original: $message")
jar

private def runJarProcessor(
input: Path,
output: Path
): Either[String, Unit] =
try
val result = JarProcessor.process(input.toNIO, output.toNIO)
if result.failedClasses > 0 then
val details = if result.errors.nonEmpty then s": ${result.errors.mkString("; ")}" else ""
Left(s"Failed to patch lazy vals in $input ($result.failedClasses failed classes)$details")
else Right(())
catch
case NonFatal(e) =>
Left(s"Failed to patch lazy vals in $input: ${e.getMessage}")

private def sha1(path: Path): String =
val md = MessageDigest.getInstance("SHA-1")
md.update(os.read.bytes(path))
String.format("%040x", new BigInteger(1, md.digest()))
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ object DirectivesPreprocessingUtils {
directives.ScalaNative.handler,
directives.ScalaVersion.handler,
directives.Sources.handler,
directives.Sloth.handler,
directives.Watching.handler,
directives.Tests.handler,
directives.Wasm.handler
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -518,4 +518,16 @@ class DirectiveTests extends TestUtil.ScalaCliBuildSuite {
(_, _, maybeBuild) => expect(maybeBuild.exists(_.success))
}
}

test("sloth directive") {
val testInputs = TestInputs(
os.rel / "simple.sc" ->
"""//> using sloth
|""".stripMargin
)
testInputs.withBuild(baseOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) =>
val build = maybeBuild.orThrow
expect(build.options.notForBloopOptions.slothOpt.contains(true))
}
}
}
23 changes: 16 additions & 7 deletions modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import scala.build.input.Inputs
import scala.build.internal.{Constants, Runner}
import scala.build.options.ScalacOpt.{filterScalacOptionKeys, noDashPrefixes}
import scala.build.options.{BuildOptions, JavaOpt, Scope}
import scala.build.postprocessing.SlothPatcher
import scala.cli.CurrentParams
import scala.cli.commands.run.Run.{createPythonInstance, orPythonDetectionError, pythonPathEnv}
import scala.cli.commands.run.RunMode
Expand Down Expand Up @@ -346,6 +347,11 @@ object Repl extends ScalaCommand[ReplOptions] with BuildCommandHelpers {
val additionalArgs =
pythonReplArgs ++ options.scalaOptions.scalacOptions.toSeq.map(_.value.value)

val javaCommand = options.javaHome().value.javaCommand

def patchClassPath(classPath: Seq[os.Path]): Seq[os.Path] =
value(SlothPatcher.transformClassPath(classPath, options, logger))

def maybeRunRepl(
replArtifacts: ReplArtifacts,
replArgs: Seq[String],
Expand All @@ -354,22 +360,23 @@ object Repl extends ScalaCommand[ReplOptions] with BuildCommandHelpers {
): Unit = {
if dryRun then logger.message("Dry run, not running REPL.")
else {
val depClassPath = patchClassPath(mainJarsOrClassDirs ++ replArtifacts.depsClassPath)
val replClassPath = patchClassPath(replArtifacts.replClassPath)
val depClassPathArgs: Seq[String] =
if mainJarsOrClassDirs.nonEmpty || replArtifacts.depsClassPath.nonEmpty
if depClassPath.nonEmpty
then
Seq(
"-classpath",
(mainJarsOrClassDirs ++ replArtifacts.depsClassPath)
.map(_.toString).mkString(File.pathSeparator)
depClassPath.map(_.toString).mkString(File.pathSeparator)
)
else Nil
val retCode = Runner.runJvm(
javaCommand = options.javaHome().value.javaCommand,
javaCommand = javaCommand,
javaArgs = scalapyJavaOpts ++
replArtifacts.replJavaOpts ++
options.javaOptions.javaOpts.toSeq.map(_.value.value) ++
extraProps.toVector.sorted.map { case (k, v) => s"-D$k=$v" },
classPath = replArtifacts.replClassPath,
classPath = replClassPath,
mainClass = replArtifacts.replMainClass,
args = maybeAdaptForWindows(depClassPathArgs ++ replArgs),
logger = logger,
Expand Down Expand Up @@ -398,12 +405,14 @@ object Repl extends ScalaCommand[ReplOptions] with BuildCommandHelpers {
}
}
if shouldUseJshell then
val javaHomeInfo = options.javaHome().value
val javaHomeInfo = options.javaHome().value
val jshellClassPath =
patchClassPath(mainJarsOrClassDirs ++ allArtifacts.flatMap(_.classPath).distinct)
val jshellCommand0 = value(
JShellRunner.commandFor(
javaHomeInfo = javaHomeInfo,
javaOpts = scalapyJavaOpts ++ options.javaOptions.javaOpts.toSeq.map(_.value.value),
classPath = mainJarsOrClassDirs ++ allArtifacts.flatMap(_.classPath).distinct,
classPath = jshellClassPath,
programArgs = programArgs,
initScriptOpt = initScriptOpt,
quitAfterInit = quitAfterInit,
Expand Down
13 changes: 11 additions & 2 deletions modules/cli/src/main/scala/scala/cli/commands/run/Run.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import scala.build.internals.ConsoleUtils.ScalaCliConsole
import scala.build.internals.ConsoleUtils.ScalaCliConsole.warnPrefix
import scala.build.internals.EnvVar
import scala.build.options.{BuildOptions, JSRuntime, JavaOpt, PackageType, Platform, Scope}
import scala.build.postprocessing.SlothPatcher
import scala.cli.CurrentParams
import scala.cli.commands.package0.Package
import scala.cli.commands.setupide.SetupIde
Expand Down Expand Up @@ -689,6 +690,14 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
++ Seq(s"-Dscala.sources=$sources", s"-Dscala.source.names=$sourceNames")
val setupPython =
build.options.notForBloopOptions.doSetupPython.getOrElse(false)
val classPath0 = builds.flatMap(_.fullClassPathMaybeAsJar(asJar)).distinct
val classPath = value(
SlothPatcher.transformClassPath(
classPath0,
build.options,
logger
)
)
val (pythonJavaProps, pythonExtraEnv) =
if setupPython then {
val scalapyProps = value {
Expand All @@ -713,7 +722,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
Runner.jvmCommand(
build.options.javaHome().value.javaCommand,
allJavaOpts,
builds.flatMap(_.fullClassPathMaybeAsJar(asJar)).distinct,
classPath,
mainClass,
args,
extraEnv = pythonExtraEnv,
Expand All @@ -725,7 +734,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
val proc = Runner.runJvm(
javaCommand = build.options.javaHome().value.javaCommand,
javaArgs = allJavaOpts,
classPath = builds.flatMap(_.fullClassPathMaybeAsJar(asJar)).distinct,
classPath = classPath,
mainClass = mainClass,
args = args,
logger = logger,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,11 @@ final case class SharedOptions(
@HelpMessage("Force object wrapper for scripts")
@Tag(tags.experimental)
objectWrapper: Option[Boolean] = None,
@HelpMessage("Patch Scala 3.0-3.7.x lazy val bytecode on the classpath for JDK 26+ compatibility")
@Name("lazyvalgrade")
Comment thread
Gedochao marked this conversation as resolved.
@Name("patch-lazy-vals")
@Tag(tags.experimental)
sloth: Option[Boolean] = None,
@Group(HelpGroup.Scala.toString)
@HelpMessage(
"Automatically generate BSP configuration in `.bsp/` when running build commands. Enabled by default."
Expand Down Expand Up @@ -490,7 +495,8 @@ final case class SharedOptions(
addRunnerDependencyOpt = runner,
python = sharedPython.python,
pythonSetup = sharedPython.pythonSetup,
scalaPyVersion = sharedPython.scalaPyVersion
scalaPyVersion = sharedPython.scalaPyVersion,
slothOpt = sloth
),
useBuildServer = compilationServer.server
).orElse(watchOptions.buildOptions())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,14 @@ class RunOptionsTests extends munit.FunSuite {
expect(toolkitDep.name == "toolkit")
expect(toolkitDep.version == "latest.release")
}

test("sloth option") {
val runOptions = RunOptions(
shared = SharedOptions(
sloth = Some(true)
)
)
val buildOptions = Run.buildOptions(runOptions).value
expect(buildOptions.notForBloopOptions.slothOpt.contains(true))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package scala.build.preprocessing.directives

import scala.build.directives.*
import scala.build.errors.BuildException
import scala.build.options.{BuildOptions, PostBuildOptions}
import scala.cli.commands.SpecificationLevel

@DirectiveExamples("//> using sloth")
@DirectiveUsage("//> using sloth", "`//> using sloth`")
@DirectiveDescription(
"Patch Scala 3.0-3.7.x lazy val bytecode on the classpath for JDK 26+ compatibility"
)
@DirectiveLevel(SpecificationLevel.EXPERIMENTAL)
final case class Sloth(
@DirectiveName("lazyvalgrade")
Comment thread
Gedochao marked this conversation as resolved.
@DirectiveName("lazyValPatching")
sloth: Boolean = false
) extends HasBuildOptions {
def buildOptions: Either[BuildException, BuildOptions] =
Right(BuildOptions(
notForBloopOptions = PostBuildOptions(slothOpt = Some(true))
))
}

object Sloth {
val handler: DirectiveHandler[Sloth] = DirectiveHandler.derive
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package scala.cli.integration

trait LazyValTests {
private val lazyValsLibOrg = "test.lazyvals"
private val lazyValsLibName = "lazyvals-lib"
private val lazyValsLibVersion = "0.1.0"
private val lazyValsLibDep = s"$lazyValsLibOrg::$lazyValsLibName:$lazyValsLibVersion"

protected def publishLazyValsLib(
scalaVersion: String,
workspace: os.Path
): (String, os.Path) = {
val libDir = workspace / "lazyvals-lib"
val repoDir = workspace / "test-repo"
os.write(
libDir / "LazyValsLib.scala",
"""package lazyvalslib
|object LazyValsLib {
| lazy val greeting: String = "Hello"
|}
|""".stripMargin,
createFolders = true
)
os.proc(
TestUtil.cli,
"--power",
"publish",
libDir,
"--organization",
lazyValsLibOrg,
"--name",
lazyValsLibName,
"--project-version",
lazyValsLibVersion,
"--scala",
scalaVersion,
"--publish-repo",
repoDir.toNIO.toUri.toASCIIString
).call(cwd = workspace, stdin = os.Inherit, stdout = os.Inherit)
os.remove.all(libDir)
(lazyValsLibDep, repoDir)
}
}
Loading
Loading