From b17923513fa8b4467f6457092a11e9e10bed5d96 Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Wed, 24 Jun 2026 13:48:49 +0200 Subject: [PATCH 1/6] sun.misc.Unsafe tests --- .../scala/cli/integration/LazyValTests.scala | 43 ++++++++++++++++ .../cli/integration/ReplTestDefinitions.scala | 50 ++++++++++++++++++- .../cli/integration/RunTestDefinitions.scala | 38 +++++++++++++- 3 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 modules/integration/src/test/scala/scala/cli/integration/LazyValTests.scala diff --git a/modules/integration/src/test/scala/scala/cli/integration/LazyValTests.scala b/modules/integration/src/test/scala/scala/cli/integration/LazyValTests.scala new file mode 100644 index 0000000000..b423a88586 --- /dev/null +++ b/modules/integration/src/test/scala/scala/cli/integration/LazyValTests.scala @@ -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) + } +} diff --git a/modules/integration/src/test/scala/scala/cli/integration/ReplTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/ReplTestDefinitions.scala index 735e09c038..76dabbbb11 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/ReplTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/ReplTestDefinitions.scala @@ -5,7 +5,8 @@ import com.eed3si9n.expecty.Expecty.expect import scala.cli.integration.TestUtil.removeAnsiColors import scala.util.Properties -abstract class ReplTestDefinitions extends ScalaCliSuite with TestScalaVersionArgs { +abstract class ReplTestDefinitions extends ScalaCliSuite with TestScalaVersionArgs + with LazyValTests { this: TestScalaVersion => protected lazy val extraOptions: Seq[String] = scalaVersionArgs ++ TestUtil.extraOptions @@ -334,6 +335,53 @@ abstract class ReplTestDefinitions extends ScalaCliSuite with TestScalaVersionAr ) } + if isScala38OrNewer then { + val latestJava = Constants.allJavaVersions.max.toString + test(s"$runInReplPrefix dont warn about sun.misc.Unsafe on JDK $latestJava (no dependency)") { + val expectedMessage = "Hello" + val code = s"""println("$expectedMessage")""" + TestInputs.empty.fromRoot { root => + val res = os.proc( + TestUtil.cli, + "repl", + "--repl-quit-after-init", + "--repl-init-script", + code, + "--jvm", + latestJava, + extraOptions + ).call(cwd = root, stderr = os.Pipe) + expect(res.out.trim().contains(expectedMessage)) + expect(!res.err.trim().contains("sun.misc.Unsafe")) + } + } + + test(s"$runInReplPrefix 3.3 lazy vals dont warn about sun.misc.Unsafe on JDK $latestJava") { + val expectedMessage = "Hello" + val code = "println(lazyvalslib.LazyValsLib.greeting)" + TestInputs.empty.fromRoot { root => + val (dep, repoDir) = publishLazyValsLib(Constants.scala3Lts, root) + val res = os.proc( + TestUtil.cli, + "repl", + ".", + "--repl-quit-after-init", + "--repl-init-script", + code, + extraOptions, + "--dep", + dep, + "--repository", + repoDir.toNIO.toUri.toASCIIString, + "--jvm", + latestJava + ).call(cwd = root, stderr = os.Pipe) + expect(res.out.trim().contains(expectedMessage)) + expect(!res.err.trim().contains("sun.misc.Unsafe")) + } + } + } + if !isScala38OrNewer then // TODO rewrite this test to work with Scala 3.8+ once 3.8.0 stable is out test(s"$runInReplPrefix as jar") { diff --git a/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala index 295bdd5c87..2c5edfaa28 100755 --- a/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala @@ -23,7 +23,8 @@ abstract class RunTestDefinitions with RunScalaPyTestDefinitions with RunZipTestDefinitions with RunJdkTestDefinitions - with CoursierScalaInstallationTestHelper { this: TestScalaVersion => + with CoursierScalaInstallationTestHelper + with LazyValTests { this: TestScalaVersion => protected lazy val extraOptions: Seq[String] = scalaVersionArgs ++ TestUtil.extraOptions protected val emptyInputs: TestInputs = TestInputs(os.rel / ".placeholder" -> "") @@ -2568,4 +2569,39 @@ abstract class RunTestDefinitions expect(output == message) } } + + if isScala38OrNewer then { + val latestJava = Constants.allJavaVersions.max + + def lazyValsUnsafeTest(libScalaVersion: String): Unit = + test(s"$libScalaVersion lazy vals dont warn about sun.misc.Unsafe on JDK $latestJava") { + val expectedMessage = "Hello" + TestInputs.empty.fromRoot { root => + val (dep, repoDir) = publishLazyValsLib(libScalaVersion, root) + os.write( + root / "script.sc", + s"""//> using dep $dep + |println(lazyvalslib.LazyValsLib.greeting) + |""".stripMargin + ) + val r = os.proc( + TestUtil.cli, + extraOptions, + "script.sc", + "--repository", + repoDir.toNIO.toUri.toASCIIString, + "--jvm", + latestJava + ).call(cwd = root, stderr = os.Pipe) + expect(r.out.trim() == expectedMessage) + expect(!r.err.trim().contains("sun.misc.Unsafe")) + } + } + + val highest30 = Constants.legacyScala3Versions + .filter(_.startsWith("3.0.")) + .maxBy(_.coursierVersion) + lazyValsUnsafeTest(highest30) + lazyValsUnsafeTest(Constants.scala3Lts) + } } From 0561d03cf40498eb703ae0b705bdd786617de125 Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Wed, 24 Jun 2026 15:37:53 +0200 Subject: [PATCH 2/6] Add lazyvalgrade support --- build.mill | 4 + .../postprocessing/LazyValGradePatcher.scala | 121 ++++++++++++++++++ .../DirectivesPreprocessingUtils.scala | 1 + .../scala/build/tests/DirectiveTests.scala | 12 ++ .../scala/scala/cli/commands/repl/Repl.scala | 23 +++- .../scala/scala/cli/commands/run/Run.scala | 14 +- .../cli/commands/shared/SharedOptions.scala | 6 +- .../cli/commands/tests/RunOptionsTests.scala | 10 ++ .../directives/LazyValGrade.scala | 26 ++++ .../cli/integration/ReplTestDefinitions.scala | 10 +- .../cli/integration/RunTestDefinitions.scala | 2 + .../build/options/PostBuildOptions.scala | 3 +- project/deps/package.mill | 4 + website/docs/reference/cli-options.md | 4 + website/docs/reference/directives.md | 9 ++ 15 files changed, 236 insertions(+), 13 deletions(-) create mode 100644 modules/build/src/main/scala/scala/build/postprocessing/LazyValGradePatcher.scala create mode 100644 modules/directives/src/main/scala/scala/build/preprocessing/directives/LazyValGrade.scala diff --git a/build.mill b/build.mill index 23a0fbea24..708041f707 100644 --- a/build.mill +++ b/build.mill @@ -587,6 +587,10 @@ trait Core extends ScalaCliCrossSbtModule | def mavenAppVersion = "${Deps.Versions.mavenAppVersion}" | | def scalafixVersion = "${Deps.Versions.scalafix}" + | + | def lazyvalgradeOrganization = "${Deps.lazyvalgradeOrganization}" + | def lazyvalgradeModuleName = "${Deps.lazyvalgradeModuleName}" + | def lazyvalgradeVersion = "${Deps.Versions.lazyvalgrade}" | | def alpineVersion = "$alpineVersion" |} diff --git a/modules/build/src/main/scala/scala/build/postprocessing/LazyValGradePatcher.scala b/modules/build/src/main/scala/scala/build/postprocessing/LazyValGradePatcher.scala new file mode 100644 index 0000000000..3c8bb61a0a --- /dev/null +++ b/modules/build/src/main/scala/scala/build/postprocessing/LazyValGradePatcher.scala @@ -0,0 +1,121 @@ +package scala.build.postprocessing + +import coursier.LocalRepositories +import coursier.cache.FileCache +import dependency.* +import os.Path + +import java.io.File +import java.security.MessageDigest + +import scala.build.EitherCps.{either, value} +import scala.build.errors.BuildException +import scala.build.internal.Constants +import scala.build.internal.CsLoggerUtil.* +import scala.build.options.BuildOptions +import scala.build.{Artifacts, Directories, Logger, Positioned} + +object LazyValGradePatcher { + + private val cacheDir = Directories.directories.cacheDir / "lazyvalgrade" + + def transformClassPath( + classPath: Seq[Path], + options: BuildOptions, + javaCommand: String, + logger: Logger + ): Either[BuildException, Seq[Path]] = + if options.notForBloopOptions.lazyValGradeOpt.contains(true) then + either { + val toolClassPath = value(fetchToolClassPath(options, logger)) + value(patchedClassPath(classPath, javaCommand, toolClassPath, logger)) + } + else Right(classPath) + + private def fetchToolClassPath( + options: BuildOptions, + logger: Logger + ): Either[BuildException, Seq[Path]] = + either { + val cache = options.internal.cache.getOrElse(FileCache()) + val repositories = value(options.finalRepositories) ++ Seq(LocalRepositories.ivy2Local) + val scalaParams = value(options.scalaParams).getOrElse( + ScalaParameters(Constants.defaultScalaVersion) + ) + val lazyValGradeDeps = + Seq( + dep"${Constants.lazyvalgradeOrganization}::${Constants.lazyvalgradeModuleName}:${Constants.lazyvalgradeVersion}" + ) + val artifacts = value( + Artifacts.artifacts( + lazyValGradeDeps.map(Positioned.none), + repositories, + Some(scalaParams), + logger, + cache.withMessage(s"Downloading lazyvalgrade ${Constants.lazyvalgradeVersion}") + ) + ) + artifacts.map(_._2) + } + + private def patchedClassPath( + classPath: Seq[Path], + javaCommand: String, + toolClassPath: Seq[Path], + logger: Logger + ): Either[BuildException, Seq[Path]] = + either { + val toolCp = toolClassPath.map(_.toString).mkString(File.pathSeparator) + classPath.map { entry => + if entry.ext == "jar" then value(patchJar(entry, javaCommand, toolCp, logger)) + else entry + } + } + + private def patchJar( + jar: Path, + javaCommand: String, + toolClassPath: String, + logger: Logger + ): Either[BuildException, Path] = + either { + val digest = sha1(jar) + val cachedDir = cacheDir / Constants.lazyvalgradeVersion / digest + val cached = cachedDir / jar.last + if os.exists(cached) then cached + else { + os.makeDir.all(cachedDir) + os.copy(jar, cached, replaceExisting = true, createFolders = true) + val exitCode = os.proc( + javaCommand, + "-cp", + toolClassPath, + "lazyvalgrade.cli.Main", + cached.toString + ).call( + stdout = os.Pipe, + stderr = os.Pipe, + check = false + ).exitCode + if exitCode != 0 then + value( + Left(new BuildException(s"Failed to patch lazy vals in $jar (exit code $exitCode)") {}) + ) + logger.debug(s"Patched lazy vals in $jar -> $cached") + cached + } + } + + private def sha1(path: Path): String = { + val md = MessageDigest.getInstance("SHA-1") + val buffer = new Array[Byte](8192) + val stream = os.read.inputStream(path) + try + var read = stream.read(buffer) + while read >= 0 do + md.update(buffer, 0, read) + read = stream.read(buffer) + finally stream.close() + md.digest().map("%02x".format(_)).mkString + } +} diff --git a/modules/build/src/main/scala/scala/build/preprocessing/directives/DirectivesPreprocessingUtils.scala b/modules/build/src/main/scala/scala/build/preprocessing/directives/DirectivesPreprocessingUtils.scala index dfacd593fa..586017b0bc 100644 --- a/modules/build/src/main/scala/scala/build/preprocessing/directives/DirectivesPreprocessingUtils.scala +++ b/modules/build/src/main/scala/scala/build/preprocessing/directives/DirectivesPreprocessingUtils.scala @@ -17,6 +17,7 @@ object DirectivesPreprocessingUtils { directives.Exclude.handler, directives.JavaHome.handler, directives.Jvm.handler, + directives.LazyValGrade.handler, directives.MainClass.handler, directives.ObjectWrapper.handler, directives.Packaging.handler, diff --git a/modules/build/src/test/scala/scala/build/tests/DirectiveTests.scala b/modules/build/src/test/scala/scala/build/tests/DirectiveTests.scala index c9d233f834..a361d6de2a 100644 --- a/modules/build/src/test/scala/scala/build/tests/DirectiveTests.scala +++ b/modules/build/src/test/scala/scala/build/tests/DirectiveTests.scala @@ -518,4 +518,16 @@ class DirectiveTests extends TestUtil.ScalaCliBuildSuite { (_, _, maybeBuild) => expect(maybeBuild.exists(_.success)) } } + + test("lazyvalgrade directive") { + val testInputs = TestInputs( + os.rel / "simple.sc" -> + """//> using lazyvalgrade + |""".stripMargin + ) + testInputs.withBuild(baseOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => + val build = maybeBuild.orThrow + expect(build.options.notForBloopOptions.lazyValGradeOpt.contains(true)) + } + } } diff --git a/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala b/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala index 99041df486..cc4ff128a8 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala @@ -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.LazyValGradePatcher import scala.cli.CurrentParams import scala.cli.commands.run.Run.{createPythonInstance, orPythonDetectionError, pythonPathEnv} import scala.cli.commands.run.RunMode @@ -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(LazyValGradePatcher.transformClassPath(classPath, options, javaCommand, logger)) + def maybeRunRepl( replArtifacts: ReplArtifacts, replArgs: Seq[String], @@ -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, @@ -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, diff --git a/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala b/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala index d63ce9f89e..cc41d34468 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala @@ -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.LazyValGradePatcher import scala.cli.CurrentParams import scala.cli.commands.package0.Package import scala.cli.commands.setupide.SetupIde @@ -689,6 +690,15 @@ 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( + LazyValGradePatcher.transformClassPath( + classPath0, + build.options, + build.options.javaHome().value.javaCommand, + logger + ) + ) val (pythonJavaProps, pythonExtraEnv) = if setupPython then { val scalapyProps = value { @@ -713,7 +723,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, @@ -725,7 +735,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, diff --git a/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala index 2db2f5e40f..a6ac4a49f9 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala @@ -210,6 +210,9 @@ 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") + @Tag(tags.experimental) + lazyvalgrade: Option[Boolean] = None, @Group(HelpGroup.Scala.toString) @HelpMessage( "Automatically generate BSP configuration in `.bsp/` when running build commands. Enabled by default." @@ -490,7 +493,8 @@ final case class SharedOptions( addRunnerDependencyOpt = runner, python = sharedPython.python, pythonSetup = sharedPython.pythonSetup, - scalaPyVersion = sharedPython.scalaPyVersion + scalaPyVersion = sharedPython.scalaPyVersion, + lazyValGradeOpt = lazyvalgrade ), useBuildServer = compilationServer.server ).orElse(watchOptions.buildOptions()) diff --git a/modules/cli/src/test/scala/cli/commands/tests/RunOptionsTests.scala b/modules/cli/src/test/scala/cli/commands/tests/RunOptionsTests.scala index 6aaa38a03b..17347349bf 100644 --- a/modules/cli/src/test/scala/cli/commands/tests/RunOptionsTests.scala +++ b/modules/cli/src/test/scala/cli/commands/tests/RunOptionsTests.scala @@ -50,4 +50,14 @@ class RunOptionsTests extends munit.FunSuite { expect(toolkitDep.name == "toolkit") expect(toolkitDep.version == "latest.release") } + + test("lazyvalgrade option") { + val runOptions = RunOptions( + shared = SharedOptions( + lazyvalgrade = Some(true) + ) + ) + val buildOptions = Run.buildOptions(runOptions).value + expect(buildOptions.notForBloopOptions.lazyValGradeOpt.contains(true)) + } } diff --git a/modules/directives/src/main/scala/scala/build/preprocessing/directives/LazyValGrade.scala b/modules/directives/src/main/scala/scala/build/preprocessing/directives/LazyValGrade.scala new file mode 100644 index 0000000000..c6cd068df3 --- /dev/null +++ b/modules/directives/src/main/scala/scala/build/preprocessing/directives/LazyValGrade.scala @@ -0,0 +1,26 @@ +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 lazyvalgrade") +@DirectiveUsage("//> using lazyvalgrade", "`//> using lazyvalgrade`") +@DirectiveDescription( + "Patch Scala 3.0-3.7.x lazy val bytecode on the classpath for JDK 26+ compatibility" +) +@DirectiveLevel(SpecificationLevel.EXPERIMENTAL) +final case class LazyValGrade( + @DirectiveName("lazy.val.grade") + lazyvalgrade: Boolean = false +) extends HasBuildOptions { + def buildOptions: Either[BuildException, BuildOptions] = + Right(BuildOptions( + notForBloopOptions = PostBuildOptions(lazyValGradeOpt = Some(true)) + )) +} + +object LazyValGrade { + val handler: DirectiveHandler[LazyValGrade] = DirectiveHandler.derive +} diff --git a/modules/integration/src/test/scala/scala/cli/integration/ReplTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/ReplTestDefinitions.scala index 76dabbbb11..7ed760f50f 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/ReplTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/ReplTestDefinitions.scala @@ -337,9 +337,11 @@ abstract class ReplTestDefinitions extends ScalaCliSuite with TestScalaVersionAr if isScala38OrNewer then { val latestJava = Constants.allJavaVersions.max.toString - test(s"$runInReplPrefix dont warn about sun.misc.Unsafe on JDK $latestJava (no dependency)") { + test( + s"$runInReplPrefix dont warn about sun.misc.Unsafe on JDK $latestJava (no dependency)" + ) { val expectedMessage = "Hello" - val code = s"""println("$expectedMessage")""" + val code = s"""println("$expectedMessage")""" TestInputs.empty.fromRoot { root => val res = os.proc( TestUtil.cli, @@ -349,6 +351,8 @@ abstract class ReplTestDefinitions extends ScalaCliSuite with TestScalaVersionAr code, "--jvm", latestJava, + "--power", + "--lazyvalgrade", extraOptions ).call(cwd = root, stderr = os.Pipe) expect(res.out.trim().contains(expectedMessage)) @@ -369,6 +373,8 @@ abstract class ReplTestDefinitions extends ScalaCliSuite with TestScalaVersionAr "--repl-init-script", code, extraOptions, + "--power", + "--lazyvalgrade", "--dep", dep, "--repository", diff --git a/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala index 2c5edfaa28..b588b12a22 100755 --- a/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala @@ -2587,6 +2587,8 @@ abstract class RunTestDefinitions val r = os.proc( TestUtil.cli, extraOptions, + "--power", + "--lazyvalgrade", "script.sc", "--repository", repoDir.toNIO.toUri.toASCIIString, diff --git a/modules/options/src/main/scala/scala/build/options/PostBuildOptions.scala b/modules/options/src/main/scala/scala/build/options/PostBuildOptions.scala index f3bca301ed..93189bd11d 100644 --- a/modules/options/src/main/scala/scala/build/options/PostBuildOptions.scala +++ b/modules/options/src/main/scala/scala/build/options/PostBuildOptions.scala @@ -11,7 +11,8 @@ final case class PostBuildOptions( pythonSetup: Option[Boolean] = None, python: Option[Boolean] = None, scalaPyVersion: Option[String] = None, - addRunnerDependencyOpt: Option[Boolean] = None + addRunnerDependencyOpt: Option[Boolean] = None, + lazyValGradeOpt: Option[Boolean] = None ) { def doSetupPython: Option[Boolean] = diff --git a/project/deps/package.mill b/project/deps/package.mill index 17b633e042..93a962403a 100644 --- a/project/deps/package.mill +++ b/project/deps/package.mill @@ -151,8 +151,12 @@ object Deps { def mavenAppGroupId = "com.example" def mavenAppVersion = "0.1-SNAPSHOT" def scalafix = "0.14.7" + def lazyvalgrade = "0.1.0-SNAPSHOT" } + def lazyvalgradeOrganization = "org.virtuslab" + def lazyvalgradeModuleName = "lazyvalgrade-cli" + def argonautShapeless = mvn"com.github.alexarchambault:argonaut-shapeless_6.3_2.13:${Versions.argonautShapeless}" def asm = mvn"org.ow2.asm:asm:9.10.1" diff --git a/website/docs/reference/cli-options.md b/website/docs/reference/cli-options.md index da37782a80..ad7103c4ac 100644 --- a/website/docs/reference/cli-options.md +++ b/website/docs/reference/cli-options.md @@ -1708,6 +1708,10 @@ Exclude sources Force object wrapper for scripts +### `--lazyvalgrade` + +Patch Scala 3.0-3.7.x lazy val bytecode on the classpath for JDK 26+ compatibility + ### `--auto-setup-ide` Aliases: `--auto-setup-bsp` diff --git a/website/docs/reference/directives.md b/website/docs/reference/directives.md index 761efc429b..72414829cd 100644 --- a/website/docs/reference/directives.md +++ b/website/docs/reference/directives.md @@ -260,6 +260,15 @@ Add Javac options which will be passed when compiling sources. `//> using test.javacOpt -source 1.8 -target 1.8` +### LazyValGrade + +Patch Scala 3.0-3.7.x lazy val bytecode on the classpath for JDK 26+ compatibility + +`//> using lazyvalgrade` + +#### Examples +`//> using lazyvalgrade` + ### Main class Specify default main class From f32f193ccdae7675f9e2c9bbb38a150db6ecfbaa Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Tue, 30 Jun 2026 10:20:35 +0200 Subject: [PATCH 3/6] Switch from `lazyvalgrade-cli` to `lazyvalgrade-core` --- .../postprocessing/LazyValGradePatcher.scala | 99 ++++++++++++------- .../scala/scala/cli/commands/repl/Repl.scala | 2 +- .../scala/scala/cli/commands/run/Run.scala | 1 - project/deps/package.mill | 2 +- 4 files changed, 67 insertions(+), 37 deletions(-) diff --git a/modules/build/src/main/scala/scala/build/postprocessing/LazyValGradePatcher.scala b/modules/build/src/main/scala/scala/build/postprocessing/LazyValGradePatcher.scala index 3c8bb61a0a..281ab42fb1 100644 --- a/modules/build/src/main/scala/scala/build/postprocessing/LazyValGradePatcher.scala +++ b/modules/build/src/main/scala/scala/build/postprocessing/LazyValGradePatcher.scala @@ -5,10 +5,13 @@ import coursier.cache.FileCache import dependency.* import os.Path -import java.io.File +import java.io.{ByteArrayOutputStream, PrintStream} +import java.net.URLClassLoader +import java.nio.file.Path as NioPath import java.security.MessageDigest import scala.build.EitherCps.{either, value} +import scala.build.Ops.* import scala.build.errors.BuildException import scala.build.internal.Constants import scala.build.internal.CsLoggerUtil.* @@ -22,16 +25,41 @@ object LazyValGradePatcher { def transformClassPath( classPath: Seq[Path], options: BuildOptions, - javaCommand: String, logger: Logger ): Either[BuildException, Seq[Path]] = if options.notForBloopOptions.lazyValGradeOpt.contains(true) then either { val toolClassPath = value(fetchToolClassPath(options, logger)) - value(patchedClassPath(classPath, javaCommand, toolClassPath, logger)) + val toolLoader = isolatedToolLoader(toolClassPath) + // lazyvalgrade 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 the debug log instead. + value { + captureStdio(logger) { + classPath.iterator.map { entry => + if entry.ext == "jar" then patchJar(entry, toolLoader, logger) + else Right(entry) + }.sequence0 + } + } } 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 fetchToolClassPath( options: BuildOptions, logger: Logger @@ -58,24 +86,15 @@ object LazyValGradePatcher { artifacts.map(_._2) } - private def patchedClassPath( - classPath: Seq[Path], - javaCommand: String, - toolClassPath: Seq[Path], - logger: Logger - ): Either[BuildException, Seq[Path]] = - either { - val toolCp = toolClassPath.map(_.toString).mkString(File.pathSeparator) - classPath.map { entry => - if entry.ext == "jar" then value(patchJar(entry, javaCommand, toolCp, logger)) - else entry - } - } + /** Loads lazyvalgrade in an isolated classloader (null parent) so that its ASM dependency does + * not clash with the one used by scala-cli itself. + */ + private def isolatedToolLoader(toolClassPath: Seq[Path]): URLClassLoader = + new URLClassLoader(toolClassPath.map(_.toNIO.toUri.toURL).toArray, null) private def patchJar( jar: Path, - javaCommand: String, - toolClassPath: String, + toolLoader: URLClassLoader, logger: Logger ): Either[BuildException, Path] = either { @@ -85,27 +104,39 @@ object LazyValGradePatcher { if os.exists(cached) then cached else { os.makeDir.all(cachedDir) - os.copy(jar, cached, replaceExisting = true, createFolders = true) - val exitCode = os.proc( - javaCommand, - "-cp", - toolClassPath, - "lazyvalgrade.cli.Main", - cached.toString - ).call( - stdout = os.Pipe, - stderr = os.Pipe, - check = false - ).exitCode - if exitCode != 0 then - value( - Left(new BuildException(s"Failed to patch lazy vals in $jar (exit code $exitCode)") {}) - ) + value(runJarProcessor(jar, cached, toolLoader)) logger.debug(s"Patched lazy vals in $jar -> $cached") cached } } + /** Invokes `lazyvalgrade.jar.JarProcessor.process` reflectively through the isolated loader. */ + private def runJarProcessor( + input: Path, + output: Path, + toolLoader: URLClassLoader + ): Either[BuildException, Unit] = + try { + val moduleClass = toolLoader.loadClass("lazyvalgrade.jar.JarProcessor$") + val module = moduleClass.getField("MODULE$").get(null) + val processMethod = moduleClass.getMethod("process", classOf[NioPath], classOf[NioPath]) + val result = processMethod.invoke(module, input.toNIO, output.toNIO) + val failedClasses = result.getClass.getMethod("failedClasses").invoke(result) + .asInstanceOf[Int] + if failedClasses > 0 then + os.remove.all(output / os.up) + Left(new BuildException(s"Failed to patch lazy vals in $input") {}) + else Right(()) + } + catch { + case e: Throwable => + os.remove.all(output / os.up) + Left(new BuildException( + s"Failed to patch lazy vals in $input: ${e.getMessage}", + cause = e + ) {}) + } + private def sha1(path: Path): String = { val md = MessageDigest.getInstance("SHA-1") val buffer = new Array[Byte](8192) diff --git a/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala b/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala index cc4ff128a8..05c85a75c9 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala @@ -350,7 +350,7 @@ object Repl extends ScalaCommand[ReplOptions] with BuildCommandHelpers { val javaCommand = options.javaHome().value.javaCommand def patchClassPath(classPath: Seq[os.Path]): Seq[os.Path] = - value(LazyValGradePatcher.transformClassPath(classPath, options, javaCommand, logger)) + value(LazyValGradePatcher.transformClassPath(classPath, options, logger)) def maybeRunRepl( replArtifacts: ReplArtifacts, diff --git a/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala b/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala index cc41d34468..77446acd7b 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala @@ -695,7 +695,6 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { LazyValGradePatcher.transformClassPath( classPath0, build.options, - build.options.javaHome().value.javaCommand, logger ) ) diff --git a/project/deps/package.mill b/project/deps/package.mill index 93a962403a..ca99240928 100644 --- a/project/deps/package.mill +++ b/project/deps/package.mill @@ -155,7 +155,7 @@ object Deps { } def lazyvalgradeOrganization = "org.virtuslab" - def lazyvalgradeModuleName = "lazyvalgrade-cli" + def lazyvalgradeModuleName = "lazyvalgrade-core" def argonautShapeless = mvn"com.github.alexarchambault:argonaut-shapeless_6.3_2.13:${Versions.argonautShapeless}" From 66242f6c45bd2046d0a2953e0d36e0e66c960a57 Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Tue, 30 Jun 2026 10:37:35 +0200 Subject: [PATCH 4/6] Refactor --- .../postprocessing/LazyValGradePatcher.scala | 105 ++++++++---------- .../build/errors/LazyValGradeError.scala | 4 + .../build/options/PostBuildOptions.scala | 3 + 3 files changed, 55 insertions(+), 57 deletions(-) create mode 100644 modules/core/src/main/scala/scala/build/errors/LazyValGradeError.scala diff --git a/modules/build/src/main/scala/scala/build/postprocessing/LazyValGradePatcher.scala b/modules/build/src/main/scala/scala/build/postprocessing/LazyValGradePatcher.scala index 281ab42fb1..acc7af5d11 100644 --- a/modules/build/src/main/scala/scala/build/postprocessing/LazyValGradePatcher.scala +++ b/modules/build/src/main/scala/scala/build/postprocessing/LazyValGradePatcher.scala @@ -6,39 +6,48 @@ import dependency.* import os.Path import java.io.{ByteArrayOutputStream, PrintStream} +import java.math.BigInteger import java.net.URLClassLoader import java.nio.file.Path as NioPath import java.security.MessageDigest import scala.build.EitherCps.{either, value} import scala.build.Ops.* -import scala.build.errors.BuildException +import scala.build.errors.{BuildException, LazyValGradeError} import scala.build.internal.Constants import scala.build.internal.CsLoggerUtil.* import scala.build.options.BuildOptions import scala.build.{Artifacts, Directories, Logger, Positioned} +import scala.util.Using +import scala.util.control.NonFatal object LazyValGradePatcher { private val cacheDir = Directories.directories.cacheDir / "lazyvalgrade" + private val jarProcessorModule = "lazyvalgrade.jar.JarProcessor$" + private val moduleField = "MODULE$" + private val processMethod = "process" + private val failedClassesMethod = "failedClasses" + def transformClassPath( classPath: Seq[Path], options: BuildOptions, logger: Logger ): Either[BuildException, Seq[Path]] = - if options.notForBloopOptions.lazyValGradeOpt.contains(true) then + if options.notForBloopOptions.lazyValGrade then either { val toolClassPath = value(fetchToolClassPath(options, logger)) - val toolLoader = isolatedToolLoader(toolClassPath) - // lazyvalgrade 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 the debug log instead. value { - captureStdio(logger) { - classPath.iterator.map { entry => - if entry.ext == "jar" then patchJar(entry, toolLoader, logger) - else Right(entry) - }.sequence0 + Using.resource(isolatedToolLoader(toolClassPath)) { toolLoader => + // lazyvalgrade 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.iterator.map { entry => + if entry.ext == "jar" then patchJar(entry, toolLoader, logger) + else Right(entry) + }.sequence0 + } } } } @@ -70,13 +79,11 @@ object LazyValGradePatcher { val scalaParams = value(options.scalaParams).getOrElse( ScalaParameters(Constants.defaultScalaVersion) ) - val lazyValGradeDeps = - Seq( - dep"${Constants.lazyvalgradeOrganization}::${Constants.lazyvalgradeModuleName}:${Constants.lazyvalgradeVersion}" - ) + val dependency = + dep"${Constants.lazyvalgradeOrganization}::${Constants.lazyvalgradeModuleName}:${Constants.lazyvalgradeVersion}" val artifacts = value( Artifacts.artifacts( - lazyValGradeDeps.map(Positioned.none), + Seq(Positioned.none(dependency)), repositories, Some(scalaParams), logger, @@ -97,18 +104,18 @@ object LazyValGradePatcher { toolLoader: URLClassLoader, logger: Logger ): Either[BuildException, Path] = - either { - val digest = sha1(jar) - val cachedDir = cacheDir / Constants.lazyvalgradeVersion / digest - val cached = cachedDir / jar.last - if os.exists(cached) then cached - else { - os.makeDir.all(cachedDir) - value(runJarProcessor(jar, cached, toolLoader)) - logger.debug(s"Patched lazy vals in $jar -> $cached") - cached - } - } + val cachedDir = cacheDir / Constants.lazyvalgradeVersion / sha1(jar) + val cached = cachedDir / jar.last + if os.exists(cached) then Right(cached) + else + os.makeDir.all(cachedDir) + runJarProcessor(jar, cached, toolLoader) match + case Right(()) => + logger.debug(s"Patched lazy vals in $jar -> $cached") + Right(cached) + case Left(error) => + os.remove.all(cachedDir) + Left(error) /** Invokes `lazyvalgrade.jar.JarProcessor.process` reflectively through the isolated loader. */ private def runJarProcessor( @@ -116,37 +123,21 @@ object LazyValGradePatcher { output: Path, toolLoader: URLClassLoader ): Either[BuildException, Unit] = - try { - val moduleClass = toolLoader.loadClass("lazyvalgrade.jar.JarProcessor$") - val module = moduleClass.getField("MODULE$").get(null) - val processMethod = moduleClass.getMethod("process", classOf[NioPath], classOf[NioPath]) - val result = processMethod.invoke(module, input.toNIO, output.toNIO) - val failedClasses = result.getClass.getMethod("failedClasses").invoke(result) + try + val moduleClass = toolLoader.loadClass(jarProcessorModule) + val module = moduleClass.getField(moduleField).get(null) + val process = moduleClass.getMethod(processMethod, classOf[NioPath], classOf[NioPath]) + val result = process.invoke(module, input.toNIO, output.toNIO) + val failedClasses = result.getClass.getMethod(failedClassesMethod).invoke(result) .asInstanceOf[Int] - if failedClasses > 0 then - os.remove.all(output / os.up) - Left(new BuildException(s"Failed to patch lazy vals in $input") {}) + if failedClasses > 0 then Left(LazyValGradeError(s"Failed to patch lazy vals in $input")) else Right(()) - } - catch { - case e: Throwable => - os.remove.all(output / os.up) - Left(new BuildException( - s"Failed to patch lazy vals in $input: ${e.getMessage}", - cause = e - ) {}) - } + catch + case NonFatal(e) => + Left(LazyValGradeError(s"Failed to patch lazy vals in $input: ${e.getMessage}", e)) - private def sha1(path: Path): String = { - val md = MessageDigest.getInstance("SHA-1") - val buffer = new Array[Byte](8192) - val stream = os.read.inputStream(path) - try - var read = stream.read(buffer) - while read >= 0 do - md.update(buffer, 0, read) - read = stream.read(buffer) - finally stream.close() - md.digest().map("%02x".format(_)).mkString - } + 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())) } diff --git a/modules/core/src/main/scala/scala/build/errors/LazyValGradeError.scala b/modules/core/src/main/scala/scala/build/errors/LazyValGradeError.scala new file mode 100644 index 0000000000..95a7295765 --- /dev/null +++ b/modules/core/src/main/scala/scala/build/errors/LazyValGradeError.scala @@ -0,0 +1,4 @@ +package scala.build.errors + +final class LazyValGradeError(message: String, cause: Throwable = null) + extends BuildException(message, cause = cause) diff --git a/modules/options/src/main/scala/scala/build/options/PostBuildOptions.scala b/modules/options/src/main/scala/scala/build/options/PostBuildOptions.scala index 93189bd11d..ea1f910d34 100644 --- a/modules/options/src/main/scala/scala/build/options/PostBuildOptions.scala +++ b/modules/options/src/main/scala/scala/build/options/PostBuildOptions.scala @@ -17,6 +17,9 @@ final case class PostBuildOptions( def doSetupPython: Option[Boolean] = pythonSetup.orElse(python) + + def lazyValGrade: Boolean = + lazyValGradeOpt.getOrElse(false) } object PostBuildOptions { From 330d1d054b94ae3dce77553e9484fb0b7f5e2223 Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Thu, 2 Jul 2026 16:03:11 +0200 Subject: [PATCH 5/6] Migrate to `sloth` --- build.mill | 6 ++-- ...lGradePatcher.scala => SlothPatcher.scala} | 31 +++++++++---------- .../DirectivesPreprocessingUtils.scala | 2 +- .../scala/build/tests/DirectiveTests.scala | 6 ++-- .../scala/scala/cli/commands/repl/Repl.scala | 4 +-- .../scala/scala/cli/commands/run/Run.scala | 4 +-- .../cli/commands/shared/SharedOptions.scala | 6 ++-- .../cli/commands/tests/RunOptionsTests.scala | 6 ++-- ...zyValGradeError.scala => SlothError.scala} | 2 +- .../{LazyValGrade.scala => Sloth.scala} | 17 +++++----- .../cli/integration/ReplTestDefinitions.scala | 4 +-- .../cli/integration/RunTestDefinitions.scala | 2 +- .../build/options/PostBuildOptions.scala | 6 ++-- project/deps/package.mill | 6 ++-- website/docs/reference/cli-options.md | 4 ++- website/docs/reference/directives.md | 18 +++++------ 16 files changed, 64 insertions(+), 60 deletions(-) rename modules/build/src/main/scala/scala/build/postprocessing/{LazyValGradePatcher.scala => SlothPatcher.scala} (76%) rename modules/core/src/main/scala/scala/build/errors/{LazyValGradeError.scala => SlothError.scala} (52%) rename modules/directives/src/main/scala/scala/build/preprocessing/directives/{LazyValGrade.scala => Sloth.scala} (57%) diff --git a/build.mill b/build.mill index 708041f707..d980cdaace 100644 --- a/build.mill +++ b/build.mill @@ -588,9 +588,9 @@ trait Core extends ScalaCliCrossSbtModule | | def scalafixVersion = "${Deps.Versions.scalafix}" | - | def lazyvalgradeOrganization = "${Deps.lazyvalgradeOrganization}" - | def lazyvalgradeModuleName = "${Deps.lazyvalgradeModuleName}" - | def lazyvalgradeVersion = "${Deps.Versions.lazyvalgrade}" + | def slothOrganization = "${Deps.slothOrganization}" + | def slothModuleName = "${Deps.slothModuleName}" + | def slothVersion = "${Deps.Versions.sloth}" | | def alpineVersion = "$alpineVersion" |} diff --git a/modules/build/src/main/scala/scala/build/postprocessing/LazyValGradePatcher.scala b/modules/build/src/main/scala/scala/build/postprocessing/SlothPatcher.scala similarity index 76% rename from modules/build/src/main/scala/scala/build/postprocessing/LazyValGradePatcher.scala rename to modules/build/src/main/scala/scala/build/postprocessing/SlothPatcher.scala index acc7af5d11..6c471e23e5 100644 --- a/modules/build/src/main/scala/scala/build/postprocessing/LazyValGradePatcher.scala +++ b/modules/build/src/main/scala/scala/build/postprocessing/SlothPatcher.scala @@ -1,6 +1,5 @@ package scala.build.postprocessing -import coursier.LocalRepositories import coursier.cache.FileCache import dependency.* import os.Path @@ -13,7 +12,7 @@ import java.security.MessageDigest import scala.build.EitherCps.{either, value} import scala.build.Ops.* -import scala.build.errors.{BuildException, LazyValGradeError} +import scala.build.errors.{BuildException, SlothError} import scala.build.internal.Constants import scala.build.internal.CsLoggerUtil.* import scala.build.options.BuildOptions @@ -21,11 +20,11 @@ import scala.build.{Artifacts, Directories, Logger, Positioned} import scala.util.Using import scala.util.control.NonFatal -object LazyValGradePatcher { +object SlothPatcher { - private val cacheDir = Directories.directories.cacheDir / "lazyvalgrade" + private val cacheDir = Directories.directories.cacheDir / "sloth" - private val jarProcessorModule = "lazyvalgrade.jar.JarProcessor$" + private val jarProcessorModule = "sloth.jar.JarProcessor$" private val moduleField = "MODULE$" private val processMethod = "process" private val failedClassesMethod = "failedClasses" @@ -35,12 +34,12 @@ object LazyValGradePatcher { options: BuildOptions, logger: Logger ): Either[BuildException, Seq[Path]] = - if options.notForBloopOptions.lazyValGrade then + if options.notForBloopOptions.sloth then either { val toolClassPath = value(fetchToolClassPath(options, logger)) value { Using.resource(isolatedToolLoader(toolClassPath)) { toolLoader => - // lazyvalgrade logs to stdout/stderr via its own bundled logging; capture it so it + // 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.iterator.map { entry => @@ -75,26 +74,26 @@ object LazyValGradePatcher { ): Either[BuildException, Seq[Path]] = either { val cache = options.internal.cache.getOrElse(FileCache()) - val repositories = value(options.finalRepositories) ++ Seq(LocalRepositories.ivy2Local) + val repositories = value(options.finalRepositories) val scalaParams = value(options.scalaParams).getOrElse( ScalaParameters(Constants.defaultScalaVersion) ) val dependency = - dep"${Constants.lazyvalgradeOrganization}::${Constants.lazyvalgradeModuleName}:${Constants.lazyvalgradeVersion}" + dep"${Constants.slothOrganization}::${Constants.slothModuleName}:${Constants.slothVersion}" val artifacts = value( Artifacts.artifacts( Seq(Positioned.none(dependency)), repositories, Some(scalaParams), logger, - cache.withMessage(s"Downloading lazyvalgrade ${Constants.lazyvalgradeVersion}") + cache.withMessage(s"Downloading sloth ${Constants.slothVersion}") ) ) artifacts.map(_._2) } - /** Loads lazyvalgrade in an isolated classloader (null parent) so that its ASM dependency does - * not clash with the one used by scala-cli itself. + /** Loads sloth in an isolated classloader (null parent) so that its ASM dependency does not clash + * with the one used by scala-cli itself. */ private def isolatedToolLoader(toolClassPath: Seq[Path]): URLClassLoader = new URLClassLoader(toolClassPath.map(_.toNIO.toUri.toURL).toArray, null) @@ -104,7 +103,7 @@ object LazyValGradePatcher { toolLoader: URLClassLoader, logger: Logger ): Either[BuildException, Path] = - val cachedDir = cacheDir / Constants.lazyvalgradeVersion / sha1(jar) + val cachedDir = cacheDir / Constants.slothVersion / sha1(jar) val cached = cachedDir / jar.last if os.exists(cached) then Right(cached) else @@ -117,7 +116,7 @@ object LazyValGradePatcher { os.remove.all(cachedDir) Left(error) - /** Invokes `lazyvalgrade.jar.JarProcessor.process` reflectively through the isolated loader. */ + /** Invokes `sloth.jar.JarProcessor.process` reflectively through the isolated loader. */ private def runJarProcessor( input: Path, output: Path, @@ -130,11 +129,11 @@ object LazyValGradePatcher { val result = process.invoke(module, input.toNIO, output.toNIO) val failedClasses = result.getClass.getMethod(failedClassesMethod).invoke(result) .asInstanceOf[Int] - if failedClasses > 0 then Left(LazyValGradeError(s"Failed to patch lazy vals in $input")) + if failedClasses > 0 then Left(SlothError(s"Failed to patch lazy vals in $input")) else Right(()) catch case NonFatal(e) => - Left(LazyValGradeError(s"Failed to patch lazy vals in $input: ${e.getMessage}", e)) + Left(SlothError(s"Failed to patch lazy vals in $input: ${e.getMessage}", e)) private def sha1(path: Path): String = val md = MessageDigest.getInstance("SHA-1") diff --git a/modules/build/src/main/scala/scala/build/preprocessing/directives/DirectivesPreprocessingUtils.scala b/modules/build/src/main/scala/scala/build/preprocessing/directives/DirectivesPreprocessingUtils.scala index 586017b0bc..6eb1efaf7a 100644 --- a/modules/build/src/main/scala/scala/build/preprocessing/directives/DirectivesPreprocessingUtils.scala +++ b/modules/build/src/main/scala/scala/build/preprocessing/directives/DirectivesPreprocessingUtils.scala @@ -17,7 +17,6 @@ object DirectivesPreprocessingUtils { directives.Exclude.handler, directives.JavaHome.handler, directives.Jvm.handler, - directives.LazyValGrade.handler, directives.MainClass.handler, directives.ObjectWrapper.handler, directives.Packaging.handler, @@ -32,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 diff --git a/modules/build/src/test/scala/scala/build/tests/DirectiveTests.scala b/modules/build/src/test/scala/scala/build/tests/DirectiveTests.scala index a361d6de2a..e3b50efe68 100644 --- a/modules/build/src/test/scala/scala/build/tests/DirectiveTests.scala +++ b/modules/build/src/test/scala/scala/build/tests/DirectiveTests.scala @@ -519,15 +519,15 @@ class DirectiveTests extends TestUtil.ScalaCliBuildSuite { } } - test("lazyvalgrade directive") { + test("sloth directive") { val testInputs = TestInputs( os.rel / "simple.sc" -> - """//> using lazyvalgrade + """//> using sloth |""".stripMargin ) testInputs.withBuild(baseOptions, buildThreads, bloopConfigOpt) { (_, _, maybeBuild) => val build = maybeBuild.orThrow - expect(build.options.notForBloopOptions.lazyValGradeOpt.contains(true)) + expect(build.options.notForBloopOptions.slothOpt.contains(true)) } } } diff --git a/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala b/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala index 05c85a75c9..cd734ef752 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/repl/Repl.scala @@ -13,7 +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.LazyValGradePatcher +import scala.build.postprocessing.SlothPatcher import scala.cli.CurrentParams import scala.cli.commands.run.Run.{createPythonInstance, orPythonDetectionError, pythonPathEnv} import scala.cli.commands.run.RunMode @@ -350,7 +350,7 @@ object Repl extends ScalaCommand[ReplOptions] with BuildCommandHelpers { val javaCommand = options.javaHome().value.javaCommand def patchClassPath(classPath: Seq[os.Path]): Seq[os.Path] = - value(LazyValGradePatcher.transformClassPath(classPath, options, logger)) + value(SlothPatcher.transformClassPath(classPath, options, logger)) def maybeRunRepl( replArtifacts: ReplArtifacts, diff --git a/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala b/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala index 77446acd7b..83c2034143 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala @@ -19,7 +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.LazyValGradePatcher +import scala.build.postprocessing.SlothPatcher import scala.cli.CurrentParams import scala.cli.commands.package0.Package import scala.cli.commands.setupide.SetupIde @@ -692,7 +692,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { build.options.notForBloopOptions.doSetupPython.getOrElse(false) val classPath0 = builds.flatMap(_.fullClassPathMaybeAsJar(asJar)).distinct val classPath = value( - LazyValGradePatcher.transformClassPath( + SlothPatcher.transformClassPath( classPath0, build.options, logger diff --git a/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala b/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala index a6ac4a49f9..4d5ae0c8af 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/shared/SharedOptions.scala @@ -211,8 +211,10 @@ final case class SharedOptions( @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") + @Name("patch-lazy-vals") @Tag(tags.experimental) - lazyvalgrade: Option[Boolean] = None, + sloth: Option[Boolean] = None, @Group(HelpGroup.Scala.toString) @HelpMessage( "Automatically generate BSP configuration in `.bsp/` when running build commands. Enabled by default." @@ -494,7 +496,7 @@ final case class SharedOptions( python = sharedPython.python, pythonSetup = sharedPython.pythonSetup, scalaPyVersion = sharedPython.scalaPyVersion, - lazyValGradeOpt = lazyvalgrade + slothOpt = sloth ), useBuildServer = compilationServer.server ).orElse(watchOptions.buildOptions()) diff --git a/modules/cli/src/test/scala/cli/commands/tests/RunOptionsTests.scala b/modules/cli/src/test/scala/cli/commands/tests/RunOptionsTests.scala index 17347349bf..6a0d9b6f44 100644 --- a/modules/cli/src/test/scala/cli/commands/tests/RunOptionsTests.scala +++ b/modules/cli/src/test/scala/cli/commands/tests/RunOptionsTests.scala @@ -51,13 +51,13 @@ class RunOptionsTests extends munit.FunSuite { expect(toolkitDep.version == "latest.release") } - test("lazyvalgrade option") { + test("sloth option") { val runOptions = RunOptions( shared = SharedOptions( - lazyvalgrade = Some(true) + sloth = Some(true) ) ) val buildOptions = Run.buildOptions(runOptions).value - expect(buildOptions.notForBloopOptions.lazyValGradeOpt.contains(true)) + expect(buildOptions.notForBloopOptions.slothOpt.contains(true)) } } diff --git a/modules/core/src/main/scala/scala/build/errors/LazyValGradeError.scala b/modules/core/src/main/scala/scala/build/errors/SlothError.scala similarity index 52% rename from modules/core/src/main/scala/scala/build/errors/LazyValGradeError.scala rename to modules/core/src/main/scala/scala/build/errors/SlothError.scala index 95a7295765..451a099850 100644 --- a/modules/core/src/main/scala/scala/build/errors/LazyValGradeError.scala +++ b/modules/core/src/main/scala/scala/build/errors/SlothError.scala @@ -1,4 +1,4 @@ package scala.build.errors -final class LazyValGradeError(message: String, cause: Throwable = null) +final class SlothError(message: String, cause: Throwable = null) extends BuildException(message, cause = cause) diff --git a/modules/directives/src/main/scala/scala/build/preprocessing/directives/LazyValGrade.scala b/modules/directives/src/main/scala/scala/build/preprocessing/directives/Sloth.scala similarity index 57% rename from modules/directives/src/main/scala/scala/build/preprocessing/directives/LazyValGrade.scala rename to modules/directives/src/main/scala/scala/build/preprocessing/directives/Sloth.scala index c6cd068df3..456252bae0 100644 --- a/modules/directives/src/main/scala/scala/build/preprocessing/directives/LazyValGrade.scala +++ b/modules/directives/src/main/scala/scala/build/preprocessing/directives/Sloth.scala @@ -5,22 +5,23 @@ import scala.build.errors.BuildException import scala.build.options.{BuildOptions, PostBuildOptions} import scala.cli.commands.SpecificationLevel -@DirectiveExamples("//> using lazyvalgrade") -@DirectiveUsage("//> using lazyvalgrade", "`//> using lazyvalgrade`") +@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 LazyValGrade( - @DirectiveName("lazy.val.grade") - lazyvalgrade: Boolean = false +final case class Sloth( + @DirectiveName("lazyvalgrade") + @DirectiveName("lazyValPatching") + sloth: Boolean = false ) extends HasBuildOptions { def buildOptions: Either[BuildException, BuildOptions] = Right(BuildOptions( - notForBloopOptions = PostBuildOptions(lazyValGradeOpt = Some(true)) + notForBloopOptions = PostBuildOptions(slothOpt = Some(true)) )) } -object LazyValGrade { - val handler: DirectiveHandler[LazyValGrade] = DirectiveHandler.derive +object Sloth { + val handler: DirectiveHandler[Sloth] = DirectiveHandler.derive } diff --git a/modules/integration/src/test/scala/scala/cli/integration/ReplTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/ReplTestDefinitions.scala index 7ed760f50f..bb9cf9683c 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/ReplTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/ReplTestDefinitions.scala @@ -352,7 +352,7 @@ abstract class ReplTestDefinitions extends ScalaCliSuite with TestScalaVersionAr "--jvm", latestJava, "--power", - "--lazyvalgrade", + "--sloth", extraOptions ).call(cwd = root, stderr = os.Pipe) expect(res.out.trim().contains(expectedMessage)) @@ -374,7 +374,7 @@ abstract class ReplTestDefinitions extends ScalaCliSuite with TestScalaVersionAr code, extraOptions, "--power", - "--lazyvalgrade", + "--sloth", "--dep", dep, "--repository", diff --git a/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala index b588b12a22..b7265018b9 100755 --- a/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/RunTestDefinitions.scala @@ -2588,7 +2588,7 @@ abstract class RunTestDefinitions TestUtil.cli, extraOptions, "--power", - "--lazyvalgrade", + "--sloth", "script.sc", "--repository", repoDir.toNIO.toUri.toASCIIString, diff --git a/modules/options/src/main/scala/scala/build/options/PostBuildOptions.scala b/modules/options/src/main/scala/scala/build/options/PostBuildOptions.scala index ea1f910d34..181598e7df 100644 --- a/modules/options/src/main/scala/scala/build/options/PostBuildOptions.scala +++ b/modules/options/src/main/scala/scala/build/options/PostBuildOptions.scala @@ -12,14 +12,14 @@ final case class PostBuildOptions( python: Option[Boolean] = None, scalaPyVersion: Option[String] = None, addRunnerDependencyOpt: Option[Boolean] = None, - lazyValGradeOpt: Option[Boolean] = None + slothOpt: Option[Boolean] = None ) { def doSetupPython: Option[Boolean] = pythonSetup.orElse(python) - def lazyValGrade: Boolean = - lazyValGradeOpt.getOrElse(false) + def sloth: Boolean = + slothOpt.getOrElse(false) } object PostBuildOptions { diff --git a/project/deps/package.mill b/project/deps/package.mill index ca99240928..36d5f3a9e9 100644 --- a/project/deps/package.mill +++ b/project/deps/package.mill @@ -151,11 +151,11 @@ object Deps { def mavenAppGroupId = "com.example" def mavenAppVersion = "0.1-SNAPSHOT" def scalafix = "0.14.7" - def lazyvalgrade = "0.1.0-SNAPSHOT" + def sloth = "0.1.0-M1" } - def lazyvalgradeOrganization = "org.virtuslab" - def lazyvalgradeModuleName = "lazyvalgrade-core" + def slothOrganization = "org.virtuslab" + def slothModuleName = "sloth-core" def argonautShapeless = mvn"com.github.alexarchambault:argonaut-shapeless_6.3_2.13:${Versions.argonautShapeless}" diff --git a/website/docs/reference/cli-options.md b/website/docs/reference/cli-options.md index ad7103c4ac..f25f6b84b5 100644 --- a/website/docs/reference/cli-options.md +++ b/website/docs/reference/cli-options.md @@ -1708,7 +1708,9 @@ Exclude sources Force object wrapper for scripts -### `--lazyvalgrade` +### `--sloth` + +Aliases: `--lazyvalgrade`, `--patch-lazy-vals` Patch Scala 3.0-3.7.x lazy val bytecode on the classpath for JDK 26+ compatibility diff --git a/website/docs/reference/directives.md b/website/docs/reference/directives.md index 72414829cd..cd9ec43afb 100644 --- a/website/docs/reference/directives.md +++ b/website/docs/reference/directives.md @@ -260,15 +260,6 @@ Add Javac options which will be passed when compiling sources. `//> using test.javacOpt -source 1.8 -target 1.8` -### LazyValGrade - -Patch Scala 3.0-3.7.x lazy val bytecode on the classpath for JDK 26+ compatibility - -`//> using lazyvalgrade` - -#### Examples -`//> using lazyvalgrade` - ### Main class Specify default main class @@ -689,6 +680,15 @@ Add Scala.js options `//> using jsEmitWasm` +### Sloth + +Patch Scala 3.0-3.7.x lazy val bytecode on the classpath for JDK 26+ compatibility + +`//> using sloth` + +#### Examples +`//> using sloth` + ### Test framework Set the test framework From 1ccbaaf56c5bf8bfffef4fe0ee890b356fffe63f Mon Sep 17 00:00:00 2001 From: Piotr Chabelski Date: Fri, 3 Jul 2026 09:48:27 +0200 Subject: [PATCH 6/6] Fix --sloth lazy-val patching on the GraalVM native launcher --- build.mill | 3 +- .../build/postprocessing/SlothPatcher.scala | 103 ++++-------------- .../scala/scala/build/errors/SlothError.scala | 4 - project/deps/package.mill | 1 + 4 files changed, 26 insertions(+), 85 deletions(-) delete mode 100644 modules/core/src/main/scala/scala/build/errors/SlothError.scala diff --git a/build.mill b/build.mill index d980cdaace..988186d7a4 100644 --- a/build.mill +++ b/build.mill @@ -588,8 +588,6 @@ trait Core extends ScalaCliCrossSbtModule | | def scalafixVersion = "${Deps.Versions.scalafix}" | - | def slothOrganization = "${Deps.slothOrganization}" - | def slothModuleName = "${Deps.slothModuleName}" | def slothVersion = "${Deps.Versions.sloth}" | | def alpineVersion = "$alpineVersion" @@ -748,6 +746,7 @@ trait Build extends ScalaCliCrossSbtModule Deps.nativeTestRunner, Deps.osLib, Deps.pprint, + Deps.slothCore, Deps.scalaJsEnvNodeJs, Deps.scalaJsTestAdapter, Deps.swoval, diff --git a/modules/build/src/main/scala/scala/build/postprocessing/SlothPatcher.scala b/modules/build/src/main/scala/scala/build/postprocessing/SlothPatcher.scala index 6c471e23e5..e79a01cf08 100644 --- a/modules/build/src/main/scala/scala/build/postprocessing/SlothPatcher.scala +++ b/modules/build/src/main/scala/scala/build/postprocessing/SlothPatcher.scala @@ -1,52 +1,35 @@ package scala.build.postprocessing -import coursier.cache.FileCache -import dependency.* import os.Path +import sloth.jar.JarProcessor import java.io.{ByteArrayOutputStream, PrintStream} import java.math.BigInteger -import java.net.URLClassLoader -import java.nio.file.Path as NioPath import java.security.MessageDigest -import scala.build.EitherCps.{either, value} -import scala.build.Ops.* -import scala.build.errors.{BuildException, SlothError} +import scala.build.errors.BuildException import scala.build.internal.Constants -import scala.build.internal.CsLoggerUtil.* import scala.build.options.BuildOptions -import scala.build.{Artifacts, Directories, Logger, Positioned} -import scala.util.Using +import scala.build.{Directories, Logger} import scala.util.control.NonFatal object SlothPatcher { private val cacheDir = Directories.directories.cacheDir / "sloth" - private val jarProcessorModule = "sloth.jar.JarProcessor$" - private val moduleField = "MODULE$" - private val processMethod = "process" - private val failedClassesMethod = "failedClasses" - def transformClassPath( classPath: Seq[Path], options: BuildOptions, logger: Logger ): Either[BuildException, Seq[Path]] = if options.notForBloopOptions.sloth then - either { - val toolClassPath = value(fetchToolClassPath(options, logger)) - value { - Using.resource(isolatedToolLoader(toolClassPath)) { toolLoader => - // 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.iterator.map { entry => - if entry.ext == "jar" then patchJar(entry, toolLoader, logger) - else Right(entry) - }.sequence0 - } + 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 } } } @@ -68,72 +51,34 @@ object SlothPatcher { } } - private def fetchToolClassPath( - options: BuildOptions, - logger: Logger - ): Either[BuildException, Seq[Path]] = - either { - val cache = options.internal.cache.getOrElse(FileCache()) - val repositories = value(options.finalRepositories) - val scalaParams = value(options.scalaParams).getOrElse( - ScalaParameters(Constants.defaultScalaVersion) - ) - val dependency = - dep"${Constants.slothOrganization}::${Constants.slothModuleName}:${Constants.slothVersion}" - val artifacts = value( - Artifacts.artifacts( - Seq(Positioned.none(dependency)), - repositories, - Some(scalaParams), - logger, - cache.withMessage(s"Downloading sloth ${Constants.slothVersion}") - ) - ) - artifacts.map(_._2) - } - - /** Loads sloth in an isolated classloader (null parent) so that its ASM dependency does not clash - * with the one used by scala-cli itself. - */ - private def isolatedToolLoader(toolClassPath: Seq[Path]): URLClassLoader = - new URLClassLoader(toolClassPath.map(_.toNIO.toUri.toURL).toArray, null) - - private def patchJar( - jar: Path, - toolLoader: URLClassLoader, - logger: Logger - ): Either[BuildException, Path] = + 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 Right(cached) + if os.exists(cached) then cached else os.makeDir.all(cachedDir) - runJarProcessor(jar, cached, toolLoader) match + runJarProcessor(jar, cached) match case Right(()) => logger.debug(s"Patched lazy vals in $jar -> $cached") - Right(cached) - case Left(error) => + cached + case Left(message) => os.remove.all(cachedDir) - Left(error) + logger.message(s"Could not patch lazy vals in $jar, using original: $message") + jar - /** Invokes `sloth.jar.JarProcessor.process` reflectively through the isolated loader. */ private def runJarProcessor( input: Path, - output: Path, - toolLoader: URLClassLoader - ): Either[BuildException, Unit] = + output: Path + ): Either[String, Unit] = try - val moduleClass = toolLoader.loadClass(jarProcessorModule) - val module = moduleClass.getField(moduleField).get(null) - val process = moduleClass.getMethod(processMethod, classOf[NioPath], classOf[NioPath]) - val result = process.invoke(module, input.toNIO, output.toNIO) - val failedClasses = result.getClass.getMethod(failedClassesMethod).invoke(result) - .asInstanceOf[Int] - if failedClasses > 0 then Left(SlothError(s"Failed to patch lazy vals in $input")) + 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(SlothError(s"Failed to patch lazy vals in $input: ${e.getMessage}", e)) + Left(s"Failed to patch lazy vals in $input: ${e.getMessage}") private def sha1(path: Path): String = val md = MessageDigest.getInstance("SHA-1") diff --git a/modules/core/src/main/scala/scala/build/errors/SlothError.scala b/modules/core/src/main/scala/scala/build/errors/SlothError.scala deleted file mode 100644 index 451a099850..0000000000 --- a/modules/core/src/main/scala/scala/build/errors/SlothError.scala +++ /dev/null @@ -1,4 +0,0 @@ -package scala.build.errors - -final class SlothError(message: String, cause: Throwable = null) - extends BuildException(message, cause = cause) diff --git a/project/deps/package.mill b/project/deps/package.mill index 36d5f3a9e9..2b0aae1479 100644 --- a/project/deps/package.mill +++ b/project/deps/package.mill @@ -156,6 +156,7 @@ object Deps { def slothOrganization = "org.virtuslab" def slothModuleName = "sloth-core" + def slothCore = mvn"org.virtuslab::sloth-core:${Versions.sloth}" def argonautShapeless = mvn"com.github.alexarchambault:argonaut-shapeless_6.3_2.13:${Versions.argonautShapeless}"