From 6ac45711979f8b844f58d8501bbdbe54b34ccc7e Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sat, 21 Oct 2017 01:27:40 -0500 Subject: [PATCH] Adds "reboot dev" This adds a new option `dev` to the `reboot` command, which deletes the only the current sbt artifacts from the boot directory. `reboot dev` reads actively from `build.properties` instead of using the current state since `reboot` can restart into another sbt version. In general, `reboot dev` is intended for the local development of sbt. Fixes #3590 --- build.sbt | 2 ++ .../main/scala/sbt/BasicCommandStrings.scala | 11 ++++--- .../src/main/scala/sbt/BasicCommands.scala | 15 ++++++++-- main-command/src/main/scala/sbt/State.scala | 29 ++++++++++++++---- main/src/main/scala/sbt/MainLoop.scala | 30 ++++++++++++++++++- notes/1.1.0/reboot.md | 3 ++ 6 files changed, 77 insertions(+), 13 deletions(-) create mode 100644 notes/1.1.0/reboot.md diff --git a/build.sbt b/build.sbt index 93f3e3576..5ed988166 100644 --- a/build.sbt +++ b/build.sbt @@ -309,6 +309,8 @@ lazy val commandProj = (project in file("main-command")) exclude[ReversedMissingMethodProblem]("sbt.internal.server.ServerInstance.*"), // Added method to CommandChannel. internal. exclude[ReversedMissingMethodProblem]("sbt.internal.CommandChannel.*"), + // Added an overload to reboot. The overload is private[sbt]. + exclude[ReversedMissingMethodProblem]("sbt.StateOps.reboot"), ) ) .configure( diff --git a/main-command/src/main/scala/sbt/BasicCommandStrings.scala b/main-command/src/main/scala/sbt/BasicCommandStrings.scala index 7d5f120d7..d6b8761dc 100644 --- a/main-command/src/main/scala/sbt/BasicCommandStrings.scala +++ b/main-command/src/main/scala/sbt/BasicCommandStrings.scala @@ -117,14 +117,17 @@ $HelpCommand def RebootCommand = "reboot" def RebootDetailed = - RebootCommand + """ [full] + RebootCommand + """ [dev | full] This command is equivalent to exiting sbt, restarting, and running the remaining commands with the exception that the JVM is not shut down. - If 'full' is specified, the boot directory (`~/.sbt/boot` by default) - is deleted before restarting. This forces an update of sbt and Scala - and is useful when working with development versions of sbt or Scala.""" + If 'dev' is specified, the current sbt artifacts from the boot directory + (`~/.sbt/boot` by default) are deleted before restarting. + 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. +""" def Multi = ";" def MultiBrief = diff --git a/main-command/src/main/scala/sbt/BasicCommands.scala b/main-command/src/main/scala/sbt/BasicCommands.scala index 2ae3e6431..82bb7b883 100644 --- a/main-command/src/main/scala/sbt/BasicCommands.scala +++ b/main-command/src/main/scala/sbt/BasicCommands.scala @@ -175,10 +175,19 @@ object BasicCommands { } def reboot: Command = - Command(RebootCommand, Help.more(RebootCommand, RebootDetailed))(rebootParser)((s, full) => - s reboot full) + Command(RebootCommand, Help.more(RebootCommand, RebootDetailed))(rebootOptionParser) { + case (s, (full, currentOnly)) => + s.reboot(full, currentOnly) + } - def rebootParser(s: State): Parser[Boolean] = token(Space ~> "full" ^^^ true) ?? false + @deprecated("Use rebootOptionParser", "1.1.0") + def rebootParser(s: State): Parser[Boolean] = + rebootOptionParser(s) map { case (full, currentOnly) => full } + + private[sbt] def rebootOptionParser(s: State): Parser[(Boolean, Boolean)] = + token( + Space ~> (("full" ^^^ ((true, false))) | + ("dev" ^^^ ((false, true))))) ?? ((false, false)) def call: Command = Command(ApplyCommand, Help.more(ApplyCommand, ApplyDetailed))(_ => callParser) { diff --git a/main-command/src/main/scala/sbt/State.scala b/main-command/src/main/scala/sbt/State.scala index 6259a02a9..9d0e401c6 100644 --- a/main-command/src/main/scala/sbt/State.scala +++ b/main-command/src/main/scala/sbt/State.scala @@ -89,6 +89,17 @@ trait StateOps { */ def reboot(full: Boolean): State + /** + * Reboots sbt. A reboot restarts execution from the entry point of the launcher. + * A reboot is designed to be as close as possible to actually restarting the JVM without actually doing so. + * Because the JVM is not restarted, JVM exit hooks are not run. + * State.exitHooks should be used instead and those will be run before rebooting. + * If `full` is true, the boot directory is deleted before starting again. + * If `currentOnly` is true, the artifacts for the current sbt version is deleted. + * This command is currently implemented to not return, but may be implemented in the future to only reboot at the next command processing step. + */ + private[sbt] def reboot(full: Boolean, currentOnly: Boolean): State + /** Sets the next command processing action to do.*/ def setNext(n: State.Next): State @@ -248,12 +259,18 @@ object State { def baseDir: File = s.configuration.baseDirectory def setNext(n: Next) = s.copy(next = n) def continue = setNext(Continue) - def reboot(full: Boolean) = { - runExitHooks(); - throw new xsbti.FullReload( - (s.remainingCommands map { case e: Exec => e.commandLine }).toArray, - full) + + /** Implementation of reboot. */ + def reboot(full: Boolean): State = reboot(full, false) + + /** Implementation of reboot. */ + private[sbt] def reboot(full: Boolean, currentOnly: Boolean): State = { + runExitHooks() + val rs = s.remainingCommands map { case e: Exec => e.commandLine } + if (currentOnly) throw new RebootCurrent(rs) + else throw new xsbti.FullReload(rs.toArray, full) } + def reload = runExitHooks().setNext(new Return(defaultReload(s))) def clearGlobalLog = setNext(ClearGlobalLog) def keepLastLog = setNext(KeepLastLog) @@ -320,3 +337,5 @@ object State { private[sbt] def getBoolean(s: State, key: AttributeKey[Boolean], default: Boolean): Boolean = s.get(key) getOrElse default } + +private[sbt] final class RebootCurrent(val arguments: List[String]) extends RuntimeException diff --git a/main/src/main/scala/sbt/MainLoop.scala b/main/src/main/scala/sbt/MainLoop.scala index 11f2c0fb3..83b38b4c8 100644 --- a/main/src/main/scala/sbt/MainLoop.scala +++ b/main/src/main/scala/sbt/MainLoop.scala @@ -7,11 +7,12 @@ package sbt +import java.util.Properties import scala.annotation.tailrec import scala.util.control.NonFatal import jline.TerminalFactory -import sbt.io.Using +import sbt.io.{ IO, Using } import sbt.internal.util.{ ErrorHandling, GlobalLogBacking } import sbt.internal.util.complete.DefaultParsers import sbt.util.Logger @@ -58,6 +59,10 @@ object MainLoop { case e: xsbti.FullReload => deleteLastLog(logBacking) throw e // pass along a reboot request + case e: RebootCurrent => + deleteLastLog(logBacking) + deleteCurrentArtifacts(state) + throw new xsbti.FullReload(e.arguments.toArray, false) case NonFatal(e) => System.err.println( "sbt appears to be exiting abnormally.\n The log file for this session is at " + logBacking.file) @@ -69,6 +74,28 @@ object MainLoop { def deleteLastLog(logBacking: GlobalLogBacking): Unit = logBacking.last.foreach(_.delete()) + /** Deletes the current sbt artifacts from boot. */ + private[sbt] def deleteCurrentArtifacts(state: State): Unit = { + import sbt.io.syntax._ + val provider = state.configuration.provider + val appId = provider.id + // If we can obtain boot directory more accurately it'd be better. + val defaultBoot = BuildPaths.defaultGlobalBase / "boot" + val buildProps = state.baseDir / "project" / "build.properties" + // First try reading the sbt version from build.properties file. + val sbtVersionOpt = if (buildProps.exists) { + val buildProperties = new Properties() + IO.load(buildProperties, buildProps) + Option(buildProperties.getProperty("sbt.version")) + } else None + val sbtVersion = sbtVersionOpt.getOrElse(appId.version) + val currentArtDirs = defaultBoot * "*" / appId.groupID / appId.name / sbtVersion + currentArtDirs.get foreach { dir => + state.log.info(s"Deleting $dir") + IO.delete(dir) + } + } + /** 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 => @@ -109,6 +136,7 @@ object MainLoop { ErrorHandling.wideConvert { state.process(processCommand) } match { case Right(s) => s case Left(t: xsbti.FullReload) => throw t + case Left(t: RebootCurrent) => throw t case Left(t) => state.handleError(t) } diff --git a/notes/1.1.0/reboot.md b/notes/1.1.0/reboot.md new file mode 100644 index 000000000..0b7b06504 --- /dev/null +++ b/notes/1.1.0/reboot.md @@ -0,0 +1,3 @@ +### Improvements + +- Adds `reboot dev` command, which deletes the current artifact from the boot directory. This is useful when working with development versions of sbt.