From ad0ab07e99c98f0bb40fcafc22358cdabf17b86a Mon Sep 17 00:00:00 2001 From: Friendseeker <66892505+Friendseeker@users.noreply.github.com> Date: Mon, 25 Nov 2024 17:38:12 -0800 Subject: [PATCH 01/24] Return jvmBuildTarget for workspace/buildTargets --- .../internal/server/BuildServerProtocol.scala | 36 +++++++++++++- .../sbt/internal/bsp/JvmBuildTarget.scala | 48 +++++++++++++++++++ .../sbt/internal/bsp/ScalaBuildTarget.scala | 23 ++++++--- .../sbt/internal/bsp/codec/JsonProtocol.scala | 1 + .../bsp/codec/JvmBuildTargetFormats.scala | 29 +++++++++++ .../bsp/codec/SbtBuildTargetFormats.scala | 2 +- .../bsp/codec/ScalaBuildTargetFormats.scala | 6 ++- protocol/src/main/contraband/bsp.contra | 14 ++++++ 8 files changed, 147 insertions(+), 12 deletions(-) create mode 100644 protocol/src/main/contraband-scala/sbt/internal/bsp/JvmBuildTarget.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/bsp/codec/JvmBuildTargetFormats.scala diff --git a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala index 88e07d77de..b063cfa287 100644 --- a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala +++ b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala @@ -34,6 +34,7 @@ import sjsonnew.support.scalajson.unsafe.{ CompactPrinter, Converter, Parser => import xsbti.CompileFailed import java.io.File +import java.nio.file.Paths import java.util.concurrent.atomic.AtomicBoolean import scala.collection.mutable @@ -614,12 +615,19 @@ object BuildServerProtocol { val thisProjectRef = Keys.thisProjectRef.value val thisConfig = Keys.configuration.value val scalaJars = Keys.scalaInstance.value.allJars.map(_.toURI.toString) + val (javaHomeForTarget, isForkedJava) = javaHome.value match { + case Some(forkedJava) => (Some(forkedJava.toURI), true) + case None => (sys.props.get("java.home").map(Paths.get(_)).map(_.toUri), false) + } + val javaVersionForTarget = extractJavaVersion(javacOptions.value, isForkedJava) + val jvmBuildTarget = JvmBuildTarget(javaHomeForTarget, javaVersionForTarget) val compileData = ScalaBuildTarget( scalaOrganization = scalaOrganization.value, scalaVersion = scalaVersion.value, scalaBinaryVersion = scalaBinaryVersion.value, platform = ScalaPlatform.JVM, - jars = scalaJars.toVector + jars = scalaJars.toVector, + jvmBuildTarget = jvmBuildTarget, ) val configuration = Keys.configuration.value val displayName = BuildTargetName.fromScope(thisProject.id, configuration.name) @@ -659,7 +667,11 @@ object BuildServerProtocol { scalaVersion = scalaProvider.version(), scalaBinaryVersion = binaryScalaVersion(scalaProvider.version()), platform = ScalaPlatform.JVM, - jars = scalaJars.toVector.map(_.toURI.toString) + jars = scalaJars.toVector.map(_.toURI.toString), + jvmBuildTarget = JvmBuildTarget( + sys.props.get("java.home").map(Paths.get(_)).map(_.toUri), + sys.props.get("java.version") + ), ) val sbtVersionValue = sbtVersion.value val sbtData = SbtBuildTarget( @@ -985,6 +997,26 @@ object BuildServerProtocol { ) } + private def extractJavaVersion( + javacOptions: Seq[String], + isForkedJava: Boolean + ): Option[String] = { + def getVersionAfterFlag(flag: String): Option[String] = { + val index = javacOptions.indexOf(flag) + if (index >= 0) javacOptions.lift(index + 1) + else None + } + + val versionFromJavacOption = getVersionAfterFlag("--release") + .orElse(getVersionAfterFlag("--target")) + .orElse(getVersionAfterFlag("-target")) + + versionFromJavacOption.orElse { + // TODO: extract java version from forked javac + if (isForkedJava) None else sys.props.get("java.version") + } + } + // naming convention still seems like the only reliable way to get IntelliJ to import this correctly // https://github.com/JetBrains/intellij-scala/blob/a54c2a7c157236f35957049cbfd8c10587c9e60c/scala/scala-impl/src/org/jetbrains/sbt/language/SbtFileImpl.scala#L82-L84 private def toSbtTargetIdName(ref: LoadedBuildUnit): String = { diff --git a/protocol/src/main/contraband-scala/sbt/internal/bsp/JvmBuildTarget.scala b/protocol/src/main/contraband-scala/sbt/internal/bsp/JvmBuildTarget.scala new file mode 100644 index 0000000000..488b8b8e68 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/bsp/JvmBuildTarget.scala @@ -0,0 +1,48 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.bsp +/** + * Contains jvm-specific metadata, specifically JDK reference + * @param javaHome Uri representing absolute path to jdk + * @param javaVersion The java version this target is supposed to use (can be set using javac `-target` flag) + */ +final class JvmBuildTarget private ( + val javaHome: Option[java.net.URI], + val javaVersion: Option[String]) extends Serializable { + + + + override def equals(o: Any): Boolean = this.eq(o.asInstanceOf[AnyRef]) || (o match { + case x: JvmBuildTarget => (this.javaHome == x.javaHome) && (this.javaVersion == x.javaVersion) + case _ => false + }) + override def hashCode: Int = { + 37 * (37 * (37 * (17 + "sbt.internal.bsp.JvmBuildTarget".##) + javaHome.##) + javaVersion.##) + } + override def toString: String = { + "JvmBuildTarget(" + javaHome + ", " + javaVersion + ")" + } + private[this] def copy(javaHome: Option[java.net.URI] = javaHome, javaVersion: Option[String] = javaVersion): JvmBuildTarget = { + new JvmBuildTarget(javaHome, javaVersion) + } + def withJavaHome(javaHome: Option[java.net.URI]): JvmBuildTarget = { + copy(javaHome = javaHome) + } + def withJavaHome(javaHome: java.net.URI): JvmBuildTarget = { + copy(javaHome = Option(javaHome)) + } + def withJavaVersion(javaVersion: Option[String]): JvmBuildTarget = { + copy(javaVersion = javaVersion) + } + def withJavaVersion(javaVersion: String): JvmBuildTarget = { + copy(javaVersion = Option(javaVersion)) + } +} +object JvmBuildTarget { + + def apply(javaHome: Option[java.net.URI], javaVersion: Option[String]): JvmBuildTarget = new JvmBuildTarget(javaHome, javaVersion) + def apply(javaHome: java.net.URI, javaVersion: String): JvmBuildTarget = new JvmBuildTarget(Option(javaHome), Option(javaVersion)) +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/bsp/ScalaBuildTarget.scala b/protocol/src/main/contraband-scala/sbt/internal/bsp/ScalaBuildTarget.scala index 32e5885a91..e686d84dd5 100644 --- a/protocol/src/main/contraband-scala/sbt/internal/bsp/ScalaBuildTarget.scala +++ b/protocol/src/main/contraband-scala/sbt/internal/bsp/ScalaBuildTarget.scala @@ -14,28 +14,30 @@ package sbt.internal.bsp For example, 2.12 if scalaVersion is 2.12.4. * @param platform The target platform for this target * @param jars A sequence of Scala jars such as scala-library, scala-compiler and scala-reflect. + * @param jvmBuildTarget The jvm build target describing jdk to be used */ final class ScalaBuildTarget private ( val scalaOrganization: String, val scalaVersion: String, val scalaBinaryVersion: String, val platform: Int, - val jars: Vector[String]) extends Serializable { + val jars: Vector[String], + val jvmBuildTarget: Option[sbt.internal.bsp.JvmBuildTarget]) extends Serializable { override def equals(o: Any): Boolean = this.eq(o.asInstanceOf[AnyRef]) || (o match { - case x: ScalaBuildTarget => (this.scalaOrganization == x.scalaOrganization) && (this.scalaVersion == x.scalaVersion) && (this.scalaBinaryVersion == x.scalaBinaryVersion) && (this.platform == x.platform) && (this.jars == x.jars) + case x: ScalaBuildTarget => (this.scalaOrganization == x.scalaOrganization) && (this.scalaVersion == x.scalaVersion) && (this.scalaBinaryVersion == x.scalaBinaryVersion) && (this.platform == x.platform) && (this.jars == x.jars) && (this.jvmBuildTarget == x.jvmBuildTarget) case _ => false }) override def hashCode: Int = { - 37 * (37 * (37 * (37 * (37 * (37 * (17 + "sbt.internal.bsp.ScalaBuildTarget".##) + scalaOrganization.##) + scalaVersion.##) + scalaBinaryVersion.##) + platform.##) + jars.##) + 37 * (37 * (37 * (37 * (37 * (37 * (37 * (17 + "sbt.internal.bsp.ScalaBuildTarget".##) + scalaOrganization.##) + scalaVersion.##) + scalaBinaryVersion.##) + platform.##) + jars.##) + jvmBuildTarget.##) } override def toString: String = { - "ScalaBuildTarget(" + scalaOrganization + ", " + scalaVersion + ", " + scalaBinaryVersion + ", " + platform + ", " + jars + ")" + "ScalaBuildTarget(" + scalaOrganization + ", " + scalaVersion + ", " + scalaBinaryVersion + ", " + platform + ", " + jars + ", " + jvmBuildTarget + ")" } - private[this] def copy(scalaOrganization: String = scalaOrganization, scalaVersion: String = scalaVersion, scalaBinaryVersion: String = scalaBinaryVersion, platform: Int = platform, jars: Vector[String] = jars): ScalaBuildTarget = { - new ScalaBuildTarget(scalaOrganization, scalaVersion, scalaBinaryVersion, platform, jars) + private[this] def copy(scalaOrganization: String = scalaOrganization, scalaVersion: String = scalaVersion, scalaBinaryVersion: String = scalaBinaryVersion, platform: Int = platform, jars: Vector[String] = jars, jvmBuildTarget: Option[sbt.internal.bsp.JvmBuildTarget] = jvmBuildTarget): ScalaBuildTarget = { + new ScalaBuildTarget(scalaOrganization, scalaVersion, scalaBinaryVersion, platform, jars, jvmBuildTarget) } def withScalaOrganization(scalaOrganization: String): ScalaBuildTarget = { copy(scalaOrganization = scalaOrganization) @@ -52,8 +54,15 @@ final class ScalaBuildTarget private ( def withJars(jars: Vector[String]): ScalaBuildTarget = { copy(jars = jars) } + def withJvmBuildTarget(jvmBuildTarget: Option[sbt.internal.bsp.JvmBuildTarget]): ScalaBuildTarget = { + copy(jvmBuildTarget = jvmBuildTarget) + } + def withJvmBuildTarget(jvmBuildTarget: sbt.internal.bsp.JvmBuildTarget): ScalaBuildTarget = { + copy(jvmBuildTarget = Option(jvmBuildTarget)) + } } object ScalaBuildTarget { - def apply(scalaOrganization: String, scalaVersion: String, scalaBinaryVersion: String, platform: Int, jars: Vector[String]): ScalaBuildTarget = new ScalaBuildTarget(scalaOrganization, scalaVersion, scalaBinaryVersion, platform, jars) + def apply(scalaOrganization: String, scalaVersion: String, scalaBinaryVersion: String, platform: Int, jars: Vector[String], jvmBuildTarget: Option[sbt.internal.bsp.JvmBuildTarget]): ScalaBuildTarget = new ScalaBuildTarget(scalaOrganization, scalaVersion, scalaBinaryVersion, platform, jars, jvmBuildTarget) + def apply(scalaOrganization: String, scalaVersion: String, scalaBinaryVersion: String, platform: Int, jars: Vector[String], jvmBuildTarget: sbt.internal.bsp.JvmBuildTarget): ScalaBuildTarget = new ScalaBuildTarget(scalaOrganization, scalaVersion, scalaBinaryVersion, platform, jars, Option(jvmBuildTarget)) } diff --git a/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/JsonProtocol.scala b/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/JsonProtocol.scala index f6cae48d80..c98c92a87b 100644 --- a/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/JsonProtocol.scala +++ b/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/JsonProtocol.scala @@ -55,6 +55,7 @@ trait JsonProtocol extends sjsonnew.BasicJsonProtocol with sbt.internal.bsp.codec.TestResultFormats with sbt.internal.bsp.codec.RunParamsFormats with sbt.internal.bsp.codec.RunResultFormats + with sbt.internal.bsp.codec.JvmBuildTargetFormats with sbt.internal.bsp.codec.ScalaBuildTargetFormats with sbt.internal.bsp.codec.ScalacOptionsParamsFormats with sbt.internal.bsp.codec.ScalacOptionsItemFormats diff --git a/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/JvmBuildTargetFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/JvmBuildTargetFormats.scala new file mode 100644 index 0000000000..301e778ba8 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/JvmBuildTargetFormats.scala @@ -0,0 +1,29 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.bsp.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait JvmBuildTargetFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val JvmBuildTargetFormat: JsonFormat[sbt.internal.bsp.JvmBuildTarget] = new JsonFormat[sbt.internal.bsp.JvmBuildTarget] { + override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.bsp.JvmBuildTarget = { + __jsOpt match { + case Some(__js) => + unbuilder.beginObject(__js) + val javaHome = unbuilder.readField[Option[java.net.URI]]("javaHome") + val javaVersion = unbuilder.readField[Option[String]]("javaVersion") + unbuilder.endObject() + sbt.internal.bsp.JvmBuildTarget(javaHome, javaVersion) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.internal.bsp.JvmBuildTarget, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("javaHome", obj.javaHome) + builder.addField("javaVersion", obj.javaVersion) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/SbtBuildTargetFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/SbtBuildTargetFormats.scala index ff96c4211e..4e057a969d 100644 --- a/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/SbtBuildTargetFormats.scala +++ b/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/SbtBuildTargetFormats.scala @@ -5,7 +5,7 @@ // DO NOT EDIT MANUALLY package sbt.internal.bsp.codec import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } -trait SbtBuildTargetFormats { self: sbt.internal.bsp.codec.ScalaBuildTargetFormats with sjsonnew.BasicJsonProtocol with sbt.internal.bsp.codec.BuildTargetIdentifierFormats => +trait SbtBuildTargetFormats { self: sbt.internal.bsp.codec.ScalaBuildTargetFormats with sbt.internal.bsp.codec.JvmBuildTargetFormats with sjsonnew.BasicJsonProtocol with sbt.internal.bsp.codec.BuildTargetIdentifierFormats => implicit lazy val SbtBuildTargetFormat: JsonFormat[sbt.internal.bsp.SbtBuildTarget] = new JsonFormat[sbt.internal.bsp.SbtBuildTarget] { override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.bsp.SbtBuildTarget = { __jsOpt match { diff --git a/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/ScalaBuildTargetFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/ScalaBuildTargetFormats.scala index 900994c4ed..67b1389a36 100644 --- a/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/ScalaBuildTargetFormats.scala +++ b/protocol/src/main/contraband-scala/sbt/internal/bsp/codec/ScalaBuildTargetFormats.scala @@ -5,7 +5,7 @@ // DO NOT EDIT MANUALLY package sbt.internal.bsp.codec import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } -trait ScalaBuildTargetFormats { self: sjsonnew.BasicJsonProtocol => +trait ScalaBuildTargetFormats { self: sbt.internal.bsp.codec.JvmBuildTargetFormats with sjsonnew.BasicJsonProtocol => implicit lazy val ScalaBuildTargetFormat: JsonFormat[sbt.internal.bsp.ScalaBuildTarget] = new JsonFormat[sbt.internal.bsp.ScalaBuildTarget] { override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.bsp.ScalaBuildTarget = { __jsOpt match { @@ -16,8 +16,9 @@ implicit lazy val ScalaBuildTargetFormat: JsonFormat[sbt.internal.bsp.ScalaBuild val scalaBinaryVersion = unbuilder.readField[String]("scalaBinaryVersion") val platform = unbuilder.readField[Int]("platform") val jars = unbuilder.readField[Vector[String]]("jars") + val jvmBuildTarget = unbuilder.readField[Option[sbt.internal.bsp.JvmBuildTarget]]("jvmBuildTarget") unbuilder.endObject() - sbt.internal.bsp.ScalaBuildTarget(scalaOrganization, scalaVersion, scalaBinaryVersion, platform, jars) + sbt.internal.bsp.ScalaBuildTarget(scalaOrganization, scalaVersion, scalaBinaryVersion, platform, jars, jvmBuildTarget) case None => deserializationError("Expected JsObject but found None") } @@ -29,6 +30,7 @@ implicit lazy val ScalaBuildTargetFormat: JsonFormat[sbt.internal.bsp.ScalaBuild builder.addField("scalaBinaryVersion", obj.scalaBinaryVersion) builder.addField("platform", obj.platform) builder.addField("jars", obj.jars) + builder.addField("jvmBuildTarget", obj.jvmBuildTarget) builder.endObject() } } diff --git a/protocol/src/main/contraband/bsp.contra b/protocol/src/main/contraband/bsp.contra index a931a8fb63..1956cf4404 100644 --- a/protocol/src/main/contraband/bsp.contra +++ b/protocol/src/main/contraband/bsp.contra @@ -605,6 +605,17 @@ type RunResult { } +# JVM extension + +## Contains jvm-specific metadata, specifically JDK reference +type JvmBuildTarget { + ## Uri representing absolute path to jdk + javaHome: java.net.URI + + ## The java version this target is supposed to use (can be set using javac `-target` flag) + javaVersion: String +} + # Scala Extension ## Contains scala-specific metadata for compiling a target containing Scala sources. @@ -626,6 +637,9 @@ type ScalaBuildTarget { ## A sequence of Scala jars such as scala-library, scala-compiler and scala-reflect. jars: [String]! + + ## The jvm build target describing jdk to be used + jvmBuildTarget: sbt.internal.bsp.JvmBuildTarget } ## Scalac options From 655310061f2fd7cc6d201f046f87286dc16961dc Mon Sep 17 00:00:00 2001 From: Friendseeker <66892505+Friendseeker@users.noreply.github.com> Date: Thu, 26 Dec 2024 14:56:18 -0800 Subject: [PATCH 02/24] Add unit test --- .../src/test/scala/testpkg/BuildServerTest.scala | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/server-test/src/test/scala/testpkg/BuildServerTest.scala b/server-test/src/test/scala/testpkg/BuildServerTest.scala index 15ec8a1da9..c24694587e 100644 --- a/server-test/src/test/scala/testpkg/BuildServerTest.scala +++ b/server-test/src/test/scala/testpkg/BuildServerTest.scala @@ -53,6 +53,16 @@ object BuildServerTest extends AbstractServerTest { result.targets.find(_.displayName.contains("buildserver-build")).get assert(buildServerBuildTarget.id.uri.toString.endsWith("#buildserver-build")) assert(!result.targets.exists(_.displayName.contains("badBuildTarget"))) + // Check for JVM based Scala Project, built target should contain Java version information + val scalaBuildTarget = + Converter.fromJsonOptionUnsafe[ScalaBuildTarget](utilTarget.data) + val javaTarget = scalaBuildTarget.jvmBuildTarget + (javaTarget.flatMap(_.javaVersion), javaTarget.flatMap(_.javaHome)) match { + case (Some(javaVersion), Some(javaHome)) => + assert(javaVersion.equals(sys.props("java.version"))) + assert(javaHome.equals(Paths.get(sys.props("java.home")).toUri)) + case _ => fail("JVM build target should contain javaVersion and javaHome") + } } test("buildTarget/sources") { _ => From 838eee97cdac8d7e3c04859301a51fae4dc83525 Mon Sep 17 00:00:00 2001 From: Friendseeker <66892505+Friendseeker@users.noreply.github.com> Date: Thu, 26 Dec 2024 15:05:00 -0800 Subject: [PATCH 03/24] Fix CI --- project/build.properties | 2 +- sbt-app/src/sbt-test/project/scripted13/test | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/project/build.properties b/project/build.properties index e88a0d817d..73df629ac1 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.6 +sbt.version=1.10.7 diff --git a/sbt-app/src/sbt-test/project/scripted13/test b/sbt-app/src/sbt-test/project/scripted13/test index d40ea3d077..2fd6077fb1 100644 --- a/sbt-app/src/sbt-test/project/scripted13/test +++ b/sbt-app/src/sbt-test/project/scripted13/test @@ -1,6 +1,6 @@ # This tests that this sbt scripted plugin can launch the previous one -> ^^1.10.6 +> ^^1.10.7 $ copy-file changes/A.scala src/sbt-test/a/b/A.scala > scripted From e23419efedc8243776b643a0e80b67701b5fb404 Mon Sep 17 00:00:00 2001 From: friendseeker <66892505+Friendseeker@users.noreply.github.com> Date: Sun, 22 Dec 2024 01:12:49 -0800 Subject: [PATCH 04/24] Clean Zinc Cache for 'Compile / clean', 'Test / clean' --- main/src/main/scala/sbt/Defaults.scala | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 942fccd555..6400d4080d 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -928,7 +928,20 @@ object Defaults extends BuildCommon { tastyFiles.map(_.getAbsoluteFile) } else Nil }.value, - clean := (compileOutputs / clean).value, + clean := { + val _ = (compileOutputs / clean).value + val setup = compileIncSetup.value + try { + val store = AnalysisUtil.staticCachedStore( + analysisFile = setup.cacheFile.toPath, + useTextAnalysis = !enableBinaryCompileAnalysis.value, + useConsistent = enableConsistentCompileAnalysis.value, + ) + store.clearCache() + } catch { + case NonFatal(_) => () + } + }, earlyOutputPing := Def.promise[Boolean], compileProgress := { val s = streams.value From 13373415b3b49fb51e2c4a480042b6b16ff2cb4e Mon Sep 17 00:00:00 2001 From: Friendseeker <66892505+Friendseeker@users.noreply.github.com> Date: Thu, 26 Dec 2024 18:43:04 -0800 Subject: [PATCH 05/24] Fix CI --- main/src/main/scala/sbt/Defaults.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 6400d4080d..71c1a49126 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -930,10 +930,10 @@ object Defaults extends BuildCommon { }.value, clean := { val _ = (compileOutputs / clean).value - val setup = compileIncSetup.value + val analysisFile = compileAnalysisFile.value try { val store = AnalysisUtil.staticCachedStore( - analysisFile = setup.cacheFile.toPath, + analysisFile = analysisFile.toPath, useTextAnalysis = !enableBinaryCompileAnalysis.value, useConsistent = enableConsistentCompileAnalysis.value, ) From 1a8fa65af388c298f8eaa7dfec84c04f78862e38 Mon Sep 17 00:00:00 2001 From: Friendseeker <66892505+Friendseeker@users.noreply.github.com> Date: Mon, 30 Dec 2024 17:55:42 -0800 Subject: [PATCH 06/24] Avoid upstream compilation when calling previousCompile --- main/src/main/scala/sbt/Defaults.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 71c1a49126..70987ad57b 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -2580,9 +2580,9 @@ object Defaults extends BuildCommon { private[sbt] def jnone[A]: Optional[A] = none[A].toOptional def compileAnalysisSettings: Seq[Setting[_]] = Seq( previousCompile := { - val setup = compileIncSetup.value + val analysisFile = compileAnalysisFile.value val store = AnalysisUtil.staticCachedStore( - analysisFile = setup.cacheFile.toPath, + analysisFile = analysisFile.toPath, useTextAnalysis = !enableBinaryCompileAnalysis.value, useConsistent = enableConsistentCompileAnalysis.value, ) From c834f500b91d36155a3542bbb4b8fb47a965f3f7 Mon Sep 17 00:00:00 2001 From: friendseeker <66892505+Friendseeker@users.noreply.github.com> Date: Mon, 30 Dec 2024 19:30:15 -0800 Subject: [PATCH 07/24] Add comment --- main/src/main/scala/sbt/Defaults.scala | 1 + 1 file changed, 1 insertion(+) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 70987ad57b..a5e114b815 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -2580,6 +2580,7 @@ object Defaults extends BuildCommon { private[sbt] def jnone[A]: Optional[A] = none[A].toOptional def compileAnalysisSettings: Seq[Setting[_]] = Seq( previousCompile := { + // Avoid compileIncSetup since it would trigger upstream compilation val analysisFile = compileAnalysisFile.value val store = AnalysisUtil.staticCachedStore( analysisFile = analysisFile.toPath, From a13bfd3ef96f1e7ac8906bb8f3fe70575caf6677 Mon Sep 17 00:00:00 2001 From: Derek Wickern Date: Sat, 11 Jan 2025 12:46:35 -0800 Subject: [PATCH 08/24] fix race condition in NetworkChannel --- main/src/main/scala/sbt/internal/server/NetworkChannel.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/main/src/main/scala/sbt/internal/server/NetworkChannel.scala b/main/src/main/scala/sbt/internal/server/NetworkChannel.scala index 95c0e16c0d..dfe167cf44 100644 --- a/main/src/main/scala/sbt/internal/server/NetworkChannel.scala +++ b/main/src/main/scala/sbt/internal/server/NetworkChannel.scala @@ -241,7 +241,6 @@ final class NetworkChannel( } } } - thread.start() private[sbt] def isLanguageServerProtocol: Boolean = true @@ -365,7 +364,6 @@ final class NetworkChannel( impl() }, s"sbt-$name-write-thread") writeThread.setDaemon(true) - writeThread.start() def publishBytes(event: Array[Byte], delimit: Boolean): Unit = try pendingWrites.put(event -> delimit) @@ -914,6 +912,9 @@ final class NetworkChannel( } } private[sbt] def isAttached: Boolean = attached.get + + thread.start() + writeThread.start() } object NetworkChannel { From f0afda3dd0ad477a42beb2d51c63f23f151f173b Mon Sep 17 00:00:00 2001 From: Derek Wickern Date: Sat, 11 Jan 2025 13:08:18 -0800 Subject: [PATCH 09/24] make NetworkChannel#thread private --- main/src/main/scala/sbt/internal/server/NetworkChannel.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/src/main/scala/sbt/internal/server/NetworkChannel.scala b/main/src/main/scala/sbt/internal/server/NetworkChannel.scala index dfe167cf44..6bf3558c63 100644 --- a/main/src/main/scala/sbt/internal/server/NetworkChannel.scala +++ b/main/src/main/scala/sbt/internal/server/NetworkChannel.scala @@ -163,7 +163,7 @@ final class NetworkChannel( } } - val thread = new Thread(s"sbt-networkchannel-${connection.getPort}") { + private[this] val thread = new Thread(s"sbt-networkchannel-${connection.getPort}") { private val ct = "Content-Type: " private val x1 = "application/sbt-x1" override def run(): Unit = { From e46843bfd9b87c3d733c6e4105ee8dc714cf3252 Mon Sep 17 00:00:00 2001 From: Lukas Rytz Date: Fri, 17 Jan 2025 15:53:24 +0100 Subject: [PATCH 10/24] Add setting to allow demoting the SIP-51 build failure Add a `allowUnsafeScalaLibUpgrade` setting (default is `false`) to demote the SIP-51 build failure to a warning. If the scalaVersion is 2.13.12 but some dependency pulls in scala-library 2.13.13, the compiler will stay at 2.13.12, but the dependency classpath will contain scala-library 2.13.13. This usually works, the compiler can run fine with a newer scala-library on its dependency classpath. Macro expansion may fail, if the macro uses some library class / method that doesn't exist in the old version. The macro itself is loaded from the dependency classpath into the class loader running the compiler, where the older Scala library is on the runtime classpath. Using the Scala REPL in sbt may also fail in a similar fashion. --- main/src/main/scala/sbt/Defaults.scala | 52 ++++++++++++------- main/src/main/scala/sbt/Keys.scala | 1 + .../main/scala/sbt/internal/LintUnused.scala | 1 + .../stdlib-unfreeze-warn/a/A.scala | 27 ++++++++++ .../stdlib-unfreeze-warn/b/B.scala | 7 +++ .../stdlib-unfreeze-warn/build.sbt | 39 ++++++++++++++ .../stdlib-unfreeze-warn/c/C.scala | 7 +++ .../stdlib-unfreeze-warn/test | 11 ++++ 8 files changed, 127 insertions(+), 18 deletions(-) create mode 100644 sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-warn/a/A.scala create mode 100644 sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-warn/b/B.scala create mode 100644 sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-warn/build.sbt create mode 100644 sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-warn/c/C.scala create mode 100644 sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-warn/test diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index a5e114b815..fa9b6f5090 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -180,6 +180,7 @@ object Defaults extends BuildCommon { apiMappings := Map.empty, autoScalaLibrary :== true, managedScalaInstance :== true, + allowUnsafeScalaLibUpgrade :== false, classpathEntryDefinesClass := { (file: File) => sys.error("use classpathEntryDefinesClassVF instead") }, @@ -1178,6 +1179,7 @@ object Defaults extends BuildCommon { def scalaInstanceFromUpdate: Initialize[Task[ScalaInstance]] = Def.task { val sv = scalaVersion.value val fullReport = update.value + val s = streams.value // For Scala 3, update scala-library.jar in `scala-tool` and `scala-doc-tool` in case a newer version // is present in the `compile` configuration. This is needed once forwards binary compatibility is dropped @@ -1204,24 +1206,38 @@ object Defaults extends BuildCommon { ) if (Classpaths.isScala213(sv)) { - for { - compileReport <- fullReport.configuration(Configurations.Compile) - libName <- ScalaArtifacts.Artifacts - } { - for (lib <- compileReport.modules.find(_.module.name == libName)) { - val libVer = lib.module.revision - val n = name.value - if (VersionNumber(sv).matchesSemVer(SemanticSelector(s"<$libVer"))) - sys.error( - s"""expected `$n/scalaVersion` to be "$libVer" or later, - |but found "$sv"; upgrade scalaVersion to fix the build. - | - |to support backwards-only binary compatibility (SIP-51), - |the Scala 2.13 compiler cannot be older than $libName on the - |dependency classpath. - |see `$n/evicted` to know why $libName $libVer is getting pulled in. - |""".stripMargin - ) + val scalaDeps = for { + compileReport <- fullReport.configuration(Configurations.Compile).iterator + libName <- ScalaArtifacts.Artifacts.iterator + lib <- compileReport.modules.find(_.module.name == libName) + } yield lib + for (lib <- scalaDeps.take(1)) { + val libVer = lib.module.revision + val libName = lib.module.name + val n = name.value + if (VersionNumber(sv).matchesSemVer(SemanticSelector(s"<$libVer"))) { + val err = !allowUnsafeScalaLibUpgrade.value + val fix = + if (err) + """Upgrade the `scalaVersion` to fix the build. If upgrading the Scala compiler version is + |not possible (for example due to a regression in the compiler or a missing dependency), + |this error can be demoted by setting `allowUnsafeScalaLibUpgrade := true`.""".stripMargin + else + s"""Note that the dependency classpath and the runtime classpath of your project + |contain the newer $libName $libVer, even if the scalaVersion is $sv. + |Compilation (macro expansion) or using the Scala REPL in sbt may fail with a LinkageError.""".stripMargin + + val msg = + s"""Expected `$n/scalaVersion` to be $libVer or later, but found $sv. + |To support backwards-only binary compatibility (SIP-51), the Scala 2.13 compiler + |should not be older than $libName on the dependency classpath. + | + |$fix + | + |See `$n/evicted` to know why $libName $libVer is getting pulled in. + |""".stripMargin + if (err) sys.error(msg) + else s.log.warn(msg) } } } diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 9349773d7f..94b1f7def7 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -571,6 +571,7 @@ object Keys { val conflictManager = settingKey[ConflictManager]("Selects the conflict manager to use for dependency management.").withRank(CSetting) val autoScalaLibrary = settingKey[Boolean]("Adds a dependency on scala-library if true.").withRank(ASetting) val managedScalaInstance = settingKey[Boolean]("Automatically obtains Scala tools as managed dependencies if true.").withRank(BSetting) + val allowUnsafeScalaLibUpgrade = settingKey[Boolean]("Allow the Scala library on the compilation classpath to be newer than the scalaVersion (see Scala SIP-51).").withRank(CSetting) val sbtResolver = settingKey[Resolver]("Provides a resolver for obtaining sbt as a dependency.").withRank(BMinusSetting) val sbtResolvers = settingKey[Seq[Resolver]]("The external resolvers for sbt and plugin dependencies.").withRank(BMinusSetting) val sbtDependency = settingKey[ModuleID]("Provides a definition for declaring the current version of sbt.").withRank(BMinusSetting) diff --git a/main/src/main/scala/sbt/internal/LintUnused.scala b/main/src/main/scala/sbt/internal/LintUnused.scala index c2d2db0062..8df675c15b 100644 --- a/main/src/main/scala/sbt/internal/LintUnused.scala +++ b/main/src/main/scala/sbt/internal/LintUnused.scala @@ -34,6 +34,7 @@ object LintUnused { commands, crossScalaVersions, crossSbtVersions, + allowUnsafeScalaLibUpgrade, initialize, lintUnusedKeysOnLoad, onLoad, diff --git a/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-warn/a/A.scala b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-warn/a/A.scala new file mode 100644 index 0000000000..c66551e1bd --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-warn/a/A.scala @@ -0,0 +1,27 @@ +import scala.language.reflectiveCalls + + +package scala.collection.immutable { + object Exp { + // Access RedBlackTree.validate added in Scala 2.13.13 + def v = RedBlackTree.validate(null)(null) + } +} + + +object A extends App { + println(scala.util.Properties.versionString) +} + +object AMacro { + import scala.language.experimental.macros + import scala.reflect.macros.blackbox.Context + + def m(x: Int): Int = macro impl + + def impl(c: Context)(x: c.Expr[Int]): c.Expr[Int] = { + import c.universe._ + println(scala.collection.immutable.Exp.v) + c.Expr(q"2 + $x") + } +} diff --git a/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-warn/b/B.scala b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-warn/b/B.scala new file mode 100644 index 0000000000..f75b7905ef --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-warn/b/B.scala @@ -0,0 +1,7 @@ +import java.nio.file.{Paths, Files} +import java.nio.charset.StandardCharsets + +object B extends App { + println(AMacro.m(33)) // fails + Files.write(Paths.get(s"s${scala.util.Properties.versionNumberString}.txt"), "nix".getBytes) +} diff --git a/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-warn/build.sbt b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-warn/build.sbt new file mode 100644 index 0000000000..c757f26481 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-warn/build.sbt @@ -0,0 +1,39 @@ +import sbt.librarymanagement.InclExclRule + +lazy val a = project.settings( + scalaVersion := "2.13.13", + libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value, + TaskKey[Unit]("checkLibs") := checkLibs("2.13.13", (Compile/dependencyClasspath).value, ".*scala-(library|reflect).*"), +) + +lazy val b = project.dependsOn(a).settings( + allowUnsafeScalaLibUpgrade := true, + scalaVersion := "2.13.12", + + // dependencies are upgraded to 2.13.13 + TaskKey[Unit]("checkLibs") := checkLibs("2.13.13", (Compile/dependencyClasspath).value, ".*scala-(library|reflect).*"), + + // check the compiler uses the 2.13.12 library on its runtime classpath + TaskKey[Unit]("checkScala") := { + val i = scalaInstance.value + i.libraryJars.filter(_.toString.contains("scala-library")).toList match { + case List(l) => assert(l.toString.contains("2.13.12"), i.toString) + } + assert(i.compilerJars.filter(_.toString.contains("scala-library")).isEmpty, i.toString) + assert(i.otherJars.filter(_.toString.contains("scala-library")).isEmpty, i.toString) + }, +) + +lazy val c = project.dependsOn(a).settings( + allowUnsafeScalaLibUpgrade := true, + scalaVersion := "2.13.12", + TaskKey[Unit]("checkLibs") := checkLibs("2.13.13", (Compile/dependencyClasspath).value, ".*scala-(library|reflect).*"), +) + +def checkLibs(v: String, cp: Classpath, filter: String): Unit = { + for (p <- cp) + if (p.toString.matches(filter)) { + println(s"$p -- $v") + assert(p.toString.contains(v), p) + } +} diff --git a/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-warn/c/C.scala b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-warn/c/C.scala new file mode 100644 index 0000000000..de0f7c0848 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-warn/c/C.scala @@ -0,0 +1,7 @@ +import java.nio.file.{Paths, Files} +import java.nio.charset.StandardCharsets + +object C extends App { + assert(scala.collection.immutable.Exp.v == null) + Files.write(Paths.get(s"s${scala.util.Properties.versionNumberString}.txt"), "nix".getBytes) +} diff --git a/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-warn/test b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-warn/test new file mode 100644 index 0000000000..f347b35342 --- /dev/null +++ b/sbt-app/src/sbt-test/dependency-management/stdlib-unfreeze-warn/test @@ -0,0 +1,11 @@ +> a/checkLibs +> b/checkLibs +> b/checkScala +> c/checkLibs + +# macro expansion fails +-> b/compile + +> c/run +$ exists s2.13.13.txt +$ delete s2.13.13.txt From 55b1fdeddbc1b58373b1caf0946d2e900615169c Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Tue, 28 Jan 2025 00:08:28 -0500 Subject: [PATCH 11/24] fix: Fix Chrome tracing file **Problem** We changed the content of Chrome tracing file incorrectly and renamed tid to tname. **Solution** 1. This renames tname back to to tid. 2. To retain the fix to avoid Thread#getId, this calls either the JDK 8 way or the JDK 19 way reflectively. --- .../main/scala/sbt/internal/util/Util.scala | 37 +++++++++++++++++++ .../sbt/internal/AbstractTaskProgress.scala | 2 + .../scala/sbt/internal/TaskTraceEvent.scala | 2 +- 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/Util.scala b/internal/util-collection/src/main/scala/sbt/internal/util/Util.scala index 324c62dfb2..b78ae61c8f 100644 --- a/internal/util-collection/src/main/scala/sbt/internal/util/Util.scala +++ b/internal/util-collection/src/main/scala/sbt/internal/util/Util.scala @@ -12,6 +12,8 @@ import java.util.Locale import scala.reflect.macros.blackbox import scala.language.experimental.macros +import scala.language.reflectiveCalls +import scala.util.control.NonFatal object Util { def makeList[T](size: Int, value: T): List[T] = List.fill(size)(value) @@ -77,4 +79,39 @@ object Util { class Macro(val c: blackbox.Context) { def ignore(f: c.Tree): c.Expr[Unit] = c.universe.reify({ c.Expr[Any](f).splice; () }) } + + lazy val majorJavaVersion: Int = + try { + val javaVersion = sys.props.get("java.version").getOrElse("1.0") + if (javaVersion.startsWith("1.")) { + javaVersion.split("\\.")(1).toInt + } else { + javaVersion.split("\\.")(0).toInt + } + } catch { + case NonFatal(_) => 0 + } + + private type GetId = { + def getId: Long + } + private type ThreadId = { + def threadId: Long + } + + /** + * Returns current thread id. + * Thread.threadId was added in JDK 19, and deprecated Thread#getId + * https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Thread.html#threadId() + */ + def threadId: Long = + if (majorJavaVersion < 19) { + (Thread.currentThread(): AnyRef) match { + case g: GetId @unchecked => g.getId + } + } else { + (Thread.currentThread(): AnyRef) match { + case g: ThreadId @unchecked => g.threadId + } + } } diff --git a/main/src/main/scala/sbt/internal/AbstractTaskProgress.scala b/main/src/main/scala/sbt/internal/AbstractTaskProgress.scala index aabeb84014..bce7bb2baa 100644 --- a/main/src/main/scala/sbt/internal/AbstractTaskProgress.scala +++ b/main/src/main/scala/sbt/internal/AbstractTaskProgress.scala @@ -9,6 +9,7 @@ package sbt package internal +import sbt.internal.util.Util import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicLong import scala.collection.JavaConverters._ @@ -123,6 +124,7 @@ object AbstractTaskExecuteProgress { private[sbt] class Timer() { val startNanos: Long = System.nanoTime() val threadName: String = Thread.currentThread().getName + val threadId: Long = Util.threadId var endNanos: Long = 0L def stop(): Unit = { endNanos = System.nanoTime() diff --git a/main/src/main/scala/sbt/internal/TaskTraceEvent.scala b/main/src/main/scala/sbt/internal/TaskTraceEvent.scala index 37f6ecdb6b..914ee84a19 100644 --- a/main/src/main/scala/sbt/internal/TaskTraceEvent.scala +++ b/main/src/main/scala/sbt/internal/TaskTraceEvent.scala @@ -61,7 +61,7 @@ private[sbt] final class TaskTraceEvent def durationEvent(name: String, cat: String, t: Timer): String = { val sb = new java.lang.StringBuilder(name.length + 2) CompactPrinter.print(new JString(name), sb) - s"""{"name": ${sb.toString}, "cat": "$cat", "ph": "X", "ts": ${(t.startMicros)}, "dur": ${(t.durationMicros)}, "pid": 0, "tname": "${t.threadName}"}""" + s"""{"name": ${sb.toString}, "cat": "$cat", "ph": "X", "ts": ${(t.startMicros)}, "dur": ${(t.durationMicros)}, "pid": 0, "tid": "${t.threadId}"}""" } val entryIterator = currentTimings while (entryIterator.hasNext) { From 2ee5eb7fa7740cc65f92537a282b2b064c13b0d0 Mon Sep 17 00:00:00 2001 From: Josh Soref <2119212+jsoref@users.noreply.github.com> Date: Mon, 27 Jan 2025 11:34:26 -0500 Subject: [PATCH 12/24] Make timing outputs consistently show hours and hint at time format Signed-off-by: Josh Soref <2119212+jsoref@users.noreply.github.com> --- .../sbt/internal/client/NetworkClient.scala | 8 ++++---- .../scala/sbt/internal/AggregationSpec.scala | 18 +++++++++--------- 2 files changed, 13 insertions(+), 13 deletions(-) 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 45ec9b52c7..72f3983ba8 100644 --- a/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala +++ b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala @@ -1130,13 +1130,13 @@ object NetworkClient { val totalString = s"$total s" + (if (total <= 60) "" else { - val maybeHours = total / 3600 match { - case 0 => "" - case h => f"$h%02d:" + val hours = total / 3600 match { + case 0 => "0" + case h => f"$h%02d" } val mins = f"${total % 3600 / 60}%02d" val secs = f"${total % 60}%02d" - s" ($maybeHours$mins:$secs)" + s" ($hours:$mins:$secs.0)" }) s"Total time: $totalString, completed $nowString" } diff --git a/main/src/test/scala/sbt/internal/AggregationSpec.scala b/main/src/test/scala/sbt/internal/AggregationSpec.scala index 8ac2d8f8a4..b6cbdb681e 100644 --- a/main/src/test/scala/sbt/internal/AggregationSpec.scala +++ b/main/src/test/scala/sbt/internal/AggregationSpec.scala @@ -12,15 +12,15 @@ object AggregationSpec extends verify.BasicTestSuite { val timing = Aggregation.timing(Aggregation.defaultFormat, 0, _: Long) test("timing should format total time properly") { - assert(timing(101).startsWith("Total time: 0 s,")) - assert(timing(1000).startsWith("Total time: 1 s,")) - assert(timing(3000).startsWith("Total time: 3 s,")) - assert(timing(30399).startsWith("Total time: 30 s,")) - assert(timing(60399).startsWith("Total time: 60 s,")) - assert(timing(60699).startsWith("Total time: 61 s (01:01),")) - assert(timing(303099).startsWith("Total time: 303 s (05:03),")) - assert(timing(6003099).startsWith("Total time: 6003 s (01:40:03),")) - assert(timing(96003099).startsWith("Total time: 96003 s (26:40:03),")) + assert(timing(101).startsWith("Total time: 0 s")) + assert(timing(1000).startsWith("Total time: 1 s")) + assert(timing(3000).startsWith("Total time: 3 s")) + assert(timing(30399).startsWith("Total time: 30 s")) + assert(timing(60399).startsWith("Total time: 60 s")) + assert(timing(60699).startsWith("Total time: 61 s (0:01:01.0)")) + assert(timing(303099).startsWith("Total time: 303 s (0:05:03.0)")) + assert(timing(6003099).startsWith("Total time: 6003 s (01:40:03.0)")) + assert(timing(96003099).startsWith("Total time: 96003 s (26:40:03.0)")) } test("timing should not emit special space characters") { From a7d862a08be0f940042a3f2bd72c03f873f64667 Mon Sep 17 00:00:00 2001 From: Dmitrii Naumenko Date: Tue, 4 Feb 2025 18:11:06 +0100 Subject: [PATCH 13/24] detect user-specific jdk installations on macOs (fixes #8031) User-specific JDKs are installed, for example, by IntelliJ IDEA --- main/src/main/scala/sbt/internal/CrossJava.scala | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/main/src/main/scala/sbt/internal/CrossJava.scala b/main/src/main/scala/sbt/internal/CrossJava.scala index 38765232c0..6761b30f1a 100644 --- a/main/src/main/scala/sbt/internal/CrossJava.scala +++ b/main/src/main/scala/sbt/internal/CrossJava.scala @@ -412,13 +412,19 @@ private[sbt] object CrossJava { class MacOsDiscoverConfig extends JavaDiscoverConf { val base: File = file("/Library") / "Java" / "JavaVirtualMachines" + // User-specific JDKs are installed, for example, by IntelliJ IDEA + private val baseInUserHome: File = Path.userHome / "Library" / "Java" / "JavaVirtualMachines" def javaHomes: Vector[(String, File)] = - wrapNull(base.list()) - .collect { - case dir @ JavaHomeDir(version) => - version -> (base / dir / "Contents" / "Home") - } + findAllHomes(base) ++ + findAllHomes(baseInUserHome) + + private def findAllHomes(root: File): Vector[(String, File)] = { + wrapNull(root.list()).collect { + case dir @ JavaHomeDir(version) => + version -> (root / dir / "Contents" / "Home") + } + } } class JabbaDiscoverConfig extends JavaDiscoverConf { From 06acd261d5353cd7e27e6927467aeb3821b288a5 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sun, 9 Feb 2025 17:33:23 -0500 Subject: [PATCH 14/24] Scala 2.13.16 --- .github/workflows/ci.yml | 8 ++++---- project/Dependencies.scala | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 846327e2aa..dbc83f1d74 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,10 +32,10 @@ jobs: java: 21 distribution: temurin jobtype: 5 - - os: ubuntu-latest - java: 8 - distribution: adopt - jobtype: 6 + # - os: ubuntu-latest + # java: 8 + # distribution: adopt + # jobtype: 6 - os: ubuntu-latest java: 8 distribution: adopt diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 9e4b893be7..4c35c591db 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -5,7 +5,7 @@ import sbt.contraband.ContrabandPlugin.autoImport._ object Dependencies { // WARNING: Please Scala update versions in PluginCross.scala too val scala212 = "2.12.20" - val scala213 = "2.13.15" + val scala213 = "2.13.16" val checkPluginCross = settingKey[Unit]("Make sure scalaVersion match up") val baseScalaVersion = scala212 def nightlyVersion: Option[String] = From a75e847a08f9729cf26a0c87123c039ec05a95cd Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sun, 9 Feb 2025 17:07:48 -0500 Subject: [PATCH 15/24] refactor: Refactor response handler **Problem** The sbtn response handling code is relatively stragightforward, but it's a bit messy. **Solution** This cleans it up a bit, similar to the style used by Unfiltered back then (not sure how Unfiltered plans are written nowadays) by expressing each event handling as a partial function, and composing them together using `orElse`. --- .../main/scala/sbt/internal/util/Util.scala | 7 ++ .../sbt/internal/client/NetworkClient.scala | 108 ++++++++++-------- 2 files changed, 69 insertions(+), 46 deletions(-) diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/Util.scala b/internal/util-collection/src/main/scala/sbt/internal/util/Util.scala index b78ae61c8f..c182bad16e 100644 --- a/internal/util-collection/src/main/scala/sbt/internal/util/Util.scala +++ b/internal/util-collection/src/main/scala/sbt/internal/util/Util.scala @@ -80,6 +80,13 @@ object Util { def ignore(f: c.Tree): c.Expr[Unit] = c.universe.reify({ c.Expr[Any](f).splice; () }) } + /** + * Given a list of event handlers expressed partial functions, combine them + * together using orElse from the left. + */ + def reduceIntents[A1, A2](intents: PartialFunction[A1, A2]*): PartialFunction[A1, A2] = + intents.toList.reduceLeft(_ orElse _) + lazy val majorJavaVersion: Int = try { val javaVersion = sys.props.get("java.version").getOrElse("1.0") 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 72f3983ba8..160291f31d 100644 --- a/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala +++ b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala @@ -529,61 +529,77 @@ class NetworkClient( .getOrElse(1) case _ => 1 } - private def completeExec(execId: String, exitCode: => Int): Unit = + + private val onAttachResponse: PartialFunction[JsonRpcResponseMessage, Unit] = { + case msg if attachUUID.get == msg.id => + attachUUID.set(null) + attached.set(true) + Option(inputThread.get).foreach(_.drain()) + () + } + def completeExec(execId: String, exitCode: Int) = { pendingResults.remove(execId) match { - case null => + case null => () case (q, startTime, name) => val now = System.currentTimeMillis val message = NetworkClient.timing(startTime, now) - val ec = exitCode if (batchMode.get || !attached.get) { - if (ec == 0) console.success(message) + if (exitCode == 0) console.success(message) else console.appendLog(Level.Error, message) } - Util.ignoreResult(q.offer(ec)) - } - def onResponse(msg: JsonRpcResponseMessage): Unit = { - completeExec(msg.id, getExitCode(msg.result)) - pendingCancellations.remove(msg.id) match { - case null => - case q => q.offer(msg.toString.contains("Task cancelled")) - } - msg.id match { - case execId => - if (attachUUID.get == msg.id) { - attachUUID.set(null) - attached.set(true) - Option(inputThread.get).foreach(_.drain()) - } - pendingCompletions.remove(execId) match { - case null => - case completions => - completions(msg.result match { - case Some(o: JObject) => - o.value - .foldLeft(CompletionResponse(Vector.empty[String])) { - case (resp, i) => - if (i.field == "items") - resp.withItems( - Converter - .fromJson[Vector[String]](i.value) - .getOrElse(Vector.empty[String]) - ) - else if (i.field == "cachedTestNames") - resp.withCachedTestNames( - Converter.fromJson[Boolean](i.value).getOrElse(true) - ) - else if (i.field == "cachedMainClassNames") - resp.withCachedMainClassNames( - Converter.fromJson[Boolean](i.value).getOrElse(true) - ) - else resp - } - case _ => CompletionResponse(Vector.empty[String]) - }) - } + Util.ignoreResult(q.offer(exitCode)) } } + private val onExecResponse: PartialFunction[JsonRpcResponseMessage, Unit] = { + case msg if pendingResults.containsKey(msg.id) => + completeExec(msg.id, getExitCode(msg.result)) + } + private val onCancellationResponse: PartialFunction[JsonRpcResponseMessage, Unit] = { + case msg if pendingCancellations.containsKey(msg.id) => + pendingCancellations.remove(msg.id) match { + case null => () + case q => Util.ignoreResult(q.offer(msg.toString.contains("Task cancelled"))) + } + } + private val onCompletionResponse: PartialFunction[JsonRpcResponseMessage, Unit] = { + case msg if pendingCompletions.containsKey(msg.id) => + pendingCompletions.remove(msg.id) match { + case null => () + case completions => + completions(msg.result match { + case Some(o: JObject) => + o.value + .foldLeft(CompletionResponse(Vector.empty[String])) { + case (resp, i) => + if (i.field == "items") + resp.withItems( + Converter + .fromJson[Vector[String]](i.value) + .getOrElse(Vector.empty[String]) + ) + else if (i.field == "cachedTestNames") + resp.withCachedTestNames( + Converter.fromJson[Boolean](i.value).getOrElse(true) + ) + else if (i.field == "cachedMainClassNames") + resp.withCachedMainClassNames( + Converter.fromJson[Boolean](i.value).getOrElse(true) + ) + else resp + } + case _ => CompletionResponse(Vector.empty[String]) + }) + } + } + // cache the composed plan + private val responsePlan = Util.reduceIntents[JsonRpcResponseMessage, Unit]( + onExecResponse, + onCancellationResponse, + onAttachResponse, + onCompletionResponse, + { case _ => () }, + ) + def onResponse(msg: JsonRpcResponseMessage): Unit = responsePlan(msg) def onNotification(msg: JsonRpcNotificationMessage): Unit = { def splitToMessage: Vector[(Level.Value, String)] = From a266e105568195ac01c367eca5d2dbc0691cc7e9 Mon Sep 17 00:00:00 2001 From: mehdi Date: Sat, 15 Feb 2025 18:27:34 +0100 Subject: [PATCH 16/24] move arch detection into darwin block --- sbt | 1 + 1 file changed, 1 insertion(+) diff --git a/sbt b/sbt index d178f16c3f..9a7c36aa57 100755 --- a/sbt +++ b/sbt @@ -200,6 +200,7 @@ acquire_sbtn () { exit 2 fi elif [[ "$OSTYPE" == "darwin"* ]]; then + arch="universal" archive_target="$p/sbtn-universal-apple-darwin-${sbtn_v}.tar.gz" url="https://github.com/sbt/sbtn-dist/releases/download/v${sbtn_v}/sbtn-universal-apple-darwin-${sbtn_v}.tar.gz" elif [[ "$OSTYPE" == "cygwin" ]] || [[ "$OSTYPE" == "msys" ]] || [[ "$OSTYPE" == "win32" ]]; then From 7ce978a5f2b0bdb31cb1e69ca03f8b97f9a88156 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Fri, 28 Feb 2025 13:47:33 -0800 Subject: [PATCH 17/24] Fix stdout freshness issue **Problem** When ForkOptions outputStrategy is None, Run code currently tries to use LoggedOutput, which buffers the output when connectInput is true, which effectively breaks the experience. **Solution** This stops falling back to LoggedOutput when connectInput is true. --- run/src/main/scala/sbt/Run.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/run/src/main/scala/sbt/Run.scala b/run/src/main/scala/sbt/Run.scala index 9e46f2b46c..5445c0da51 100644 --- a/run/src/main/scala/sbt/Run.scala +++ b/run/src/main/scala/sbt/Run.scala @@ -58,7 +58,7 @@ class ForkRun(config: ForkOptions) extends ScalaRun { } private def configLogged(log: Logger): ForkOptions = { - if (config.outputStrategy.isDefined) config + if (config.outputStrategy.isDefined || config.connectInput) config else config.withOutputStrategy(OutputStrategy.LoggedOutput(log)) } From 67265638c65111843dbe168d2767d1566518414d Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Thu, 13 Feb 2025 03:35:00 -0500 Subject: [PATCH 18/24] Implement client-side run **Problem** `run` task blocks the server, but during the run the server is just waiting for the built program to finish. **Solution** This implements client-side run where the server creates a sandbox environment, and sends the information to the client, and the client forks a new JVM to perform the run. --- build.sbt | 3 +- .../scala/sbt/internal/CommandChannel.scala | 27 +++--- .../sbt/internal/client/NetworkClient.scala | 88 ++++++++++++++++- .../sbt/internal/server/ServerHandler.scala | 1 + .../main/scala/sbt/internal/ui/UITask.scala | 27 ++++-- .../main/scala/sbt/BackgroundJobService.scala | 2 + main/src/main/scala/sbt/Defaults.scala | 5 +- main/src/main/scala/sbt/Keys.scala | 3 + .../DefaultBackgroundJobService.scala | 13 ++- .../internal/server/BuildServerProtocol.scala | 4 +- .../scala/sbt/internal/server/ClientJob.scala | 94 +++++++++++++++++++ .../server/LanguageServerProtocol.scala | 1 + .../sbt/internal/server/NetworkChannel.scala | 67 +++++++------ .../internal/protocol/InitializeOption.scala | 28 ++++-- .../codec/InitializeOptionFormats.scala | 4 +- .../sbt/internal/worker/ClientJobParams.scala | 45 +++++++++ .../sbt/internal/worker/FilePath.scala | 36 +++++++ .../sbt/internal/worker/JvmRunInfo.scala | 84 +++++++++++++++++ .../sbt/internal/worker/NativeRunInfo.scala | 69 ++++++++++++++ .../sbt/internal/worker/RunInfo.scala | 49 ++++++++++ .../worker/codec/ClientJobParamsFormats.scala | 27 ++++++ .../worker/codec/FilePathFormats.scala | 29 ++++++ .../internal/worker/codec/JsonProtocol.scala | 13 +++ .../worker/codec/JvmRunInfoFormats.scala | 47 ++++++++++ .../worker/codec/NativeRunInfoFormats.scala | 41 ++++++++ .../worker/codec/RunInfoFormats.scala | 31 ++++++ .../sbt/protocol/InitCommand.scala | 24 +++-- .../codec/CommandMessageFormats.scala | 2 +- .../protocol/codec/InitCommandFormats.scala | 6 +- .../sbt/protocol/codec/JsonProtocol.scala | 3 +- protocol/src/main/contraband/portfile.contra | 3 + protocol/src/main/contraband/server.contra | 1 + protocol/src/main/contraband/worker.contra | 51 ++++++++++ .../scala/sbt/protocol/Serialization.scala | 14 +-- run/src/main/scala/sbt/Fork.scala | 68 ++++++++------ run/src/main/scala/sbt/Run.scala | 22 ++--- server-test/src/server-test/client/build.sbt | 4 +- .../server-test/client/src/main/scala/A.scala | 4 +- .../client/src/test/scala/FooSpec.scala | 2 +- .../src/test/scala/testpkg/ClientTest.scala | 20 +++- .../src/test/scala/testpkg/TestServer.scala | 2 +- 41 files changed, 925 insertions(+), 139 deletions(-) create mode 100644 main/src/main/scala/sbt/internal/server/ClientJob.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/worker/ClientJobParams.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/worker/FilePath.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/worker/JvmRunInfo.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/worker/NativeRunInfo.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/worker/RunInfo.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/worker/codec/ClientJobParamsFormats.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/worker/codec/FilePathFormats.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/worker/codec/JsonProtocol.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/worker/codec/JvmRunInfoFormats.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/worker/codec/NativeRunInfoFormats.scala create mode 100644 protocol/src/main/contraband-scala/sbt/internal/worker/codec/RunInfoFormats.scala create mode 100644 protocol/src/main/contraband/worker.contra diff --git a/build.sbt b/build.sbt index eebbfdd3cd..c2b7db293c 100644 --- a/build.sbt +++ b/build.sbt @@ -758,7 +758,7 @@ lazy val protocolProj = (project in file("protocol")) // General command support and core commands not specific to a build system lazy val commandProj = (project in file("main-command")) .enablePlugins(ContrabandPlugin, JsonCodecPlugin) - .dependsOn(protocolProj, completeProj, utilLogging) + .dependsOn(protocolProj, completeProj, utilLogging, runProj) .settings( testedBaseSettings, name := "Command", @@ -1072,6 +1072,7 @@ lazy val mainProj = (project in file("main")) exclude[IncompatibleTemplateDefProblem]("sbt.internal.server.BuildServerReporter"), exclude[MissingClassProblem]("sbt.internal.CustomHttp*"), exclude[ReversedMissingMethodProblem]("sbt.JobHandle.isAutoCancel"), + exclude[ReversedMissingMethodProblem]("sbt.BackgroundJobService.createWorkingDirectory"), ) ) .configure( diff --git a/main-command/src/main/scala/sbt/internal/CommandChannel.scala b/main-command/src/main/scala/sbt/internal/CommandChannel.scala index db53b95580..3e0d7a518d 100644 --- a/main-command/src/main/scala/sbt/internal/CommandChannel.scala +++ b/main-command/src/main/scala/sbt/internal/CommandChannel.scala @@ -63,6 +63,8 @@ abstract class CommandChannel { } } } + protected def appendExec(commandLine: String, execId: Option[String]): Boolean = + append(Exec(commandLine, execId.orElse(Some(Exec.newExecId)), Some(CommandSource(name)))) def poll: Option[Exec] = Option(commandQueue.poll) def prompt(e: ConsolePromptEvent): Unit = userThread.onConsolePromptEvent(e) @@ -81,20 +83,21 @@ abstract class CommandChannel { private[sbt] final def logLevel: Level.Value = level.get private[this] def setLevel(value: Level.Value, cmd: String): Boolean = { level.set(value) - append(Exec(cmd, Some(Exec.newExecId), Some(CommandSource(name)))) + appendExec(cmd, None) } - private[sbt] def onCommand: String => Boolean = { - case "error" => setLevel(Level.Error, "error") - case "debug" => setLevel(Level.Debug, "debug") - case "info" => setLevel(Level.Info, "info") - case "warn" => setLevel(Level.Warn, "warn") - case cmd => - if (cmd.nonEmpty) append(Exec(cmd, Some(Exec.newExecId), Some(CommandSource(name)))) - else false - } - private[sbt] def onFastTrackTask: String => Boolean = { s: String => + private[sbt] def onCommandLine(cmd: String): Boolean = + cmd match { + case "error" => setLevel(Level.Error, "error") + case "debug" => setLevel(Level.Debug, "debug") + case "info" => setLevel(Level.Info, "info") + case "warn" => setLevel(Level.Warn, "warn") + case cmd => + if (cmd.nonEmpty) appendExec(cmd, None) + else false + } + private[sbt] def onFastTrackTask(cmd: String): Boolean = { fastTrack.synchronized(fastTrack.forEach { q => - q.add(new FastTrackTask(this, s)) + q.add(new FastTrackTask(this, cmd)) () }) true 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 160291f31d..3eea88c11a 100644 --- a/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala +++ b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala @@ -23,8 +23,16 @@ import java.text.DateFormat import sbt.BasicCommandStrings.{ DashDashDetachStdio, DashDashServer, Shutdown, TerminateAction } import sbt.internal.client.NetworkClient.Arguments import sbt.internal.langserver.{ LogMessageParams, MessageType, PublishDiagnosticsParams } +import sbt.internal.worker.{ ClientJobParams, JvmRunInfo, NativeRunInfo, RunInfo } import sbt.internal.protocol._ -import sbt.internal.util.{ ConsoleAppender, ConsoleOut, Signals, Terminal, Util } +import sbt.internal.util.{ + ConsoleAppender, + ConsoleOut, + MessageOnlyException, + Signals, + Terminal, + Util +} import sbt.io.IO import sbt.io.syntax._ import sbt.protocol._ @@ -43,6 +51,7 @@ import Serialization.{ attach, cancelReadSystemIn, cancelRequest, + clientJob, promptChannel, readSystemIn, systemIn, @@ -63,6 +72,7 @@ import Serialization.{ } import NetworkClient.Arguments import java.util.concurrent.TimeoutException +import sbt.util.Logger trait ConsoleInterface { def appendLog(level: Level.Value, message: => String): Unit @@ -166,6 +176,11 @@ class NetworkClient( case null => inputThread.set(new RawInputThread) case _ => } + private lazy val log: Logger = new Logger { + def trace(t: => Throwable): Unit = () + def success(message: => String): Unit = () + def log(level: Level.Value, message: => String): Unit = console.appendLog(level, message) + } private[sbt] def connectOrStartServerAndConnect( promptCompleteUsers: Boolean, @@ -295,7 +310,18 @@ class NetworkClient( } // initiate handshake val execId = UUID.randomUUID.toString - val initCommand = InitCommand(tkn, Option(execId), Some(true)) + val skipAnalysis = true + val opts = InitializeOption( + token = tkn, + skipAnalysis = Some(skipAnalysis), + canWork = Some(true), + ) + val initCommand = InitCommand( + token = tkn, // duplicated with opts for compatibility + execId = Option(execId), + skipAnalysis = Some(skipAnalysis), // duplicated with opts for compatibility + initializationOptions = Some(opts), + ) conn.sendString(Serialization.serializeCommandAsJsonMessage(initCommand)) connectionHolder.set(conn) conn @@ -641,6 +667,12 @@ class NetworkClient( case Success(params) => splitDiagnostics(params); Vector() case Failure(_) => Vector() } + case (`clientJob`, Some(json)) => + import sbt.internal.worker.codec.JsonProtocol._ + Converter.fromJson[ClientJobParams](json) match { + case Success(params) => clientSideRun(params).get; Vector.empty + case Failure(_) => Vector.empty + } case (`Shutdown`, Some(_)) => Vector.empty case (msg, _) if msg.startsWith("build/") => Vector.empty case _ => @@ -687,6 +719,58 @@ class NetworkClient( } } + private def clientSideRun(params: ClientJobParams): Try[Unit] = + params.runInfo match { + case Some(info) => clientSideRun(info) + case _ => Failure(new MessageOnlyException(s"runInfo is not specified in $params")) + } + + private def clientSideRun(runInfo: RunInfo): Try[Unit] = { + 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(new File(_)), + options = info.args, + log = log + ) + } + def nativeRun(info: NativeRunInfo): Try[Unit] = { + import java.lang.{ ProcessBuilder => JProcessBuilder } + val option = ForkOptions( + javaHome = None, + outputStrategy = None, // TODO: Handle buffered output etc + bootJars = Vector.empty, + workingDirectory = info.workingDirectory.map(new File(_)), + runJVMOptions = Vector.empty, + connectInput = info.connectInput, + envVars = info.environmentVariables, + ) + val command = info.cmd :: info.args.toList + val jpb = new JProcessBuilder(command: _*) + val exitCode = try Fork.blockForExitCode(Fork.forkInternal(option, Nil, jpb)) + catch { + case _: InterruptedException => + log.warn("run canceled") + 1 + } + Run.processExitCode(exitCode, "runner") + } + if (runInfo.jvm) jvmRun(runInfo.jvmRunInfo.getOrElse(sys.error("missing jvmRunInfo"))) + else nativeRun(runInfo.nativeRunInfo.getOrElse(sys.error("missing nativeRunInfo"))) + } + def onRequest(msg: JsonRpcRequestMessage): Unit = { import sbt.protocol.codec.JsonProtocol._ (msg.method, msg.params) match { diff --git a/main-command/src/main/scala/sbt/internal/server/ServerHandler.scala b/main-command/src/main/scala/sbt/internal/server/ServerHandler.scala index b503e503c0..9a20f82532 100644 --- a/main-command/src/main/scala/sbt/internal/server/ServerHandler.scala +++ b/main-command/src/main/scala/sbt/internal/server/ServerHandler.scala @@ -79,6 +79,7 @@ trait ServerCallback { private[sbt] def authOptions: Set[ServerAuthentication] private[sbt] def authenticate(token: String): Boolean private[sbt] def setInitialized(value: Boolean): Unit + private[sbt] def setInitializeOption(opts: InitializeOption): Unit private[sbt] def onSettingQuery(execId: Option[String], req: Q): Unit private[sbt] def onCompletionRequest(execId: Option[String], cp: CP): Unit private[sbt] def onCancellationRequest(execId: Option[String], crp: CRP): Unit diff --git a/main-command/src/main/scala/sbt/internal/ui/UITask.scala b/main-command/src/main/scala/sbt/internal/ui/UITask.scala index 6e9fcd1bed..b4ceb047dd 100644 --- a/main-command/src/main/scala/sbt/internal/ui/UITask.scala +++ b/main-command/src/main/scala/sbt/internal/ui/UITask.scala @@ -28,7 +28,7 @@ private[sbt] trait UITask extends Runnable with AutoCloseable { private[sbt] val reader: UITask.Reader private[this] final def handleInput(s: Either[String, String]): Boolean = s match { case Left(m) => channel.onFastTrackTask(m) - case Right(cmd) => channel.onCommand(cmd) + case Right(cmd) => channel.onCommandLine(cmd) } private[this] val isStopped = new AtomicBoolean(false) override def run(): Unit = { @@ -56,6 +56,20 @@ private[sbt] object UITask { object Reader { // Avoid filling the stack trace since it isn't helpful here object interrupted extends InterruptedException + + /** + * Return Left for fast track commands, otherwise return Right(...). + */ + def splitCommand(cmd: String): Either[String, String] = + // We need to put the empty string on the fast track queue so that we can + // reprompt the user if another command is running on the server. + if (cmd.isEmpty()) Left("") + else + cmd match { + case Shutdown | TerminateAction | Cancel => Left(cmd) + case cmd => Right(cmd) + } + def terminalReader(parser: Parser[_])( terminal: Terminal, state: State @@ -78,15 +92,8 @@ private[sbt] object UITask { Right("") // should be unreachable // JLine returns null on ctrl+d when there is no other input. This interprets // ctrl+d with no imput as an exit - case None => Left(TerminateAction) - case Some(s: String) => - s.trim() match { - // We need to put the empty string on the fast track queue so that we can - // reprompt the user if another command is running on the server. - case "" => Left("") - case cmd @ (`Shutdown` | `TerminateAction` | `Cancel`) => Left(cmd) - case cmd => Right(cmd) - } + case None => Left(TerminateAction) + case Some(s: String) => splitCommand(s.trim()) } } terminal.setPrompt(Prompt.Pending) diff --git a/main/src/main/scala/sbt/BackgroundJobService.scala b/main/src/main/scala/sbt/BackgroundJobService.scala index 62aaec76d6..2b113c1c1f 100644 --- a/main/src/main/scala/sbt/BackgroundJobService.scala +++ b/main/src/main/scala/sbt/BackgroundJobService.scala @@ -70,6 +70,8 @@ abstract class BackgroundJobService extends Closeable { def waitFor(job: JobHandle): Unit + private[sbt] def createWorkingDirectory: File + /** Copies classpath to temporary directories. */ def copyClasspath(products: Classpath, full: Classpath, workingDirectory: File): Classpath diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index fa9b6f5090..f3a36453c8 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -51,6 +51,7 @@ import sbt.internal.server.{ BspCompileTask, BuildServerProtocol, BuildServerReporter, + ClientJob, Definition, LanguageServerProtocol, ServerHandler, @@ -222,7 +223,7 @@ object Defaults extends BuildCommon { closeClassLoaders :== SysProp.closeClassLoaders, allowZombieClassLoaders :== true, packageTimestamp :== Package.defaultTimestamp, - ) ++ BuildServerProtocol.globalSettings + ) ++ BuildServerProtocol.globalSettings ++ ClientJob.globalSettings private[sbt] lazy val globalIvyCore: Seq[Setting[_]] = Seq( @@ -2717,7 +2718,7 @@ object Defaults extends BuildCommon { lazy val configSettings: Seq[Setting[_]] = Classpaths.configSettings ++ configTasks ++ configPaths ++ packageConfig ++ Classpaths.compilerPluginConfig ++ deprecationSettings ++ - BuildServerProtocol.configSettings + BuildServerProtocol.configSettings ++ ClientJob.configSettings lazy val compileSettings: Seq[Setting[_]] = configSettings ++ (mainBgRunMainTask +: mainBgRunTask) ++ Classpaths.addUnmanagedLibrary diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 94b1f7def7..dd290c3db7 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -29,6 +29,7 @@ import sbt.internal.remotecache.RemoteCacheArtifact import sbt.internal.server.BuildServerProtocol.BspFullWorkspace import sbt.internal.server.{ BuildServerReporter, ServerHandler } import sbt.internal.util.{ AttributeKey, ProgressState, SourcePosition } +import sbt.internal.worker.ClientJobParams import sbt.io._ import sbt.librarymanagement.Configurations.CompilerPlugin import sbt.librarymanagement.LibraryManagementCodec._ @@ -437,6 +438,8 @@ object Keys { val bspScalaMainClasses = inputKey[Unit]("Implementation of buildTarget/scalaMainClasses").withRank(DTask) val bspScalaMainClassesItem = taskKey[ScalaMainClassesItem]("").withRank(DTask) val bspReporter = taskKey[BuildServerReporter]("").withRank(DTask) + val clientJob = inputKey[ClientJobParams]("Translates a task into a job specification").withRank(Invisible) + val clientJobRunInfo = inputKey[ClientJobParams]("Translates the run task into a job specification").withRank(Invisible) val useCoursier = settingKey[Boolean]("Use Coursier for dependency resolution.").withRank(BSetting) val csrCacheDirectory = settingKey[File]("Coursier cache directory. Uses -Dsbt.coursier.home or Coursier's default.").withRank(CSetting) diff --git a/main/src/main/scala/sbt/internal/DefaultBackgroundJobService.scala b/main/src/main/scala/sbt/internal/DefaultBackgroundJobService.scala index 218cb32893..232204d467 100644 --- a/main/src/main/scala/sbt/internal/DefaultBackgroundJobService.scala +++ b/main/src/main/scala/sbt/internal/DefaultBackgroundJobService.scala @@ -144,6 +144,16 @@ private[sbt] abstract class AbstractBackgroundJobService extends BackgroundJobSe override val isAutoCancel = false } + private[sbt] def createWorkingDirectory: File = { + val id = nextId.getAndIncrement() + createWorkingDirectory(id) + } + private[sbt] def createWorkingDirectory(id: Long): File = { + val workingDir = serviceTempDir / s"job-$id" + IO.createDirectory(workingDir) + workingDir + } + def doRunInBackground( spawningTask: ScopedKey[_], state: State, @@ -153,8 +163,7 @@ private[sbt] abstract class AbstractBackgroundJobService extends BackgroundJobSe val extracted = Project.extract(state) val logger = LogManager.constructBackgroundLog(extracted.structure.data, state, context)(spawningTask) - val workingDir = serviceTempDir / s"job-$id" - IO.createDirectory(workingDir) + val workingDir = createWorkingDirectory(id) val job = try { new ThreadJobHandle(id, spawningTask, logger, workingDir, start(logger, workingDir)) } catch { diff --git a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala index b063cfa287..9bb371a78e 100644 --- a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala +++ b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala @@ -248,7 +248,7 @@ object BuildServerProtocol { state.respondEvent(result) } }.evaluated, - bspScalaMainClasses / aggregate := false + bspScalaMainClasses / aggregate := false, ) // This will be scoped to Compile, Test, IntegrationTest etc @@ -345,7 +345,7 @@ object BuildServerProtocol { } else { new BuildServerForwarder(meta, logger, underlying) } - } + }, ) private[sbt] object Method { final val Initialize = "build/initialize" diff --git a/main/src/main/scala/sbt/internal/server/ClientJob.scala b/main/src/main/scala/sbt/internal/server/ClientJob.scala new file mode 100644 index 0000000000..e016b98f00 --- /dev/null +++ b/main/src/main/scala/sbt/internal/server/ClientJob.scala @@ -0,0 +1,94 @@ +/* + * 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 server + +import java.io.File +import sbt.BuildSyntax._ +import sbt.Def._ +import sbt.Keys._ +import sbt.SlashSyntax0._ +import sbt.internal.util.complete.Parser +import sbt.internal.worker.{ ClientJobParams, FilePath, JvmRunInfo, RunInfo } +import sbt.io.IO +import sbt.protocol.Serialization + +/** + * A ClientJob represents a unit of work that sbt server process + * can outsourse back to the client. Initially intended for sbtn client-side run. + */ +object ClientJob { + lazy val globalSettings: Seq[Def.Setting[_]] = Seq( + clientJob := clientJobTask.evaluated, + clientJob / aggregate := false, + ) + + private def clientJobTask: Def.Initialize[InputTask[ClientJobParams]] = Def.inputTaskDyn { + val tokens = spaceDelimited().parsed + val state = Keys.state.value + val p = Act.aggregatedKeyParser(state) + if (tokens.isEmpty) { + sys.error("expected an argument, for example foo/run") + } + val scopedKey = Parser.parse(tokens.head, p) match { + case Right(x :: Nil) => x + case Right(xs) => sys.error("too many keys") + case Left(err) => sys.error(err) + } + if (scopedKey.key == run.key) + clientJobRunInfo.in(scopedKey.scope).toTask(" " + tokens.tail.mkString(" ")) + else sys.error(s"unsupported task for clientJob $scopedKey") + } + + // This will be scoped to Compile, Test, etc + lazy val configSettings: Seq[Def.Setting[_]] = Seq( + clientJobRunInfo := clientJobRunInfoTask.evaluated, + ) + + private def clientJobRunInfoTask: Def.Initialize[InputTask[ClientJobParams]] = Def.inputTask { + val state = Keys.state.value + val args = spaceDelimited().parsed + val mainClass = (Keys.run / Keys.mainClass).value + val service = bgJobService.value + val fo = (Keys.run / Keys.forkOptions).value + val workingDir = service.createWorkingDirectory + val cp = service.copyClasspath( + exportedProductJars.value, + fullClasspathAsJars.value, + workingDir, + hashContents = true, + ) + val strategy = fo.outputStrategy.map(_.getClass().getSimpleName().filter(_ != '$')) + // sbtn doesn't set java.home, so we need to do the fallback here + val javaHome = + fo.javaHome.map(IO.toURI).orElse(sys.props.get("java.home").map(x => IO.toURI(new File(x)))) + val jvmRunInfo = JvmRunInfo( + args = args.toVector, + classpath = cp.map(x => IO.toURI(x.data)).map(FilePath(_, "")).toVector, + mainClass = mainClass.getOrElse(sys.error("no main class")), + connectInput = fo.connectInput, + javaHome = javaHome, + outputStrategy = strategy, + workingDirectory = fo.workingDirectory.map(IO.toURI), + jvmOptions = fo.runJVMOptions, + environmentVariables = fo.envVars.toMap, + ) + val info = RunInfo( + jvm = true, + jvmRunInfo = jvmRunInfo, + ) + val result = ClientJobParams( + runInfo = info + ) + import sbt.internal.worker.codec.JsonProtocol._ + state.notifyEvent(Serialization.clientJob, result) + result + } +} diff --git a/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala b/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala index 2bf2878bda..5ea1918128 100644 --- a/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala +++ b/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala @@ -62,6 +62,7 @@ private[sbt] object LanguageServerProtocol { else throw LangServerError(ErrorCodes.InvalidRequest, "invalid token") } else () setInitialized(true) + setInitializeOption(opt) if (!opt.skipAnalysis.getOrElse(false)) appendExec("collectAnalyses", None) jsonRpcRespond(InitializeResult(serverCapabilities), Some(r.id)) diff --git a/main/src/main/scala/sbt/internal/server/NetworkChannel.scala b/main/src/main/scala/sbt/internal/server/NetworkChannel.scala index 6bf3558c63..6eebe449af 100644 --- a/main/src/main/scala/sbt/internal/server/NetworkChannel.scala +++ b/main/src/main/scala/sbt/internal/server/NetworkChannel.scala @@ -24,6 +24,7 @@ import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference } import sbt.BasicCommandStrings.{ Shutdown, TerminateAction } import sbt.internal.langserver.{ CancelRequestParams, ErrorCodes, LogMessageParams, MessageType } import sbt.internal.protocol.{ + InitializeOption, JsonRpcNotificationMessage, JsonRpcRequestMessage, JsonRpcResponseError, @@ -83,6 +84,10 @@ final class NetworkChannel( private val delimiter: Byte = '\n'.toByte private val out = connection.getOutputStream private var initialized = false + + /** Reference to the client-side custom options + */ + private val initializeOption = new AtomicReference[InitializeOption](null) private val pendingRequests: mutable.Map[String, JsonRpcRequestMessage] = mutable.Map() private[this] val inputBuffer = new LinkedBlockingQueue[Int]() @@ -124,7 +129,7 @@ final class NetworkChannel( self.jsonRpcNotify(method, params) def appendExec(commandLine: String, execId: Option[String]): Boolean = - self.append(Exec(commandLine, execId, Some(CommandSource(name)))) + self.appendExec(commandLine, execId) def appendExec(exec: Exec): Boolean = self.append(exec) @@ -133,6 +138,8 @@ final class NetworkChannel( private[sbt] def authOptions: Set[ServerAuthentication] = self.authOptions private[sbt] def authenticate(token: String): Boolean = self.authenticate(token) private[sbt] def setInitialized(value: Boolean): Unit = self.setInitialized(value) + private[sbt] def setInitializeOption(opts: InitializeOption): Unit = + self.setInitializeOption(opts) private[sbt] def onSettingQuery(execId: Option[String], req: SettingQuery): Unit = self.onSettingQuery(execId, req) private[sbt] def onCompletionRequest(execId: Option[String], cp: CompletionParams): Unit = @@ -141,6 +148,30 @@ final class NetworkChannel( self.onCancellationRequest(execId, crp) } + // Take over commandline for network channel + private val networkCommand: PartialFunction[String, String] = { + case cmd if cmd.split(" ").head.split("/").last == "run" => + s"clientJob $cmd" + } + override protected def appendExec(commandLine: String, execId: Option[String]): Boolean = + if (clientCanWork && networkCommand.isDefinedAt(commandLine)) + super.appendExec(networkCommand(commandLine), execId) + else super.appendExec(commandLine, execId) + + override private[sbt] def onCommandLine(cmd: String): Boolean = + if (clientCanWork && networkCommand.isDefinedAt(cmd)) + appendExec(networkCommand(cmd), None) + else super.onCommandLine(cmd) + + protected def setInitializeOption(opts: InitializeOption): Unit = initializeOption.set(opts) + + // Returns true if sbtn has declared with canWork: true + protected def clientCanWork: Boolean = + Option(initializeOption.get) match { + case Some(opts) => opts.canWork.getOrElse(false) + case _ => false + } + protected def authenticate(token: String): Boolean = instance.authenticate(token) protected def setInitialized(value: Boolean): Unit = initialized = value @@ -369,40 +400,6 @@ final class NetworkChannel( try pendingWrites.put(event -> delimit) catch { case _: InterruptedException => } - def onCommand(command: CommandMessage): Unit = command match { - case x: InitCommand => onInitCommand(x) - case x: ExecCommand => onExecCommand(x) - case x: SettingQuery => onSettingQuery(None, x) - } - - private def onInitCommand(cmd: InitCommand): Unit = { - if (auth(ServerAuthentication.Token)) { - cmd.token match { - case Some(x) => - authenticate(x) match { - case true => - initialized = true - notifyEvent(ChannelAcceptedEvent(name)) - case _ => sys.error("invalid token") - } - case None => sys.error("init command but without token.") - } - } else { - initialized = true - } - } - - private def onExecCommand(cmd: ExecCommand) = { - if (initialized) { - append( - Exec(cmd.commandLine, cmd.execId orElse Some(Exec.newExecId), Some(CommandSource(name))) - ) - () - } else { - log.warn(s"ignoring command $cmd before initialization") - } - } - protected def onSettingQuery(execId: Option[String], req: SettingQuery) = { if (initialized) { StandardMain.exchange.withState { s => diff --git a/protocol/src/main/contraband-scala/sbt/internal/protocol/InitializeOption.scala b/protocol/src/main/contraband-scala/sbt/internal/protocol/InitializeOption.scala index b900d641d6..0a5f72a079 100644 --- a/protocol/src/main/contraband-scala/sbt/internal/protocol/InitializeOption.scala +++ b/protocol/src/main/contraband-scala/sbt/internal/protocol/InitializeOption.scala @@ -4,24 +4,30 @@ // DO NOT EDIT MANUALLY package sbt.internal.protocol +/** + * Passed into InitializeParams as part of "initialize" request as the user-defined option. + * https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize + */ final class InitializeOption private ( val token: Option[String], - val skipAnalysis: Option[Boolean]) extends Serializable { + val skipAnalysis: Option[Boolean], + val canWork: Option[Boolean]) extends Serializable { - private def this(token: Option[String]) = this(token, None) + private def this(token: Option[String]) = this(token, None, None) + private def this(token: Option[String], skipAnalysis: Option[Boolean]) = this(token, skipAnalysis, None) override def equals(o: Any): Boolean = this.eq(o.asInstanceOf[AnyRef]) || (o match { - case x: InitializeOption => (this.token == x.token) && (this.skipAnalysis == x.skipAnalysis) + case x: InitializeOption => (this.token == x.token) && (this.skipAnalysis == x.skipAnalysis) && (this.canWork == x.canWork) case _ => false }) override def hashCode: Int = { - 37 * (37 * (37 * (17 + "sbt.internal.protocol.InitializeOption".##) + token.##) + skipAnalysis.##) + 37 * (37 * (37 * (37 * (17 + "sbt.internal.protocol.InitializeOption".##) + token.##) + skipAnalysis.##) + canWork.##) } override def toString: String = { - "InitializeOption(" + token + ", " + skipAnalysis + ")" + "InitializeOption(" + token + ", " + skipAnalysis + ", " + canWork + ")" } - private[this] def copy(token: Option[String] = token, skipAnalysis: Option[Boolean] = skipAnalysis): InitializeOption = { - new InitializeOption(token, skipAnalysis) + private[this] def copy(token: Option[String] = token, skipAnalysis: Option[Boolean] = skipAnalysis, canWork: Option[Boolean] = canWork): InitializeOption = { + new InitializeOption(token, skipAnalysis, canWork) } def withToken(token: Option[String]): InitializeOption = { copy(token = token) @@ -35,6 +41,12 @@ final class InitializeOption private ( def withSkipAnalysis(skipAnalysis: Boolean): InitializeOption = { copy(skipAnalysis = Option(skipAnalysis)) } + def withCanWork(canWork: Option[Boolean]): InitializeOption = { + copy(canWork = canWork) + } + def withCanWork(canWork: Boolean): InitializeOption = { + copy(canWork = Option(canWork)) + } } object InitializeOption { @@ -42,4 +54,6 @@ object InitializeOption { def apply(token: String): InitializeOption = new InitializeOption(Option(token)) def apply(token: Option[String], skipAnalysis: Option[Boolean]): InitializeOption = new InitializeOption(token, skipAnalysis) def apply(token: String, skipAnalysis: Boolean): InitializeOption = new InitializeOption(Option(token), Option(skipAnalysis)) + def apply(token: Option[String], skipAnalysis: Option[Boolean], canWork: Option[Boolean]): InitializeOption = new InitializeOption(token, skipAnalysis, canWork) + def apply(token: String, skipAnalysis: Boolean, canWork: Boolean): InitializeOption = new InitializeOption(Option(token), Option(skipAnalysis), Option(canWork)) } diff --git a/protocol/src/main/contraband-scala/sbt/internal/protocol/codec/InitializeOptionFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/protocol/codec/InitializeOptionFormats.scala index f7f3a09e79..a5b04d6a42 100644 --- a/protocol/src/main/contraband-scala/sbt/internal/protocol/codec/InitializeOptionFormats.scala +++ b/protocol/src/main/contraband-scala/sbt/internal/protocol/codec/InitializeOptionFormats.scala @@ -13,8 +13,9 @@ implicit lazy val InitializeOptionFormat: JsonFormat[sbt.internal.protocol.Initi unbuilder.beginObject(__js) val token = unbuilder.readField[Option[String]]("token") val skipAnalysis = unbuilder.readField[Option[Boolean]]("skipAnalysis") + val canWork = unbuilder.readField[Option[Boolean]]("canWork") unbuilder.endObject() - sbt.internal.protocol.InitializeOption(token, skipAnalysis) + sbt.internal.protocol.InitializeOption(token, skipAnalysis, canWork) case None => deserializationError("Expected JsObject but found None") } @@ -23,6 +24,7 @@ implicit lazy val InitializeOptionFormat: JsonFormat[sbt.internal.protocol.Initi builder.beginObject() builder.addField("token", obj.token) builder.addField("skipAnalysis", obj.skipAnalysis) + builder.addField("canWork", obj.canWork) builder.endObject() } } diff --git a/protocol/src/main/contraband-scala/sbt/internal/worker/ClientJobParams.scala b/protocol/src/main/contraband-scala/sbt/internal/worker/ClientJobParams.scala new file mode 100644 index 0000000000..d72bf2b9f2 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/worker/ClientJobParams.scala @@ -0,0 +1,45 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.worker +/** + * Client-side job support. + * + * Notification: sbt/clientJob + * + * Parameter for the sbt/clientJob notification. + * A client-side job represents a unit of work that sbt server + * can outsourse back to the client, for example for run task. + */ +final class ClientJobParams private ( + val runInfo: Option[sbt.internal.worker.RunInfo]) extends Serializable { + + + + override def equals(o: Any): Boolean = this.eq(o.asInstanceOf[AnyRef]) || (o match { + case x: ClientJobParams => (this.runInfo == x.runInfo) + case _ => false + }) + override def hashCode: Int = { + 37 * (37 * (17 + "sbt.internal.worker.ClientJobParams".##) + runInfo.##) + } + override def toString: String = { + "ClientJobParams(" + runInfo + ")" + } + private[this] def copy(runInfo: Option[sbt.internal.worker.RunInfo] = runInfo): ClientJobParams = { + new ClientJobParams(runInfo) + } + def withRunInfo(runInfo: Option[sbt.internal.worker.RunInfo]): ClientJobParams = { + copy(runInfo = runInfo) + } + def withRunInfo(runInfo: sbt.internal.worker.RunInfo): ClientJobParams = { + copy(runInfo = Option(runInfo)) + } +} +object ClientJobParams { + + def apply(runInfo: Option[sbt.internal.worker.RunInfo]): ClientJobParams = new ClientJobParams(runInfo) + def apply(runInfo: sbt.internal.worker.RunInfo): ClientJobParams = new ClientJobParams(Option(runInfo)) +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/worker/FilePath.scala b/protocol/src/main/contraband-scala/sbt/internal/worker/FilePath.scala new file mode 100644 index 0000000000..24647f3c09 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/worker/FilePath.scala @@ -0,0 +1,36 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.worker +final class FilePath private ( + val path: java.net.URI, + val digest: String) extends Serializable { + + + + override def equals(o: Any): Boolean = this.eq(o.asInstanceOf[AnyRef]) || (o match { + case x: FilePath => (this.path == x.path) && (this.digest == x.digest) + case _ => false + }) + override def hashCode: Int = { + 37 * (37 * (37 * (17 + "sbt.internal.worker.FilePath".##) + path.##) + digest.##) + } + override def toString: String = { + "FilePath(" + path + ", " + digest + ")" + } + private[this] def copy(path: java.net.URI = path, digest: String = digest): FilePath = { + new FilePath(path, digest) + } + def withPath(path: java.net.URI): FilePath = { + copy(path = path) + } + def withDigest(digest: String): FilePath = { + copy(digest = digest) + } +} +object FilePath { + + def apply(path: java.net.URI, digest: String): FilePath = new FilePath(path, digest) +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/worker/JvmRunInfo.scala b/protocol/src/main/contraband-scala/sbt/internal/worker/JvmRunInfo.scala new file mode 100644 index 0000000000..d0d6b5b73d --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/worker/JvmRunInfo.scala @@ -0,0 +1,84 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.worker +final class JvmRunInfo private ( + val args: Vector[String], + val classpath: Vector[sbt.internal.worker.FilePath], + val mainClass: String, + val connectInput: Boolean, + val javaHome: Option[java.net.URI], + val outputStrategy: Option[String], + val workingDirectory: Option[java.net.URI], + val jvmOptions: Vector[String], + val environmentVariables: scala.collection.immutable.Map[String, String], + val inputs: Vector[sbt.internal.worker.FilePath], + val outputs: Vector[sbt.internal.worker.FilePath]) extends Serializable { + + private def this(args: Vector[String], classpath: Vector[sbt.internal.worker.FilePath], mainClass: String, connectInput: Boolean, javaHome: Option[java.net.URI], outputStrategy: Option[String], workingDirectory: Option[java.net.URI], jvmOptions: Vector[String], environmentVariables: scala.collection.immutable.Map[String, String]) = this(args, classpath, mainClass, connectInput, javaHome, outputStrategy, workingDirectory, jvmOptions, environmentVariables, Vector(), Vector()) + + override def equals(o: Any): Boolean = this.eq(o.asInstanceOf[AnyRef]) || (o match { + case x: JvmRunInfo => (this.args == x.args) && (this.classpath == x.classpath) && (this.mainClass == x.mainClass) && (this.connectInput == x.connectInput) && (this.javaHome == x.javaHome) && (this.outputStrategy == x.outputStrategy) && (this.workingDirectory == x.workingDirectory) && (this.jvmOptions == x.jvmOptions) && (this.environmentVariables == x.environmentVariables) && (this.inputs == x.inputs) && (this.outputs == x.outputs) + case _ => false + }) + override def hashCode: Int = { + 37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (17 + "sbt.internal.worker.JvmRunInfo".##) + args.##) + classpath.##) + mainClass.##) + connectInput.##) + javaHome.##) + outputStrategy.##) + workingDirectory.##) + jvmOptions.##) + environmentVariables.##) + inputs.##) + outputs.##) + } + override def toString: String = { + "JvmRunInfo(" + args + ", " + classpath + ", " + mainClass + ", " + connectInput + ", " + javaHome + ", " + outputStrategy + ", " + workingDirectory + ", " + jvmOptions + ", " + environmentVariables + ", " + inputs + ", " + outputs + ")" + } + private[this] def copy(args: Vector[String] = args, classpath: Vector[sbt.internal.worker.FilePath] = classpath, mainClass: String = mainClass, connectInput: Boolean = connectInput, javaHome: Option[java.net.URI] = javaHome, outputStrategy: Option[String] = outputStrategy, workingDirectory: Option[java.net.URI] = workingDirectory, jvmOptions: Vector[String] = jvmOptions, environmentVariables: scala.collection.immutable.Map[String, String] = environmentVariables, inputs: Vector[sbt.internal.worker.FilePath] = inputs, outputs: Vector[sbt.internal.worker.FilePath] = outputs): JvmRunInfo = { + new JvmRunInfo(args, classpath, mainClass, connectInput, javaHome, outputStrategy, workingDirectory, jvmOptions, environmentVariables, inputs, outputs) + } + def withArgs(args: Vector[String]): JvmRunInfo = { + copy(args = args) + } + def withClasspath(classpath: Vector[sbt.internal.worker.FilePath]): JvmRunInfo = { + copy(classpath = classpath) + } + def withMainClass(mainClass: String): JvmRunInfo = { + copy(mainClass = mainClass) + } + def withConnectInput(connectInput: Boolean): JvmRunInfo = { + copy(connectInput = connectInput) + } + def withJavaHome(javaHome: Option[java.net.URI]): JvmRunInfo = { + copy(javaHome = javaHome) + } + def withJavaHome(javaHome: java.net.URI): JvmRunInfo = { + copy(javaHome = Option(javaHome)) + } + def withOutputStrategy(outputStrategy: Option[String]): JvmRunInfo = { + copy(outputStrategy = outputStrategy) + } + def withOutputStrategy(outputStrategy: String): JvmRunInfo = { + copy(outputStrategy = Option(outputStrategy)) + } + def withWorkingDirectory(workingDirectory: Option[java.net.URI]): JvmRunInfo = { + copy(workingDirectory = workingDirectory) + } + def withWorkingDirectory(workingDirectory: java.net.URI): JvmRunInfo = { + copy(workingDirectory = Option(workingDirectory)) + } + def withJvmOptions(jvmOptions: Vector[String]): JvmRunInfo = { + copy(jvmOptions = jvmOptions) + } + def withEnvironmentVariables(environmentVariables: scala.collection.immutable.Map[String, String]): JvmRunInfo = { + copy(environmentVariables = environmentVariables) + } + def withInputs(inputs: Vector[sbt.internal.worker.FilePath]): JvmRunInfo = { + copy(inputs = inputs) + } + def withOutputs(outputs: Vector[sbt.internal.worker.FilePath]): JvmRunInfo = { + copy(outputs = outputs) + } +} +object JvmRunInfo { + + def apply(args: Vector[String], classpath: Vector[sbt.internal.worker.FilePath], mainClass: String, connectInput: Boolean, javaHome: Option[java.net.URI], outputStrategy: Option[String], workingDirectory: Option[java.net.URI], jvmOptions: Vector[String], environmentVariables: scala.collection.immutable.Map[String, String]): JvmRunInfo = new JvmRunInfo(args, classpath, mainClass, connectInput, javaHome, outputStrategy, workingDirectory, jvmOptions, environmentVariables) + def apply(args: Vector[String], classpath: Vector[sbt.internal.worker.FilePath], mainClass: String, connectInput: Boolean, javaHome: java.net.URI, outputStrategy: String, workingDirectory: java.net.URI, jvmOptions: Vector[String], environmentVariables: scala.collection.immutable.Map[String, String]): JvmRunInfo = new JvmRunInfo(args, classpath, mainClass, connectInput, Option(javaHome), Option(outputStrategy), Option(workingDirectory), jvmOptions, environmentVariables) + def apply(args: Vector[String], classpath: Vector[sbt.internal.worker.FilePath], mainClass: String, connectInput: Boolean, javaHome: Option[java.net.URI], outputStrategy: Option[String], workingDirectory: Option[java.net.URI], jvmOptions: Vector[String], environmentVariables: scala.collection.immutable.Map[String, String], inputs: Vector[sbt.internal.worker.FilePath], outputs: Vector[sbt.internal.worker.FilePath]): JvmRunInfo = new JvmRunInfo(args, classpath, mainClass, connectInput, javaHome, outputStrategy, workingDirectory, jvmOptions, environmentVariables, inputs, outputs) + def apply(args: Vector[String], classpath: Vector[sbt.internal.worker.FilePath], mainClass: String, connectInput: Boolean, javaHome: java.net.URI, outputStrategy: String, workingDirectory: java.net.URI, jvmOptions: Vector[String], environmentVariables: scala.collection.immutable.Map[String, String], inputs: Vector[sbt.internal.worker.FilePath], outputs: Vector[sbt.internal.worker.FilePath]): JvmRunInfo = new JvmRunInfo(args, classpath, mainClass, connectInput, Option(javaHome), Option(outputStrategy), Option(workingDirectory), jvmOptions, environmentVariables, inputs, outputs) +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/worker/NativeRunInfo.scala b/protocol/src/main/contraband-scala/sbt/internal/worker/NativeRunInfo.scala new file mode 100644 index 0000000000..5caffe8fd4 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/worker/NativeRunInfo.scala @@ -0,0 +1,69 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.worker +final class NativeRunInfo private ( + val cmd: String, + val args: Vector[String], + val connectInput: Boolean, + val outputStrategy: Option[String], + val workingDirectory: Option[java.net.URI], + val environmentVariables: scala.collection.immutable.Map[String, String], + val inputs: Vector[sbt.internal.worker.FilePath], + val outputs: Vector[sbt.internal.worker.FilePath]) extends Serializable { + + private def this(cmd: String, args: Vector[String], connectInput: Boolean, outputStrategy: Option[String], workingDirectory: Option[java.net.URI], environmentVariables: scala.collection.immutable.Map[String, String]) = this(cmd, args, connectInput, outputStrategy, workingDirectory, environmentVariables, Vector(), Vector()) + + override def equals(o: Any): Boolean = this.eq(o.asInstanceOf[AnyRef]) || (o match { + case x: NativeRunInfo => (this.cmd == x.cmd) && (this.args == x.args) && (this.connectInput == x.connectInput) && (this.outputStrategy == x.outputStrategy) && (this.workingDirectory == x.workingDirectory) && (this.environmentVariables == x.environmentVariables) && (this.inputs == x.inputs) && (this.outputs == x.outputs) + case _ => false + }) + override def hashCode: Int = { + 37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (37 * (17 + "sbt.internal.worker.NativeRunInfo".##) + cmd.##) + args.##) + connectInput.##) + outputStrategy.##) + workingDirectory.##) + environmentVariables.##) + inputs.##) + outputs.##) + } + override def toString: String = { + "NativeRunInfo(" + cmd + ", " + args + ", " + connectInput + ", " + outputStrategy + ", " + workingDirectory + ", " + environmentVariables + ", " + inputs + ", " + outputs + ")" + } + private[this] def copy(cmd: String = cmd, args: Vector[String] = args, connectInput: Boolean = connectInput, outputStrategy: Option[String] = outputStrategy, workingDirectory: Option[java.net.URI] = workingDirectory, environmentVariables: scala.collection.immutable.Map[String, String] = environmentVariables, inputs: Vector[sbt.internal.worker.FilePath] = inputs, outputs: Vector[sbt.internal.worker.FilePath] = outputs): NativeRunInfo = { + new NativeRunInfo(cmd, args, connectInput, outputStrategy, workingDirectory, environmentVariables, inputs, outputs) + } + def withCmd(cmd: String): NativeRunInfo = { + copy(cmd = cmd) + } + def withArgs(args: Vector[String]): NativeRunInfo = { + copy(args = args) + } + def withConnectInput(connectInput: Boolean): NativeRunInfo = { + copy(connectInput = connectInput) + } + def withOutputStrategy(outputStrategy: Option[String]): NativeRunInfo = { + copy(outputStrategy = outputStrategy) + } + def withOutputStrategy(outputStrategy: String): NativeRunInfo = { + copy(outputStrategy = Option(outputStrategy)) + } + def withWorkingDirectory(workingDirectory: Option[java.net.URI]): NativeRunInfo = { + copy(workingDirectory = workingDirectory) + } + def withWorkingDirectory(workingDirectory: java.net.URI): NativeRunInfo = { + copy(workingDirectory = Option(workingDirectory)) + } + def withEnvironmentVariables(environmentVariables: scala.collection.immutable.Map[String, String]): NativeRunInfo = { + copy(environmentVariables = environmentVariables) + } + def withInputs(inputs: Vector[sbt.internal.worker.FilePath]): NativeRunInfo = { + copy(inputs = inputs) + } + def withOutputs(outputs: Vector[sbt.internal.worker.FilePath]): NativeRunInfo = { + copy(outputs = outputs) + } +} +object NativeRunInfo { + + def apply(cmd: String, args: Vector[String], connectInput: Boolean, outputStrategy: Option[String], workingDirectory: Option[java.net.URI], environmentVariables: scala.collection.immutable.Map[String, String]): NativeRunInfo = new NativeRunInfo(cmd, args, connectInput, outputStrategy, workingDirectory, environmentVariables) + def apply(cmd: String, args: Vector[String], connectInput: Boolean, outputStrategy: String, workingDirectory: java.net.URI, environmentVariables: scala.collection.immutable.Map[String, String]): NativeRunInfo = new NativeRunInfo(cmd, args, connectInput, Option(outputStrategy), Option(workingDirectory), environmentVariables) + def apply(cmd: String, args: Vector[String], connectInput: Boolean, outputStrategy: Option[String], workingDirectory: Option[java.net.URI], environmentVariables: scala.collection.immutable.Map[String, String], inputs: Vector[sbt.internal.worker.FilePath], outputs: Vector[sbt.internal.worker.FilePath]): NativeRunInfo = new NativeRunInfo(cmd, args, connectInput, outputStrategy, workingDirectory, environmentVariables, inputs, outputs) + def apply(cmd: String, args: Vector[String], connectInput: Boolean, outputStrategy: String, workingDirectory: java.net.URI, environmentVariables: scala.collection.immutable.Map[String, String], inputs: Vector[sbt.internal.worker.FilePath], outputs: Vector[sbt.internal.worker.FilePath]): NativeRunInfo = new NativeRunInfo(cmd, args, connectInput, Option(outputStrategy), Option(workingDirectory), environmentVariables, inputs, outputs) +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/worker/RunInfo.scala b/protocol/src/main/contraband-scala/sbt/internal/worker/RunInfo.scala new file mode 100644 index 0000000000..855bd06d38 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/worker/RunInfo.scala @@ -0,0 +1,49 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.worker +final class RunInfo private ( + val jvm: Boolean, + val jvmRunInfo: Option[sbt.internal.worker.JvmRunInfo], + val nativeRunInfo: Option[sbt.internal.worker.NativeRunInfo]) extends Serializable { + + private def this(jvm: Boolean, jvmRunInfo: Option[sbt.internal.worker.JvmRunInfo]) = this(jvm, jvmRunInfo, None) + + override def equals(o: Any): Boolean = this.eq(o.asInstanceOf[AnyRef]) || (o match { + case x: RunInfo => (this.jvm == x.jvm) && (this.jvmRunInfo == x.jvmRunInfo) && (this.nativeRunInfo == x.nativeRunInfo) + case _ => false + }) + override def hashCode: Int = { + 37 * (37 * (37 * (37 * (17 + "sbt.internal.worker.RunInfo".##) + jvm.##) + jvmRunInfo.##) + nativeRunInfo.##) + } + override def toString: String = { + "RunInfo(" + jvm + ", " + jvmRunInfo + ", " + nativeRunInfo + ")" + } + private[this] def copy(jvm: Boolean = jvm, jvmRunInfo: Option[sbt.internal.worker.JvmRunInfo] = jvmRunInfo, nativeRunInfo: Option[sbt.internal.worker.NativeRunInfo] = nativeRunInfo): RunInfo = { + new RunInfo(jvm, jvmRunInfo, nativeRunInfo) + } + def withJvm(jvm: Boolean): RunInfo = { + copy(jvm = jvm) + } + def withJvmRunInfo(jvmRunInfo: Option[sbt.internal.worker.JvmRunInfo]): RunInfo = { + copy(jvmRunInfo = jvmRunInfo) + } + def withJvmRunInfo(jvmRunInfo: sbt.internal.worker.JvmRunInfo): RunInfo = { + copy(jvmRunInfo = Option(jvmRunInfo)) + } + def withNativeRunInfo(nativeRunInfo: Option[sbt.internal.worker.NativeRunInfo]): RunInfo = { + copy(nativeRunInfo = nativeRunInfo) + } + def withNativeRunInfo(nativeRunInfo: sbt.internal.worker.NativeRunInfo): RunInfo = { + copy(nativeRunInfo = Option(nativeRunInfo)) + } +} +object RunInfo { + + def apply(jvm: Boolean, jvmRunInfo: Option[sbt.internal.worker.JvmRunInfo]): RunInfo = new RunInfo(jvm, jvmRunInfo) + def apply(jvm: Boolean, jvmRunInfo: sbt.internal.worker.JvmRunInfo): RunInfo = new RunInfo(jvm, Option(jvmRunInfo)) + def apply(jvm: Boolean, jvmRunInfo: Option[sbt.internal.worker.JvmRunInfo], nativeRunInfo: Option[sbt.internal.worker.NativeRunInfo]): RunInfo = new RunInfo(jvm, jvmRunInfo, nativeRunInfo) + def apply(jvm: Boolean, jvmRunInfo: sbt.internal.worker.JvmRunInfo, nativeRunInfo: sbt.internal.worker.NativeRunInfo): RunInfo = new RunInfo(jvm, Option(jvmRunInfo), Option(nativeRunInfo)) +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/worker/codec/ClientJobParamsFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/worker/codec/ClientJobParamsFormats.scala new file mode 100644 index 0000000000..e045d628ca --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/worker/codec/ClientJobParamsFormats.scala @@ -0,0 +1,27 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.worker.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait ClientJobParamsFormats { self: sbt.internal.worker.codec.RunInfoFormats with sbt.internal.worker.codec.JvmRunInfoFormats with sbt.internal.worker.codec.FilePathFormats with sjsonnew.BasicJsonProtocol with sbt.internal.worker.codec.NativeRunInfoFormats => +implicit lazy val ClientJobParamsFormat: JsonFormat[sbt.internal.worker.ClientJobParams] = new JsonFormat[sbt.internal.worker.ClientJobParams] { + override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.worker.ClientJobParams = { + __jsOpt match { + case Some(__js) => + unbuilder.beginObject(__js) + val runInfo = unbuilder.readField[Option[sbt.internal.worker.RunInfo]]("runInfo") + unbuilder.endObject() + sbt.internal.worker.ClientJobParams(runInfo) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.internal.worker.ClientJobParams, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("runInfo", obj.runInfo) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/worker/codec/FilePathFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/worker/codec/FilePathFormats.scala new file mode 100644 index 0000000000..ebbac551ff --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/worker/codec/FilePathFormats.scala @@ -0,0 +1,29 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.worker.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait FilePathFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val FilePathFormat: JsonFormat[sbt.internal.worker.FilePath] = new JsonFormat[sbt.internal.worker.FilePath] { + override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.worker.FilePath = { + __jsOpt match { + case Some(__js) => + unbuilder.beginObject(__js) + val path = unbuilder.readField[java.net.URI]("path") + val digest = unbuilder.readField[String]("digest") + unbuilder.endObject() + sbt.internal.worker.FilePath(path, digest) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.internal.worker.FilePath, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("path", obj.path) + builder.addField("digest", obj.digest) + 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 new file mode 100644 index 0000000000..fa29c174cd --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/worker/codec/JsonProtocol.scala @@ -0,0 +1,13 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.worker.codec +trait JsonProtocol extends sjsonnew.BasicJsonProtocol + with sbt.internal.worker.codec.FilePathFormats + with sbt.internal.worker.codec.JvmRunInfoFormats + with sbt.internal.worker.codec.NativeRunInfoFormats + with sbt.internal.worker.codec.RunInfoFormats + with sbt.internal.worker.codec.ClientJobParamsFormats +object JsonProtocol extends JsonProtocol \ No newline at end of file diff --git a/protocol/src/main/contraband-scala/sbt/internal/worker/codec/JvmRunInfoFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/worker/codec/JvmRunInfoFormats.scala new file mode 100644 index 0000000000..793828b5ef --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/worker/codec/JvmRunInfoFormats.scala @@ -0,0 +1,47 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.worker.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait JvmRunInfoFormats { self: sbt.internal.worker.codec.FilePathFormats with sjsonnew.BasicJsonProtocol => +implicit lazy val JvmRunInfoFormat: JsonFormat[sbt.internal.worker.JvmRunInfo] = new JsonFormat[sbt.internal.worker.JvmRunInfo] { + override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.worker.JvmRunInfo = { + __jsOpt match { + case Some(__js) => + unbuilder.beginObject(__js) + val args = unbuilder.readField[Vector[String]]("args") + val classpath = unbuilder.readField[Vector[sbt.internal.worker.FilePath]]("classpath") + val mainClass = unbuilder.readField[String]("mainClass") + val connectInput = unbuilder.readField[Boolean]("connectInput") + val javaHome = unbuilder.readField[Option[java.net.URI]]("javaHome") + val outputStrategy = unbuilder.readField[Option[String]]("outputStrategy") + val workingDirectory = unbuilder.readField[Option[java.net.URI]]("workingDirectory") + val jvmOptions = unbuilder.readField[Vector[String]]("jvmOptions") + val environmentVariables = unbuilder.readField[scala.collection.immutable.Map[String, String]]("environmentVariables") + val inputs = unbuilder.readField[Vector[sbt.internal.worker.FilePath]]("inputs") + val outputs = unbuilder.readField[Vector[sbt.internal.worker.FilePath]]("outputs") + unbuilder.endObject() + sbt.internal.worker.JvmRunInfo(args, classpath, mainClass, connectInput, javaHome, outputStrategy, workingDirectory, jvmOptions, environmentVariables, inputs, outputs) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.internal.worker.JvmRunInfo, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("args", obj.args) + builder.addField("classpath", obj.classpath) + builder.addField("mainClass", obj.mainClass) + builder.addField("connectInput", obj.connectInput) + builder.addField("javaHome", obj.javaHome) + builder.addField("outputStrategy", obj.outputStrategy) + builder.addField("workingDirectory", obj.workingDirectory) + builder.addField("jvmOptions", obj.jvmOptions) + builder.addField("environmentVariables", obj.environmentVariables) + builder.addField("inputs", obj.inputs) + builder.addField("outputs", obj.outputs) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/worker/codec/NativeRunInfoFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/worker/codec/NativeRunInfoFormats.scala new file mode 100644 index 0000000000..73588aa9f3 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/worker/codec/NativeRunInfoFormats.scala @@ -0,0 +1,41 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.worker.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait NativeRunInfoFormats { self: sbt.internal.worker.codec.FilePathFormats with sjsonnew.BasicJsonProtocol => +implicit lazy val NativeRunInfoFormat: JsonFormat[sbt.internal.worker.NativeRunInfo] = new JsonFormat[sbt.internal.worker.NativeRunInfo] { + override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.worker.NativeRunInfo = { + __jsOpt match { + case Some(__js) => + unbuilder.beginObject(__js) + val cmd = unbuilder.readField[String]("cmd") + val args = unbuilder.readField[Vector[String]]("args") + val connectInput = unbuilder.readField[Boolean]("connectInput") + val outputStrategy = unbuilder.readField[Option[String]]("outputStrategy") + val workingDirectory = unbuilder.readField[Option[java.net.URI]]("workingDirectory") + val environmentVariables = unbuilder.readField[scala.collection.immutable.Map[String, String]]("environmentVariables") + val inputs = unbuilder.readField[Vector[sbt.internal.worker.FilePath]]("inputs") + val outputs = unbuilder.readField[Vector[sbt.internal.worker.FilePath]]("outputs") + unbuilder.endObject() + sbt.internal.worker.NativeRunInfo(cmd, args, connectInput, outputStrategy, workingDirectory, environmentVariables, inputs, outputs) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.internal.worker.NativeRunInfo, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("cmd", obj.cmd) + builder.addField("args", obj.args) + builder.addField("connectInput", obj.connectInput) + builder.addField("outputStrategy", obj.outputStrategy) + builder.addField("workingDirectory", obj.workingDirectory) + builder.addField("environmentVariables", obj.environmentVariables) + builder.addField("inputs", obj.inputs) + builder.addField("outputs", obj.outputs) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/internal/worker/codec/RunInfoFormats.scala b/protocol/src/main/contraband-scala/sbt/internal/worker/codec/RunInfoFormats.scala new file mode 100644 index 0000000000..16e66747ea --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/internal/worker/codec/RunInfoFormats.scala @@ -0,0 +1,31 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.internal.worker.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait RunInfoFormats { self: sbt.internal.worker.codec.JvmRunInfoFormats with sbt.internal.worker.codec.FilePathFormats with sjsonnew.BasicJsonProtocol with sbt.internal.worker.codec.NativeRunInfoFormats => +implicit lazy val RunInfoFormat: JsonFormat[sbt.internal.worker.RunInfo] = new JsonFormat[sbt.internal.worker.RunInfo] { + override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.internal.worker.RunInfo = { + __jsOpt match { + case Some(__js) => + unbuilder.beginObject(__js) + val jvm = unbuilder.readField[Boolean]("jvm") + val jvmRunInfo = unbuilder.readField[Option[sbt.internal.worker.JvmRunInfo]]("jvmRunInfo") + val nativeRunInfo = unbuilder.readField[Option[sbt.internal.worker.NativeRunInfo]]("nativeRunInfo") + unbuilder.endObject() + sbt.internal.worker.RunInfo(jvm, jvmRunInfo, nativeRunInfo) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.internal.worker.RunInfo, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("jvm", obj.jvm) + builder.addField("jvmRunInfo", obj.jvmRunInfo) + builder.addField("nativeRunInfo", obj.nativeRunInfo) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/InitCommand.scala b/protocol/src/main/contraband-scala/sbt/protocol/InitCommand.scala index ddfe85f45f..26511bc1ee 100644 --- a/protocol/src/main/contraband-scala/sbt/protocol/InitCommand.scala +++ b/protocol/src/main/contraband-scala/sbt/protocol/InitCommand.scala @@ -7,22 +7,24 @@ package sbt.protocol final class InitCommand private ( val token: Option[String], val execId: Option[String], - val skipAnalysis: Option[Boolean]) extends sbt.protocol.CommandMessage() with Serializable { + val skipAnalysis: Option[Boolean], + val initializationOptions: Option[sbt.internal.protocol.InitializeOption]) extends sbt.protocol.CommandMessage() with Serializable { - private def this(token: Option[String], execId: Option[String]) = this(token, execId, None) + private def this(token: Option[String], execId: Option[String]) = this(token, execId, None, None) + private def this(token: Option[String], execId: Option[String], skipAnalysis: Option[Boolean]) = this(token, execId, skipAnalysis, None) override def equals(o: Any): Boolean = this.eq(o.asInstanceOf[AnyRef]) || (o match { - case x: InitCommand => (this.token == x.token) && (this.execId == x.execId) && (this.skipAnalysis == x.skipAnalysis) + case x: InitCommand => (this.token == x.token) && (this.execId == x.execId) && (this.skipAnalysis == x.skipAnalysis) && (this.initializationOptions == x.initializationOptions) case _ => false }) override def hashCode: Int = { - 37 * (37 * (37 * (37 * (17 + "sbt.protocol.InitCommand".##) + token.##) + execId.##) + skipAnalysis.##) + 37 * (37 * (37 * (37 * (37 * (17 + "sbt.protocol.InitCommand".##) + token.##) + execId.##) + skipAnalysis.##) + initializationOptions.##) } override def toString: String = { - "InitCommand(" + token + ", " + execId + ", " + skipAnalysis + ")" + "InitCommand(" + token + ", " + execId + ", " + skipAnalysis + ", " + initializationOptions + ")" } - private[this] def copy(token: Option[String] = token, execId: Option[String] = execId, skipAnalysis: Option[Boolean] = skipAnalysis): InitCommand = { - new InitCommand(token, execId, skipAnalysis) + private[this] def copy(token: Option[String] = token, execId: Option[String] = execId, skipAnalysis: Option[Boolean] = skipAnalysis, initializationOptions: Option[sbt.internal.protocol.InitializeOption] = initializationOptions): InitCommand = { + new InitCommand(token, execId, skipAnalysis, initializationOptions) } def withToken(token: Option[String]): InitCommand = { copy(token = token) @@ -42,6 +44,12 @@ final class InitCommand private ( def withSkipAnalysis(skipAnalysis: Boolean): InitCommand = { copy(skipAnalysis = Option(skipAnalysis)) } + def withInitializationOptions(initializationOptions: Option[sbt.internal.protocol.InitializeOption]): InitCommand = { + copy(initializationOptions = initializationOptions) + } + def withInitializationOptions(initializationOptions: sbt.internal.protocol.InitializeOption): InitCommand = { + copy(initializationOptions = Option(initializationOptions)) + } } object InitCommand { @@ -49,4 +57,6 @@ object InitCommand { def apply(token: String, execId: String): InitCommand = new InitCommand(Option(token), Option(execId)) def apply(token: Option[String], execId: Option[String], skipAnalysis: Option[Boolean]): InitCommand = new InitCommand(token, execId, skipAnalysis) def apply(token: String, execId: String, skipAnalysis: Boolean): InitCommand = new InitCommand(Option(token), Option(execId), Option(skipAnalysis)) + def apply(token: Option[String], execId: Option[String], skipAnalysis: Option[Boolean], initializationOptions: Option[sbt.internal.protocol.InitializeOption]): InitCommand = new InitCommand(token, execId, skipAnalysis, initializationOptions) + def apply(token: String, execId: String, skipAnalysis: Boolean, initializationOptions: sbt.internal.protocol.InitializeOption): InitCommand = new InitCommand(Option(token), Option(execId), Option(skipAnalysis), Option(initializationOptions)) } diff --git a/protocol/src/main/contraband-scala/sbt/protocol/codec/CommandMessageFormats.scala b/protocol/src/main/contraband-scala/sbt/protocol/codec/CommandMessageFormats.scala index 6f95b6f48a..a705d1b488 100644 --- a/protocol/src/main/contraband-scala/sbt/protocol/codec/CommandMessageFormats.scala +++ b/protocol/src/main/contraband-scala/sbt/protocol/codec/CommandMessageFormats.scala @@ -6,6 +6,6 @@ package sbt.protocol.codec import _root_.sjsonnew.JsonFormat -trait CommandMessageFormats { self: sjsonnew.BasicJsonProtocol with sbt.protocol.codec.InitCommandFormats with sbt.protocol.codec.ExecCommandFormats with sbt.protocol.codec.SettingQueryFormats with sbt.protocol.codec.AttachFormats with sbt.protocol.codec.TerminalCapabilitiesQueryFormats with sbt.protocol.codec.TerminalSetAttributesCommandFormats with sbt.protocol.codec.TerminalAttributesQueryFormats with sbt.protocol.codec.TerminalGetSizeQueryFormats with sbt.protocol.codec.TerminalSetSizeCommandFormats with sbt.protocol.codec.TerminalSetEchoCommandFormats with sbt.protocol.codec.TerminalSetRawModeCommandFormats => +trait CommandMessageFormats { self: sbt.internal.protocol.codec.InitializeOptionFormats with sjsonnew.BasicJsonProtocol with sbt.protocol.codec.InitCommandFormats with sbt.protocol.codec.ExecCommandFormats with sbt.protocol.codec.SettingQueryFormats with sbt.protocol.codec.AttachFormats with sbt.protocol.codec.TerminalCapabilitiesQueryFormats with sbt.protocol.codec.TerminalSetAttributesCommandFormats with sbt.protocol.codec.TerminalAttributesQueryFormats with sbt.protocol.codec.TerminalGetSizeQueryFormats with sbt.protocol.codec.TerminalSetSizeCommandFormats with sbt.protocol.codec.TerminalSetEchoCommandFormats with sbt.protocol.codec.TerminalSetRawModeCommandFormats => implicit lazy val CommandMessageFormat: JsonFormat[sbt.protocol.CommandMessage] = flatUnionFormat11[sbt.protocol.CommandMessage, sbt.protocol.InitCommand, sbt.protocol.ExecCommand, sbt.protocol.SettingQuery, sbt.protocol.Attach, sbt.protocol.TerminalCapabilitiesQuery, sbt.protocol.TerminalSetAttributesCommand, sbt.protocol.TerminalAttributesQuery, sbt.protocol.TerminalGetSizeQuery, sbt.protocol.TerminalSetSizeCommand, sbt.protocol.TerminalSetEchoCommand, sbt.protocol.TerminalSetRawModeCommand]("type") } diff --git a/protocol/src/main/contraband-scala/sbt/protocol/codec/InitCommandFormats.scala b/protocol/src/main/contraband-scala/sbt/protocol/codec/InitCommandFormats.scala index 827b6dc7c0..7d552b17b3 100644 --- a/protocol/src/main/contraband-scala/sbt/protocol/codec/InitCommandFormats.scala +++ b/protocol/src/main/contraband-scala/sbt/protocol/codec/InitCommandFormats.scala @@ -5,7 +5,7 @@ // DO NOT EDIT MANUALLY package sbt.protocol.codec import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } -trait InitCommandFormats { self: sjsonnew.BasicJsonProtocol => +trait InitCommandFormats { self: sbt.internal.protocol.codec.InitializeOptionFormats with sjsonnew.BasicJsonProtocol => implicit lazy val InitCommandFormat: JsonFormat[sbt.protocol.InitCommand] = new JsonFormat[sbt.protocol.InitCommand] { override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.InitCommand = { __jsOpt match { @@ -14,8 +14,9 @@ implicit lazy val InitCommandFormat: JsonFormat[sbt.protocol.InitCommand] = new val token = unbuilder.readField[Option[String]]("token") val execId = unbuilder.readField[Option[String]]("execId") val skipAnalysis = unbuilder.readField[Option[Boolean]]("skipAnalysis") + val initializationOptions = unbuilder.readField[Option[sbt.internal.protocol.InitializeOption]]("initializationOptions") unbuilder.endObject() - sbt.protocol.InitCommand(token, execId, skipAnalysis) + sbt.protocol.InitCommand(token, execId, skipAnalysis, initializationOptions) case None => deserializationError("Expected JsObject but found None") } @@ -25,6 +26,7 @@ implicit lazy val InitCommandFormat: JsonFormat[sbt.protocol.InitCommand] = new builder.addField("token", obj.token) builder.addField("execId", obj.execId) builder.addField("skipAnalysis", obj.skipAnalysis) + builder.addField("initializationOptions", obj.initializationOptions) builder.endObject() } } diff --git a/protocol/src/main/contraband-scala/sbt/protocol/codec/JsonProtocol.scala b/protocol/src/main/contraband-scala/sbt/protocol/codec/JsonProtocol.scala index 2df56d1ad3..32852fe440 100644 --- a/protocol/src/main/contraband-scala/sbt/protocol/codec/JsonProtocol.scala +++ b/protocol/src/main/contraband-scala/sbt/protocol/codec/JsonProtocol.scala @@ -4,7 +4,8 @@ // DO NOT EDIT MANUALLY package sbt.protocol.codec -trait JsonProtocol extends sjsonnew.BasicJsonProtocol +trait JsonProtocol extends sbt.internal.protocol.codec.InitializeOptionFormats + with sjsonnew.BasicJsonProtocol with sbt.protocol.codec.InitCommandFormats with sbt.protocol.codec.ExecCommandFormats with sbt.protocol.codec.SettingQueryFormats diff --git a/protocol/src/main/contraband/portfile.contra b/protocol/src/main/contraband/portfile.contra index 2e138c3159..ffd5dc6c9a 100644 --- a/protocol/src/main/contraband/portfile.contra +++ b/protocol/src/main/contraband/portfile.contra @@ -16,7 +16,10 @@ type TokenFile { token: String! } +## Passed into InitializeParams as part of "initialize" request as the user-defined option. +## https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize type InitializeOption { token: String skipAnalysis: Boolean @since("1.4.0") + canWork: Boolean @since("1.10.8") } diff --git a/protocol/src/main/contraband/server.contra b/protocol/src/main/contraband/server.contra index 18ec0a0d2f..176db450a7 100644 --- a/protocol/src/main/contraband/server.contra +++ b/protocol/src/main/contraband/server.contra @@ -11,6 +11,7 @@ type InitCommand implements CommandMessage { token: String execId: String skipAnalysis: Boolean @since("1.4.0") + initializationOptions: sbt.internal.protocol.InitializeOption @since("1.10.8") } ## Command to execute sbt command. diff --git a/protocol/src/main/contraband/worker.contra b/protocol/src/main/contraband/worker.contra new file mode 100644 index 0000000000..45a68eb741 --- /dev/null +++ b/protocol/src/main/contraband/worker.contra @@ -0,0 +1,51 @@ +package sbt.internal.worker +@target(Scala) +@codecPackage("sbt.internal.worker.codec") +@fullCodec("JsonProtocol") + +type FilePath { + path: java.net.URI! + digest: String! +} + +type JvmRunInfo { + args: [String], + classpath: [sbt.internal.worker.FilePath], + mainClass: String! + connectInput: Boolean! + javaHome: java.net.URI + outputStrategy: String + workingDirectory: java.net.URI + jvmOptions: [String] + environmentVariables: StringStringMap! + inputs: [sbt.internal.worker.FilePath] @since("0.1.0"), + outputs: [sbt.internal.worker.FilePath] @since("0.1.0"), +} + +type NativeRunInfo { + cmd: String!, + args: [String], + connectInput: Boolean! + outputStrategy: String + workingDirectory: java.net.URI + environmentVariables: StringStringMap! + inputs: [sbt.internal.worker.FilePath] @since("0.1.0"), + outputs: [sbt.internal.worker.FilePath] @since("0.1.0"), +} + +type RunInfo { + jvm: Boolean! + jvmRunInfo: sbt.internal.worker.JvmRunInfo, + nativeRunInfo: sbt.internal.worker.NativeRunInfo @since("0.1.0"), +} + +## Client-side job support. +## +## Notification: sbt/clientJob +## +## Parameter for the sbt/clientJob notification. +## A client-side job represents a unit of work that sbt server +## can outsourse back to the client, for example for run task. +type ClientJobParams { + runInfo: sbt.internal.worker.RunInfo +} diff --git a/protocol/src/main/scala/sbt/protocol/Serialization.scala b/protocol/src/main/scala/sbt/protocol/Serialization.scala index 2dcd5ae981..1384fb4f24 100644 --- a/protocol/src/main/scala/sbt/protocol/Serialization.scala +++ b/protocol/src/main/scala/sbt/protocol/Serialization.scala @@ -27,6 +27,7 @@ object Serialization { private[sbt] val VsCode = "application/vscode-jsonrpc; charset=utf-8" val readSystemIn = "sbt/readSystemIn" val cancelReadSystemIn = "sbt/cancelReadSystemIn" + val clientJob = "sbt/clientJob" val systemIn = "sbt/systemIn" val systemOut = "sbt/systemOut" val systemErr = "sbt/systemErr" @@ -67,15 +68,10 @@ object Serialization { command match { case x: InitCommand => val execId = x.execId.getOrElse(UUID.randomUUID.toString) - val analysis = s""""skipAnalysis" : ${x.skipAnalysis.getOrElse(false)}""" - val opt = x.token match { - case Some(t) => - val json: JValue = Converter.toJson[String](t).get - val v = CompactPrinter(json) - s"""{ "token": $v, $analysis }""" - case None => s"{ $analysis }" - } - s"""{ "jsonrpc": "2.0", "id": "$execId", "method": "initialize", "params": { "initializationOptions": $opt } }""" + val opts = x.initializationOptions.getOrElse(sys.error("expected initializationOptions")) + import sbt.protocol.codec.JsonProtocol._ + val optsJson = CompactPrinter(Converter.toJson(opts).get) + s"""{ "jsonrpc": "2.0", "id": "$execId", "method": "initialize", "params": { "initializationOptions": $optsJson } }""" case x: ExecCommand => val execId = x.execId.getOrElse(UUID.randomUUID.toString) val json: JValue = Converter.toJson[String](x.commandLine).get diff --git a/run/src/main/scala/sbt/Fork.scala b/run/src/main/scala/sbt/Fork.scala index 3335f53f33..07fc33924b 100644 --- a/run/src/main/scala/sbt/Fork.scala +++ b/run/src/main/scala/sbt/Fork.scala @@ -33,15 +33,8 @@ final class Fork(val commandName: String, val runnerClass: Option[String]) { * It is configured according to `config`. * If `runnerClass` is defined for this Fork instance, it is prepended to `arguments` to define the arguments passed to the forked command. */ - def apply(config: ForkOptions, arguments: Seq[String]): Int = { - val p = fork(config, arguments) - RunningProcesses.add(p) - try p.exitValue() - finally { - if (p.isAlive()) p.destroy() - RunningProcesses.remove(p) - } - } + def apply(config: ForkOptions, arguments: Seq[String]): Int = + Fork.blockForExitCode(fork(config, arguments)) /** * Forks the configured process and returns a `Process` that can be used to wait for completion or to terminate the forked process. @@ -50,37 +43,22 @@ final class Fork(val commandName: String, val runnerClass: Option[String]) { * If `runnerClass` is defined for this Fork instance, it is prepended to `arguments` to define the arguments passed to the forked command. */ def fork(config: ForkOptions, arguments: Seq[String]): Process = { - import config.{ envVars => env, _ } + import config._ val executable = Fork.javaCommand(javaHome, commandName).getAbsolutePath val preOptions = makeOptions(runJVMOptions, bootJars, arguments) val (classpathEnv, options) = Fork.fitClasspath(preOptions) val command = executable +: options - - val environment: List[(String, String)] = env.toList ++ - (classpathEnv map { value => - Fork.ClasspathEnvKey -> value - }) val jpb = if (Fork.shouldUseArgumentsFile(options)) new JProcessBuilder(executable, Fork.createArgumentsFile(options)) else new JProcessBuilder(command.toArray: _*) - workingDirectory foreach (jpb directory _) - environment foreach { case (k, v) => jpb.environment.put(k, v) } - if (connectInput) { - jpb.redirectInput(Redirect.INHERIT) - () - } - val process = Process(jpb) - - outputStrategy.getOrElse(StdoutOutput: OutputStrategy) match { - case StdoutOutput => process.run(connectInput = false) - case out: BufferedOutput => - out.logger.buffer { process.run(out.logger, connectInput = false) } - case out: LoggedOutput => process.run(out.logger, connectInput = false) - case out: CustomOutput => (process #> out.output).run(connectInput = false) + val extraEnv = classpathEnv.toList.map { value => + Fork.ClasspathEnvKey -> value } + Fork.forkInternal(config, extraEnv, jpb) } + private[this] def makeOptions( jvmOptions: Seq[String], bootJars: Iterable[File], @@ -185,4 +163,36 @@ object Fork { pw.close() s"@${file.getAbsolutePath}" } + + private[sbt] def forkInternal( + config: ForkOptions, + extraEnv: List[(String, String)], + jpb: JProcessBuilder + ): Process = { + import config.{ envVars => env, _ } + val environment: List[(String, String)] = env.toList ++ extraEnv + workingDirectory.foreach(jpb directory _) + environment.foreach { case (k, v) => jpb.environment.put(k, v) } + if (connectInput) { + jpb.redirectInput(Redirect.INHERIT) + () + } + val process = Process(jpb) + outputStrategy.getOrElse(StdoutOutput: OutputStrategy) match { + case StdoutOutput => process.run(connectInput = false) + case out: BufferedOutput => + out.logger.buffer { process.run(out.logger, connectInput = false) } + case out: LoggedOutput => process.run(out.logger, connectInput = false) + case out: CustomOutput => (process #> out.output).run(connectInput = false) + } + } + + private[sbt] def blockForExitCode(p: Process): Int = { + RunningProcesses.add(p) + try p.exitValue() + finally { + if (p.isAlive()) p.destroy() + RunningProcesses.remove(p) + } + } } diff --git a/run/src/main/scala/sbt/Run.scala b/run/src/main/scala/sbt/Run.scala index 5445c0da51..cda9e58884 100644 --- a/run/src/main/scala/sbt/Run.scala +++ b/run/src/main/scala/sbt/Run.scala @@ -26,25 +26,16 @@ sealed trait ScalaRun { } class ForkRun(config: ForkOptions) extends ScalaRun { def run(mainClass: String, classpath: Seq[File], options: Seq[String], log: Logger): Try[Unit] = { - def processExitCode(exitCode: Int, label: String): Try[Unit] = - if (exitCode == 0) Success(()) - else - Failure( - new MessageOnlyException( - s"""Nonzero exit code returned from $label: $exitCode""".stripMargin - ) - ) - log.info(s"running (fork) $mainClass ${Run.runOptionsStr(options)}") val c = configLogged(log) val scalaOpts = scalaOptions(mainClass, classpath, options) val exitCode = try Fork.java(c, scalaOpts) catch { case _: InterruptedException => - log.warn("Run canceled.") + log.warn("run canceled") 1 } - processExitCode(exitCode, "runner") + Run.processExitCode(exitCode, "runner") } def fork(mainClass: String, classpath: Seq[File], options: Seq[String], log: Logger): Process = { @@ -195,4 +186,13 @@ object Run { case str if str.contains(" ") => "\"" + str + "\"" case str => str }).mkString(" ") + + private[sbt] def processExitCode(exitCode: Int, label: String): Try[Unit] = + if (exitCode == 0) Success(()) + else + Failure( + new MessageOnlyException( + s"""nonzero exit code returned from $label: $exitCode""".stripMargin + ) + ) } diff --git a/server-test/src/server-test/client/build.sbt b/server-test/src/server-test/client/build.sbt index 3225bd76da..686d2a7a8d 100644 --- a/server-test/src/server-test/client/build.sbt +++ b/server-test/src/server-test/client/build.sbt @@ -1,7 +1,9 @@ +scalaVersion := "3.6.3" + TaskKey[Unit]("willSucceed") := println("success") TaskKey[Unit]("willFail") := { throw new Exception("failed") } -libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.8" % "test" +libraryDependencies += "org.scalameta" %% "munit" % "1.0.4" % Test TaskKey[Unit]("fooBar") := { () } diff --git a/server-test/src/server-test/client/src/main/scala/A.scala b/server-test/src/server-test/client/src/main/scala/A.scala index 69c493db21..171b96e913 100644 --- a/server-test/src/server-test/client/src/main/scala/A.scala +++ b/server-test/src/server-test/client/src/main/scala/A.scala @@ -1 +1,3 @@ -object A +class A + +@main def hello() = println("Hello, World!") diff --git a/server-test/src/server-test/client/src/test/scala/FooSpec.scala b/server-test/src/server-test/client/src/test/scala/FooSpec.scala index 269be56244..fb5352fd9b 100644 --- a/server-test/src/server-test/client/src/test/scala/FooSpec.scala +++ b/server-test/src/server-test/client/src/test/scala/FooSpec.scala @@ -1,3 +1,3 @@ package test.pkg -class FooSpec extends org.scalatest.FlatSpec +class FooSpec extends munit.FunSuite diff --git a/server-test/src/test/scala/testpkg/ClientTest.scala b/server-test/src/test/scala/testpkg/ClientTest.scala index b4352062a0..be9a29a137 100644 --- a/server-test/src/test/scala/testpkg/ClientTest.scala +++ b/server-test/src/test/scala/testpkg/ClientTest.scala @@ -57,7 +57,7 @@ object ClientTest extends AbstractServerTest { case r => r } } - private def client(args: String*): Int = { + private def client(args: String*): Int = background( NetworkClient.client( testPath.toFile, @@ -68,6 +68,19 @@ object ClientTest extends AbstractServerTest { false ) ) + private def clientWithStdoutLines(args: String*): (Int, Seq[String]) = { + val out = new CachingPrintStream + val exitCode = background( + NetworkClient.client( + testPath.toFile, + args.toArray, + NullInputStream, + out, + NullPrintStream, + false + ) + ) + (exitCode, out.lines) } // This ensures that the completion command will send a tab that triggers // sbt to call definedTestNames or discoveredMainClasses if there hasn't @@ -107,6 +120,11 @@ object ClientTest extends AbstractServerTest { test("three commands with middle failure") { _ => assert(client("compile;willFail;willSucceed") == 1) } + test("run") { _ => + val (exitCode, lines) = clientWithStdoutLines("run") + assert(exitCode == 0) + assert(lines.toList.exists(_.endsWith("Hello, World!"))) + } test("compi completions") { _ => val expected = Vector( "compile", diff --git a/server-test/src/test/scala/testpkg/TestServer.scala b/server-test/src/test/scala/testpkg/TestServer.scala index dac89ff79e..c3a78bc6ad 100644 --- a/server-test/src/test/scala/testpkg/TestServer.scala +++ b/server-test/src/test/scala/testpkg/TestServer.scala @@ -220,7 +220,7 @@ case class TestServer( // initiate handshake sendJsonRpc( - s"""{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "initializationOptions": { "skipAnalysis": true } } }""" + s"""{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "initializationOptions": { "skipAnalysis": true, "canWork": true } } }""" ) def test(f: TestServer => Future[Assertion]): Future[Assertion] = { From 7409de3c405d6a7da76d440137f632438794cbf8 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sun, 2 Mar 2025 21:02:55 -0500 Subject: [PATCH 19/24] fix: sbt init **Problem** `sbt init` no longer works because of --allow-empty check. **Solution** Skip allow empty check for sbt init. --- launcher-package/src/universal/bin/sbt.bat | 5 +++++ sbt | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/launcher-package/src/universal/bin/sbt.bat b/launcher-package/src/universal/bin/sbt.bat index 2bd4dd49e6..3de1fbd88b 100755 --- a/launcher-package/src/universal/bin/sbt.bat +++ b/launcher-package/src/universal/bin/sbt.bat @@ -476,6 +476,11 @@ if "%~0" == "new" ( set sbt_new=true ) ) +if "%~0" == "init" ( + if not defined SBT_ARGS ( + set sbt_new=true + ) +) if "%g:~0,2%" == "-D" ( rem special handling for -D since '=' gets parsed away diff --git a/sbt b/sbt index 9a7c36aa57..e414ad7bb2 100755 --- a/sbt +++ b/sbt @@ -639,7 +639,7 @@ process_my_args () { -allow-empty|--allow-empty|-sbt-create|--sbt-create) allow_empty=true && shift ;; - new) sbt_new=true && addResidual "$1" && shift ;; + new|init) sbt_new=true && addResidual "$1" && shift ;; *) addResidual "$1" && shift ;; esac From 444362c735d764893df74098879db778ddd003a1 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sun, 2 Mar 2025 22:30:45 -0500 Subject: [PATCH 20/24] lm 1.10.4 --- project/Dependencies.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 4c35c591db..e20d54c805 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -12,9 +12,9 @@ object Dependencies { sys.env.get("BUILD_VERSION") orElse sys.props.get("sbt.build.version") // sbt modules - private val ioVersion = nightlyVersion.getOrElse("1.10.3") + private val ioVersion = nightlyVersion.getOrElse("1.10.4") private val lmVersion = - sys.props.get("sbt.build.lm.version").orElse(nightlyVersion).getOrElse("1.10.3") + sys.props.get("sbt.build.lm.version").orElse(nightlyVersion).getOrElse("1.10.4") val zincVersion = nightlyVersion.getOrElse("1.10.7") private val sbtIO = "org.scala-sbt" %% "io" % ioVersion From a18ed19cbc3c3137fd9cd8a294278b1426035baa Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sun, 15 Dec 2024 02:47:35 -0500 Subject: [PATCH 21/24] fix: Use JDK path, not JRE path **Problem** There are a few places where javaHome or java path is set, using java.home system property. The problem is that it points to JRE, not JDK, so it would break on Java compilation etc. **Solution** If the path ends with jre, go up one directory. --- .../main/scala/sbt/internal/util/Util.scala | 5 +++++ main/src/main/scala/sbt/Defaults.scala | 6 ++--- .../internal/bsp/BuildServerConnection.scala | 3 ++- .../test/scala/testpkg/BuildServerTest.scala | 22 +++++++++++++------ 4 files changed, 24 insertions(+), 12 deletions(-) diff --git a/internal/util-collection/src/main/scala/sbt/internal/util/Util.scala b/internal/util-collection/src/main/scala/sbt/internal/util/Util.scala index c182bad16e..4e4d3e8b7c 100644 --- a/internal/util-collection/src/main/scala/sbt/internal/util/Util.scala +++ b/internal/util-collection/src/main/scala/sbt/internal/util/Util.scala @@ -8,6 +8,7 @@ package sbt.internal.util +import java.nio.file.{ Path, Paths } import java.util.Locale import scala.reflect.macros.blackbox @@ -121,4 +122,8 @@ object Util { case g: ThreadId @unchecked => g.threadId } } + + lazy val javaHome: Path = + if (sys.props("java.home").endsWith("jre")) Paths.get(sys.props("java.home")).getParent() + else Paths.get(sys.props("java.home")) } diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index f3a36453c8..0b54ba2e5f 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -9,8 +9,7 @@ package sbt import java.io.{ File, PrintWriter } -import java.net.{ URI, URL } -import java.nio.file.{ Paths, Path => NioPath } +import java.nio.file.{ Path => NioPath } import java.util.Optional import java.util.concurrent.TimeUnit import lmcoursier.CoursierDependencyResolution @@ -408,13 +407,12 @@ object Defaults extends BuildCommon { val boot = app.provider.scalaProvider.launcher.bootDirectory val ih = app.provider.scalaProvider.launcher.ivyHome val coursierCache = csrCacheDirectory.value - val javaHome = Paths.get(sys.props("java.home")) Map( "BASE" -> base.toPath, "SBT_BOOT" -> boot.toPath, "CSR_CACHE" -> coursierCache.toPath, "IVY_HOME" -> ih.toPath, - "JAVA_HOME" -> javaHome, + "JAVA_HOME" -> Util.javaHome, ) }, fileConverter := MappedFileConverter(rootPaths.value, allowMachinePath.value), diff --git a/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala b/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala index e03b67eae5..ba9042dde8 100644 --- a/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala +++ b/protocol/src/main/scala/sbt/internal/bsp/BuildServerConnection.scala @@ -9,6 +9,7 @@ package sbt.internal.bsp import sbt.internal.bsp.codec.JsonProtocol.BspConnectionDetailsFormat +import sbt.internal.util.Util import sbt.io.IO import sjsonnew.support.scalajson.unsafe.{ CompactPrinter, Converter } @@ -25,7 +26,7 @@ object BuildServerConnection { private[sbt] def writeConnectionFile(sbtVersion: String, baseDir: File): Unit = { val bspConnectionFile = new File(baseDir, ".bsp/sbt.json") - val javaHome = System.getProperty("java.home") + val javaHome = Util.javaHome val classPath = System.getProperty("java.class.path") val sbtScript = Option(System.getProperty("sbt.script")) diff --git a/server-test/src/test/scala/testpkg/BuildServerTest.scala b/server-test/src/test/scala/testpkg/BuildServerTest.scala index c24694587e..0c0306fe9d 100644 --- a/server-test/src/test/scala/testpkg/BuildServerTest.scala +++ b/server-test/src/test/scala/testpkg/BuildServerTest.scala @@ -228,7 +228,6 @@ object BuildServerTest extends AbstractServerTest { val buildTarget = buildTargetUri("javaProj", "Compile") compile(buildTarget) - assertMessage( "build/publishDiagnostics", "Hello.java", @@ -251,16 +250,17 @@ object BuildServerTest extends AbstractServerTest { val testFile = new File(svr.baseDirectory, s"java-proj/src/main/java/example/Hello.java") val otherBuildFile = new File(svr.baseDirectory, "force-java-out-of-process-compiler.sbt") - // Setting `javaHome` will force SBT to shell out to an external Java compiler instead + // Setting `javaHome` will force sbt to shell out to an external Java compiler instead // of using the local compilation service offered by the JVM running this SBT instance. IO.write( otherBuildFile, """ + |def jdk: File = sbt.internal.util.Util.javaHome.toFile() |lazy val javaProj = project | .in(file("java-proj")) | .settings( | javacOptions += "-Xlint:all", - | javaHome := Some(file(System.getProperty("java.home"))) + | javaHome := Some(jdk) | ) |""".stripMargin ) @@ -272,16 +272,17 @@ object BuildServerTest extends AbstractServerTest { "build/publishDiagnostics", "Hello.java", """"severity":2""", - """found raw type: List""" - )(message = "should send publishDiagnostics with severity 2 for Hello.java") + """found raw type""" + )(message = "should send publishDiagnostics with severity 2 for Hello.java", debug = false) assertMessage( "build/publishDiagnostics", "Hello.java", """"severity":1""", - """incompatible types: int cannot be converted to String""" + """incompatible types: int cannot be converted""" )( - message = "should send publishDiagnostics with severity 1 for Hello.java" + message = "should send publishDiagnostics with severity 1 for Hello.java", + debug = true ) // Note the messages changed slightly in both cases. That's interesting… @@ -304,6 +305,7 @@ object BuildServerTest extends AbstractServerTest { compile(buildTarget) + /* assertMessage( "build/publishDiagnostics", "Hello.java", @@ -312,6 +314,7 @@ object BuildServerTest extends AbstractServerTest { )( message = "should send publishDiagnostics with empty diagnostics" ) + */ IO.delete(otherBuildFile) reloadWorkspace() @@ -685,6 +688,11 @@ object BuildServerTest extends AbstractServerTest { def assertion = svr.waitForString(duration) { msg => if (debug) println(msg) + if (debug) + parts.foreach { p => + if (msg.contains(p)) println(s"> $msg contains $p") + else () + } parts.forall(msg.contains) } if (message.nonEmpty) assert.apply(assertion, message) else assert(assertion) From 5d5fe21ec5aeb201bd3ac747935261ae99bbdfeb Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Mon, 3 Mar 2025 04:34:19 -0500 Subject: [PATCH 22/24] Revert run switching on 1.x **Problem** client-side run apparently won't work for Scala.JS, so forcing sbtn users to client-side run will break the Scala.JS users. **Solution** This reverts the client-side run on sbt 1.x, while retaining the mechanism for sbt 2.x usages via sbtn. Now, if `run / connectInput := true` is true, stdout will not display on sbtn. --- .../sbt/internal/server/NetworkChannel.scala | 15 --------------- .../src/test/scala/testpkg/ClientTest.scala | 7 +------ 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/main/src/main/scala/sbt/internal/server/NetworkChannel.scala b/main/src/main/scala/sbt/internal/server/NetworkChannel.scala index 6eebe449af..36ace91f14 100644 --- a/main/src/main/scala/sbt/internal/server/NetworkChannel.scala +++ b/main/src/main/scala/sbt/internal/server/NetworkChannel.scala @@ -148,21 +148,6 @@ final class NetworkChannel( self.onCancellationRequest(execId, crp) } - // Take over commandline for network channel - private val networkCommand: PartialFunction[String, String] = { - case cmd if cmd.split(" ").head.split("/").last == "run" => - s"clientJob $cmd" - } - override protected def appendExec(commandLine: String, execId: Option[String]): Boolean = - if (clientCanWork && networkCommand.isDefinedAt(commandLine)) - super.appendExec(networkCommand(commandLine), execId) - else super.appendExec(commandLine, execId) - - override private[sbt] def onCommandLine(cmd: String): Boolean = - if (clientCanWork && networkCommand.isDefinedAt(cmd)) - appendExec(networkCommand(cmd), None) - else super.onCommandLine(cmd) - protected def setInitializeOption(opts: InitializeOption): Unit = initializeOption.set(opts) // Returns true if sbtn has declared with canWork: true diff --git a/server-test/src/test/scala/testpkg/ClientTest.scala b/server-test/src/test/scala/testpkg/ClientTest.scala index be9a29a137..628cbbda3b 100644 --- a/server-test/src/test/scala/testpkg/ClientTest.scala +++ b/server-test/src/test/scala/testpkg/ClientTest.scala @@ -68,7 +68,7 @@ object ClientTest extends AbstractServerTest { false ) ) - private def clientWithStdoutLines(args: String*): (Int, Seq[String]) = { + def clientWithStdoutLines(args: String*): (Int, Seq[String]) = { val out = new CachingPrintStream val exitCode = background( NetworkClient.client( @@ -120,11 +120,6 @@ object ClientTest extends AbstractServerTest { test("three commands with middle failure") { _ => assert(client("compile;willFail;willSucceed") == 1) } - test("run") { _ => - val (exitCode, lines) = clientWithStdoutLines("run") - assert(exitCode == 0) - assert(lines.toList.exists(_.endsWith("Hello, World!"))) - } test("compi completions") { _ => val expected = Vector( "compile", From 290431bfc5e249d9002959ea743d9f44ab15b3c7 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Mon, 3 Mar 2025 12:22:09 -0500 Subject: [PATCH 23/24] Zinc 1.10.8 + sbtn 1.10.8 --- launcher-package/build.sbt | 2 +- project/Dependencies.scala | 2 +- sbt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/launcher-package/build.sbt b/launcher-package/build.sbt index 6ef8c07582..b58d8b2770 100755 --- a/launcher-package/build.sbt +++ b/launcher-package/build.sbt @@ -121,7 +121,7 @@ val root = (project in file(".")). file }, // update sbt.sh at root - sbtnVersion := "1.10.5", + sbtnVersion := "1.10.8", sbtnJarsBaseUrl := "https://github.com/sbt/sbtn-dist/releases/download", sbtnJarsMappings := { val baseUrl = sbtnJarsBaseUrl.value diff --git a/project/Dependencies.scala b/project/Dependencies.scala index e20d54c805..8e040b7e50 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -15,7 +15,7 @@ object Dependencies { private val ioVersion = nightlyVersion.getOrElse("1.10.4") private val lmVersion = sys.props.get("sbt.build.lm.version").orElse(nightlyVersion).getOrElse("1.10.4") - val zincVersion = nightlyVersion.getOrElse("1.10.7") + val zincVersion = nightlyVersion.getOrElse("1.10.8") private val sbtIO = "org.scala-sbt" %% "io" % ioVersion diff --git a/sbt b/sbt index e414ad7bb2..e956473c64 100755 --- a/sbt +++ b/sbt @@ -24,7 +24,7 @@ declare build_props_sbt_version= declare use_sbtn= declare no_server= declare sbtn_command="$SBTN_CMD" -declare sbtn_version="1.10.5" +declare sbtn_version="1.10.8" declare use_colors=1 ### ------------------------------- ### From a3a72b32454ca282ea40facc928d8317fa8f43e4 Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Mon, 3 Mar 2025 13:06:40 -0500 Subject: [PATCH 24/24] sbt 1.10.9 --- sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sbt b/sbt index e956473c64..3c7e9ab015 100755 --- a/sbt +++ b/sbt @@ -1,7 +1,7 @@ #!/usr/bin/env bash set +e -declare builtin_sbt_version="1.10.7" +declare builtin_sbt_version="1.10.9" declare -a residual_args declare -a java_args declare -a scalac_args