[2.x] Fix: strip pipelining scalacOptions before launching the REPL (#8986)

**Problem**
When usePipelining := true in a multi-project build, sbt appends  
-Ypickle-java and -Ypickle-write <path>/early.jar to scalacOptions 
for fast parallel compilation. These flags were being leaked into the Scala  
REPL, causing failure on console

**Solution**
Strip pipelining flags before forwarding to the REPL  in Deafults.scala.
This commit is contained in:
corevibe555 2026-04-05 20:47:36 +03:00 committed by GitHub
parent bff386d276
commit 606bb3b59c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 249 additions and 0 deletions

View File

@ -1087,9 +1087,16 @@ object Defaults extends BuildCommon with DefExtra {
},
compileIncSetup := Def.uncached(compileIncSetupTask.value),
console := Compiler.consoleTask.value,
// Strip pipelining flags before they reach the REPL (#8921).
console / scalacOptions := Def.uncached {
Compiler.toConsoleScalacOptions(scalacOptions.value)
},
console / forkOptions := Def.uncached(Compiler.consoleForkOptions.value),
collectAnalyses := Definition.collectAnalysesTask.map(_ => ()).value,
consoleQuick := consoleQuickTask.value,
consoleQuick / scalacOptions := Def.uncached {
Compiler.toConsoleScalacOptions(scalacOptions.value)
},
consoleQuick / forkOptions := Def.uncached((console / forkOptions).value),
discoveredMainClasses := compile
.map(discoverMainClasses)

View File

@ -558,4 +558,18 @@ object Compiler:
)
.withEnvVars(sys.env)
}
/**
* Strips `-Ypickle-java` and `-Ypickle-write <path>` from scalac options so they don't
* reach the REPL, where they cause file-lock errors on Windows and spurious
* `InterruptedException`s on all platforms (see #8921).
*/
private[sbt] def toConsoleScalacOptions(options: Seq[String]): Seq[String] =
options match
case "-Ypickle-write" +: (_ +: rest) => toConsoleScalacOptions(rest)
case "-Ypickle-write" +: _ => Seq.empty
case "-Ypickle-java" +: rest => toConsoleScalacOptions(rest)
case head +: rest => head +: toConsoleScalacOptions(rest)
case _ => Seq.empty
end Compiler

View File

@ -0,0 +1,188 @@
package sbt.internal
import hedgehog.*
import hedgehog.runner.*
/**
* Tests for [[Compiler.toConsoleScalacOptions]] pipelining flags must be stripped before
* reaching the REPL (#8921).
*/
object CompilerConsoleOptsTest extends Properties:
override def tests: List[Test] = List(
// ── deterministic unit cases ──────────────────────────────────────────────
example("empty list stays empty", check(Seq.empty, Seq.empty)),
example(
"list with no pipelining flags is unchanged",
check(Seq("-deprecation", "-feature"), Seq("-deprecation", "-feature"))
),
example("-Ypickle-java alone is removed", check(Seq("-Ypickle-java"), Seq.empty)),
example(
"-Ypickle-java at the start is removed",
check(Seq("-Ypickle-java", "-deprecation"), Seq("-deprecation"))
),
example(
"-Ypickle-java at the end is removed",
check(Seq("-feature", "-Ypickle-java"), Seq("-feature"))
),
example(
"-Ypickle-java in the middle is removed",
check(Seq("-deprecation", "-Ypickle-java", "-feature"), Seq("-deprecation", "-feature"))
),
example(
"-Ypickle-write with its argument are both removed",
check(Seq("-Ypickle-write", "/tmp/early.jar"), Seq.empty)
),
example(
"-Ypickle-write at start removes flag and argument, keeps tail",
check(Seq("-Ypickle-write", "/tmp/early.jar", "-deprecation"), Seq("-deprecation"))
),
example(
"-Ypickle-write at end removes flag and argument",
check(Seq("-feature", "-Ypickle-write", "/tmp/early.jar"), Seq("-feature"))
),
example(
"-Ypickle-write in the middle removes flag and argument",
check(
Seq("-deprecation", "-Ypickle-write", "/tmp/early.jar", "-feature"),
Seq("-deprecation", "-feature")
)
),
example(
"-Ypickle-write without argument (trailing flag) is removed safely",
check(Seq("-deprecation", "-Ypickle-write"), Seq("-deprecation"))
),
example(
"both pipelining flags together are removed",
check(Seq("-Ypickle-java", "-Ypickle-write", "/tmp/early.jar"), Seq.empty)
),
example(
"both pipelining flags with surrounding options",
check(
Seq("-encoding", "utf8", "-Ypickle-java", "-Ypickle-write", "/tmp/early.jar", "-feature"),
Seq("-encoding", "utf8", "-feature")
)
),
example(
"multiple occurrences of -Ypickle-java are all removed",
check(Seq("-Ypickle-java", "-opt:l:inline", "-Ypickle-java"), Seq("-opt:l:inline"))
),
example(
"multiple -Ypickle-write occurrences are all removed with their args",
check(
Seq("-Ypickle-write", "/a.jar", "-deprecation", "-Ypickle-write", "/b.jar"),
Seq("-deprecation")
)
),
example(
"real-world pipelining option list is cleaned",
check(
Seq(
"-Ypickle-java",
"-Ypickle-write",
"/target/early/early.jar",
"-encoding",
"utf8",
"-deprecation",
"-feature"
),
Seq("-encoding", "utf8", "-deprecation", "-feature")
)
),
example(
"Vector input (as produced by sbt pipelining) is handled correctly",
check(
Vector(
"-Ypickle-java",
"-Ypickle-write",
"/target/out/early/subproject_3-0.1.0-SNAPSHOT.jar",
"-encoding",
"utf8"
),
Seq("-encoding", "utf8")
)
),
// ── property-based cases ──────────────────────────────────────────────────
property(
"result never contains -Ypickle-java or -Ypickle-write",
propNoPipeliningFlagsInResult
),
property(
"non-pipelining options are always preserved",
propNonPipeliningOptionsPreserved
),
property(
"idempotent: applying twice gives the same result as applying once",
propIdempotent
),
)
// ── helpers ───────────────────────────────────────────────────────────────
private def check(input: Seq[String], expected: Seq[String]): Result =
val got = Compiler.toConsoleScalacOptions(input)
Result
.assert(got == expected)
.log(s"input: $input")
.log(s"expected: $expected")
.log(s"got: $got")
private val pipeliningFlags = List("-Ypickle-java", "-Ypickle-write")
/** Generate an arbitrary scalac-option token (flag or path-like argument). */
private def genOption: Gen[String] =
Gen.frequency1(
7 -> Gen.element1(
"-deprecation",
"-feature",
"-encoding",
"utf8",
"-opt:l:inline",
"-Xfatal-warnings",
"-unchecked"
),
1 -> Gen.element1("-Ypickle-java"),
1 -> Gen.element1("-Ypickle-write"),
1 -> Gen.string(Gen.alphaNum, Range.linear(1, 30)).map("/" + _ + ".jar"),
)
private def genOptions: Gen[List[String]] =
Gen.list(genOption, Range.linear(0, 20))
def propNoPipeliningFlagsInResult: Property =
for options <- genOptions.forAll
yield
val result = Compiler.toConsoleScalacOptions(options)
Result
.assert(!result.contains("-Ypickle-java"))
.and(Result.assert(!result.contains("-Ypickle-write")))
.log(s"input: $options")
.log(s"result: $result")
def propNonPipeliningOptionsPreserved: Property =
for
options <- genOptions.forAll
// build a list that contains only non-pipelining flags
clean = options.filterNot(pipeliningFlags.contains)
yield
// inject the clean options as-is (no pipelining flags) and verify they survive
val result = Compiler.toConsoleScalacOptions(clean)
Result
.assert(result == clean)
.log(s"clean input: $clean")
.log(s"result: $result")
def propIdempotent: Property =
for options <- genOptions.forAll
yield
val once = Compiler.toConsoleScalacOptions(options)
val twice = Compiler.toConsoleScalacOptions(once)
Result
.assert(once == twice)
.log(s"input: $options")
.log(s"once: $once")
.log(s"twice: $twice")
end CompilerConsoleOptsTest

View File

@ -0,0 +1,32 @@
// Regression test for #8921: pipelining flags must not leak into console/scalacOptions.
ThisBuild / usePipelining := true
ThisBuild / scalaVersion := "3.8.1"
val checkConsoleScalacOptions = taskKey[Unit](
"Fails if console/scalacOptions still contains pipelining flags (-Ypickle-java / -Ypickle-write)"
)
lazy val subproject = project
.in(file("modules/subproject"))
.settings(
checkConsoleScalacOptions := {
val opts = (Compile / console / scalacOptions).value
val bad = opts.filter(o => o == "-Ypickle-java" || o == "-Ypickle-write")
if (bad.nonEmpty)
sys.error(s"pipelining flags must not reach the REPL, found: $bad")
}
)
lazy val root = project
.in(file("."))
.dependsOn(subproject)
.aggregate(subproject)
.settings(
checkConsoleScalacOptions := {
val opts = (Compile / console / scalacOptions).value
val bad = opts.filter(o => o == "-Ypickle-java" || o == "-Ypickle-write")
if (bad.nonEmpty)
sys.error(s"pipelining flags must not reach the REPL, found: $bad")
}
)

View File

@ -0,0 +1,3 @@
object Subproject {
val value: List[Int] = Nil
}

View File

@ -0,0 +1,5 @@
# Verify pipelining flags are stripped from console/scalacOptions (#8921).
> compile
> subproject/checkConsoleScalacOptions
> checkConsoleScalacOptions