diff --git a/build.sbt b/build.sbt index 77159fe48..c3ec08cc4 100644 --- a/build.sbt +++ b/build.sbt @@ -530,6 +530,27 @@ lazy val testAgentProj = (project in file("testing") / "agent") mimaSettings, ) +lazy val workerProj = (project in file("worker")) + .dependsOn(exampleWorkProj % Test) + .settings( + name := "worker", + testedBaseSettings, + Compile / doc / javacOptions := Nil, + autoScalaLibrary := false, + libraryDependencies += gson, + libraryDependencies += "org.scala-lang" %% "scala3-library" % scalaVersion.value % Test, + // run / fork := false, + Test / fork := true, + ) + .configure(addSbtIOForTest) + +lazy val exampleWorkProj = (project in file("internal") / "example-work") + .settings( + minimalSettings, + name := "example work", + publish / skip := true, + ) + // Basic task engine lazy val taskProj = (project in file("tasks")) .dependsOn(collectionProj, utilControl) @@ -656,6 +677,8 @@ lazy val actionsProj = (project in file("main-actions")) utilLogging, utilRelation, utilTracking, + workerProj, + protocolProj, ) .settings( testedBaseSettings, @@ -666,18 +689,9 @@ lazy val actionsProj = (project in file("main-actions")) baseDirectory.value / "src" / "main" / "contraband-scala", Compile / generateContrabands / sourceManaged := baseDirectory.value / "src" / "main" / "contraband-scala", Compile / generateContrabands / contrabandFormatsForType := ContrabandConfig.getFormats, + // Test / fork := true, + Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.Flat, mimaSettings, - mimaBinaryIssueFilters ++= Seq( - // Removed unused private[sbt] nested class - exclude[MissingClassProblem]("sbt.Doc$Scaladoc"), - // Removed no longer used private[sbt] method - exclude[DirectMissingMethodProblem]("sbt.Doc.generate"), - exclude[DirectMissingMethodProblem]("sbt.compiler.Eval.filesModifiedBytes"), - exclude[DirectMissingMethodProblem]("sbt.compiler.Eval.fileModifiedBytes"), - exclude[DirectMissingMethodProblem]("sbt.Doc.$init$"), - // Added field in nested private[this] class - exclude[ReversedMissingMethodProblem]("sbt.compiler.Eval#EvalType.sourceName"), - ), ) .dependsOn(lmCore) .configure( @@ -1231,6 +1245,7 @@ def allProjects = lmCoursier, lmCoursierShaded, lmCoursierShadedPublishing, + workerProj, ) ++ lowerUtilProjects // These need to be cross published to 2.12 and 2.13 for Zinc diff --git a/internal/example-work/src/main/scala/Hello.scala b/internal/example-work/src/main/scala/Hello.scala new file mode 100644 index 000000000..44224ff98 --- /dev/null +++ b/internal/example-work/src/main/scala/Hello.scala @@ -0,0 +1,9 @@ +package example + +class Hello + +object Hello: + def main(args: Array[String]): Unit = + if args.toList == List("boom") then sys.error("boom") + else println(s"${args.mkString}") +end Hello diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 3d2ec8b80..4e0a7dda8 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -58,6 +58,7 @@ object Dependencies { } def addSbtIO = addSbtModule(sbtIoPath, "io", sbtIO) + def addSbtIOForTest = addSbtModule(sbtIoPath, "io", sbtIO, Some(Test)) def addSbtCompilerInterface = addSbtModule(sbtZincPath, "compilerInterface", compilerInterface) def addSbtCompilerClasspath = addSbtModule(sbtZincPath, "zincClasspath", compilerClasspath) @@ -91,6 +92,7 @@ object Dependencies { val templateResolverApi = "org.scala-sbt" % "template-resolver" % "0.1" val remoteapis = "com.eed3si9n.remoteapis.shaded" % "shaded-remoteapis-java" % "2.3.0-M1-52317e00d8d4c37fa778c628485d220fb68a8d08" + val gson = "com.google.code.gson" % "gson" % "2.13.1" val scalaCompiler = "org.scala-lang" %% "scala3-compiler" % scala3 val scala3Library = "org.scala-lang" %% "scala3-library" % scala3 diff --git a/worker/src/main/java/sbt/internal/worker1/FilePath.java b/worker/src/main/java/sbt/internal/worker1/FilePath.java new file mode 100644 index 000000000..ab120ce4e --- /dev/null +++ b/worker/src/main/java/sbt/internal/worker1/FilePath.java @@ -0,0 +1,13 @@ +package sbt.internal.worker1; + +import java.net.URI; + +public class FilePath { + public URI path; + public String digest; + + public FilePath(URI path, String digest) { + this.path = path; + this.digest = digest; + } +} diff --git a/worker/src/main/java/sbt/internal/worker1/RunInfo.java b/worker/src/main/java/sbt/internal/worker1/RunInfo.java new file mode 100644 index 000000000..39744a7b7 --- /dev/null +++ b/worker/src/main/java/sbt/internal/worker1/RunInfo.java @@ -0,0 +1,43 @@ +/* + * 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.ArrayList; + +public class RunInfo { + public class JvmRunInfo { + public ArrayList args; + public ArrayList classpath; + public String mainClass; + public boolean connectInput; + + public JvmRunInfo( + ArrayList args, + ArrayList classpath, + String mainClass, + boolean connectInput) { + this.args = args; + this.classpath = classpath; + this.mainClass = mainClass; + this.connectInput = connectInput; + } + } + + public class NativeRunInfo {} + + public boolean jvm; + public JvmRunInfo jvmRunInfo; + public NativeRunInfo nativeRunInfo; + + public RunInfo(boolean jvm, JvmRunInfo jvmRunInfo, NativeRunInfo nativeRunInfo) { + this.jvm = jvm; + this.jvmRunInfo = jvmRunInfo; + this.nativeRunInfo = nativeRunInfo; + } +} diff --git a/worker/src/main/java/sbt/internal/worker1/WorkerMain.java b/worker/src/main/java/sbt/internal/worker1/WorkerMain.java new file mode 100644 index 000000000..1f6ccc058 --- /dev/null +++ b/worker/src/main/java/sbt/internal/worker1/WorkerMain.java @@ -0,0 +1,108 @@ +/* + * 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 com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Scanner; + +public final class WorkerMain { + private PrintStream originalOut; + + public static void main(final String[] args) throws Exception { + try { + if (args.length == 0) { + WorkerMain app = new WorkerMain(); + app.consoleWork(); + } else { + System.err.println("missing args"); + System.exit(1); + } + } catch (Throwable e) { + e.printStackTrace(); + System.exit(1); + } + } + + WorkerMain() { + this.originalOut = System.out; + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + System.setOut(new PrintStream(baos)); + } + + void consoleWork() throws Exception { + Scanner input = new Scanner(System.in); + if (input.hasNextLine()) { + String line = input.nextLine(); + process(line); + } + } + + void process(String json) throws Exception { + JsonElement elem = JsonParser.parseString(json); + JsonObject o = elem.getAsJsonObject(); + if (!o.has("jsonrpc")) { + throw new RuntimeException("missing jsonprc element"); + } + long id = o.getAsJsonPrimitive("id").getAsLong(); + String method = o.getAsJsonPrimitive("method").getAsString(); + JsonObject params = o.getAsJsonObject("params"); + switch (method) { + case "run": + Gson g = new Gson(); + RunInfo info = g.fromJson(params, RunInfo.class); + run(info); + break; + } + } + + void run(RunInfo info) throws Exception { + if (info.jvm) { + if (info.jvmRunInfo == null) { + throw new RuntimeException("missing jvmRunInfo element"); + } + RunInfo.JvmRunInfo jvmRunInfo = info.jvmRunInfo; + URL[] urls = + jvmRunInfo + .classpath + .stream() + .map( + filePath -> { + try { + return filePath.path.toURL(); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + }) + .toArray(URL[]::new); + URLClassLoader cl = new URLClassLoader(urls, ClassLoader.getSystemClassLoader()); + try { + Class mainClass = cl.loadClass(jvmRunInfo.mainClass); + Method mainMethod = mainClass.getMethod("main", String[].class); + String[] mainArgs = jvmRunInfo.args.stream().toArray(String[]::new); + mainMethod.invoke(null, (Object) mainArgs); + } finally { + cl.close(); + } + } else { + throw new RuntimeException("only jvm is supported"); + } + } +} diff --git a/worker/src/test/scala/sbt/internal/worker1/WorkerTest.scala b/worker/src/test/scala/sbt/internal/worker1/WorkerTest.scala new file mode 100644 index 000000000..f516ab45c --- /dev/null +++ b/worker/src/test/scala/sbt/internal/worker1/WorkerTest.scala @@ -0,0 +1,19 @@ +package sbt.internal.worker1 + +import sbt.io.IO + +object WorkerTest extends verify.BasicTestSuite: + val main = WorkerMain() + + test("process") { + val u0 = IO.classLocationPath(classOf[example.Hello]).toUri() + val u1 = IO.classLocationPath(classOf[scala.quoted.Quotes]).toUri() + val u2 = IO.classLocationPath(classOf[scala.AnyVal]).toUri() + val cp = + s"""[{ "path": "${u0}", "digest": "" }, { "path": "${u1}", "digest": "" }, { "path": "${u2}", "digest": "" }]""" + val runInfo = + s"""{ "jvm": true, "jvmRunInfo": { "args": ["hi"], "classpath": $cp, "mainClass": "example.Hello" } }""" + val json = s"""{ "jsonrpc": "2.0", "id": 1, "method": "run", "params": $runInfo }""" + main.process(json) + } +end WorkerTest