Skip to content

Commit 72008ba

Browse files
authored
Add support for JShell in repl (#4262)
* Add support for JSHELL in `repl` * Add support for `--repl-init-script-file` to enable testing JSHELL on Windows * Extract Jshell integration tests to a dedicated trait * Test JSHELL with the supported JVMs * Refactor * Refactor some more * Make JSHELL non-experimental * Refactor some more * Allow to run pure Java projects in Scala REPL * Enable usage of Scala dependencies in pure Java projects * Refactor some more
1 parent 5c92c57 commit 72008ba

22 files changed

Lines changed: 964 additions & 82 deletions

File tree

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package scala.cli.commands.repl
2+
3+
import java.io.File
4+
5+
import scala.build.Logger
6+
import scala.build.errors.BuildException
7+
import scala.build.internal.Runner
8+
import scala.build.options.BuildOptions
9+
import scala.util.Properties
10+
11+
object JShellRunner {
12+
final case class Command(
13+
jshellCommand: String,
14+
args: Seq[String],
15+
extraEnv: Map[String, String]
16+
) {
17+
def processCommand: Seq[String] = Seq(jshellCommand) ++ args
18+
def displayedCommand: Seq[String] = Runner.envCommand(extraEnv) ++ processCommand
19+
}
20+
21+
final class JShellUnavailable(message0: String)
22+
extends BuildException(message0)
23+
24+
private def executableExt(isWindows: Boolean): String = if isWindows then ".exe" else ""
25+
26+
def commandFor(
27+
javaHomeInfo: BuildOptions.JavaHomeInfo,
28+
javaOpts: Seq[String],
29+
classPath: Seq[os.Path],
30+
programArgs: Seq[String],
31+
initScriptOpt: Option[String],
32+
quitAfterInit: Boolean,
33+
currentEnv: Map[String, String],
34+
isWindows: Boolean = Properties.isWin
35+
): Either[BuildException, Command] =
36+
if javaHomeInfo.version < 9 then
37+
Left(
38+
JShellUnavailable(
39+
s"JShell requires JDK >= 9, but the selected JDK is ${javaHomeInfo.version}. Consider using --jvm 17."
40+
)
41+
)
42+
else {
43+
val jshellPath = javaHomeInfo.javaHome / "bin" / s"jshell${executableExt(isWindows)}"
44+
val jshellCommand = jshellPath.toString
45+
if !os.exists(jshellPath) then
46+
Left(
47+
JShellUnavailable(
48+
s"JShell executable not found at $jshellCommand. Ensure the selected JVM is a full JDK (for example with --jvm 17)."
49+
)
50+
)
51+
else {
52+
val classPathArg = classPath.map(_.toString).distinct.mkString(File.pathSeparator)
53+
val startupArgs = initScriptOpt.toSeq.flatMap { script =>
54+
val scriptFile = os.temp(
55+
prefix = "scala-cli-jshell-init-",
56+
suffix = ".jsh",
57+
deleteOnExit = false
58+
)
59+
os.write.over(scriptFile, script + System.lineSeparator())
60+
Seq("--startup", "DEFAULT", "--startup", scriptFile.toString)
61+
}
62+
val quitAfterInitArgs =
63+
if quitAfterInit then {
64+
val exitFile = os.temp(
65+
prefix = "scala-cli-jshell-exit-",
66+
suffix = ".jsh",
67+
deleteOnExit = false
68+
)
69+
os.write.over(exitFile, "/exit" + System.lineSeparator())
70+
Seq(exitFile.toString)
71+
}
72+
else Nil
73+
val vmArgs = javaOpts.map(opt => s"-J$opt")
74+
val args = Seq("--class-path", classPathArg) ++
75+
vmArgs ++
76+
startupArgs ++
77+
programArgs ++
78+
quitAfterInitArgs
79+
val extraEnv = javaHomeInfo.envUpdates(currentEnv)
80+
Right(Command(jshellCommand, args, extraEnv))
81+
}
82+
}
83+
84+
def run(
85+
command: Command,
86+
logger: Logger,
87+
allowExecve: Boolean,
88+
dryRun: Boolean
89+
): Either[BuildException, Unit] = {
90+
logger.log(
91+
s"Running ${command.displayedCommand.mkString(" ")}",
92+
" Running" + System.lineSeparator() +
93+
command.displayedCommand.iterator.map(_ + System.lineSeparator()).mkString
94+
)
95+
if dryRun then {
96+
logger.message(s"JShell command: ${command.processCommand.mkString(" ")}")
97+
logger.message("Dry run, not running REPL.")
98+
Right(())
99+
}
100+
else {
101+
val process =
102+
if allowExecve then
103+
Runner.maybeExec("jshell", command.processCommand, logger, extraEnv = command.extraEnv)
104+
else Runner.run(command.processCommand, logger, extraEnv = command.extraEnv)
105+
val retCode = process.waitFor()
106+
if retCode == 0 then Right(())
107+
else Left(new Repl.ReplError(retCode))
108+
}
109+
}
110+
}

0 commit comments

Comments
 (0)