Merge pull request #2613 from jroper/sbt-doge

Replaced cross building support with sbt-doge
This commit is contained in:
eugene yokota 2016-05-16 19:02:20 -07:00
commit 5ecbf50e85
10 changed files with 359 additions and 112 deletions

View File

@ -4,128 +4,269 @@
package sbt
import Keys._
import sbt.internal.{ GCUtil, CommandStrings }
import sbt.internal.util.complete.{ DefaultParsers, Parser }
import sbt.internal.util.AttributeKey
import DefaultParsers._
import Def.{ ScopedKey, Setting }
import Scope.GlobalScope
import sbt.internal.CommandStrings.{ CrossCommand, crossHelp, SwitchCommand, switchHelp }
import sbt.internal.CommandStrings.{ CrossCommand, CrossRestoreSessionCommand, SwitchCommand, crossHelp, crossRestoreSessionHelp, switchHelp }
import java.io.File
import sbt.internal.inc.ScalaInstance
import sbt.internal.inc.ScalaInstance
import sbt.io.IO
import sbt.librarymanagement.CrossVersion
object Cross {
@deprecated("Moved to CommandStrings.Switch", "0.13.0")
final val Switch = CommandStrings.SwitchCommand
@deprecated("Moved to CommandStrings.Cross", "0.13.0")
final val Cross = CommandStrings.CrossCommand
private def spacedFirst(name: String) = opOrIDSpaced(name) ~ any.+
def switchParser(state: State): Parser[(String, String)] =
{
def versionAndCommand(spacePresent: Boolean) = {
val knownVersions = crossVersions(state)
val version = token(StringBasic.examples(knownVersions: _*))
val spacedVersion = if (spacePresent) version else version & spacedFirst(SwitchCommand)
val optionalCommand = token(Space ~> matched(state.combinedParser)) ?? ""
spacedVersion ~ optionalCommand
}
token(SwitchCommand ~> OptSpace) flatMap { sp => versionAndCommand(sp.nonEmpty) }
}
def spacedFirst(name: String) = opOrIDSpaced(name) ~ any.+
private case class Switch(version: ScalaVersion, verbose: Boolean, command: Option[String])
private trait ScalaVersion {
def force: Boolean
}
private case class NamedScalaVersion(name: String, force: Boolean) extends ScalaVersion
private case class ScalaHomeVersion(home: File, resolveVersion: Option[String], force: Boolean) extends ScalaVersion
lazy val switchVersion = Command.arb(requireSession(switchParser), switchHelp) {
case (state, (arg, command)) =>
private def switchParser(state: State): Parser[Switch] = {
import DefaultParsers._
def versionAndCommand(spacePresent: Boolean) = {
val x = Project.extract(state)
import x._
val (resolveVersion, homePath) = arg.split("=") match {
case Array(v, h) => (v, h)
case _ => ("", arg)
}
val home = IO.resolve(x.currentProject.base, new File(homePath))
// Basic Algorithm.
// 1. First we figure out what the new scala instances should be, create settings for them.
// 2. Find any non-overridden scalaVersion setting in the whole build and force it to delegate
// to the new global settings.
// 3. Append these to the session, so that the session is up-to-date and
// things like set/session clear, etc. work.
val (add, exclude) =
if (home.exists) {
val instance = ScalaInstance(home)(state.classLoaderCache.apply _)
state.log.info("Setting Scala home to " + home + " with actual version " + instance.actualVersion)
val version = if (resolveVersion.isEmpty) instance.actualVersion else resolveVersion
state.log.info("\tand using " + version + " for resolving dependencies.")
val settings = Seq(
scalaVersion in GlobalScope :== version,
scalaHome in GlobalScope :== Some(home),
scalaInstance in GlobalScope :== instance
)
(settings, excludeKeys(Set(scalaVersion.key, scalaHome.key, scalaInstance.key)))
} else if (!resolveVersion.isEmpty) {
sys.error("Scala home directory did not exist: " + home)
} else {
state.log.info("Setting version to " + arg)
val settings = Seq(
scalaVersion in GlobalScope :== arg,
scalaHome in GlobalScope :== None
)
(settings, excludeKeys(Set(scalaVersion.key, scalaHome.key)))
val knownVersions = crossVersions(x, currentRef)
val version = token(StringBasic.examples(knownVersions: _*)).map { arg =>
val force = arg.endsWith("!")
val versionArg = if (force) arg.dropRight(1) else arg
versionArg.split("=", 2) match {
case Array(home) if new File(home).exists() => ScalaHomeVersion(new File(home), None, force)
case Array(v) => NamedScalaVersion(v, force)
case Array(v, home) => ScalaHomeVersion(new File(home), Some(v).filterNot(_.isEmpty), force)
}
val isForceGc = getOpt(Keys.forcegc in Global) getOrElse GCUtil.defaultForceGarbageCollection
// This is how to get the interval, but ignore it, and just forcegc
// val gcInterval = getOpt(Keys.minForcegcInterval in Global) getOrElse GCUtil.defaultMinForcegcInterval
if (isForceGc) {
GCUtil.forceGc(state.log)
}
// TODO - Track delegates and avoid regenerating.
val delegates: Seq[Setting[_]] = session.mergeSettings collect {
case x if exclude(x) => delegateToGlobal(x.key)
val spacedVersion = if (spacePresent) version else version & spacedFirst(SwitchCommand)
val verbose = Parser.opt(token(Space ~> "-v"))
val optionalCommand = Parser.opt(token(Space ~> matched(state.combinedParser)))
(spacedVersion ~ verbose ~ optionalCommand).map {
case v ~ verbose ~ command =>
Switch(v, verbose.isDefined, command)
}
val fixedSession = session.appendRaw(add ++ delegates)
val fixedState = BuiltinCommands.reapply(fixedSession, structure, state)
if (!command.isEmpty) command :: fixedState
else fixedState
}
token(SwitchCommand ~> OptSpace) flatMap { sp => versionAndCommand(sp.nonEmpty) }
}
// Creates a delegate for a scoped key that pulls the setting from the global scope.
private[this] def delegateToGlobal[T](key: ScopedKey[T]): Setting[_] =
SettingKey[T](key.key) in key.scope := (SettingKey[T](key.key) in GlobalScope).value
private case class CrossArgs(command: String, verbose: Boolean)
@deprecated("No longer used.", "0.13.0")
def crossExclude(s: Setting[_]): Boolean = excludeKeys(Set(scalaVersion.key, scalaHome.key))(s)
private[this] def excludeKeys(keys: Set[AttributeKey[_]]): Setting[_] => Boolean =
_.key match {
case ScopedKey(Scope(_, Global, Global, _), key) if keys.contains(key) => true
case _ => false
private def crossParser(state: State): Parser[CrossArgs] =
token(CrossCommand <~ OptSpace) flatMap { _ =>
(token(Parser.opt("-v" <~ Space)) ~ token(matched(state.combinedParser))).map {
case (verbose, command) => CrossArgs(command, verbose.isDefined)
} & spacedFirst(CrossCommand)
}
def crossParser(state: State): Parser[String] =
token(CrossCommand <~ OptSpace) flatMap { _ => token(matched(state.combinedParser & spacedFirst(CrossCommand))) }
private def crossRestoreSessionParser(state: State): Parser[String] = token(CrossRestoreSessionCommand)
lazy val crossBuild = Command.arb(requireSession(crossParser), crossHelp) { (state, command) =>
val x = Project.extract(state)
import x._
val versions = crossVersions(state)
val current = scalaVersion in currentRef get structure.data map (SwitchCommand + " " + _) toList;
if (versions.isEmpty) command :: state
else {
versions.map(v => s"$SwitchCommand $v $command") ::: current ::: state
}
}
def crossVersions(state: State): Seq[String] =
{
val x = Project.extract(state)
import x._
crossScalaVersions in currentRef get structure.data getOrElse Nil
}
def requireSession[T](p: State => Parser[T]): State => Parser[T] = s =>
private def requireSession[T](p: State => Parser[T]): State => Parser[T] = s =>
if (s get sessionSettings isEmpty) failure("No project loaded") else p(s)
private def resolveAggregates(extracted: Extracted): Seq[ProjectRef] = {
import extracted._
def findAggregates(project: ProjectRef): List[ProjectRef] = {
project :: (structure.allProjects(project.build).find(_.id == project.project) match {
case Some(resolved) => resolved.aggregate.toList.flatMap(findAggregates)
case None => Nil
})
}
(currentRef :: currentProject.aggregate.toList.flatMap(findAggregates)).distinct
}
private def crossVersions(extracted: Extracted, proj: ProjectRef): Seq[String] = {
import extracted._
(crossScalaVersions in proj get structure.data) getOrElse {
// reading scalaVersion is a one-time deal
(scalaVersion in proj get structure.data).toSeq
}
}
/**
* Parse the given command into either an aggregate command or a command for a project
*/
private def parseCommand(command: String): Either[String, (String, String)] = {
import DefaultParsers._
val parser = (OpOrID <~ charClass(_ == '/', "/")) ~ any.* map {
case project ~ cmd => (project, cmd.mkString)
}
Parser.parse(command, parser).left.map(_ => command)
}
def crossBuild: Command =
Command.arb(requireSession(crossParser), crossHelp)(crossBuildCommandImpl)
private def crossBuildCommandImpl(state: State, args: CrossArgs): State = {
val x = Project.extract(state)
import x._
val (aggs, aggCommand) = parseCommand(args.command) match {
case Right((project, cmd)) =>
(structure.allProjectRefs.filter(_.project == project), cmd)
case Left(cmd) => (resolveAggregates(x), cmd)
}
// if we support scalaVersion, projVersions should be cached somewhere since
// running ++2.11.1 is at the root level is going to mess with the scalaVersion for the aggregated subproj
val projVersions = (aggs flatMap { proj =>
crossVersions(x, proj) map { (proj.project, _) }
}).toList
val verbose = if (args.verbose) "-v" else ""
if (projVersions.isEmpty) {
state
} else {
// Group all the projects by scala version
val allCommands = projVersions.groupBy(_._2).mapValues(_.map(_._1)).toSeq.flatMap {
case (version, Seq(project)) =>
// If only one project for a version, issue it directly
Seq(s"$SwitchCommand $verbose $version $project/$aggCommand")
case (version, projects) if aggCommand.contains(" ") =>
// If the command contains a space, then the all command won't work because it doesn't support issuing
// commands with spaces, so revert to running the command on each project one at a time
s"$SwitchCommand $verbose $version" :: projects.map(project => s"$project/$aggCommand")
case (version, projects) =>
// First switch scala version, then use the all command to run the command on each project concurrently
Seq(s"$SwitchCommand $verbose $version", projects.map(_ + "/" + aggCommand).mkString("all ", " ", ""))
}
allCommands ::: CrossRestoreSessionCommand :: captureCurrentSession(state, x)
}
}
def crossRestoreSession: Command =
Command.arb(crossRestoreSessionParser, crossRestoreSessionHelp)(crossRestoreSessionImpl)
private def crossRestoreSessionImpl(state: State, arg: String): State = {
restoreCapturedSession(state, Project.extract(state))
}
private val CapturedSession = AttributeKey[Seq[Setting[_]]]("crossCapturedSession")
private def captureCurrentSession(state: State, extracted: Extracted): State = {
state.put(CapturedSession, extracted.session.rawAppend)
}
private def restoreCapturedSession(state: State, extracted: Extracted): State = {
state.get(CapturedSession) match {
case Some(rawAppend) =>
val restoredSession = extracted.session.copy(rawAppend = rawAppend)
BuiltinCommands.reapply(restoredSession, extracted.structure, state).remove(CapturedSession)
case None => state
}
}
def switchVersion: Command =
Command.arb(requireSession(switchParser), switchHelp)(switchCommandImpl)
private def switchCommandImpl(state: State, args: Switch): State = {
val switchedState = switchScalaVersion(args, state)
args.command.toSeq ::: switchedState
}
private def switchScalaVersion(switch: Switch, state: State): State = {
val x = Project.extract(state)
import x._
val (version, instance) = switch.version match {
case ScalaHomeVersion(homePath, resolveVersion, _) =>
val home = IO.resolve(x.currentProject.base, homePath)
if (home.exists()) {
val instance = ScalaInstance(home)(state.classLoaderCache.apply _)
val version = resolveVersion.getOrElse(instance.actualVersion)
(version, Some((home, instance)))
} else {
sys.error(s"Scala home directory did not exist: $home")
}
case NamedScalaVersion(v, _) => (v, None)
}
val binaryVersion = CrossVersion.binaryScalaVersion(version)
def logSwitchInfo(included: Seq[(ProjectRef, Seq[String])], excluded: Seq[(ProjectRef, Seq[String])]) = {
instance.foreach {
case (home, instance) =>
state.log.info(s"Using Scala home $home with actual version ${instance.actualVersion}")
}
if (switch.version.force) {
state.log.info(s"Forcing Scala version to $version on all projects.")
} else {
state.log.info(s"Setting Scala version to $version on ${included.size} projects.")
}
if (excluded.nonEmpty && !switch.verbose) {
state.log.info(s"Excluded ${excluded.size} projects, run ++ $version -v for more details.")
}
def detailedLog(msg: => String) = if (switch.verbose) state.log.info(msg) else state.log.debug(msg)
def logProject: (ProjectRef, Seq[String]) => Unit = (proj, scalaVersions) => {
val current = if (proj == currentRef) "*" else " "
detailedLog(s" $current ${proj.project} ${scalaVersions.mkString("(", ", ", ")")}")
}
detailedLog("Switching Scala version on:")
included.foreach(logProject.tupled)
detailedLog("Excluding projects:")
excluded.foreach(logProject.tupled)
}
val projects: Seq[Reference] = {
val projectScalaVersions = structure.allProjectRefs.map(proj => proj -> crossVersions(x, proj))
if (switch.version.force) {
logSwitchInfo(projectScalaVersions, Nil)
structure.allProjectRefs ++ structure.units.keys.map(BuildRef.apply)
} else {
val (included, excluded) = projectScalaVersions.partition {
case (proj, scalaVersions) => scalaVersions.exists(v => CrossVersion.binaryScalaVersion(v) == binaryVersion)
}
logSwitchInfo(included, excluded)
included.map(_._1)
}
}
setScalaVersionForProjects(version, instance, projects, state, x)
}
private def setScalaVersionForProjects(version: String, instance: Option[(File, ScalaInstance)],
projects: Seq[Reference], state: State, extracted: Extracted): State = {
import extracted._
val newSettings = projects.flatMap { project =>
val scope = Scope(Select(project), Global, Global, Global)
instance match {
case Some((home, inst)) => Seq(
scalaVersion in scope := version,
scalaHome in scope := Some(home),
scalaInstance in scope := inst
)
case None => Seq(
scalaVersion in scope := version,
scalaHome in scope := None
)
}
}
val filterKeys: Set[AttributeKey[_]] = Set(scalaVersion, scalaHome, scalaInstance).map(_.key)
// Filter out any old scala version settings that were added, this is just for hygiene.
val filteredRawAppend = session.rawAppend.filter(_.key match {
case ScopedKey(Scope(Select(ref), Global, Global, Global), key) if filterKeys.contains(key) && projects.contains(ref) => false
case _ => true
})
val newSession = session.copy(rawAppend = filteredRawAppend ++ newSettings)
BuiltinCommands.reapply(newSession, structure, state)
}
}

View File

@ -92,7 +92,7 @@ object BuiltinCommands {
def ScriptCommands: Seq[Command] = Seq(ignore, exit, Script.command, setLogLevel, early, act, nop)
def DefaultCommands: Seq[Command] = Seq(ignore, help, completionsCommand, about, tasks, settingsCommand, loadProject,
projects, project, reboot, read, history, set, sessionCommand, inspect, loadProjectImpl, loadFailed, Cross.crossBuild, Cross.switchVersion,
setOnFailure, clearOnFailure, stashOnFailure, popOnFailure, setLogLevel, plugin, plugins,
Cross.crossRestoreSession, setOnFailure, clearOnFailure, stashOnFailure, popOnFailure, setLogLevel, plugin, plugins,
ifLast, multi, shell, continuous, eval, alias, append, last, lastGrep, export, boot, nop, call, exit, early, initialize, act) ++
compatCommands
def DefaultBootCommands: Seq[String] = LoadProject :: (IfLast + " " + Shell) :: Nil

View File

@ -293,31 +293,47 @@ defaults
Nil
val CrossCommand = "+"
val CrossRestoreSessionCommand = "+-"
val SwitchCommand = "++"
def crossHelp: Help = Help.more(CrossCommand, CrossDetailed)
def crossRestoreSessionHelp = Help.more(CrossRestoreSessionCommand, CrossRestoreSessionDetailed)
def switchHelp: Help = Help.more(SwitchCommand, SwitchDetailed)
def CrossDetailed =
s"""$CrossCommand <command>
s"""$CrossCommand [-v] <command>
Runs <command> for each Scala version specified for cross-building.
For each string in `crossScalaVersions` in the current project, this command sets the
`scalaVersion` of all projects to that version, reloads the build, and
executes <command>. When finished, it reloads the build with the original
Scala version.
For each string in `crossScalaVersions` in each project project, this command sets
the `scalaVersion` of all projects that list that Scala version with that Scala
version reloads the build, and then executes <command> for those projects. When
finished, it resets the build to its original state.
If -v is supplied, verbose logging of the Scala version switching is done.
See also `help $SwitchCommand`
"""
def CrossRestoreSessionDetailed =
s"""$CrossRestoreSessionCommand
Restores a session that was captured by the cross command, +.
"""
def SwitchDetailed =
s"""$SwitchCommand <scala-version> [<command>]
s"""$SwitchCommand <scala-version>[!] [-v] [<command>]
Changes the Scala version and runs a command.
Sets the `scalaVersion` of all projects to <scala-version> and reloads the build.
Sets the `scalaVersion` of all projects that define a Scala cross version that is binary
compatible with <scala-version> and reloads the build. If ! is supplied, then the
version is forced on all projects regardless of whether they are binary compatible or
not.
If -v is supplied, verbose logging of the Scala version switching is done.
If <command> is provided, it is then executed.
$SwitchCommand [<scala-version>=]<scala-home> [<command>]
$SwitchCommand [<scala-version>=]<scala-home>[!] [-v] [<command>]
Uses the Scala installation at <scala-home> by configuring the scalaHome setting for
all projects.
@ -325,6 +341,12 @@ $SwitchCommand [<scala-version>=]<scala-home> [<command>]
This is important when using managed dependencies. This version will determine the
cross-version used as well as transitive dependencies.
Only projects that are listed to be binary compatible with the selected Scala version
have their Scala version switched. If ! is supplied, then all projects projects have
their Scala version switched.
If -v is supplied, verbose logging of the Scala version switching is done.
If <command> is provided, it is then executed.
See also `help $CrossCommand`

View File

@ -0,0 +1,10 @@
[@jroper]: https://github.com/jroper
[2613]: https://github.com/sbt/sbt/pull/2613
### Fixes with compatibility implications
### Improvements
- Replace cross building support with sbt-doge. This allows builds with projects that have multiple different combinations of cross scala versions to be cross built correctly. The behaviour of ++ is changed so that it only updates the Scala version of projects that support that Scala version, but the Scala version can be post fixed with ! to force it to change for all projects. A -v argument has been added that prints verbose information about which projects are having their settings changed along with their cross scala versions. [#2613][2613] by [@jroper][@jroper].
### Bug fixes

View File

@ -0,0 +1,18 @@
lazy val rootProj = (project in file(".")).
aggregate(libProj, fooPlugin)
lazy val libProj = (project in file("lib")).
settings(
name := "foo-lib",
scalaVersion := "2.11.8",
crossScalaVersions := Seq("2.11.8", "2.10.4")
)
lazy val fooPlugin =(project in file("sbt-foo")).
settings(
name := "sbt-foo",
sbtPlugin := true,
scalaVersion := "2.10.4",
crossScalaVersions := Seq("2.10.4")
)

View File

@ -0,0 +1,5 @@
package foo
object Foo {
}

View File

@ -0,0 +1,5 @@
package foo.sbt
object SbtFoo {
}

View File

@ -0,0 +1,46 @@
> + compile
$ exists lib/target/scala-2.11
$ exists lib/target/scala-2.10
$ exists sbt-foo/target/scala-2.10
-$ exists sbt-foo/target/scala-2.11
> clean
> + libProj/compile
$ exists lib/target/scala-2.11
$ exists lib/target/scala-2.10
-$ exists sbt-foo/target/scala-2.10
-$ exists sbt-foo/target/scala-2.11
> clean
> ++ 2.11.1 compile
$ exists lib/target/scala-2.11
-$ exists lib/target/scala-2.10
$ exists sbt-foo/target/scala-2.10
-$ exists sbt-foo/target/scala-2.11
> clean
> ++ 2.10.4 compile
-$ exists lib/target/scala-2.11
$ exists lib/target/scala-2.10
$ exists sbt-foo/target/scala-2.10
-$ exists sbt-foo/target/scala-2.11
> clean
> ++ 2.11.5 compile
$ exists lib/target/scala-2.11
-$ exists lib/target/scala-2.10
$ exists sbt-foo/target/scala-2.10
-$ exists sbt-foo/target/scala-2.11
> clean
> ++ 2.11.5! compile
$ exists lib/target/scala-2.11
-$ exists lib/target/scala-2.10
-$ exists sbt-foo/target/scala-2.10
$ exists sbt-foo/target/scala-2.11

View File

@ -1,7 +1,7 @@
> check 2.7.7 2.9.1 2.9.0-1
> ++ 2.8.2
> ++ 2.8.2!
> check 2.8.2 2.8.2 2.8.1
> ++ 2.10.4
> ++ 2.10.4!
> set resolvers ++= Nil
> check 2.10.4 2.10.4 2.10.4
> session clear-all

View File

@ -1,7 +1,7 @@
# A.scala needs B.scala, it won't be in source list
> ++2.11.4
> ++2.11.4!
-> compile
# A.scala needs B.scala, it would be in source list
> ++2.10.4
> ++2.10.4!
> compile