From 3045cd0c46275f42893d589589a24818e7e70900 Mon Sep 17 00:00:00 2001 From: azdrojowa123 <33667003+azdrojowa123@users.noreply.github.com> Date: Wed, 19 Aug 2026 11:36:38 +0200 Subject: [PATCH] [2.x] Add a resolvedScalacOptions task that resolves cache placeholders (#9610) Add a resolvedScalacOptions task that resolves cache placeholders in scalacOptions to absolute machine paths. Co-Authored-By: Claude Opus 4.8 (1M context) --- main/src/main/scala/sbt/Defaults.scala | 3 ++ main/src/main/scala/sbt/Keys.scala | 3 ++ .../internal/server/BuildServerProtocol.scala | 6 ++-- .../resolved-scalac-options/build.sbt | 26 +++++++++++++++ .../resolved-scalac-options/test | 2 ++ .../src/server-test/buildserver/build.sbt | 6 ++++ .../test/scala/testpkg/BuildServerTest.scala | 33 +++++++++++++++++++ 7 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 sbt-app/src/sbt-test/compiler-project/resolved-scalac-options/build.sbt create mode 100644 sbt-app/src/sbt-test/compiler-project/resolved-scalac-options/test diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 8e0b2bd27..d249a7093 100644 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -1147,6 +1147,9 @@ object Defaults extends BuildCommon with DefExtra { consoleQuick / scalacOptions := Def.uncached { Compiler.toConsoleScalacOptions(scalacOptions.value) }, + resolvedScalacOptions := Def.uncached { + Compiler.resolveVirtualizedScalacOptions(scalacOptions.value, rootPaths.value) + }, consoleQuick / forkOptions := Def.uncached((console / forkOptions).value), discoveredMainClasses := compile .map(discoverMainClasses) diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 2be0e0d42..0a24a3b36 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -217,6 +217,8 @@ object Keys { val autoCompilerPlugins = settingKey[Boolean]("If true, enables automatically generating -Xplugin arguments to the compiler based on the classpath for the " + CompilerPlugin.name + " configuration.").withRank(AMinusSetting) val maxErrors = settingKey[Int]("The maximum number of errors, such as compile errors, to list.").withRank(ASetting) val scalacOptions = taskKey[Seq[String]]("Options for the Scala compiler.").withRank(BPlusTask) + @transient + val resolvedScalacOptions = taskKey[Seq[String]]("scalacOptions with cache placeholders (e.g. ${CSR_CACHE}) resolved to absolute machine paths.").withRank(BPlusTask) val javacOptions = taskKey[Seq[String]]("Options for the Java compiler.").withRank(BPlusTask) val incOptions = taskKey[IncOptions]("Options for the incremental compiler.").withRank(BTask) val extraIncOptions = taskKey[Seq[(String, String)]]("Extra options for the incremental compiler").withRank(CTask) @@ -509,6 +511,7 @@ object Keys { val bspBuildTargetRun = inputKey[Unit]("Corresponds to buildTarget/run request").withRank(DTask) val bspBuildTargetCleanCache = inputKey[Unit]("Corresponds to buildTarget/cleanCache request").withRank(DTask) val bspBuildTargetScalacOptions = inputKey[Unit]("").withRank(DTask) + @transient val bspBuildTargetScalacOptionsItem = taskKey[ScalacOptionsItem]("").withRank(DTask) val bspBuildTargetJavacOptions = inputKey[Unit]("Implementation of buildTarget/javacOptions").withRank(DTask) val bspBuildTargetJavacOptionsItem = taskKey[JavacOptionsItem]("Item of buildTarget/javacOptions").withRank(DTask) diff --git a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala index 882b4d063..66389bb44 100644 --- a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala +++ b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala @@ -219,9 +219,11 @@ object BuildServerProtocol { val items = bspBuildTargetScalacOptionsItem.result.all(filter).value val appProvider = appConfiguration.value.provider() val sbtJars = appProvider.mainClasspath() + val rootPaths = Keys.rootPaths.value val buildItems = workspace.builds.map { build => val plugins: LoadedPlugins = build._2.unit.plugins - val scalacOptions = plugins.pluginData.scalacOptions + val scalacOptions = + Compiler.resolveVirtualizedScalacOptions(plugins.pluginData.scalacOptions, rootPaths) val pluginClasspath = plugins.classpath val converter = plugins.pluginData.converter val classpath = @@ -330,7 +332,7 @@ object BuildServerProtocol { bspBuildTargetRun := bspRunTask.evaluated, bspBuildTargetScalacOptionsItem := { val target = Keys.bspTargetIdentifier.value - val scalacOptions = Keys.scalacOptions.value.toVector + val scalacOptions = Keys.resolvedScalacOptions.value.toVector val classDirectory = Keys.classDirectory.value val classpath = classpathTask.value ScalacOptionsItem(target, scalacOptions, classpath, classDirectory.toURI) diff --git a/sbt-app/src/sbt-test/compiler-project/resolved-scalac-options/build.sbt b/sbt-app/src/sbt-test/compiler-project/resolved-scalac-options/build.sbt new file mode 100644 index 000000000..2284ed2c6 --- /dev/null +++ b/sbt-app/src/sbt-test/compiler-project/resolved-scalac-options/build.sbt @@ -0,0 +1,26 @@ +val check = taskKey[Unit]("Verify resolvedScalacOptions resolves cache placeholders (#9578).") + +scalaVersion := "2.13.16" + +addCompilerPlugin(("org.typelevel" % "kind-projector" % "0.13.3").cross(CrossVersion.full)) + +check := Def.uncached { + val raw = (Compile / scalacOptions).value + val resolved = (Compile / resolvedScalacOptions).value + + assert( + raw.exists(_.contains("${CSR_CACHE}")), + s"expected a virtualized coursier-cache placeholder in raw scalacOptions, got: $raw" + ) + assert( + resolved.forall(!_.contains("${")), + s"resolvedScalacOptions still contains a cache placeholder: $resolved" + ) + + val pluginArg = resolved + .find(_.startsWith("-Xplugin:")) + .getOrElse(sys.error(s"no -Xplugin entry in resolvedScalacOptions: $resolved")) + val jar = new java.io.File(pluginArg.stripPrefix("-Xplugin:")) + assert(jar.isAbsolute, s"resolved -Xplugin path is not absolute: $jar") + assert(jar.exists, s"resolved -Xplugin jar does not exist: $jar") +} diff --git a/sbt-app/src/sbt-test/compiler-project/resolved-scalac-options/test b/sbt-app/src/sbt-test/compiler-project/resolved-scalac-options/test new file mode 100644 index 000000000..ea823cb64 --- /dev/null +++ b/sbt-app/src/sbt-test/compiler-project/resolved-scalac-options/test @@ -0,0 +1,2 @@ +# resolvedScalacOptions must resolve ${CSR_CACHE} placeholders to absolute machine paths (#9578) +> check diff --git a/server-test/src/server-test/buildserver/build.sbt b/server-test/src/server-test/buildserver/build.sbt index 02608cd43..5dcf767c5 100644 --- a/server-test/src/server-test/buildserver/build.sbt +++ b/server-test/src/server-test/buildserver/build.sbt @@ -38,6 +38,12 @@ lazy val util = project.settings( Compile / classDirectory := baseDirectory.value / "classes" ) +lazy val scalacOptionsPlugin = project + .in(file("scalac-options-plugin")) + .settings( + addCompilerPlugin(("org.typelevel" % "kind-projector" % "0.13.3").cross(CrossVersion.full)) + ) + lazy val diagnostics = project lazy val javaProj = project diff --git a/server-test/src/test/scala/testpkg/BuildServerTest.scala b/server-test/src/test/scala/testpkg/BuildServerTest.scala index 5e8a4ff38..ddb28dabc 100644 --- a/server-test/src/test/scala/testpkg/BuildServerTest.scala +++ b/server-test/src/test/scala/testpkg/BuildServerTest.scala @@ -432,6 +432,39 @@ class BuildServerTest extends AbstractServerTest { ) } + test("buildTarget/scalacOptions resolves cache placeholders (#9578)") { + val buildTargets = Seq(buildTargetUri("scalacOptionsPlugin", "Compile")) + val id = scalacOptions(buildTargets) + val result = + svr.session.waitForResultInResponseMsg[ScalacOptionsResult](30.seconds, id).get + val options = result.items.head.options + + assert( + options.forall(!_.contains("${")), + s"buildTarget/scalacOptions returned an unresolved cache placeholder: $options" + ) + + // The kind-projector compiler plugin must be reported as a concrete, absolute, + // existing jar path — not a virtualized ${CSR_CACHE}/... reference. + val xplugin = options + .find(_.startsWith("-Xplugin:")) + .getOrElse(fail(s"expected a -Xplugin compiler-plugin option, got: $options")) + val pluginPath = xplugin.stripPrefix("-Xplugin:") + val pluginFile = new File(pluginPath) + assert( + pluginFile.isAbsolute, + s"resolved -Xplugin path is not absolute: $pluginPath" + ) + assert( + pluginFile.getName.startsWith("kind-projector"), + s"expected the kind-projector plugin jar, got: $pluginPath" + ) + assert( + pluginFile.exists, + s"resolved -Xplugin jar does not exist on disk: $pluginPath" + ) + } + test("buildTarget/cleanCache") { def classFile = svr.baseDirectory.toPath.resolve( "target/out/jvm/scala-2.13.11/runandtest/classes/main/Main.class"