From edd7061f15940d5fcd4d0664f0b0c01643a1705d Mon Sep 17 00:00:00 2001 From: eugene yokota Date: Mon, 9 Feb 2026 10:55:44 -0500 Subject: [PATCH] [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. --- build.sbt | 5 +- .../main/scala/sbt/internal/ConsoleMain.scala | 161 ------------------ .../main/scala/sbt/internal/ForkConsole.scala | 119 ------------- .../sbt/internal/client/NetworkClient.scala | 31 +--- .../scala/sbt/internal/util/RunHandler.scala | 40 +++++ .../main/scala/sbt/internal/Compiler.scala | 85 +++++---- project/build.properties | 2 +- .../sbt/internal/worker/ConsoleConfig.scala | 57 ------- .../worker/codec/ConsoleConfigFormats.scala | 39 ----- .../internal/worker/codec/JsonProtocol.scala | 1 - protocol/src/main/contraband/worker.contra | 11 -- .../sbt/internal/worker1/ConsoleInfo.java | 40 +++++ .../sbt/internal/worker1/ForkConsoleMain.java | 116 +++++++++++++ .../java/sbt/internal/worker1/WorkerMain.java | 23 +++ .../java/sbt/internal/worker1/ZeroLogger.java | 24 +++ 15 files changed, 307 insertions(+), 447 deletions(-) delete mode 100644 main-actions/src/main/scala/sbt/internal/ConsoleMain.scala delete mode 100644 main-actions/src/main/scala/sbt/internal/ForkConsole.scala create mode 100644 main-command/src/main/scala/sbt/internal/util/RunHandler.scala delete mode 100644 protocol/src/main/contraband-scala/sbt/internal/worker/ConsoleConfig.scala delete mode 100644 protocol/src/main/contraband-scala/sbt/internal/worker/codec/ConsoleConfigFormats.scala create mode 100644 worker/src/main/java/sbt/internal/worker1/ConsoleInfo.java create mode 100644 worker/src/main/java/sbt/internal/worker1/ForkConsoleMain.java create mode 100644 worker/src/main/java/sbt/internal/worker1/ZeroLogger.java diff --git a/build.sbt b/build.sbt index 67bb1163b..6b4929bf4 100644 --- a/build.sbt +++ b/build.sbt @@ -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( diff --git a/main-actions/src/main/scala/sbt/internal/ConsoleMain.scala b/main-actions/src/main/scala/sbt/internal/ConsoleMain.scala deleted file mode 100644 index 190a51d99..000000000 --- a/main-actions/src/main/scala/sbt/internal/ConsoleMain.scala +++ /dev/null @@ -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: @") - sys.exit(1) -end ConsoleMain diff --git a/main-actions/src/main/scala/sbt/internal/ForkConsole.scala b/main-actions/src/main/scala/sbt/internal/ForkConsole.scala deleted file mode 100644 index cb170b1d0..000000000 --- a/main-actions/src/main/scala/sbt/internal/ForkConsole.scala +++ /dev/null @@ -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 diff --git a/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala index 84c21bdfb..71cf3aadf 100644 --- a/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala +++ b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala @@ -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"))) } diff --git a/main-command/src/main/scala/sbt/internal/util/RunHandler.scala b/main-command/src/main/scala/sbt/internal/util/RunHandler.scala new file mode 100644 index 000000000..b3505ff9f --- /dev/null +++ b/main-command/src/main/scala/sbt/internal/util/RunHandler.scala @@ -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 diff --git a/main/src/main/scala/sbt/internal/Compiler.scala b/main/src/main/scala/sbt/internal/Compiler.scala index baf677433..54d803432 100644 --- a/main/src/main/scala/sbt/internal/Compiler.scala +++ b/main/src/main/scala/sbt/internal/Compiler.scala @@ -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") diff --git a/project/build.properties b/project/build.properties index 30b7fd9fd..4d6c56708 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.12.0 +sbt.version=1.12.2 diff --git a/protocol/src/main/contraband-scala/sbt/internal/worker/ConsoleConfig.scala b/protocol/src/main/contraband-scala/sbt/internal/worker/ConsoleConfig.scala deleted file mode 100644 index 29cfca061..000000000 --- a/protocol/src/main/contraband-scala/sbt/internal/worker/ConsoleConfig.scala +++ /dev/null @@ -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) -} diff --git a/protocol/src/main/contraband-scala/sbt/internal/worker/codec/ConsoleConfigFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/worker/codec/ConsoleConfigFormats.scala deleted file mode 100644 index 29e99c68d..000000000 --- a/protocol/src/main/contraband-scala/sbt/internal/worker/codec/ConsoleConfigFormats.scala +++ /dev/null @@ -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() - } -} -} diff --git a/protocol/src/main/contraband-scala/sbt/internal/worker/codec/JsonProtocol.scala b/protocol/src/main/contraband-scala/sbt/internal/worker/codec/JsonProtocol.scala index f774de7bf..d740aec0e 100644 --- a/protocol/src/main/contraband-scala/sbt/internal/worker/codec/JsonProtocol.scala +++ b/protocol/src/main/contraband-scala/sbt/internal/worker/codec/JsonProtocol.scala @@ -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 \ No newline at end of file diff --git a/protocol/src/main/contraband/worker.contra b/protocol/src/main/contraband/worker.contra index d24fc0043..e41cf29a6 100644 --- a/protocol/src/main/contraband/worker.contra +++ b/protocol/src/main/contraband/worker.contra @@ -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! -} diff --git a/worker/src/main/java/sbt/internal/worker1/ConsoleInfo.java b/worker/src/main/java/sbt/internal/worker1/ConsoleInfo.java new file mode 100644 index 000000000..db18b09f3 --- /dev/null +++ b/worker/src/main/java/sbt/internal/worker1/ConsoleInfo.java @@ -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 toolsJars; + public ArrayList bridgeJars; + public ArrayList products; + public ArrayList classpathJars; + public ArrayList scalacOptions; + public String initialCommands; + public String cleanupCommands; + + public ConsoleInfo( + ArrayList toolsJars, + ArrayList bridgeJars, + ArrayList products, + ArrayList classpathJars, + ArrayList 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; + } +} diff --git a/worker/src/main/java/sbt/internal/worker1/ForkConsoleMain.java b/worker/src/main/java/sbt/internal/worker1/ForkConsoleMain.java new file mode 100644 index 000000000..724ab26cb --- /dev/null +++ b/worker/src/main/java/sbt/internal/worker1/ForkConsoleMain.java @@ -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 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); + } +} diff --git a/worker/src/main/java/sbt/internal/worker1/WorkerMain.java b/worker/src/main/java/sbt/internal/worker1/WorkerMain.java index 92730be2a..2a2ebf9c8 100644 --- a/worker/src/main/java/sbt/internal/worker1/WorkerMain.java +++ b/worker/src/main/java/sbt/internal/worker1/WorkerMain.java @@ -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 diff --git a/worker/src/main/java/sbt/internal/worker1/ZeroLogger.java b/worker/src/main/java/sbt/internal/worker1/ZeroLogger.java new file mode 100644 index 000000000..9bce70ec0 --- /dev/null +++ b/worker/src/main/java/sbt/internal/worker1/ZeroLogger.java @@ -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 msg) {} + + public void warn(Supplier msg) {} + + public void info(Supplier msg) {} + + public void debug(Supplier msg) {} + + public void trace(Supplier exception) {} +}