mirror of https://github.com/sbt/sbt.git
[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:
parent
20ce3abe5f
commit
edd7061f15
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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")))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
sbt.version=1.12.0
|
||||
sbt.version=1.12.2
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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!
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
}
|
||||
Loading…
Reference in New Issue