[2.x] fix: clear compiler bridge zinc cache on reboot dev

Reboot dev now removes zinc/org.scala-sbt under the resolved global zinc
directory (same layout as ZincComponentManager secondary cache).

Closes #5735
This commit is contained in:
bitloi 2026-04-10 09:07:38 +02:00
parent 7218b2a1ac
commit 47302ccfc2
No known key found for this signature in database
GPG Key ID: DB743C90C0FE29BA
3 changed files with 190 additions and 1 deletions

View File

@ -149,7 +149,9 @@ $HelpCommand <regular expression>
remaining commands with the exception that the JVM is not shut down.
If 'dev' is specified, the current sbt artifacts from the boot directory
(`~/.sbt/boot` by default) are deleted before restarting.
(`~/.sbt/boot` by default) are deleted before restarting, and the compiler bridge
secondary cache (`zinc/org.scala-sbt` under the global sbt directory, respecting
`sbt.global.base` and `sbt.global.zinc`) is removed.
This forces an update of sbt and Scala, which is useful when working with development
versions of sbt.
If 'full' is specified, the boot directory is wiped out before restarting.

View File

@ -22,6 +22,7 @@ import sbt.internal.util.{
Terminal as ITerminal
}
import sbt.io.{ IO, Using }
import sbt.librarymanagement.SbtArtifacts
import sbt.protocol.*
import sbt.util.{ Logger, LoggerContext }
@ -75,6 +76,7 @@ private[sbt] object MainLoop:
case e: RebootCurrent =>
deleteLastLog(logBacking)
deleteCurrentArtifacts(state)
deleteZincBridgeSecondaryCache(state)
throw new xsbti.FullReload(e.arguments.toArray, false)
case NonFatal(e) =>
System.err.println(
@ -109,6 +111,15 @@ private[sbt] object MainLoop:
}
}
/** Removes the Zinc compiler bridge secondary cache (`…/zinc/org.scala-sbt`). */
private[sbt] def deleteZincBridgeSecondaryCache(state: State): Unit =
import sbt.io.syntax.*
val zincDir = BuildPaths.getZincDirectory(state, BuildPaths.getGlobalBase(state))
val bridgeCache = zincDir / SbtArtifacts.Organization
if bridgeCache.exists() then
state.log.info(s"deleting $bridgeCache")
IO.delete(bridgeCache)
/** Runs the next sequence of commands with global logging in place. */
def runWithNewLog(state: State, logBacking: GlobalLogBacking): RunNext =
Using.fileWriter(append = true)(logBacking.file) { writer =>

View File

@ -0,0 +1,176 @@
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
import java.io.File
import java.util.concurrent.Callable
import sbt.internal.util.{ AttributeMap, ConsoleOut, GlobalLogging, MainAppender }
import sbt.io.IO
import sbt.io.syntax.*
import sbt.librarymanagement.SbtArtifacts
import xsbti.{
AppConfiguration,
AppMain,
AppProvider,
ApplicationID as XApplicationID,
ComponentProvider,
CrossValue,
GlobalLock,
Launcher,
Repository,
ScalaProvider,
}
object MainLoopZincCacheTest extends verify.BasicTestSuite:
private object Stubs:
val NoGlobalLock: GlobalLock = new GlobalLock:
def apply[T](lockFile: File, run: Callable[T]): T = run.call()
lazy val componentProvider: ComponentProvider = new ComponentProvider:
def componentLocation(id: String): File = new File(id)
def component(id: String): Array[File] = Array.empty
def defineComponent(id: String, jars: Array[File]): Unit = ()
def addToComponent(id: String, jars: Array[File]): Boolean = false
def lockFile(): File = new File(System.getProperty("java.io.tmpdir"), "stub-components.lock")
lazy val launcher: Launcher = new Launcher:
def getScala(version: String): ScalaProvider = Stubs.scalaProvider
def getScala(version: String, reason: String): ScalaProvider = Stubs.scalaProvider
def getScala(version: String, reason: String, scalaOrg: String): ScalaProvider =
Stubs.scalaProvider
def app(id: XApplicationID, version: String): AppProvider = Stubs.appProvider
def topLoader(): ClassLoader = classOf[String].getClassLoader
def globalLock(): GlobalLock = NoGlobalLock
def bootDirectory(): File = new File(System.getProperty("java.io.tmpdir"))
def ivyRepositories(): Array[Repository] = Array.empty
def appRepositories(): Array[Repository] = Array.empty
def isOverrideRepositories: Boolean = false
def ivyHome(): File = new File(System.getProperty("java.io.tmpdir"))
def checksums(): Array[String] = Array.empty
lazy val scalaProvider: ScalaProvider = new ScalaProvider:
def launcher(): Launcher = Stubs.launcher
def version(): String = "3.8.3"
def loader(): ClassLoader = classOf[String].getClassLoader
def jars(): Array[File] = Array.empty
def libraryJar(): File = new File("scala-library.jar")
def compilerJar(): File = new File("scala-compiler.jar")
def app(id: XApplicationID): AppProvider = Stubs.appProvider
val appId: sbt.ApplicationID = sbt.ApplicationID(
"org.scala-sbt",
"sbt",
"2.0.0",
"sbt.xMain",
Seq.empty,
CrossValue.Disabled,
Seq.empty,
)
lazy val appProvider: AppProvider = new AppProvider:
def scalaProvider(): ScalaProvider = Stubs.scalaProvider
def id(): XApplicationID = appId
def loader(): ClassLoader = classOf[String].getClassLoader
def mainClass(): Class[? <: AppMain] = classOf[xMain]
def entryPoint(): Class[?] = classOf[xMain]
def newMain(): AppMain = new xMain()
def mainClasspath(): Array[File] = Array.empty
def components(): ComponentProvider = Stubs.componentProvider
def appConfiguration(baseDir: File): AppConfiguration = new AppConfiguration:
def arguments(): Array[String] = Array.empty
def baseDirectory(): File = baseDir
def provider(): AppProvider = Stubs.appProvider
end Stubs
private def mkState(zincRoot: File, logFile: File, baseDir: File) =
val attrs = AttributeMap.empty
.put(BuildPaths.globalBaseDirectory, zincRoot.getParentFile)
.put(BuildPaths.globalZincDirectory, zincRoot)
State(
configuration = Stubs.appConfiguration(baseDir),
definedCommands = Nil,
exitHooks = Set.empty,
onFailure = None,
remainingCommands = Nil,
history = State.newHistory,
attributes = attrs,
globalLogging = GlobalLogging.initial(
MainAppender.globalDefault(ConsoleOut.globalProxy),
logFile,
ConsoleOut.globalProxy
),
currentCommand = None,
next = State.Continue
)
test("deleteZincBridgeSecondaryCache removes org.scala-sbt under global zinc"):
IO.withTemporaryDirectory: tmp =>
val zincRoot = tmp / "zinc"
val bridge = zincRoot / SbtArtifacts.Organization
IO.write(bridge / "marker.txt", "cached")
val logFile = File.createTempFile("sbt-mlz", ".log")
try
MainLoop.deleteZincBridgeSecondaryCache(mkState(zincRoot, logFile, tmp))
assert(!bridge.exists(), s"expected $bridge deleted")
finally IO.delete(logFile)
test("deleteZincBridgeSecondaryCache is a no-op when org.scala-sbt is absent"):
IO.withTemporaryDirectory: tmp =>
val zincRoot = tmp / "zinc"
IO.createDirectory(zincRoot)
val logFile = File.createTempFile("sbt-mlz", ".log")
try
MainLoop.deleteZincBridgeSecondaryCache(mkState(zincRoot, logFile, tmp))
assert(zincRoot.exists())
finally IO.delete(logFile)
test("deleteZincBridgeSecondaryCache respects sbt.global.zinc system property"):
IO.withTemporaryDirectory: customZinc =>
val prop = BuildPaths.GlobalZincProperty
val prev = sys.props.get(prop)
try
sys.props(prop) = customZinc.getAbsolutePath
val bridge = customZinc / SbtArtifacts.Organization
IO.write(bridge / "x.jar", Array.emptyByteArray)
val logFile = File.createTempFile("sbt-mlz", ".log")
try
val attrs = AttributeMap.empty.put(
BuildPaths.globalBaseDirectory,
customZinc.getParentFile / "unused-base"
)
val state = State(
configuration = Stubs.appConfiguration(customZinc.getParentFile),
definedCommands = Nil,
exitHooks = Set.empty,
onFailure = None,
remainingCommands = Nil,
history = State.newHistory,
attributes = attrs,
globalLogging = GlobalLogging.initial(
MainAppender.globalDefault(ConsoleOut.globalProxy),
logFile,
ConsoleOut.globalProxy
),
currentCommand = None,
next = State.Continue
)
MainLoop.deleteZincBridgeSecondaryCache(state)
assert(!bridge.exists())
finally IO.delete(logFile)
finally
prev match
case Some(v) => sys.props(prop) = v
case None => sys.props.remove(prop)
end MainLoopZincCacheTest