mirror of https://github.com/sbt/sbt.git
[2.x] fix: Make forked run inherit sbt's working directory (#9442)
forked run used the project's baseDirectory as the working directory, while non-forked execution inherits sbt's own working directory — so toggling fork silently changed how relative paths resolved. forked run (and forked console) now inherit sbt's working directory, consistent with non-forked execution and `sbtn` expectations.
This commit is contained in:
parent
017ee92527
commit
8e3c25c1f1
|
|
@ -1407,6 +1407,7 @@ object Defaults extends BuildCommon with DefExtra {
|
|||
)
|
||||
)
|
||||
}
|
||||
|
||||
def forkOptionsTask: Initialize[Task[ForkOptions]] =
|
||||
Def.task {
|
||||
val canUseArgumentsFile = sys.props
|
||||
|
|
@ -1425,6 +1426,10 @@ object Defaults extends BuildCommon with DefExtra {
|
|||
)
|
||||
}
|
||||
|
||||
/** Fork options for run-like tasks: the forked process inherits sbt's working directory. */
|
||||
private[sbt] def runForkOptionsTask: Initialize[Task[ForkOptions]] =
|
||||
Def.task(forkOptionsTask.value.withWorkingDirectory(None))
|
||||
|
||||
def testExecutionTask(task: Scoped): Initialize[Task[Tests.Execution]] =
|
||||
Def.task {
|
||||
new Tests.Execution(
|
||||
|
|
@ -2604,7 +2609,7 @@ object Defaults extends BuildCommon with DefExtra {
|
|||
private lazy val newRunnerSettings: Seq[Setting[?]] =
|
||||
Seq(
|
||||
runner := Def.uncached(ClassLoaders.runner.value),
|
||||
forkOptions := Def.uncached(forkOptionsTask.value)
|
||||
forkOptions := Def.uncached(runForkOptionsTask.value)
|
||||
)
|
||||
|
||||
lazy val baseTasks: Seq[Setting[?]] = projectTasks ++ packageBase
|
||||
|
|
@ -4974,7 +4979,7 @@ trait BuildExtra extends BuildCommon with DefExtra {
|
|||
}
|
||||
}
|
||||
}.evaluated
|
||||
) ++ inTask(scoped)((config / forkOptions) := Def.uncached(forkOptionsTask.value))
|
||||
) ++ inTask(scoped)((config / forkOptions) := Def.uncached(runForkOptionsTask.value))
|
||||
}
|
||||
|
||||
// public API
|
||||
|
|
@ -4996,7 +5001,7 @@ trait BuildExtra extends BuildCommon with DefExtra {
|
|||
r.run(mainClass, cp.files, arguments, s.log).get
|
||||
}
|
||||
}.value
|
||||
) ++ inTask(scoped)((config / forkOptions) := Def.uncached(forkOptionsTask.value))
|
||||
) ++ inTask(scoped)((config / forkOptions) := Def.uncached(runForkOptionsTask.value))
|
||||
|
||||
def initScoped[T](sk: ScopedKey[?], i: Initialize[T]): Initialize[T] =
|
||||
initScope(fillTaskAxis(sk.scope, sk.key), i)
|
||||
|
|
|
|||
|
|
@ -343,7 +343,7 @@ object BuildServerProtocol {
|
|||
JavacOptionsItem(target, javacOptions, classpath, classDirectory.toURI)
|
||||
},
|
||||
bspBuildTargetJVMRunEnvironment := bspInputTask { (_, filter) =>
|
||||
val items = bspBuildTargetJvmEnvironmentItem.result.all(filter).value
|
||||
val items = (run / bspBuildTargetJvmEnvironmentItem).result.all(filter).value
|
||||
val successfulItems = anyOrThrow(items)
|
||||
val result = JvmRunEnvironmentResult(successfulItems.toVector, None)
|
||||
state.value.respondEvent(result)
|
||||
|
|
@ -354,7 +354,8 @@ object BuildServerProtocol {
|
|||
val result = JvmTestEnvironmentResult(successfulItems.toVector, None)
|
||||
state.value.respondEvent(result)
|
||||
}.evaluated,
|
||||
bspBuildTargetJvmEnvironmentItem := jvmEnvironmentItem().value,
|
||||
bspBuildTargetJvmEnvironmentItem := jvmEnvironmentItem(forkOptions).value,
|
||||
run / bspBuildTargetJvmEnvironmentItem := jvmEnvironmentItem(run / forkOptions).value,
|
||||
bspInternalDependencyConfigurations := internalDependencyConfigurationsSetting.value,
|
||||
bspScalaTestClassesItem := scalaTestClassesTask.value,
|
||||
bspScalaMainClassesItem := scalaMainClassesTask.value,
|
||||
|
|
@ -793,7 +794,12 @@ object BuildServerProtocol {
|
|||
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 converter = fileConverter.value
|
||||
val classpath = Keys.fullClasspath.value
|
||||
|
|
@ -801,16 +807,17 @@ object BuildServerProtocol {
|
|||
.map(converter.toPath)
|
||||
.map(_.toFile.toURI)
|
||||
.toVector
|
||||
val jvmOptions = Keys.javaOptions.value.toVector
|
||||
val baseDir = Keys.baseDirectory.value.getAbsolutePath
|
||||
val env = envVars.value
|
||||
val opts = forkOptions.value
|
||||
val workingDir = opts.workingDirectory
|
||||
.getOrElse(new File(sys.props("user.dir")))
|
||||
.getAbsolutePath
|
||||
|
||||
JvmEnvironmentItem(
|
||||
target,
|
||||
classpath,
|
||||
jvmOptions,
|
||||
baseDir,
|
||||
env
|
||||
opts.runJVMOptions,
|
||||
workingDir,
|
||||
opts.envVars
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -947,7 +954,8 @@ object BuildServerProtocol {
|
|||
val json = jsonParser.parsed
|
||||
val runParams = json.flatMap(Converter.fromJson[RunParams]).get
|
||||
val defaultClass = Keys.mainClass.value
|
||||
val defaultJvmOptions = Keys.javaOptions.value
|
||||
val defaultOpts = (run / forkOptions).value
|
||||
val defaultEnv = bspEnvironmentVariables(defaultOpts)
|
||||
|
||||
val mainClass = runParams.dataKind match {
|
||||
case Some("scala-main-class") =>
|
||||
|
|
@ -959,9 +967,7 @@ object BuildServerProtocol {
|
|||
e.getMessage
|
||||
)
|
||||
case Success(value) =>
|
||||
value.withEnvironmentVariables(
|
||||
envVars.value.map { (k, v) => s"$k=$v" }.toVector ++ value.environmentVariables
|
||||
)
|
||||
value.withEnvironmentVariables(defaultEnv ++ value.environmentVariables)
|
||||
}
|
||||
|
||||
case Some(dataKind) =>
|
||||
|
|
@ -979,8 +985,8 @@ object BuildServerProtocol {
|
|||
)
|
||||
),
|
||||
runParams.arguments,
|
||||
defaultJvmOptions.toVector,
|
||||
envVars.value.map { (k, v) => s"$k=$v" }.toVector
|
||||
defaultOpts.runJVMOptions,
|
||||
defaultEnv
|
||||
)
|
||||
}
|
||||
runMainClassTask(mainClass, runParams.originId)
|
||||
|
|
@ -1038,21 +1044,18 @@ object BuildServerProtocol {
|
|||
val state = Keys.state.value
|
||||
val logger = Keys.streams.value.log
|
||||
val classpath = Attributed.data(fullClasspath.value)
|
||||
val forkOpts = ForkOptions(
|
||||
javaHome = javaHome.value,
|
||||
outputStrategy = outputStrategy.value,
|
||||
// bootJars is empty by default because only jars on the user's classpath should be on the boot classpath
|
||||
bootJars = Vector(),
|
||||
workingDirectory = Some(baseDirectory.value),
|
||||
runJVMOptions = mainClass.jvmOptions,
|
||||
connectInput = connectInput.value,
|
||||
envVars = mainClass.environmentVariables
|
||||
.flatMap(_.split("=", 2).toList match {
|
||||
case key :: value :: Nil => Some(key -> value)
|
||||
case _ => None
|
||||
})
|
||||
.toMap
|
||||
)
|
||||
// connectInput is disabled so non-interactive BSP output is captured as log messages
|
||||
val forkOpts = (run / forkOptions).value
|
||||
.withConnectInput(false)
|
||||
.withRunJVMOptions(mainClass.jvmOptions)
|
||||
.withEnvVars(
|
||||
mainClass.environmentVariables
|
||||
.flatMap(_.split("=", 2).toList match {
|
||||
case key :: value :: Nil => Some(key -> value)
|
||||
case _ => None
|
||||
})
|
||||
.toMap
|
||||
)
|
||||
val runner = new ForkRun(forkOpts)
|
||||
val converter = fileConverter.value
|
||||
val cp = classpath.map(converter.toPath)
|
||||
|
|
@ -1122,13 +1125,14 @@ object BuildServerProtocol {
|
|||
}
|
||||
|
||||
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(
|
||||
ScalaMainClass(
|
||||
_,
|
||||
Vector(),
|
||||
jvmOptions,
|
||||
envVars.value.map { (k, v) => s"$k=$v" }.toVector
|
||||
opts.runJVMOptions,
|
||||
env
|
||||
)
|
||||
)
|
||||
ScalaMainClassesItem(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
### Forked run starts in sbt's working directory
|
||||
|
||||
Previously, forked `run` set the forked JVM's working directory to the project's
|
||||
`baseDirectory`, while non-forked `run` 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` (and forked `console`) inherit sbt's own working
|
||||
directory by default, consistent with non-forked execution and with `sbtn`
|
||||
expectations. Forked `test` is unchanged and keeps the project's `baseDirectory`
|
||||
as its working directory. The working directory of any forked process can be
|
||||
configured via `forkOptions`:
|
||||
|
||||
```scala
|
||||
Compile / run / forkOptions := Def.uncached(
|
||||
(Compile / run / forkOptions).value.withWorkingDirectory(Some(baseDirectory.value))
|
||||
)
|
||||
```
|
||||
|
||||
The BSP `buildTarget/jvmRunEnvironment` response reports the same working
|
||||
directory that `run` uses.
|
||||
|
||||
This addresses [#1032][i1032] for `run`.
|
||||
|
||||
[i1032]: https://github.com/sbt/sbt/issues/1032
|
||||
|
|
@ -1,5 +1,17 @@
|
|||
|
||||
|
||||
## Forked run working directory
|
||||
|
||||
Forked `run` no longer runs in the project's `baseDirectory`; it inherits sbt's
|
||||
working directory, matching non-forked behavior. Forked `test` is unchanged. To
|
||||
restore the sbt 1.x behavior:
|
||||
|
||||
```scala
|
||||
Compile / run / forkOptions := Def.uncached(
|
||||
(Compile / run / forkOptions).value.withWorkingDirectory(Some(baseDirectory.value))
|
||||
)
|
||||
```
|
||||
|
||||
## files extension on Classpath
|
||||
|
||||
```scala
|
||||
|
|
|
|||
|
|
@ -1,18 +1,29 @@
|
|||
> run fork
|
||||
# non-forked run executes in sbt's working directory
|
||||
> run
|
||||
$ exists 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 run / baseDirectory := baseDirectory(_ / "forked").value
|
||||
> run
|
||||
$ exists flag
|
||||
$ absent forked/flag
|
||||
$ delete flag
|
||||
|
||||
> run forked
|
||||
# run / forkOptions configures the forked working directory
|
||||
> session clear
|
||||
> set fork := true
|
||||
> set Compile / run / forkOptions := Def.uncached((Compile / run / forkOptions).value.withWorkingDirectory(Some(baseDirectory.value / "forked")))
|
||||
$ mkdir forked
|
||||
> run
|
||||
$ exists forked/flag
|
||||
$ absent flag
|
||||
$ delete forked/flag
|
||||
|
||||
> set envVars += ("flag.name" -> "env.flag")
|
||||
> run forked
|
||||
> run
|
||||
$ exists forked/env.flag
|
||||
$ absent 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,3 @@
|
|||
Test / forkOptions := Def.uncached(
|
||||
(Test / forkOptions).value.withWorkingDirectory(Some((ThisBuild / 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 remains the project's baseDirectory
|
||||
> sub/testFull
|
||||
$ exists sub/cwd-marker
|
||||
$ absent cwd-marker
|
||||
$ delete sub/cwd-marker
|
||||
|
||||
# Test / forkOptions configures the forked working directory
|
||||
$ copy-file changes/forkdir.sbt sub/forkdir.sbt
|
||||
> reload
|
||||
> sub/testFull
|
||||
$ exists cwd-marker
|
||||
$ absent sub/cwd-marker
|
||||
|
|
@ -9,6 +9,9 @@ lazy val runAndTest = project.in(file("run-and-test"))
|
|||
libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.8" % "test",
|
||||
Compile / javaOptions := Vector("Xmx256M"),
|
||||
Compile / envVars := Map("KEY" -> "VALUE"),
|
||||
Compile / run / forkOptions := Def.uncached(
|
||||
(Compile / run / forkOptions).value.withWorkingDirectory(Some(baseDirectory.value))
|
||||
),
|
||||
|
||||
Test / javaOptions := Vector("Xmx512M"),
|
||||
Test / envVars := Map("KEY_TEST" -> "VALUE_TEST"),
|
||||
|
|
|
|||
|
|
@ -472,8 +472,12 @@ class BuildServerTest extends AbstractServerTest {
|
|||
val id = sendRequest("buildTarget/scalaMainClasses", ScalaMainClassesParams(targets, None))
|
||||
val res =
|
||||
svr.session.waitForResultInResponseMsg[ScalaMainClassesResult](30.seconds, id).get
|
||||
val classes = res.items.flatMap(_.classes.map(_.`class`))
|
||||
assert(classes.contains("main.Main"))
|
||||
val mainClasses = res.items.flatMap(_.classes)
|
||||
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") {
|
||||
|
|
@ -498,18 +502,23 @@ class BuildServerTest extends AbstractServerTest {
|
|||
|
||||
test("buildTarget/jvmRunEnvironment") {
|
||||
val buildTarget = buildTargetUri("runAndTest", "Compile")
|
||||
val targets = Vector(BuildTargetIdentifier(buildTarget))
|
||||
val utilTarget = buildTargetUri("util", "Compile")
|
||||
val targets = Vector(buildTarget, utilTarget).map(BuildTargetIdentifier.apply)
|
||||
val id = sendRequest("buildTarget/jvmRunEnvironment", JvmRunEnvironmentParams(targets, None))
|
||||
val res =
|
||||
svr.session.waitForResultInResponseMsg[JvmRunEnvironmentResult](10.seconds, id).get
|
||||
val item = res.items.head
|
||||
val item = res.items.find(_.target.uri == buildTarget).get
|
||||
assert(
|
||||
item.classpath.exists(_.toString.contains("jsoniter-scala-core_2.13-2.13.11.jar")),
|
||||
"classpath should contain compile dependency"
|
||||
)
|
||||
assert(item.jvmOptions.contains("Xmx256M"))
|
||||
assert(item.environmentVariables == Map("KEY" -> "VALUE"))
|
||||
assert(item.workingDirectory.contains("/buildserver/run-and-test"))
|
||||
// run / forkOptions is honored for the run environment
|
||||
assert(item.workingDirectory.endsWith("/run-and-test"))
|
||||
// by default, forked run inherits sbt's working directory
|
||||
val utilItem = res.items.find(_.target.uri == utilTarget).get
|
||||
assert(utilItem.workingDirectory.endsWith("/buildserver"))
|
||||
}
|
||||
|
||||
test("buildTarget/jvmTestEnvironment") {
|
||||
|
|
@ -529,6 +538,8 @@ class BuildServerTest extends AbstractServerTest {
|
|||
)
|
||||
assert(item.jvmOptions.contains("Xmx512M"))
|
||||
assert(item.environmentVariables == Map("KEY_TEST" -> "VALUE_TEST"))
|
||||
// forked tests keep the project's baseDirectory as their working directory
|
||||
assert(item.workingDirectory.endsWith("/run-and-test"))
|
||||
}
|
||||
|
||||
test("buildTarget/scalaTestClasses") {
|
||||
|
|
|
|||
Loading…
Reference in New Issue