From b721644cbc4745b7d1b79efd141fb5009a8c208c Mon Sep 17 00:00:00 2001 From: Matt Dziuban Date: Sat, 18 Apr 2026 03:29:45 -0400 Subject: [PATCH] [2.x] fix: Use rootPaths to replace virtual paths in console and doc scalac options. (#9110) Rather than using the FileConverter to replace virtual paths, this uses the rootPaths directly. It only replaces a virtual path in a scalac option if the given segment of the option begins with the root path key. --- .../main/scala/sbt/internal/Compiler.scala | 33 ++++++++++++++----- .../internal/CompilerConsoleOptsTest.scala | 20 +++++++++++ 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/main/src/main/scala/sbt/internal/Compiler.scala b/main/src/main/scala/sbt/internal/Compiler.scala index 879a4bf44..a7e2d860c 100644 --- a/main/src/main/scala/sbt/internal/Compiler.scala +++ b/main/src/main/scala/sbt/internal/Compiler.scala @@ -463,6 +463,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) @@ -481,12 +482,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, ) @@ -556,6 +559,7 @@ object Compiler: val cp = Attributed.data(Keys.dependencyClasspath.value).toList val reporter = (Keys.compile / Keys.bspReporter).value val converter = Keys.fileConverter.value + val rootPaths = Keys.rootPaths.value val tFiles = Keys.tastyFiles.value val sv = Keys.scalaVersion.value (hasScala, hasJava) match { @@ -567,13 +571,7 @@ object Compiler: if (ScalaArtifacts.isScala3(sv)) Opts.doc.externalAPIScala3(xapisFiles) else Opts.doc.externalAPI(xapisFiles) val options = sOpts ++ externalApiOpts - def convertVfRef(value: String): String = - if !value.contains("$") then value - else converter.toPath(xsbti.VirtualFileRef.of(value)).toString - val resolvedOptions = options.map { x => - if !x.contains("$") then x - else x.split(":").map(_.split(",").map(convertVfRef).mkString(",")).mkString(":") - } + val resolvedOptions = resolveVirtualizedScalacOptions(options, rootPaths) val scalac = cs.scalac match case ac: AnalyzingCompiler => ac.onArgs(exported(s, "scaladoc")) val docSrcFiles = if ScalaArtifacts.isScala3(sv) then tFiles else srcs @@ -641,4 +639,23 @@ object Compiler: 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 diff --git a/main/src/test/scala/sbt/internal/CompilerConsoleOptsTest.scala b/main/src/test/scala/sbt/internal/CompilerConsoleOptsTest.scala index 310aad604..1812e0417 100644 --- a/main/src/test/scala/sbt/internal/CompilerConsoleOptsTest.scala +++ b/main/src/test/scala/sbt/internal/CompilerConsoleOptsTest.scala @@ -2,6 +2,7 @@ package sbt.internal import hedgehog.* import hedgehog.runner.* +import java.nio.file.Files /** * Tests for [[Compiler.toConsoleScalacOptions]] — pipelining flags must be stripped before @@ -102,6 +103,10 @@ object CompilerConsoleOptsTest extends Properties: Seq("-encoding", "utf8") ) ), + example( + "virtualized compiler plugin paths are resolved for tool invocation", + checkResolvedVirtualizedOptions + ), // ── property-based cases ────────────────────────────────────────────────── @@ -129,6 +134,21 @@ object CompilerConsoleOptsTest extends Properties: .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). */