mirror of https://github.com/sbt/sbt.git
[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:
parent
bff386d276
commit
606bb3b59c
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
}
|
||||
)
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
object Subproject {
|
||||
val value: List[Int] = Nil
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Verify pipelining flags are stripped from console/scalacOptions (#8921).
|
||||
|
||||
> compile
|
||||
> subproject/checkConsoleScalacOptions
|
||||
> checkConsoleScalacOptions
|
||||
Loading…
Reference in New Issue