Skip to content

Commit de33b21

Browse files
authored
Merge pull request #2856 from tgodzik/fix-console-ammonite
bugfix: Fix running console
2 parents 2cd883e + 5d79f92 commit de33b21

File tree

2 files changed

+138
-39
lines changed

2 files changed

+138
-39
lines changed

cli/src/main/scala/bloop/cli/Default.scala

Lines changed: 87 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
package bloop.cli
22

3-
import caseapp.core.RemainingArgs
3+
import java.nio.file.Files
4+
5+
import scala.concurrent.Await
6+
import scala.concurrent.duration.Duration
7+
import scala.io.Source
8+
import scala.util.Using
49

510
import bloop.cli.options.DefaultOptions
6-
import bloop.rifle.BloopThreads
711
import bloop.rifle.BloopRifle
12+
import bloop.rifle.BloopThreads
813
import bloop.rifle.internal.Operations
9-
import scala.concurrent.Await
10-
import scala.concurrent.duration.Duration
14+
15+
import caseapp.core.RemainingArgs
1116
import caseapp.core.app.Command
1217

1318
object Default extends Command[DefaultOptions] {
@@ -42,12 +47,16 @@ object Default extends Command[DefaultOptions] {
4247
case Seq() =>
4348
// FIXME Give more details?
4449
logger.message("Bloop server is running.")
45-
case Seq(cmd, args @ _*) =>
50+
case Seq(cmd, cmdArgs @ _*) if cmd == "console" =>
51+
// Console command needs special handling because interactive REPLs
52+
// require direct terminal access which isn't available through nailgun
53+
runConsoleCommand(options, cmdArgs.toArray)
54+
case Seq(cmd, cmdArgs @ _*) =>
4655
val assumeTty = System.console() != null
4756
val cwd = os.pwd.wrapped
4857
val retCode = Operations.run(
4958
command = cmd,
50-
args = args.toArray,
59+
args = cmdArgs.toArray,
5160
workingDir = cwd,
5261
address = bloopRifleConfig.address,
5362
inOpt = Some(System.in),
@@ -68,4 +77,76 @@ object Default extends Command[DefaultOptions] {
6877
}
6978
}
7079
}
80+
81+
/**
82+
* Handles the console command specially by:
83+
* 1. Creating a temp file for the server to write the Ammonite command
84+
* 2. Running the console command on the server with --out-file
85+
* 3. Reading the command from the file and executing it locally with terminal access
86+
*/
87+
private def runConsoleCommand(options: DefaultOptions, args: Array[String]): Unit = {
88+
val logger = options.logging.logger
89+
val bloopRifleConfig = options.bloopRifleConfig
90+
val cwd = os.pwd.wrapped
91+
92+
// Create a temp file for the Ammonite command
93+
val outFile = Files.createTempFile("bloop-console-", ".txt")
94+
outFile.toFile.deleteOnExit()
95+
96+
// Add --out-file to the args
97+
val argsWithOutFile = args ++ Array("--out-file", outFile.toString)
98+
99+
val assumeTty = System.console() != null
100+
// Don't pass stdin to the server - we'll need it for Ammonite later
101+
// The server only needs to compile and write the command to the outFile
102+
val retCode = Operations.run(
103+
command = "console",
104+
args = argsWithOutFile,
105+
workingDir = cwd,
106+
address = bloopRifleConfig.address,
107+
inOpt = None,
108+
out = System.out,
109+
err = System.err,
110+
logger = logger.bloopRifleLogger,
111+
assumeInTty = false,
112+
assumeOutTty = assumeTty,
113+
assumeErrTty = assumeTty
114+
)
115+
116+
if (retCode != 0) {
117+
logger.debug(s"Console command failed with return code $retCode")
118+
sys.exit(retCode)
119+
}
120+
121+
// Read the command from the temp file
122+
if (!Files.exists(outFile) || Files.size(outFile) == 0) {
123+
logger.debug("Console command output file is empty or doesn't exist")
124+
sys.exit(1)
125+
}
126+
127+
val consoleCmd = Using(Source.fromFile(outFile.toFile)) { source =>
128+
source.getLines().toArray
129+
}.getOrElse {
130+
logger.debug("Failed to read console command from output file")
131+
sys.exit(1)
132+
}
133+
134+
// Clean up the temp file
135+
try Files.delete(outFile)
136+
catch { case _: Exception => }
137+
138+
logger.debug(s"Running console command: ${consoleCmd.mkString(" ")}")
139+
140+
// Run the command with inherited IO for proper terminal access
141+
import scala.jdk.CollectionConverters._
142+
val builder = new ProcessBuilder(consoleCmd.toList.asJava)
143+
builder.directory(cwd.toFile)
144+
builder.inheritIO()
145+
val process = builder.start()
146+
val exitCode = process.waitFor()
147+
148+
if (exitCode != 0) {
149+
sys.exit(exitCode)
150+
}
151+
}
71152
}

frontend/src/main/scala/bloop/engine/Interpreter.scala

Lines changed: 51 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -258,20 +258,59 @@ object Interpreter {
258258
cmd.cliOptions.noColor,
259259
"`console`"
260260
) { state =>
261+
// Helper to output a REPL command - either print it or write to file
262+
def outputReplCommand(
263+
replCmd: List[String],
264+
replName: String
265+
): Task[State] = {
266+
val cmdString = replCmd.mkString("\n")
267+
cmd.outFile match {
268+
case None =>
269+
// Print the command for the client to run locally (interactive REPLs
270+
// need direct terminal access which isn't available through nailgun)
271+
Task.now(state.withInfo(cmdString))
272+
case Some(outFile) =>
273+
try {
274+
Files.write(outFile, cmdString.getBytes(StandardCharsets.UTF_8))
275+
val msg = s"Wrote $replName command to $outFile"
276+
Task.now(state.withDebug(msg)(DebugFilter.All))
277+
} catch {
278+
case _: IOException =>
279+
val msg = s"Unexpected error when writing $replName command to $outFile"
280+
Task.now(state.withError(msg, ExitStatus.RunError))
281+
}
282+
}
283+
}
284+
261285
cmd.repl match {
262286
case ScalacRepl =>
263287
if (cmd.ammoniteVersion.isDefined) {
264288
val errMsg =
265289
"Specifying an Ammonite version while using the Scalac console does not work"
266290
Task.now(state.withError(errMsg, ExitStatus.InvalidCommandLineOption))
267-
} else if (cmd.args.nonEmpty) {
268-
val errMsg = "Passing arguments to the Scalac console does not work"
269-
Task.now(state.withError(errMsg, ExitStatus.InvalidCommandLineOption))
270291
} else {
271-
Tasks.console(state, project)
292+
val dag = state.build.getDagFor(project)
293+
val scalaVersion = project.scalaInstance
294+
.map(_.version)
295+
.getOrElse(ScalaInstance.scalaInstanceForJavaProjects(state.logger))
296+
297+
val classpath = project.fullRuntimeClasspath(dag, state.client)
298+
val classpathStr = classpath.map(_.syntax).mkString(java.io.File.pathSeparator)
299+
300+
val scalaCmd = List(
301+
"coursier",
302+
"launch",
303+
s"org.scala-lang:scala-compiler:$scalaVersion",
304+
"--main-class",
305+
"scala.tools.nsc.MainGenericRunner",
306+
"--",
307+
"-cp",
308+
classpathStr
309+
) ++ cmd.args
310+
311+
outputReplCommand(scalaCmd, "Scala REPL")
272312
}
273313
case AmmoniteRepl =>
274-
// Look for version of scala instance in any of the projects
275314
def findScalaVersion(dag: Dag[Project]): Option[String] = {
276315
dag match {
277316
case Aggregate(dags) => dags.flatMap(findScalaVersion).headOption
@@ -285,44 +324,23 @@ object Interpreter {
285324
}
286325

287326
val dag = state.build.getDagFor(project)
288-
// If none version is found (e.g. all Java projects), use Bloop's scala version
289327
val scalaVersion = findScalaVersion(dag)
290328
.getOrElse(ScalaInstance.scalaInstanceForJavaProjects(state.logger))
291329

292330
val ammVersion = cmd.ammoniteVersion.getOrElse("latest.release")
293-
val coursierCmd = List(
331+
val classpath = project.fullRuntimeClasspath(dag, state.client)
332+
val coursierClasspathArgs =
333+
classpath.flatMap(elem => Seq("--extra-jars", elem.syntax))
334+
335+
val ammCmd = List(
294336
"coursier",
295337
"launch",
296338
s"com.lihaoyi:ammonite_$scalaVersion:$ammVersion",
297339
"--main-class",
298340
"ammonite.Main"
299-
)
341+
) ++ coursierClasspathArgs ++ ("--" :: cmd.args)
300342

301-
val classpath = project.fullRuntimeClasspath(dag, state.client)
302-
val coursierClasspathArgs =
303-
classpath.flatMap(elem => Seq("--extra-jars", elem.syntax))
304-
305-
val ammArgs = "--" :: cmd.args
306-
307-
/**
308-
* Whenever `console` is run an extra `--out-file` parameter is added.
309-
* That file is later used to write a coursier command to and Bloopgun uses it
310-
* to download and run Ammonite.
311-
*/
312-
val ammoniteCmd = (coursierCmd ++ coursierClasspathArgs ++ ammArgs).mkString("\n")
313-
cmd.outFile match {
314-
case None => Task.now(state.withInfo(ammoniteCmd))
315-
case Some(outFile) =>
316-
try {
317-
Files.write(outFile, ammoniteCmd.getBytes(StandardCharsets.UTF_8))
318-
val msg = s"Wrote Ammonite command to $outFile"
319-
Task.now(state.withDebug(msg)(DebugFilter.All))
320-
} catch {
321-
case _: IOException =>
322-
val msg = s"Unexpected error when writing Ammonite command to $outFile"
323-
Task.now(state.withError(msg, ExitStatus.RunError))
324-
}
325-
}
343+
outputReplCommand(ammCmd, "Ammonite")
326344
}
327345
}
328346
case None => Task.now(reportMissing(project :: Nil, state))

0 commit comments

Comments
 (0)