mirror of https://github.com/sbt/sbt.git
New 'sbt-update' action enabled by mixing in UpdateSbt:
Changes 'sbt.version' after verifying the version is valid by downloading that version of sbt. Updates the current launcher jar or the location specified in the second argument with either a downloaded sbt-launch-<version>.jar if available or a copy of the current launcher jar that uses <version> by default when starting new projects.
This commit is contained in:
parent
a9b69e4425
commit
ff6395fc35
|
|
@ -0,0 +1,213 @@
|
|||
/* sbt -- Simple Build Tool
|
||||
* Copyright 2010 Mark Harrah
|
||||
*/
|
||||
package sbt
|
||||
|
||||
/** A trait that provides a task for updating sbt. */
|
||||
trait UpdateSbt extends Project
|
||||
{
|
||||
/** The first argument is the version to update to and is mandatory.
|
||||
* The second argument is the location of the launcher jar. If omitted, the launcher used to launch the currently running instance of sbt is used.*/
|
||||
lazy val sbtUpdate = task { args => task { (new Update(this))(args) } } describedAs("Updates the version of sbt used to build this project and updates the launcher jar.")
|
||||
}
|
||||
|
||||
import java.io.{File, InputStream, IOException}
|
||||
import java.net.{HttpURLConnection, URL}
|
||||
import HttpURLConnection.{HTTP_NOT_FOUND , HTTP_OK}
|
||||
import SimpleReader.readLine
|
||||
import xsbt.FileUtilities.{classLocationFile, copyFile, readLines, transfer, unzip, withTemporaryDirectory, write, zip}
|
||||
import xsbt.PathMapper.relativeTo
|
||||
import xsbt.Paths._
|
||||
import xsbt.OpenResource.{fileOutputStream, urlInputStream}
|
||||
|
||||
private class Update(project: Project)
|
||||
{
|
||||
val info = project.info
|
||||
val app = info.app
|
||||
val log = project.log
|
||||
|
||||
/** The location of the jar used to launch the currently running instance of sbt.*/
|
||||
lazy val launcherJar = classLocationFile[xsbti.AppProvider]
|
||||
/** A temporary jar file to use in the given directory. */
|
||||
def tempJar(dir: File) = dir / launcherJar.getName
|
||||
|
||||
/** Implementation of the sbt-update task: reads arguments and hands off to the other `apply`.*/
|
||||
def apply(args: Array[String]): Option[String] =
|
||||
args match
|
||||
{
|
||||
case Array(version) if validVersion(version) => apply(version, None)
|
||||
case Array(version, temporaryJar) if validVersion(version) => apply(version, Some(new File(temporaryJar) getAbsoluteFile))
|
||||
case _ => Some("Expected '<version>' or '<version> <new-launcher-file>', got '" + args.mkString(" ") + "'")
|
||||
}
|
||||
|
||||
def validVersion(version: String) = !version.trim.isEmpty
|
||||
|
||||
/** Implementation of the sbt-update task after arguments have checked. Gives user a chance to cancel and continues with `doUpdate`.*/
|
||||
def apply(version: String, temporaryJar: Option[File]): Option[String] =
|
||||
{
|
||||
readLine("Updating the sbt version requires a restart. Continue? (Y/n) ") match
|
||||
{
|
||||
case Some(line) if(isYes(line)) => doUpdate(version, temporaryJar)
|
||||
case _ => Some("Update declined.")
|
||||
}
|
||||
}
|
||||
/** Implementation of the sbt-update task: high-level control after initial verification.*/
|
||||
def doUpdate(version: String, temporaryJar: Option[File]): Option[String] =
|
||||
{
|
||||
retrieveNewVersion(version)
|
||||
log.info("Version is valid. Setting 'sbt.version' to " + version + "...")
|
||||
setNewVersion(version)
|
||||
|
||||
log.info("'sbt.version' updated.")
|
||||
if(temporaryJar.isDefined || updateInPlace(version))
|
||||
{
|
||||
log.info("Downloading new launcher ...")
|
||||
|
||||
if(downloadLauncher(version, temporaryJar))
|
||||
log.info("Downloaded launcher.")
|
||||
else
|
||||
tryUpdateLauncher(version, temporaryJar)
|
||||
}
|
||||
else
|
||||
log.info("Launcher update declined.")
|
||||
|
||||
log.info("Please restart sbt.")
|
||||
System.exit(0)
|
||||
None
|
||||
}
|
||||
/** Updates 'sbt.version' in `project/build.properties`.*/
|
||||
def setNewVersion(version: String)
|
||||
{
|
||||
project.sbtVersion() = version
|
||||
project.saveEnvironment()
|
||||
}
|
||||
/** Retrieves the given `version` of sbt in order to verify the version is valid.*/
|
||||
def retrieveNewVersion(version: String)
|
||||
{
|
||||
val newAppID = changeVersion(app.id, version)
|
||||
log.info("Checking repositories for sbt " + version + " ...")
|
||||
app.scalaProvider.app(newAppID)
|
||||
}
|
||||
/** Asks the user whether the current launcher should be overrwritten. Called when no file is explicitly specified as an argument. */
|
||||
def updateInPlace(version: String) =
|
||||
{
|
||||
val input = readLine(" The current launcher (" + launcherJar + ") will be updated in place. Continue? (Y/n) ")
|
||||
isYes(input)
|
||||
}
|
||||
def isYes(line: Option[String]): Boolean = line.filter(isYes).isDefined
|
||||
|
||||
/** Updates the launcher as in `updateLauncher` but performs various checks and logging around it. */
|
||||
def tryUpdateLauncher(version: String, temporaryJar: Option[File])
|
||||
{
|
||||
log.warn("No launcher found for '" + version + "'")
|
||||
def promptStart = if(temporaryJar.isDefined) " Copy current launcher but with " else " Modify current launcher to use "
|
||||
val input = readLine(promptStart + version + " as the default for new projects? (Y/n) ")
|
||||
val updated = isYes(input)
|
||||
if(updated) updateLauncher(version, temporaryJar)
|
||||
|
||||
def extra = if(temporaryJar.isDefined) " at " + temporaryJar.get + "." else "."
|
||||
log.info(if(updated) "Launcher updated" + extra else "Launcher not updated.")
|
||||
}
|
||||
/** The jar to copy/download to. If `userJar` is not defined, it is a temporary file in `tmpDir` that should then be moved to the current launcher file.
|
||||
* If it is defined and is a directory, the jar is defined in that directory. If it is a file, that file is returned. */
|
||||
def targetJar(tmpDir: File, userJar: Option[File]): File =
|
||||
userJar match { case Some(file) => if(file.isDirectory) tempJar(file) else file; case None => tempJar(tmpDir) }
|
||||
|
||||
/** Gets the given `version` of the launcher from Google Code. If `userProvidedJar` is defined,
|
||||
* this updated launcher is downloaded there, otherwise it overwrites the current launcher. */
|
||||
def downloadLauncher(version: String, userProvidedJar: Option[File]): Boolean =
|
||||
{
|
||||
def getLauncher(tmp: File): Boolean =
|
||||
{
|
||||
val temporaryJar = targetJar(tmp, userProvidedJar)
|
||||
temporaryJar.getParentFile.mkdirs()
|
||||
val url = launcherURL(version)
|
||||
val connection = url.openConnection.asInstanceOf[HttpURLConnection]
|
||||
connection.setInstanceFollowRedirects(false)
|
||||
|
||||
def download(in: InputStream): Unit = fileOutputStream(false)(temporaryJar) { out => transfer(in, out) }
|
||||
def checkAndRetrieve(in: InputStream): Boolean = (connection.getResponseCode == HTTP_OK) && { download(in); true }
|
||||
def handleError(e: IOException) = if(connection.getResponseCode == HTTP_NOT_FOUND ) false else throw e
|
||||
def retrieve() =
|
||||
{
|
||||
val in = connection.getInputStream
|
||||
try { checkAndRetrieve(in) } finally { in.close() }
|
||||
}
|
||||
|
||||
val success = try { retrieve() } catch { case e: IOException => handleError(e)} finally { connection.disconnect() }
|
||||
if(success && userProvidedJar.isEmpty)
|
||||
move(temporaryJar, launcherJar)
|
||||
success
|
||||
}
|
||||
withTemporaryDirectory(getLauncher)
|
||||
}
|
||||
/** The location of the launcher for the given version, if it exists. */
|
||||
def launcherURL(version: String): URL =
|
||||
new URL("http://simple-build-tool.googlecode.com/files/sbt-launch-" + version + ".jar")
|
||||
|
||||
/** True iff the given user input is empty, 'y' or 'yes' (case-insensitive).*/
|
||||
def isYes(line: String) =
|
||||
{
|
||||
val lower = line.toLowerCase
|
||||
lower.isEmpty || lower == "y" || lower == "yes"
|
||||
}
|
||||
/** Copies the current launcher but with the default 'sbt.version' set to `version`. If `userProvidedJar` is defined,
|
||||
* the updated launcher is copied there, otherwise the copy overwrites the current launcher. */
|
||||
def updateLauncher(version: String, userProvidedJar: Option[File])
|
||||
{
|
||||
def makeUpdated(base: File, newJar: File)
|
||||
{
|
||||
val files = unzip(launcherJar, base)
|
||||
updateBootProperties(files, version)
|
||||
zip(relativeTo(base)( files ), newJar)
|
||||
}
|
||||
def updateLauncher(tmp: File)
|
||||
{
|
||||
val basePath = tmp / "launcher-jar"
|
||||
val temporaryJar = targetJar(tmp, userProvidedJar)
|
||||
makeUpdated(basePath, temporaryJar)
|
||||
if(userProvidedJar.isEmpty)
|
||||
move(temporaryJar, launcherJar)
|
||||
}
|
||||
|
||||
withTemporaryDirectory(updateLauncher)
|
||||
}
|
||||
|
||||
/** Copies the `src` file to the `dest` file, preferably by renaming. The `src` file may or may not be removed.*/
|
||||
def move(src: File, dest: File)
|
||||
{
|
||||
val renameSuccess = src renameTo dest
|
||||
if(!renameSuccess)
|
||||
copyFile(src, dest)
|
||||
}
|
||||
|
||||
/** Updates the default value used for 'sbt.version' in the 'sbt.boot.properties' file in the launcher `files` to be `version`.*/
|
||||
def updateBootProperties(files: Iterable[File], version: String): Unit =
|
||||
files.find(_.getName == "sbt.boot.properties").foreach(updateBootProperties(version))
|
||||
/** Updates the default value used for 'sbt.version' in the given `file` to be `version`.*/
|
||||
def updateBootProperties(version: String)(file: File)
|
||||
{
|
||||
val newContent = readLines(file) map updateSbtVersion(version)
|
||||
write(file, newContent.mkString("\n"))
|
||||
}
|
||||
|
||||
/** If the given `line` is the 'sbt.version' configuration, update it to use the `newVersion`.*/
|
||||
def updateSbtVersion(newVersion: String)(line: String) =
|
||||
if(line.trim.startsWith("sbt.version")) sbtVersion(newVersion) else line
|
||||
|
||||
/** The configuration line that defines the 'sbt.version' property, using the provided `version` for defaults.*/
|
||||
def sbtVersion(version: String) =
|
||||
" sbt.version: quick=set(" + version + "), new=prompt(sbt version)[" + version + "], fill=prompt(sbt version)[" + version + "]"
|
||||
|
||||
/** Copies the given ApplicationID but with the specified version.*/
|
||||
def changeVersion(appID: xsbti.ApplicationID, versionA: String): xsbti.ApplicationID =
|
||||
new xsbti.ApplicationID {
|
||||
def groupID = appID.groupID
|
||||
def name = appID.name
|
||||
def version = versionA
|
||||
def mainClass = appID.mainClass
|
||||
def mainComponents = appID.mainComponents
|
||||
def crossVersioned = appID.crossVersioned
|
||||
def classpathExtra = appID.classpathExtra
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue