diff --git a/main-command/src/main/scala/sbt/BasicCommandStrings.scala b/main-command/src/main/scala/sbt/BasicCommandStrings.scala index 386208441..c76d70e33 100644 --- a/main-command/src/main/scala/sbt/BasicCommandStrings.scala +++ b/main-command/src/main/scala/sbt/BasicCommandStrings.scala @@ -149,7 +149,9 @@ $HelpCommand 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. diff --git a/main/src/main/scala/sbt/MainLoop.scala b/main/src/main/scala/sbt/MainLoop.scala index f2ebab9ba..a94ae9c33 100644 --- a/main/src/main/scala/sbt/MainLoop.scala +++ b/main/src/main/scala/sbt/MainLoop.scala @@ -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 => diff --git a/main/src/test/scala/sbt/MainLoopZincCacheTest.scala b/main/src/test/scala/sbt/MainLoopZincCacheTest.scala new file mode 100644 index 000000000..1e0d44b3f --- /dev/null +++ b/main/src/test/scala/sbt/MainLoopZincCacheTest.scala @@ -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