mirror of https://github.com/sbt/sbt.git
Merge pull request #9181 from eed3si9n/bport/console-plugin-virtualpath
[2.0.x] bport: Use rootPaths to replace virtual paths in console and doc
This commit is contained in:
commit
ce34b6231d
|
|
@ -1109,9 +1109,16 @@ object Defaults extends BuildCommon {
|
|||
},
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -344,6 +344,7 @@ object Compiler:
|
|||
Def.task {
|
||||
val s = Keys.streams.value
|
||||
val conv = Keys.fileConverter.value
|
||||
val rootPaths = Keys.rootPaths.value
|
||||
val cside = (task / Keys.clientSide).value
|
||||
val depsJars = (task / Keys.externalDependencyClasspath).value.toVector
|
||||
.map(_.data)
|
||||
|
|
@ -362,12 +363,14 @@ object Compiler:
|
|||
workingDir,
|
||||
conv,
|
||||
)
|
||||
val consoleScalacOptions =
|
||||
resolveVirtualizedScalacOptions((task / Keys.scalacOptions).value, rootPaths)
|
||||
val param = ConsoleInfo(
|
||||
ArrayList(toolJars.asJava),
|
||||
ArrayList(bridgeJars.toVector.map(vf => conv.toPath(vf).toUri()).asJava),
|
||||
ArrayList(),
|
||||
ArrayList(Attributed.data(cp).toVector.map(vf => conv.toPath(vf).toUri()).asJava),
|
||||
ArrayList((task / Keys.scalacOptions).value.asJava),
|
||||
ArrayList(consoleScalacOptions.asJava),
|
||||
(task / Keys.initialCommands).value,
|
||||
(task / Keys.cleanupCommands).value,
|
||||
)
|
||||
|
|
@ -432,4 +435,37 @@ 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
|
||||
|
||||
/**
|
||||
* Converts mapped virtual file ids in compiler plugin options back to machine paths.
|
||||
*
|
||||
* Compiler plugin options are often encoded using `FileConverter.toVirtualFile` to keep
|
||||
* paths portable in persisted settings (for example `-Xplugin:${CSR_CACHE}/...`). Before we
|
||||
* launch tools that expect concrete filesystem paths (forked console, scaladoc), these ids
|
||||
* must be resolved using the `rootPaths`.
|
||||
*/
|
||||
private[sbt] def resolveVirtualizedScalacOptions(
|
||||
options: Seq[String],
|
||||
rootPaths: Map[String, Path]
|
||||
): Seq[String] =
|
||||
def convertValue(value: String): String =
|
||||
rootPaths.find((key, _) => value.startsWith(s"$${$key}/")) match
|
||||
case Some((key, p)) => p.resolve(value.stripPrefix(s"$${$key}/")).toString()
|
||||
case None => value
|
||||
|
||||
options.map(_.split(":").map(_.split(",").map(convertValue).mkString(",")).mkString(":"))
|
||||
|
||||
end Compiler
|
||||
|
|
|
|||
|
|
@ -0,0 +1,208 @@
|
|||
package sbt.internal
|
||||
|
||||
import hedgehog.*
|
||||
import hedgehog.runner.*
|
||||
import java.nio.file.Files
|
||||
|
||||
/**
|
||||
* 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")
|
||||
)
|
||||
),
|
||||
example(
|
||||
"virtualized compiler plugin paths are resolved for tool invocation",
|
||||
checkResolvedVirtualizedOptions
|
||||
),
|
||||
|
||||
// ── 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 def checkResolvedVirtualizedOptions: Result =
|
||||
val cacheRoot = Files.createTempDirectory("compiler-console-opts")
|
||||
val rootPaths = Map("CSR_CACHE" -> cacheRoot)
|
||||
val converter = _root_.sbt.internal.inc.MappedFileConverter(rootPaths, allowMachinePath = false)
|
||||
val pluginJar = cacheRoot.resolve("plugins/acyclic.jar")
|
||||
val pluginRef = converter.toVirtualFile(pluginJar).toString
|
||||
val input = Seq(s"-Xplugin:$pluginRef", "-P:acyclic:force")
|
||||
val expected = Seq(s"-Xplugin:${pluginJar.toString}", "-P:acyclic:force")
|
||||
val got = Compiler.resolveVirtualizedScalacOptions(input, rootPaths)
|
||||
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