mirror of https://github.com/sbt/sbt.git
[2.x] fix: Make forked run/test inherit sbt's working directory (#1032)
This commit is contained in:
parent
d3141f1391
commit
b06353504f
|
|
@ -191,6 +191,7 @@ object Defaults extends BuildCommon with DefExtra {
|
||||||
apiURL := None,
|
apiURL := None,
|
||||||
releaseNotesURL := None,
|
releaseNotesURL := None,
|
||||||
javaHome :== None,
|
javaHome :== None,
|
||||||
|
forkWorkingDirectory :== None,
|
||||||
discoveredJavaHomes := CrossJava.discoverJavaHomes,
|
discoveredJavaHomes := CrossJava.discoverJavaHomes,
|
||||||
javaHomes :== ListMap.empty,
|
javaHomes :== ListMap.empty,
|
||||||
fullJavaHomes := CrossJava.expandJavaHomes(discoveredJavaHomes.value ++ javaHomes.value),
|
fullJavaHomes := CrossJava.expandJavaHomes(discoveredJavaHomes.value ++ javaHomes.value),
|
||||||
|
|
@ -1405,6 +1406,8 @@ object Defaults extends BuildCommon with DefExtra {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The forked process inherits sbt's working directory unless `forkWorkingDirectory` is set. */
|
||||||
def forkOptionsTask: Initialize[Task[ForkOptions]] =
|
def forkOptionsTask: Initialize[Task[ForkOptions]] =
|
||||||
Def.task {
|
Def.task {
|
||||||
val canUseArgumentsFile = sys.props
|
val canUseArgumentsFile = sys.props
|
||||||
|
|
@ -1415,7 +1418,7 @@ object Defaults extends BuildCommon with DefExtra {
|
||||||
outputStrategy = outputStrategy.value,
|
outputStrategy = outputStrategy.value,
|
||||||
// bootJars is empty by default because only jars on the user's classpath should be on the boot classpath
|
// bootJars is empty by default because only jars on the user's classpath should be on the boot classpath
|
||||||
bootJars = Vector(),
|
bootJars = Vector(),
|
||||||
workingDirectory = Some(baseDirectory.value),
|
workingDirectory = forkWorkingDirectory.value,
|
||||||
runJVMOptions = javaOptions.value.toVector,
|
runJVMOptions = javaOptions.value.toVector,
|
||||||
connectInput = connectInput.value,
|
connectInput = connectInput.value,
|
||||||
envVars = envVars.value,
|
envVars = envVars.value,
|
||||||
|
|
|
||||||
|
|
@ -354,6 +354,7 @@ object Keys {
|
||||||
|
|
||||||
val fork = settingKey[Boolean]("If true, forks a new JVM when running. If false, runs in the same JVM as the build.").withRank(ASetting)
|
val fork = settingKey[Boolean]("If true, forks a new JVM when running. If false, runs in the same JVM as the build.").withRank(ASetting)
|
||||||
val forkOptions = taskKey[ForkOptions]("Configures JVM forking.").withRank(DSetting)
|
val forkOptions = taskKey[ForkOptions]("Configures JVM forking.").withRank(DSetting)
|
||||||
|
val forkWorkingDirectory = settingKey[Option[File]]("Working directory of forked processes started by run, test, and console. If None, the forked process inherits the working directory of the sbt process.").withRank(BSetting)
|
||||||
val outputStrategy = settingKey[Option[sbt.OutputStrategy]]("Selects how to log output when running a main class.").withRank(DSetting)
|
val outputStrategy = settingKey[Option[sbt.OutputStrategy]]("Selects how to log output when running a main class.").withRank(DSetting)
|
||||||
val connectInput = settingKey[Boolean]("If true, connects standard input when running a main class forked.").withRank(CSetting)
|
val connectInput = settingKey[Boolean]("If true, connects standard input when running a main class forked.").withRank(CSetting)
|
||||||
val javaHome = settingKey[Option[File]]("Selects the Java installation used for compiling and forking. If None, uses the Java installation running the build.").withRank(ASetting)
|
val javaHome = settingKey[Option[File]]("Selects the Java installation used for compiling and forking. If None, uses the Java installation running the build.").withRank(ASetting)
|
||||||
|
|
|
||||||
|
|
@ -621,6 +621,7 @@ object Compiler:
|
||||||
val termEnv = sys.env.get("TERM").getOrElse("xterm-256color")
|
val termEnv = sys.env.get("TERM").getOrElse("xterm-256color")
|
||||||
ForkOptions()
|
ForkOptions()
|
||||||
.withConnectInput(true)
|
.withConnectInput(true)
|
||||||
|
.withWorkingDirectory((Keys.console / Keys.forkWorkingDirectory).value)
|
||||||
.withRunJVMOptions(
|
.withRunJVMOptions(
|
||||||
jo ++ Vector(
|
jo ++ Vector(
|
||||||
s"-Dorg.jline.terminal.type=$termEnv",
|
s"-Dorg.jline.terminal.type=$termEnv",
|
||||||
|
|
|
||||||
|
|
@ -343,7 +343,7 @@ object BuildServerProtocol {
|
||||||
JavacOptionsItem(target, javacOptions, classpath, classDirectory.toURI)
|
JavacOptionsItem(target, javacOptions, classpath, classDirectory.toURI)
|
||||||
},
|
},
|
||||||
bspBuildTargetJVMRunEnvironment := bspInputTask { (_, filter) =>
|
bspBuildTargetJVMRunEnvironment := bspInputTask { (_, filter) =>
|
||||||
val items = bspBuildTargetJvmEnvironmentItem.result.all(filter).value
|
val items = (run / bspBuildTargetJvmEnvironmentItem).result.all(filter).value
|
||||||
val successfulItems = anyOrThrow(items)
|
val successfulItems = anyOrThrow(items)
|
||||||
val result = JvmRunEnvironmentResult(successfulItems.toVector, None)
|
val result = JvmRunEnvironmentResult(successfulItems.toVector, None)
|
||||||
state.value.respondEvent(result)
|
state.value.respondEvent(result)
|
||||||
|
|
@ -354,7 +354,8 @@ object BuildServerProtocol {
|
||||||
val result = JvmTestEnvironmentResult(successfulItems.toVector, None)
|
val result = JvmTestEnvironmentResult(successfulItems.toVector, None)
|
||||||
state.value.respondEvent(result)
|
state.value.respondEvent(result)
|
||||||
}.evaluated,
|
}.evaluated,
|
||||||
bspBuildTargetJvmEnvironmentItem := jvmEnvironmentItem().value,
|
bspBuildTargetJvmEnvironmentItem := jvmEnvironmentItem(forkOptions).value,
|
||||||
|
run / bspBuildTargetJvmEnvironmentItem := jvmEnvironmentItem(run / forkOptions).value,
|
||||||
bspInternalDependencyConfigurations := internalDependencyConfigurationsSetting.value,
|
bspInternalDependencyConfigurations := internalDependencyConfigurationsSetting.value,
|
||||||
bspScalaTestClassesItem := scalaTestClassesTask.value,
|
bspScalaTestClassesItem := scalaTestClassesTask.value,
|
||||||
bspScalaMainClassesItem := scalaMainClassesTask.value,
|
bspScalaMainClassesItem := scalaMainClassesTask.value,
|
||||||
|
|
@ -793,7 +794,12 @@ object BuildServerProtocol {
|
||||||
Def.task(taskImpl(workspace, filter))
|
Def.task(taskImpl(workspace, filter))
|
||||||
}
|
}
|
||||||
|
|
||||||
private def jvmEnvironmentItem(): Initialize[Task[JvmEnvironmentItem]] = Def.task {
|
private def bspEnvironmentVariables(opts: ForkOptions): Vector[String] =
|
||||||
|
opts.envVars.map { (k, v) => s"$k=$v" }.toVector
|
||||||
|
|
||||||
|
private def jvmEnvironmentItem(
|
||||||
|
forkOptions: Initialize[Task[ForkOptions]]
|
||||||
|
): Initialize[Task[JvmEnvironmentItem]] = Def.task {
|
||||||
val target = Keys.bspTargetIdentifier.value
|
val target = Keys.bspTargetIdentifier.value
|
||||||
val converter = fileConverter.value
|
val converter = fileConverter.value
|
||||||
val classpath = Keys.fullClasspath.value
|
val classpath = Keys.fullClasspath.value
|
||||||
|
|
@ -801,16 +807,17 @@ object BuildServerProtocol {
|
||||||
.map(converter.toPath)
|
.map(converter.toPath)
|
||||||
.map(_.toFile.toURI)
|
.map(_.toFile.toURI)
|
||||||
.toVector
|
.toVector
|
||||||
val jvmOptions = Keys.javaOptions.value.toVector
|
val opts = forkOptions.value
|
||||||
val baseDir = Keys.baseDirectory.value.getAbsolutePath
|
val workingDir = opts.workingDirectory
|
||||||
val env = envVars.value
|
.getOrElse(new File(sys.props("user.dir")))
|
||||||
|
.getAbsolutePath
|
||||||
|
|
||||||
JvmEnvironmentItem(
|
JvmEnvironmentItem(
|
||||||
target,
|
target,
|
||||||
classpath,
|
classpath,
|
||||||
jvmOptions,
|
opts.runJVMOptions,
|
||||||
baseDir,
|
workingDir,
|
||||||
env
|
opts.envVars
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -947,7 +954,8 @@ object BuildServerProtocol {
|
||||||
val json = jsonParser.parsed
|
val json = jsonParser.parsed
|
||||||
val runParams = json.flatMap(Converter.fromJson[RunParams]).get
|
val runParams = json.flatMap(Converter.fromJson[RunParams]).get
|
||||||
val defaultClass = Keys.mainClass.value
|
val defaultClass = Keys.mainClass.value
|
||||||
val defaultJvmOptions = Keys.javaOptions.value
|
val defaultOpts = (run / forkOptions).value
|
||||||
|
val defaultEnv = bspEnvironmentVariables(defaultOpts)
|
||||||
|
|
||||||
val mainClass = runParams.dataKind match {
|
val mainClass = runParams.dataKind match {
|
||||||
case Some("scala-main-class") =>
|
case Some("scala-main-class") =>
|
||||||
|
|
@ -959,9 +967,7 @@ object BuildServerProtocol {
|
||||||
e.getMessage
|
e.getMessage
|
||||||
)
|
)
|
||||||
case Success(value) =>
|
case Success(value) =>
|
||||||
value.withEnvironmentVariables(
|
value.withEnvironmentVariables(defaultEnv ++ value.environmentVariables)
|
||||||
envVars.value.map { (k, v) => s"$k=$v" }.toVector ++ value.environmentVariables
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
case Some(dataKind) =>
|
case Some(dataKind) =>
|
||||||
|
|
@ -979,8 +985,8 @@ object BuildServerProtocol {
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
runParams.arguments,
|
runParams.arguments,
|
||||||
defaultJvmOptions.toVector,
|
defaultOpts.runJVMOptions,
|
||||||
envVars.value.map { (k, v) => s"$k=$v" }.toVector
|
defaultEnv
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
runMainClassTask(mainClass, runParams.originId)
|
runMainClassTask(mainClass, runParams.originId)
|
||||||
|
|
@ -1038,21 +1044,18 @@ object BuildServerProtocol {
|
||||||
val state = Keys.state.value
|
val state = Keys.state.value
|
||||||
val logger = Keys.streams.value.log
|
val logger = Keys.streams.value.log
|
||||||
val classpath = Attributed.data(fullClasspath.value)
|
val classpath = Attributed.data(fullClasspath.value)
|
||||||
val forkOpts = ForkOptions(
|
// connectInput is disabled so non-interactive BSP output is captured as log messages
|
||||||
javaHome = javaHome.value,
|
val forkOpts = (run / forkOptions).value
|
||||||
outputStrategy = outputStrategy.value,
|
.withConnectInput(false)
|
||||||
// bootJars is empty by default because only jars on the user's classpath should be on the boot classpath
|
.withRunJVMOptions(mainClass.jvmOptions)
|
||||||
bootJars = Vector(),
|
.withEnvVars(
|
||||||
workingDirectory = Some(baseDirectory.value),
|
mainClass.environmentVariables
|
||||||
runJVMOptions = mainClass.jvmOptions,
|
.flatMap(_.split("=", 2).toList match {
|
||||||
connectInput = connectInput.value,
|
case key :: value :: Nil => Some(key -> value)
|
||||||
envVars = mainClass.environmentVariables
|
case _ => None
|
||||||
.flatMap(_.split("=", 2).toList match {
|
})
|
||||||
case key :: value :: Nil => Some(key -> value)
|
.toMap
|
||||||
case _ => None
|
)
|
||||||
})
|
|
||||||
.toMap
|
|
||||||
)
|
|
||||||
val runner = new ForkRun(forkOpts)
|
val runner = new ForkRun(forkOpts)
|
||||||
val converter = fileConverter.value
|
val converter = fileConverter.value
|
||||||
val cp = classpath.map(converter.toPath)
|
val cp = classpath.map(converter.toPath)
|
||||||
|
|
@ -1122,13 +1125,14 @@ object BuildServerProtocol {
|
||||||
}
|
}
|
||||||
|
|
||||||
private def scalaMainClassesTask: Initialize[Task[ScalaMainClassesItem]] = Def.task {
|
private def scalaMainClassesTask: Initialize[Task[ScalaMainClassesItem]] = Def.task {
|
||||||
val jvmOptions = Keys.javaOptions.value.toVector
|
val opts = (run / forkOptions).value
|
||||||
|
val env = bspEnvironmentVariables(opts)
|
||||||
val mainClasses = Keys.discoveredMainClasses.value.map(
|
val mainClasses = Keys.discoveredMainClasses.value.map(
|
||||||
ScalaMainClass(
|
ScalaMainClass(
|
||||||
_,
|
_,
|
||||||
Vector(),
|
Vector(),
|
||||||
jvmOptions,
|
opts.runJVMOptions,
|
||||||
envVars.value.map { (k, v) => s"$k=$v" }.toVector
|
env
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
ScalaMainClassesItem(
|
ScalaMainClassesItem(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
### Forked run and test start in sbt's working directory
|
||||||
|
|
||||||
|
Previously, forked `run` and `test` set the forked JVM's working directory to the
|
||||||
|
project's `baseDirectory`, while non-forked `run` and `test` executed in the directory
|
||||||
|
sbt itself was started from. In a multi-project build, toggling `fork` silently changed
|
||||||
|
the directory that relative paths resolved against.
|
||||||
|
|
||||||
|
sbt 2.x makes forked `run`, `test`, and `console` inherit sbt's own working directory by
|
||||||
|
default, consistent with non-forked execution. A new setting,
|
||||||
|
`forkWorkingDirectory: Option[File]`, configures the working directory of forked
|
||||||
|
processes. To restore the sbt 1.x behavior:
|
||||||
|
|
||||||
|
```scala
|
||||||
|
Test / forkWorkingDirectory := Some(baseDirectory.value)
|
||||||
|
run / forkWorkingDirectory := Some(baseDirectory.value)
|
||||||
|
```
|
||||||
|
|
||||||
|
The BSP `buildTarget/jvmRunEnvironment` and `buildTarget/jvmTestEnvironment` responses
|
||||||
|
report the same working directory so that IDEs follow the same contract.
|
||||||
|
|
||||||
|
This addresses [#1032][i1032].
|
||||||
|
|
||||||
|
[i1032]: https://github.com/sbt/sbt/issues/1032
|
||||||
|
|
@ -1,5 +1,15 @@
|
||||||
|
|
||||||
|
|
||||||
|
## Forked run/test working directory
|
||||||
|
|
||||||
|
Forked `run` and `test` no longer run in the project's `baseDirectory`; they inherit
|
||||||
|
sbt's working directory, matching non-forked behavior. To restore the sbt 1.x behavior:
|
||||||
|
|
||||||
|
```scala
|
||||||
|
run / forkWorkingDirectory := Some(baseDirectory.value)
|
||||||
|
Test / forkWorkingDirectory := Some(baseDirectory.value)
|
||||||
|
```
|
||||||
|
|
||||||
## files extension on Classpath
|
## files extension on Classpath
|
||||||
|
|
||||||
```scala
|
```scala
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,29 @@
|
||||||
> run fork
|
# non-forked run executes in sbt's working directory
|
||||||
|
> run
|
||||||
$ exists flag
|
$ exists flag
|
||||||
$ delete flag
|
$ delete flag
|
||||||
|
|
||||||
$ mkdir forked
|
# forked run inherits sbt's working directory by default,
|
||||||
|
# even when run / baseDirectory points elsewhere (#1032)
|
||||||
> set fork := true
|
> set fork := true
|
||||||
> set run / baseDirectory := baseDirectory(_ / "forked").value
|
> set run / baseDirectory := baseDirectory(_ / "forked").value
|
||||||
|
> run
|
||||||
|
$ exists flag
|
||||||
|
$ absent forked/flag
|
||||||
|
$ delete flag
|
||||||
|
|
||||||
> run forked
|
# forkWorkingDirectory restores forking in a specific directory
|
||||||
|
> session clear
|
||||||
|
> set fork := true
|
||||||
|
> set run / forkWorkingDirectory := Some(baseDirectory.value / "forked")
|
||||||
|
$ mkdir forked
|
||||||
|
> run
|
||||||
$ exists forked/flag
|
$ exists forked/flag
|
||||||
$ absent flag
|
$ absent flag
|
||||||
$ delete forked/flag
|
$ delete forked/flag
|
||||||
|
|
||||||
> set envVars += ("flag.name" -> "env.flag")
|
> set envVars += ("flag.name" -> "env.flag")
|
||||||
> run forked
|
> run
|
||||||
$ exists forked/env.flag
|
$ exists forked/env.flag
|
||||||
$ absent flag
|
$ absent flag
|
||||||
$ absent forked/flag
|
$ absent forked/flag
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
val scalatest = "org.scalatest" %% "scalatest" % "3.2.19"
|
||||||
|
|
||||||
|
ThisBuild / scalaVersion := "3.8.4"
|
||||||
|
|
||||||
|
lazy val root = (project in file("."))
|
||||||
|
|
||||||
|
lazy val sub = project
|
||||||
|
.settings(
|
||||||
|
Test / fork := true,
|
||||||
|
libraryDependencies += scalatest % Test,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
Test / forkWorkingDirectory := Some(baseDirectory.value)
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
|
|
||||||
|
class CwdSpec extends AnyFunSuite {
|
||||||
|
test("create marker in the forked working directory") {
|
||||||
|
val marker = new java.io.File("cwd-marker").getAbsoluteFile
|
||||||
|
assert(marker.createNewFile() || marker.exists())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
# a forked test's working directory is sbt's working directory by default (#1032)
|
||||||
|
> sub/testFull
|
||||||
|
$ exists cwd-marker
|
||||||
|
$ absent sub/cwd-marker
|
||||||
|
$ delete cwd-marker
|
||||||
|
|
||||||
|
# Test / forkWorkingDirectory restores forking in the project directory
|
||||||
|
$ copy-file changes/forkdir.sbt sub/forkdir.sbt
|
||||||
|
> reload
|
||||||
|
> sub/testFull
|
||||||
|
$ exists sub/cwd-marker
|
||||||
|
$ absent cwd-marker
|
||||||
|
|
@ -9,6 +9,7 @@ lazy val runAndTest = project.in(file("run-and-test"))
|
||||||
libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.8" % "test",
|
libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.8" % "test",
|
||||||
Compile / javaOptions := Vector("Xmx256M"),
|
Compile / javaOptions := Vector("Xmx256M"),
|
||||||
Compile / envVars := Map("KEY" -> "VALUE"),
|
Compile / envVars := Map("KEY" -> "VALUE"),
|
||||||
|
run / forkWorkingDirectory := Some(baseDirectory.value),
|
||||||
|
|
||||||
Test / javaOptions := Vector("Xmx512M"),
|
Test / javaOptions := Vector("Xmx512M"),
|
||||||
Test / envVars := Map("KEY_TEST" -> "VALUE_TEST"),
|
Test / envVars := Map("KEY_TEST" -> "VALUE_TEST"),
|
||||||
|
|
|
||||||
|
|
@ -472,8 +472,12 @@ class BuildServerTest extends AbstractServerTest {
|
||||||
val id = sendRequest("buildTarget/scalaMainClasses", ScalaMainClassesParams(targets, None))
|
val id = sendRequest("buildTarget/scalaMainClasses", ScalaMainClassesParams(targets, None))
|
||||||
val res =
|
val res =
|
||||||
svr.session.waitForResultInResponseMsg[ScalaMainClassesResult](30.seconds, id).get
|
svr.session.waitForResultInResponseMsg[ScalaMainClassesResult](30.seconds, id).get
|
||||||
val classes = res.items.flatMap(_.classes.map(_.`class`))
|
val mainClasses = res.items.flatMap(_.classes)
|
||||||
assert(classes.contains("main.Main"))
|
assert(mainClasses.map(_.`class`).contains("main.Main"))
|
||||||
|
// JVM options and environment are derived from run / forkOptions
|
||||||
|
val main = mainClasses.find(_.`class` == "main.Main").get
|
||||||
|
assert(main.jvmOptions.contains("Xmx256M"))
|
||||||
|
assert(main.environmentVariables.contains("KEY=VALUE"))
|
||||||
}
|
}
|
||||||
|
|
||||||
test("buildTarget/run") {
|
test("buildTarget/run") {
|
||||||
|
|
@ -509,7 +513,8 @@ class BuildServerTest extends AbstractServerTest {
|
||||||
)
|
)
|
||||||
assert(item.jvmOptions.contains("Xmx256M"))
|
assert(item.jvmOptions.contains("Xmx256M"))
|
||||||
assert(item.environmentVariables == Map("KEY" -> "VALUE"))
|
assert(item.environmentVariables == Map("KEY" -> "VALUE"))
|
||||||
assert(item.workingDirectory.contains("/buildserver/run-and-test"))
|
// run / forkWorkingDirectory is honored for the run environment
|
||||||
|
assert(item.workingDirectory.endsWith("/run-and-test"))
|
||||||
}
|
}
|
||||||
|
|
||||||
test("buildTarget/jvmTestEnvironment") {
|
test("buildTarget/jvmTestEnvironment") {
|
||||||
|
|
@ -529,6 +534,9 @@ class BuildServerTest extends AbstractServerTest {
|
||||||
)
|
)
|
||||||
assert(item.jvmOptions.contains("Xmx512M"))
|
assert(item.jvmOptions.contains("Xmx512M"))
|
||||||
assert(item.environmentVariables == Map("KEY_TEST" -> "VALUE_TEST"))
|
assert(item.environmentVariables == Map("KEY_TEST" -> "VALUE_TEST"))
|
||||||
|
// with no Test / forkWorkingDirectory, the test environment inherits sbt's working directory
|
||||||
|
assert(item.workingDirectory.endsWith("/buildserver"))
|
||||||
|
assert(!item.workingDirectory.contains("run-and-test"))
|
||||||
}
|
}
|
||||||
|
|
||||||
test("buildTarget/scalaTestClasses") {
|
test("buildTarget/scalaTestClasses") {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue