[2.x] Minimalist console (#8722)

**Problem**
Forked console currently pulls in full Zinc, which includes JLine.

**Solution**
This implements a lighter-weight, full Java ForkConsoleMain,
which no longer depends on JLine.
This commit is contained in:
eugene yokota 2026-02-09 10:55:44 -05:00 committed by GitHub
parent 20ce3abe5f
commit edd7061f15
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 307 additions and 447 deletions

View File

@ -455,7 +455,10 @@ lazy val workerProj = (project in file("worker"))
exclude[DirectMissingMethodProblem]("sbt.internal.worker1.TestInfo.this"),
),
)
.configure(addSbtIOForTest)
.configure(
addSbtCompilerInterface,
addSbtIOForTest
)
lazy val exampleWorkProj = (project in file("internal") / "example-work")
.settings(

View File

@ -1,161 +0,0 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
package internal
import java.io.File
import java.net.URL
import java.nio.file.Paths
import sbt.internal.inc.{
AnalyzingCompiler,
PlainVirtualFile,
MappedFileConverter,
ScalaInstance,
ZincUtil
}
import sbt.internal.inc.classpath.ClasspathUtil
import sbt.internal.worker.{ ConsoleConfig, ScalaInstanceConfig }
import sbt.io.IO
import sbt.util.{ Level, Logger }
import sjsonnew.support.scalajson.unsafe.{ Parser, Converter }
import xsbti.compile.ClasspathOptionsUtil
/**
* Entry point for the forked console. This class creates a Scala REPL
* in the forked JVM with proper terminal support.
*/
class ConsoleMain:
def run(config: ConsoleConfig): Unit =
val si = scalaInstance(config.scalaInstanceConfig)
val compiler = analyzingCompiler(config, si)
given log: Logger = ConsoleMain.consoleLogger
val classpathJars = config.classpathJars.map(Paths.get(_))
val products = config.products.map(Paths.get(_))
val cpFiles = products.map(_.toFile()) ++ classpathJars.map(_.toFile())
IO.withTemporaryDirectory: tempDir =>
val fullCp = cpFiles ++ si.allJars
val loader =
ClasspathUtil.makeLoader(fullCp.map(_.toPath), ConsoleMain.jlineLoader, si, tempDir.toPath)
runConsole(
compiler = compiler,
classpath = cpFiles,
options = config.scalacOptions,
loader = loader,
initialCommands = config.initialCommands,
cleanupCommands = config.cleanupCommands,
)(using log)
private def runConsole(
compiler: AnalyzingCompiler,
classpath: Seq[File],
options: Seq[String],
loader: ClassLoader,
initialCommands: String,
cleanupCommands: String,
)(using log: Logger): Unit =
compiler.console(
classpath.map(x => PlainVirtualFile(x.toPath)),
MappedFileConverter.empty,
options,
initialCommands,
cleanupCommands,
log,
)(
Some(loader),
Nil
)
def analyzingCompiler(config: ConsoleConfig, si: ScalaInstance): AnalyzingCompiler =
val bridgeProvider = ZincUtil.constantBridgeProvider(
si,
config.bridgeJars.toList match
case x :: Nil => Paths.get(x)
case xs => sys.error(s"expected one bridge jar, but got $xs")
)
val classpathOptions = ClasspathOptionsUtil.repl()
AnalyzingCompiler(
si,
bridgeProvider,
classpathOptions,
_ => (),
None
)
def scalaInstance(siConfig: ScalaInstanceConfig): ScalaInstance =
val libraryJars = siConfig.libraryJars.map(Paths.get(_)).sortBy(_.getFileName.toString)
val allCompilerJars = siConfig.allCompilerJars
.map(Paths.get(_))
.sortBy(_.getFileName.toString)
val jlineJars = allCompilerJars.filter(_.getFileName.toString.contains("jline"))
val compilerJars =
allCompilerJars.filterNot(x => libraryJars.contains(x) || jlineJars.contains(x)).distinct
val extraToolJars0 = siConfig.extraToolJars.map(Paths.get(_)).sortBy(_.getFileName.toString())
val extraToolJars = extraToolJars0
.filterNot(jar => libraryJars.contains(jar) || compilerJars.contains(jar))
.distinct
val allJars = libraryJars ++ compilerJars ++ extraToolJars
// Use parent class loader for JLine to avoid conflicts
val libraryLoader = ClasspathUtil.toLoader(libraryJars, ConsoleMain.jlineLoader)
val compilerLoader = ClasspathUtil.toLoader(compilerJars, libraryLoader)
val fullLoader =
if extraToolJars.isEmpty then compilerLoader
else ClasspathUtil.toLoader(extraToolJars, compilerLoader)
new ScalaInstance(
version = siConfig.scalaVersion,
loader = fullLoader,
loaderCompilerOnly = compilerLoader,
loaderLibraryOnly = libraryLoader,
libraryJars = libraryJars.map(_.toFile).toArray,
compilerJars = compilerJars.map(_.toFile).toArray,
allJars = allJars.map(_.toFile).toArray,
explicitActual = Some(siConfig.scalaVersion)
)
end ConsoleMain
object ConsoleMain:
/** A simple console logger for the forked REPL process. */
private val consoleLogger: Logger = new Logger:
override def trace(t: => Throwable): Unit = t.printStackTrace()
override def success(message: => String): Unit = log(Level.Info, message)
override def log(level: Level.Value, message: => String): Unit =
level match
case Level.Debug => () // Suppress debug messages
case Level.Info => scala.Console.out.println(message)
case Level.Warn => scala.Console.err.println(s"[warn] $message")
case Level.Error => scala.Console.err.println(s"[error] $message")
class FilteredLoader(parent: ClassLoader) extends ClassLoader(parent):
override final def loadClass(className: String, resolve: Boolean): Class[?] =
if className.startsWith("org.jline.") || className.startsWith("java.") || className
.startsWith("javax.") || className.startsWith("sun.")
then super.loadClass(className, resolve)
else throw new ClassNotFoundException(className)
override def getResources(name: String): java.util.Enumeration[URL] = null
override def getResource(name: String): URL = null
end FilteredLoader
lazy val jlineLoader =
FilteredLoader(classOf[org.jline.terminal.Terminal].getClassLoader)
def main(args: Array[String]): Unit =
args.toList match
case Nil =>
scala.Console.err.println("ConsoleMain requires a config file argument starting with @")
sys.exit(1)
case arg :: Nil if arg.startsWith("@") =>
import sbt.internal.worker.codec.JsonProtocol.given
val configFile = arg.drop(1)
val content = IO.read(File(configFile))
val json = Parser.parseFromString(content).get
val config = Converter.fromJson[ConsoleConfig](json).get
val main = ConsoleMain()
main.run(config)
case _ =>
scala.Console.err.println("ConsoleMain requires exactly one argument: @<config-file>")
sys.exit(1)
end ConsoleMain

View File

@ -1,119 +0,0 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
package internal
import java.io.File
import java.net.URLClassLoader
import java.nio.file.{ Path, Paths }
import java.lang.{ ProcessBuilder as JProcessBuilder }
import sbt.internal.worker.ConsoleConfig
import sbt.io.IO
import sjsonnew.support.scalajson.unsafe.{ Converter, CompactPrinter }
/**
* Utilities for running the Scala console in a forked JVM.
*/
private[sbt] object ForkConsole:
/**
* Run the Scala console in a forked JVM.
*
* @param config Configuration for the console
* @param forkOptions Fork options (javaHome, jvmOptions, etc.)
* @return Exit code of the forked process
*/
def apply(config: ConsoleConfig, forkOptions: ForkOptions): Int =
IO.withTemporaryDirectory: tempDir =>
import sbt.internal.worker.codec.JsonProtocol.given
val json = Converter.toJson[ConsoleConfig](config).get
val params = tempDir.toPath.resolve("console-params.json")
IO.write(params.toFile, CompactPrinter(json))
run(
mainClass = classOf[ConsoleMain].getCanonicalName,
classpath = currentClasspath,
args = List(s"@$params"),
forkOptions = forkOptions,
)
/**
* Run an arbitrary main class in a forked JVM with full terminal inheritance.
* This is critical for interactive console to work properly with JLine.
*/
def run(
mainClass: String,
classpath: List[Path],
args: List[String],
forkOptions: ForkOptions,
): Int =
val fullCp = classpath.distinct
// Build environment variables for proper terminal handling
val termEnv = sys.env.get("TERM").getOrElse("xterm-256color")
val baseEnv = forkOptions.envVars ++ Map(
"TERM" -> termEnv,
"COLORTERM" -> sys.env.getOrElse("COLORTERM", "truecolor"),
)
// Add JLine-related JVM options to help with terminal detection
val jlineJvmOpts = Seq(
s"-Dorg.jline.terminal.type=$termEnv",
"-Djline.terminal=auto",
)
val allJvmOpts = forkOptions.runJVMOptions ++ jlineJvmOpts
// Build the java command
val javaHome = forkOptions.javaHome.getOrElse(new File(System.getProperty("java.home")))
val javaCmd = new File(new File(javaHome, "bin"), "java").getAbsolutePath
// Build full command line
val cmdArgs = Seq(javaCmd) ++
allJvmOpts ++
Seq("-classpath", fullCp.mkString(File.pathSeparator), mainClass) ++
args
// Use ProcessBuilder directly with inheritIO() for proper terminal handling
// This is critical for JLine arrow keys to work - all streams must be inherited
val jpb = new JProcessBuilder(cmdArgs*)
jpb.inheritIO() // Inherit stdin, stdout, stderr from parent process
forkOptions.workingDirectory.foreach(jpb.directory(_))
// Set environment variables
val env = jpb.environment()
baseEnv.foreach { case (k, v) => env.put(k, v) }
// Start and wait for process
val process = jpb.start()
process.waitFor()
/**
* Get the classpath of the current class loader.
* This is used to pass the sbt classes to the forked JVM.
*/
def currentClasspath: List[Path] =
val cl = classOf[ForkConsole.type].getClassLoader match
case cl: URLClassLoader => cl
case other =>
throw RuntimeException(
s"Expected URLClassLoader but got ${other.getClass.getName}"
)
val urls = cl.getURLs.toList
val extraJars = Vector(
IO.classLocationPath(classOf[xsbti.compile.ScalaInstance]),
IO.classLocationPath(classOf[xsbti.Logger]),
IO.classLocationPath(classOf[sbt.internal.inc.AnalyzingCompiler]),
IO.classLocationPath(classOf[sbt.internal.inc.classpath.ClasspathUtil.type]),
IO.classLocationPath(classOf[sbt.util.Logger]),
IO.classLocationPath(classOf[sjsonnew.JsonFormat[?]]),
IO.classLocationPath(classOf[jline.Terminal]),
IO.classLocationPath(classOf[org.jline.terminal.Terminal]),
IO.classLocationPath(classOf[org.jline.reader.LineReader]),
IO.classLocationPath(classOf[org.jline.utils.InfoCmp]),
IO.classLocationPath(classOf[org.jline.keymap.KeyMap[?]]),
)
(urls.map(u => Paths.get(u.toURI)) ++ extraJars).distinct
end ForkConsole

View File

@ -13,19 +13,20 @@ package client
import java.io.{ File, IOException, InputStream, PrintStream }
import java.lang.ProcessBuilder.Redirect
import java.net.{ Socket, SocketException }
import java.nio.file.{ Files, Paths }
import java.nio.file.Files
import java.util.UUID
import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference }
import java.util.concurrent.{ ConcurrentHashMap, LinkedBlockingQueue, TimeUnit }
import sbt.BasicCommandStrings.{ DashDashDetachStdio, DashDashServer, Shutdown, TerminateAction }
import sbt.internal.langserver.{ LogMessageParams, MessageType, PublishDiagnosticsParams }
import sbt.internal.worker.{ ClientJobParams, JvmRunInfo, NativeRunInfo, RunInfo }
import sbt.internal.worker.{ ClientJobParams, NativeRunInfo, RunInfo }
import sbt.internal.protocol.*
import sbt.internal.util.{
ConsoleAppender,
ConsoleOut,
MessageOnlyException,
RunHandler,
Signals,
Terminal,
Util
@ -33,7 +34,7 @@ import sbt.internal.util.{
import sbt.io.IO
import sbt.io.syntax.*
import sbt.protocol.*
import sbt.util.Level
import sbt.util.{ Level, Logger }
import sjsonnew.BasicJsonProtocol.*
import sjsonnew.shaded.scalajson.ast.unsafe.{ JObject, JValue }
import sjsonnew.support.scalajson.unsafe.Converter
@ -69,7 +70,6 @@ import Serialization.{
}
import NetworkClient.Arguments
import java.util.concurrent.TimeoutException
import sbt.util.Logger
trait ConsoleInterface {
def appendLog(level: Level.Value, message: => String): Unit
@ -756,26 +756,6 @@ class NetworkClient(
private def clientSideRun(runInfo: RunInfo): Try[Unit] = {
runInfo.windowTitle.foreach(setWindowTitle)
def jvmRun(info: JvmRunInfo): Try[Unit] = {
val option = ForkOptions(
javaHome = info.javaHome.map(new File(_)),
outputStrategy = None, // TODO: Handle buffered output etc
bootJars = Vector.empty,
workingDirectory = info.workingDirectory.map(new File(_)),
runJVMOptions = info.jvmOptions,
connectInput = info.connectInput,
envVars = info.environmentVariables,
)
// ForkRun handles exit code handling and cancellation
val runner = new ForkRun(option)
runner
.run(
mainClass = info.mainClass,
classpath = info.classpath.map(_.path).map(Paths.get),
options = info.args,
log = log
)
}
def nativeRun(info: NativeRunInfo): Try[Unit] = {
import java.lang.{ ProcessBuilder as JProcessBuilder }
val option = ForkOptions(
@ -798,7 +778,8 @@ class NetworkClient(
}
Run.processExitCode(exitCode, "runner")
}
if (runInfo.jvm) jvmRun(runInfo.jvmRunInfo.getOrElse(sys.error("missing jvmRunInfo")))
if (runInfo.jvm)
RunHandler.jvmRun(runInfo.jvmRunInfo.getOrElse(sys.error("missing jvmRunInfo")), log)
else nativeRun(runInfo.nativeRunInfo.getOrElse(sys.error("missing nativeRunInfo")))
}

View File

@ -0,0 +1,40 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
package internal
package util
import java.io.File
import java.nio.file.Paths
import sbt.internal.worker.JvmRunInfo
import sbt.util.Logger
import scala.util.Try
// Process runinfos
object RunHandler:
def jvmRun(info: JvmRunInfo, log: Logger): Try[Unit] =
val option = ForkOptions(
javaHome = info.javaHome.map(File(_)),
outputStrategy = None,
bootJars = Vector.empty,
workingDirectory = info.workingDirectory.map(File(_)),
runJVMOptions = info.jvmOptions,
connectInput = info.connectInput,
envVars = info.environmentVariables,
)
// ForkRun handles exit code handling and cancellation
val runner = new ForkRun(option)
runner
.run(
mainClass = info.mainClass,
classpath = info.classpath.map(_.path).map(Paths.get),
options = info.args,
log = log
)
end RunHandler

View File

@ -10,14 +10,16 @@ package sbt
package internal
import java.io.{ File, PrintWriter }
import java.nio.file.{ Path, Paths }
import java.util.ArrayList
import sbt.BuildExtra.*
import sbt.Keys.Classpath
import sbt.internal.CommandStrings
import sbt.internal.inc.{ AnalyzingCompiler, ScalaInstance, ZincLmUtil }
import sbt.internal.inc.classpath.ClasspathUtil
import sbt.internal.worker.{ ClientJobParams, ScalaInstanceConfig }
import sbt.internal.worker.codec.JsonProtocol.given
import sbt.internal.util.{ Attributed, MessageOnlyException, Terminal as ITerminal }
import sbt.internal.worker1.{ ConsoleInfo, WorkerMain }
import sbt.internal.util.{ Attributed, RunHandler, Terminal as ITerminal }
import sbt.io.IO
import sbt.protocol.Serialization
import sbt.librarymanagement.{
@ -31,10 +33,13 @@ import sbt.librarymanagement.{
VersionNumber
}
import sbt.util.Logger
import sjsonnew.support.scalajson.unsafe.{ Converter, CompactPrinter }
import scala.jdk.CollectionConverters.*
import scala.util.Random
import xsbti.{ HashedVirtualFileRef, ScalaProvider }
object Compiler:
private val r = Random()
def scalaInstanceTask(
configKey: TaskKey[ScalaInstanceConfig]
): Def.Initialize[Task[ScalaInstance]] =
@ -300,7 +305,8 @@ object Compiler:
classpath: Def.Initialize[Task[Classpath]],
): Def.Initialize[Task[Unit]] =
Def.taskIf {
if (task / Keys.fork).value then forkedConsoleTask(task, products, classpath).value
if (task / Keys.fork).value || (Keys.state.value.isNetworkCommand && (task / Keys.clientSide).value)
then forkedConsoleTask(task, products, classpath).value
else serverSideConsoleTask(task, products, classpath).value
}
@ -334,7 +340,6 @@ object Compiler:
classpath: Def.Initialize[Task[Classpath]],
): Def.Initialize[Task[Unit]] =
Def.task {
import sbt.internal.worker.ConsoleConfig
val s = Keys.streams.value
val conv = Keys.fileConverter.value
val cside = (task / Keys.clientSide).value
@ -344,33 +349,39 @@ object Compiler:
val siConfig = (Keys.console / Keys.scalaInstanceConfig).value
val bridgeJars = Keys.scalaCompilerBridgeJars.value
val state = Keys.state.value
val config = ConsoleConfig(
scalaInstanceConfig = siConfig,
bridgeJars = bridgeJars.toVector.map(vf => conv.toPath(vf).toUri()),
products = products.value.toVector.map(vf => conv.toPath(vf.data).toUri()),
classpathJars = classpath.value.toVector.map(vf => conv.toPath(vf.data).toUri()),
scalacOptions = (task / Keys.scalacOptions).value.toVector,
initialCommands = (task / Keys.initialCommands).value,
cleanupCommands = (task / Keys.cleanupCommands).value,
)
val toolJars = siConfig.libraryJars ++ siConfig.allCompilerJars ++ siConfig.extraToolJars
val toolJarsVf = toolJars.map(u => conv.toVirtualFile(Paths.get(u)): HashedVirtualFileRef)
val fo = (task / Keys.forkOptions).value
val service = Keys.bgJobService.value
val workingDir = service.createWorkingDirectory
val cp = service.copyClasspath(
products.value,
classpath.value,
workingDir,
conv,
)
val param = ConsoleInfo(
ArrayList(toolJars.asJava),
ArrayList(bridgeJars.toVector.map(vf => conv.toPath(vf).toUri()).asJava),
ArrayList(),
ArrayList(Attributed.data(cp).toVector.map(vf => conv.toPath(vf).toUri()).asJava),
ArrayList((task / Keys.scalacOptions).value.asJava),
(task / Keys.initialCommands).value,
(task / Keys.cleanupCommands).value,
)
val randomId = r.nextLong()
val workerMainClass = classOf[WorkerMain].getCanonicalName
val workerCp0 = workerClasspath.map: p =>
Attributed.blank(conv.toVirtualFile(p): HashedVirtualFileRef)
val workerCp = workerCp0 ++ Attributed.blankSeq(bridgeJars ++ toolJarsVf).toVector
val g = WorkerMain.mkGson()
val paramJson = g.toJson(param, param.getClass)
val json = jsonRpcRequest(randomId, "console", paramJson)
val params = workingDir.toPath.resolve("console-params.json")
IO.write(params.toFile, json)
val info =
RunUtil.mkRunInfo(Vector(s"@$params"), workerMainClass, workerCp, fo, conv, None)
if cside && state.isNetworkCommand then
val workingDir = service.createWorkingDirectory
val cp = service.copyClasspath(
products.value,
classpath.value,
workingDir,
conv,
)
val workerMainClass = classOf[ConsoleMain].getCanonicalName
val workerCp = ForkConsole.currentClasspath.map: p =>
Attributed.blank(conv.toVirtualFile(p): HashedVirtualFileRef)
val json = Converter.toJson[ConsoleConfig](config).get
val params = workingDir.toPath.resolve("console-params.json")
IO.write(params.toFile, CompactPrinter(json))
val info =
RunUtil.mkRunInfo(Vector(s"@$params"), workerMainClass, workerCp, fo, conv, None)
val result = ClientJobParams(
runInfo = info
)
@ -381,9 +392,7 @@ object Compiler:
s.log.info("running console (fork)")
try
terminal.restore()
val exitCode = ForkConsole(config, fo)
if exitCode != 0 then
throw MessageOnlyException(s"Forked console exited with code $exitCode")
RunHandler.jvmRun(info.jvmRunInfo.get, s.log).get
finally terminal.restore()
println()
}
@ -396,6 +405,18 @@ object Compiler:
try exported(w, command)
finally w.close() // workaround for gh-937
private def workerClasspath: Vector[Path] =
Vector(
IO.classLocationPath(classOf[WorkerMain]),
IO.classLocationPath(classOf[org.scalasbt.shadedgson.com.google.gson.Gson]),
IO.classLocationPath(classOf[xsbti.compile.ScalaInstance]),
IO.classLocationPath(classOf[xsbti.Logger]),
IO.classLocationPath(classOf[sbt.testing.Framework]),
)
private def jsonRpcRequest(id: Long, method: String, params: String): String =
s"""{ "jsonrpc": "2.0", "method": "$method", "params": $params, "id": $id }"""
def consoleForkOptions: Def.Initialize[Task[ForkOptions]] = Def.task {
// Build environment variables for proper terminal handling
val termEnv = sys.env.get("TERM").getOrElse("xterm-256color")

View File

@ -1 +1 @@
sbt.version=1.12.0
sbt.version=1.12.2

View File

@ -1,57 +0,0 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.internal.worker
/** Configuration for forked console. */
final class ConsoleConfig private (
val scalaInstanceConfig: sbt.internal.worker.ScalaInstanceConfig,
val bridgeJars: Vector[java.net.URI],
val products: Vector[java.net.URI],
val classpathJars: Vector[java.net.URI],
val scalacOptions: Vector[String],
val initialCommands: String,
val cleanupCommands: String) extends Serializable {
override def equals(o: Any): Boolean = this.eq(o.asInstanceOf[AnyRef]) || (o match {
case x: ConsoleConfig => (this.scalaInstanceConfig == x.scalaInstanceConfig) && (this.bridgeJars == x.bridgeJars) && (this.products == x.products) && (this.classpathJars == x.classpathJars) && (this.scalacOptions == x.scalacOptions) && (this.initialCommands == x.initialCommands) && (this.cleanupCommands == x.cleanupCommands)
case _ => false
})
override def hashCode: Int = {
37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (17 + "sbt.internal.worker.ConsoleConfig".##) + scalaInstanceConfig.##) + bridgeJars.##) + products.##) + classpathJars.##) + scalacOptions.##) + initialCommands.##) + cleanupCommands.##)
}
override def toString: String = {
"ConsoleConfig(" + scalaInstanceConfig + ", " + bridgeJars + ", " + products + ", " + classpathJars + ", " + scalacOptions + ", " + initialCommands + ", " + cleanupCommands + ")"
}
private def copy(scalaInstanceConfig: sbt.internal.worker.ScalaInstanceConfig = scalaInstanceConfig, bridgeJars: Vector[java.net.URI] = bridgeJars, products: Vector[java.net.URI] = products, classpathJars: Vector[java.net.URI] = classpathJars, scalacOptions: Vector[String] = scalacOptions, initialCommands: String = initialCommands, cleanupCommands: String = cleanupCommands): ConsoleConfig = {
new ConsoleConfig(scalaInstanceConfig, bridgeJars, products, classpathJars, scalacOptions, initialCommands, cleanupCommands)
}
def withScalaInstanceConfig(scalaInstanceConfig: sbt.internal.worker.ScalaInstanceConfig): ConsoleConfig = {
copy(scalaInstanceConfig = scalaInstanceConfig)
}
def withBridgeJars(bridgeJars: Vector[java.net.URI]): ConsoleConfig = {
copy(bridgeJars = bridgeJars)
}
def withProducts(products: Vector[java.net.URI]): ConsoleConfig = {
copy(products = products)
}
def withClasspathJars(classpathJars: Vector[java.net.URI]): ConsoleConfig = {
copy(classpathJars = classpathJars)
}
def withScalacOptions(scalacOptions: Vector[String]): ConsoleConfig = {
copy(scalacOptions = scalacOptions)
}
def withInitialCommands(initialCommands: String): ConsoleConfig = {
copy(initialCommands = initialCommands)
}
def withCleanupCommands(cleanupCommands: String): ConsoleConfig = {
copy(cleanupCommands = cleanupCommands)
}
}
object ConsoleConfig {
def apply(scalaInstanceConfig: sbt.internal.worker.ScalaInstanceConfig, bridgeJars: Vector[java.net.URI], products: Vector[java.net.URI], classpathJars: Vector[java.net.URI], scalacOptions: Vector[String], initialCommands: String, cleanupCommands: String): ConsoleConfig = new ConsoleConfig(scalaInstanceConfig, bridgeJars, products, classpathJars, scalacOptions, initialCommands, cleanupCommands)
}

View File

@ -1,39 +0,0 @@
/**
* This code is generated using [[https://www.scala-sbt.org/contraband]].
*/
// DO NOT EDIT MANUALLY
package sbt.internal.worker.codec
import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError }
trait ConsoleConfigFormats { self: sbt.internal.worker.codec.ScalaInstanceConfigFormats & sjsonnew.BasicJsonProtocol =>
given ConsoleConfigFormat: JsonFormat[sbt.internal.worker.ConsoleConfig] = new JsonFormat[sbt.internal.worker.ConsoleConfig] {
override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.worker.ConsoleConfig = {
__jsOpt match {
case Some(__js) =>
unbuilder.beginObject(__js)
val scalaInstanceConfig = unbuilder.readField[sbt.internal.worker.ScalaInstanceConfig]("scalaInstanceConfig")
val bridgeJars = unbuilder.readField[Vector[java.net.URI]]("bridgeJars")
val products = unbuilder.readField[Vector[java.net.URI]]("products")
val classpathJars = unbuilder.readField[Vector[java.net.URI]]("classpathJars")
val scalacOptions = unbuilder.readField[Vector[String]]("scalacOptions")
val initialCommands = unbuilder.readField[String]("initialCommands")
val cleanupCommands = unbuilder.readField[String]("cleanupCommands")
unbuilder.endObject()
sbt.internal.worker.ConsoleConfig(scalaInstanceConfig, bridgeJars, products, classpathJars, scalacOptions, initialCommands, cleanupCommands)
case None =>
deserializationError("Expected JsObject but found None")
}
}
override def write[J](obj: sbt.internal.worker.ConsoleConfig, builder: Builder[J]): Unit = {
builder.beginObject()
builder.addField("scalaInstanceConfig", obj.scalaInstanceConfig)
builder.addField("bridgeJars", obj.bridgeJars)
builder.addField("products", obj.products)
builder.addField("classpathJars", obj.classpathJars)
builder.addField("scalacOptions", obj.scalacOptions)
builder.addField("initialCommands", obj.initialCommands)
builder.addField("cleanupCommands", obj.cleanupCommands)
builder.endObject()
}
}
}

View File

@ -11,5 +11,4 @@ trait JsonProtocol extends sjsonnew.BasicJsonProtocol
with sbt.internal.worker.codec.RunInfoFormats
with sbt.internal.worker.codec.ClientJobParamsFormats
with sbt.internal.worker.codec.ScalaInstanceConfigFormats
with sbt.internal.worker.codec.ConsoleConfigFormats
object JsonProtocol extends JsonProtocol

View File

@ -58,14 +58,3 @@ type ScalaInstanceConfig {
allCompilerJars: [java.net.URI]
extraToolJars: [java.net.URI] @since("0.1.0")
}
## Configuration for forked console.
type ConsoleConfig {
scalaInstanceConfig: sbt.internal.worker.ScalaInstanceConfig!
bridgeJars: [java.net.URI]
products: [java.net.URI]
classpathJars: [java.net.URI]
scalacOptions: [String]
initialCommands: String!
cleanupCommands: String!
}

View File

@ -0,0 +1,40 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal.worker1;
import java.net.URI;
import java.io.Serializable;
import java.util.ArrayList;
public class ConsoleInfo implements Serializable {
public ArrayList<URI> toolsJars;
public ArrayList<URI> bridgeJars;
public ArrayList<URI> products;
public ArrayList<URI> classpathJars;
public ArrayList<String> scalacOptions;
public String initialCommands;
public String cleanupCommands;
public ConsoleInfo(
ArrayList<URI> toolsJars,
ArrayList<URI> bridgeJars,
ArrayList<URI> products,
ArrayList<URI> classpathJars,
ArrayList<String> scalacOptions,
String initialCommands,
String cleanupCommands) {
this.toolsJars = toolsJars;
this.bridgeJars = bridgeJars;
this.products = products;
this.classpathJars = classpathJars;
this.scalacOptions = scalacOptions;
this.initialCommands = initialCommands;
this.cleanupCommands = cleanupCommands;
}
}

View File

@ -0,0 +1,116 @@
package sbt.internal.worker1;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Paths;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.ServiceLoader;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import xsbti.compile.ConsoleInterface1;
public final class ForkConsoleMain {
public void run(long id, ConsoleInfo info) throws Exception {
try {
Class cls = ConsoleInterface1.class;
Iterator iter = ServiceLoader.load(cls, ForkConsoleMain.class.getClassLoader()).iterator();
List<ConsoleInterface1> list = new ArrayList<>();
while (iter.hasNext()) {
list.add((ConsoleInterface1) iter.next());
}
if (list.size() > 0) {
runInterface1(list.get(0), info);
} else {
runOldInterface(info);
}
} catch (Throwable e) {
e.printStackTrace();
System.exit(1);
}
}
private void runInterface1(ConsoleInterface1 intf, ConsoleInfo info) throws Exception {
String toolsJars =
info.toolsJars
.stream()
.map(u -> Paths.get(u).toString())
.collect(Collectors.joining(File.pathSeparator));
String classpathJars =
Stream.concat(info.products.stream(), info.classpathJars.stream())
.map(u -> Paths.get(u).toString())
.collect(Collectors.joining(File.pathSeparator));
intf.run(
info.scalacOptions.toArray(new String[0]),
toolsJars,
classpathJars,
info.initialCommands,
info.cleanupCommands,
createClassLoader(info, ForkConsoleMain.class.getClassLoader()),
new String[] {},
new Object[] {},
new ZeroLogger());
}
private void runOldInterface(ConsoleInfo info) throws Exception {
Class concrete = Class.forName("xsbt.ConsoleInterface");
Object instance = concrete.getDeclaredConstructor().newInstance();
Method m =
concrete.getMethod(
"run",
String[].class,
String.class,
String.class,
String.class,
String.class,
ClassLoader.class,
String[].class,
Object[].class,
xsbti.Logger.class);
String toolsJars =
info.toolsJars
.stream()
.map(u -> Paths.get(u).toString())
.collect(Collectors.joining(File.pathSeparator));
String classpathJars =
Stream.concat(info.products.stream(), info.classpathJars.stream())
.map(u -> Paths.get(u).toString())
.collect(Collectors.joining(File.pathSeparator));
m.invoke(
instance,
info.scalacOptions.toArray(new String[0]),
toolsJars,
classpathJars,
info.initialCommands,
info.cleanupCommands,
createClassLoader(info, concrete.getClassLoader()),
new String[] {},
new Object[] {},
new ZeroLogger());
}
private URLClassLoader createClassLoader(ConsoleInfo info, ClassLoader parent) {
URL[] urls =
Stream.concat(info.products.stream(), info.classpathJars.stream())
.map(
u -> {
try {
return u.toURL();
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
})
.toArray(URL[]::new);
return new URLClassLoader(urls, parent);
}
public static void main(long id, ConsoleInfo info) throws Exception {
new ForkConsoleMain().run(id, info);
}
}

View File

@ -21,6 +21,9 @@ import java.io.IOException;
import java.io.PrintStream;
import java.net.InetAddress;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
@ -67,6 +70,10 @@ public final class WorkerMain {
WorkerMain app = new WorkerMain();
app.consoleWork();
System.exit(0);
} else if (args.length == 1 && args[0].startsWith("@")) {
WorkerMain app = new WorkerMain();
app.argFileWork(Paths.get(args[0].substring(1)));
System.exit(0);
} else if (args.length == 2 && args[0].equals("--tcp")) {
WorkerMain app = new WorkerMain();
int serverPort = Integer.parseInt(args[1]);
@ -99,6 +106,13 @@ public final class WorkerMain {
}
}
void argFileWork(Path arg) throws Exception {
this.jsonOut = this.originalOut;
byte[] encoded = Files.readAllBytes(arg);
String line = new String(encoded, "UTF-8");
process(line);
}
void socketWork(int serverPort) throws Exception {
InetAddress loopback = InetAddress.getByName(null);
Socket client = new Socket(loopback, serverPort);
@ -132,6 +146,10 @@ public final class WorkerMain {
TestInfo testInfo = g.fromJson(params, TestInfo.class);
test(id, testInfo);
break;
case "console":
ConsoleInfo consoleInfo = g.fromJson(params, ConsoleInfo.class);
console(id, consoleInfo);
return;
case "bye":
break;
}
@ -178,6 +196,11 @@ public final class WorkerMain {
}
}
void console(long id, ConsoleInfo info) throws Exception {
ForkConsoleMain.main(id, info);
return;
}
private URLClassLoader createClassLoader(RunInfo.JvmRunInfo info, ClassLoader parent) {
URL[] urls =
info.classpath

View File

@ -0,0 +1,24 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt.internal.worker1;
import java.util.function.Supplier;
import xsbti.Logger;
public class ZeroLogger implements Logger {
public void error(Supplier<String> msg) {}
public void warn(Supplier<String> msg) {}
public void info(Supplier<String> msg) {}
public void debug(Supplier<String> msg) {}
public void trace(Supplier<Throwable> exception) {}
}