Port sbt-cross-building's ^ and ^^ commands

This ports sbt-cross-building's cross (`^`) and switch (`^^`) commands.
Instead of making it a plugin, the default settings are now changed
to use `sbtVersion in pluginCrossBuild` for the sbt dependency.
This commit is contained in:
Eugene Yokota 2017-04-22 04:12:03 -04:00
parent 596702f929
commit ff017eb475
25 changed files with 316 additions and 42 deletions

View File

@ -326,4 +326,32 @@ $SwitchCommand [<scala-version>=]<scala-home> [<command>]
See also `help $CrossCommand`
"""
val PluginCrossCommand = "^"
val PluginSwitchCommand = "^^"
def pluginCrossHelp: Help = Help.more(PluginCrossCommand, PluginCrossDetailed)
def pluginSwitchHelp: Help = Help.more(PluginSwitchCommand, PluginSwitchDetailed)
def PluginCrossDetailed =
s"""$PluginCrossCommand <command>
Runs <command> for each sbt version specified for cross-building.
For each string in `crossSbtVersions` in the current project, this command sets the
`sbtVersion in pluginCrossBuild` of all projects to that version, reloads the build,
and executes <command>. When finished, it reloads the build with the original
Scala version.
See also `help $PluginSwitchCommand`
"""
def PluginSwitchDetailed =
s"""$PluginSwitchCommand <sbt-version> [<command>]
Changes the sbt version and runs a command.
Sets the `sbtVersion in pluginCrossBuild` of all projects to <sbt-version> and
reloads the build. If <command> is provided, it is then executed.
See also `help $CrossCommand`
"""
}

View File

@ -136,6 +136,10 @@ object Defaults extends BuildCommon {
envVars :== Map.empty,
sbtVersion := appConfiguration.value.provider.id.version,
sbtBinaryVersion := binarySbtVersion(sbtVersion.value),
// `pluginCrossBuild` scoping is based on sbt-cross-building plugin.
// The idea here is to be able to define a `sbtVersion in pluginCrossBuild`, which
// directs the dependencies of the plugin to build to the specified sbt plugin version.
sbtVersion in pluginCrossBuild := sbtVersion.value,
watchingMessage := Watched.defaultWatchingMessage,
triggeredMessage := Watched.defaultTriggeredMessage,
onLoad := idFun[State],
@ -190,7 +194,16 @@ object Defaults extends BuildCommon {
sourceManaged := configSrcSub(sourceManaged).value,
scalaSource := sourceDirectory.value / "scala",
javaSource := sourceDirectory.value / "java",
unmanagedSourceDirectories := makeCrossSources(scalaSource.value, javaSource.value, scalaBinaryVersion.value, crossPaths.value),
unmanagedSourceDirectories := {
makeCrossSources(scalaSource.value,
javaSource.value,
scalaBinaryVersion.value,
crossPaths.value) ++
makePluginCrossSources(sbtPlugin.value,
scalaSource.value,
(sbtBinaryVersion in pluginCrossBuild).value,
crossPaths.value)
},
unmanagedSources := collectFiles(unmanagedSourceDirectories, includeFilter in unmanagedSources, excludeFilter in unmanagedSources).value,
watchSources in ConfigGlobal ++= unmanagedSources.value,
managedSourceDirectories := Seq(sourceManaged.value),
@ -231,17 +244,31 @@ object Defaults extends BuildCommon {
sbt.inc.ClassfileManager.transactional(crossTarget.value / "classes.bak", sbt.Logger.Null)),
scalaInstance := scalaInstanceTask.value,
crossVersion := (if (crossPaths.value) CrossVersion.binary else CrossVersion.Disabled),
crossTarget := makeCrossTarget(target.value, scalaBinaryVersion.value, sbtBinaryVersion.value, sbtPlugin.value, crossPaths.value),
scalaVersion := {
val scalaV = scalaVersion.value
val sv = (sbtBinaryVersion in pluginCrossBuild).value
if (sbtPlugin.value) scalaVersionFromSbtBinaryVersion(sv)
else scalaV
},
sbtBinaryVersion in pluginCrossBuild := binarySbtVersion((sbtVersion in pluginCrossBuild).value),
crossSbtVersions := Vector((sbtVersion in pluginCrossBuild).value),
crossTarget := makeCrossTarget(target.value,
scalaBinaryVersion.value,
(sbtBinaryVersion in pluginCrossBuild).value,
sbtPlugin.value,
crossPaths.value),
clean := {
val _ = clean.value
IvyActions.cleanCachedResolutionCache(ivyModule.value, streams.value.log)
},
scalaCompilerBridgeSource := {
// This is a workaround for sbtVersion getting set to another value.
val sv = appConfiguration.value.provider.id.version
if (ScalaInstance.isDotty(scalaVersion.value))
// Maintained at https://github.com/lampepfl/dotty/tree/master/sbt-bridge
ModuleID(scalaOrganization.value, "dotty-sbt-bridge", scalaVersion.value, Some("component")).sources()
else
ModuleID(xsbti.ArtifactInfo.SbtOrganization, "compiler-interface", sbtVersion.value, Some("component")).sources()
ModuleID(xsbti.ArtifactInfo.SbtOrganization, "compiler-interface", sv, Some("component")).sources()
}
)
// must be a val: duplication detected by object identity
@ -258,6 +285,14 @@ object Defaults extends BuildCommon {
derive(scalaBinaryVersion := binaryScalaVersion(scalaVersion.value))
))
private[sbt] def scalaVersionFromSbtBinaryVersion(sv: String): String =
VersionNumber(sv) match {
case VersionNumber(Seq(0, 12, _*), _, _) => "2.9.2"
case VersionNumber(Seq(0, 13, _*), _, _) => "2.10.6"
case VersionNumber(Seq(1, 0, _*), _, _) => "2.12.2"
case _ => sys.error(s"Unsupported sbt binary version: $sv")
}
def makeCrossSources(scalaSrcDir: File, javaSrcDir: File, sv: String, cross: Boolean): Seq[File] = {
if (cross)
Seq(scalaSrcDir.getParentFile / s"${scalaSrcDir.name}-$sv", scalaSrcDir, javaSrcDir)
@ -265,6 +300,12 @@ object Defaults extends BuildCommon {
Seq(scalaSrcDir, javaSrcDir)
}
def makePluginCrossSources(isPlugin: Boolean, scalaSrcDir: File,
sbtBinaryV: String, cross: Boolean): Seq[File] = {
if (cross && isPlugin) Vector(scalaSrcDir.getParentFile / s"${scalaSrcDir.name}-sbt-$sbtBinaryV")
else Vector()
}
def makeCrossTarget(t: File, sv: String, sbtv: String, plugin: Boolean, cross: Boolean): File =
{
val scalaBase = if (cross) t / ("scala-" + sv) else t
@ -1033,7 +1074,20 @@ object Defaults extends BuildCommon {
projectCore ++ disableAggregation ++ Seq(
// Missing but core settings
baseDirectory := thisProject.value.base,
target := baseDirectory.value / "target"
target := baseDirectory.value / "target",
// Use (sbtVersion in pluginCrossBuild) to pick the sbt module to depend from the plugin.
// Because `sbtVersion in pluginCrossBuild` can be scoped to project level,
// this setting needs to be set here too.
sbtDependency in pluginCrossBuild := {
val app = appConfiguration.value
val id = app.provider.id
val sv = (sbtVersion in pluginCrossBuild).value
val scalaV = (scalaVersion in pluginCrossBuild).value
val binVersion = (scalaBinaryVersion in pluginCrossBuild).value
val cross = if (id.crossVersioned) CrossVersion.binary else CrossVersion.Disabled
val base = ModuleID(id.groupID, id.name, sv, crossVersion = cross)
CrossVersion(scalaV, binVersion)(base).copy(crossVersion = CrossVersion.Disabled)
}
)
// build.sbt is treated a Scala source of metabuild, so to enable deprecation flag on build.sbt we set the option here.
lazy val deprecationSettings: Seq[Setting[_]] =
@ -1301,7 +1355,7 @@ object Classpaths {
// Override the default to handle mixing in the sbtPlugin + scala dependencies.
allDependencies := {
val base = projectDependencies.value ++ libraryDependencies.value
val pluginAdjust = if (sbtPlugin.value) sbtDependency.value.copy(configurations = Some(Provided.name)) +: base else base
val pluginAdjust = if (sbtPlugin.value) (sbtDependency in pluginCrossBuild).value.copy(configurations = Some(Provided.name)) +: base else base
if (scalaHome.value.isDefined || ivyScala.value.isEmpty || !managedScalaInstance.value)
pluginAdjust
else {
@ -1328,11 +1382,14 @@ object Classpaths {
case _ => base
}
}
def pluginProjectID: Initialize[ModuleID] = (sbtBinaryVersion in update, scalaBinaryVersion in update, projectID, sbtPlugin) {
(sbtBV, scalaBV, pid, isPlugin) =>
if (isPlugin) sbtPluginExtra(pid, sbtBV, scalaBV) else pid
}
def pluginProjectID: Initialize[ModuleID] =
Def.setting {
if (sbtPlugin.value)
sbtPluginExtra(projectID.value,
(sbtBinaryVersion in pluginCrossBuild).value,
(scalaBinaryVersion in pluginCrossBuild).value)
else projectID.value
}
def ivySbt0: Initialize[Task[IvySbt]] =
(ivyConfiguration, credentials, streams) map { (conf, creds, s) =>
Credentials.register(creds, s.log)
@ -1351,8 +1408,18 @@ object Classpaths {
val explicit = buildStructure.value.units(thisProjectRef.value.build).unit.plugins.pluginData.resolvers
explicit orElse bootRepositories(appConfiguration.value) getOrElse externalResolvers.value
},
ivyConfiguration := new InlineIvyConfiguration(ivyPaths.value, externalResolvers.value, Nil, Nil, offline.value, Option(lock(appConfiguration.value)),
checksums.value, Some(target.value / "resolution-cache"), UpdateOptions(), streams.value.log),
ivyConfiguration := new InlineIvyConfiguration(
ivyPaths.value,
externalResolvers.value.toVector,
Vector.empty,
Vector.empty,
offline.value,
Option(lock(appConfiguration.value)),
checksums.value.toVector,
Some(crossTarget.value / "resolution-cache"),
UpdateOptions(),
streams.value.log
),
ivySbt := ivySbt0.value,
classifiersModule := ((projectID, sbtDependency, transitiveClassifiers, loadedBuild, thisProjectRef) map { (pid, sbtDep, classifiers, lb, ref) =>
val pluginClasspath = lb.units(ref.build).unit.plugins.fullClasspath
@ -1695,13 +1762,24 @@ object Classpaths {
def unmanagedDependencies: Initialize[Task[Classpath]] =
(thisProjectRef, configuration, settingsData, buildDependencies) flatMap unmanagedDependencies0
def mkIvyConfiguration: Initialize[Task[IvyConfiguration]] =
(fullResolvers, ivyPaths, otherResolvers, moduleConfigurations, offline, checksums in update, appConfiguration,
target, updateOptions, streams) map { (rs, paths, other, moduleConfs, off, check, app, t, uo, s) =>
warnResolversConflict(rs ++: other, s.log)
val resCacheDir = t / "resolution-cache"
new InlineIvyConfiguration(paths, rs, other, moduleConfs, off, Option(lock(app)), check, Some(resCacheDir), uo, s.log)
}
Def.task {
val (rs, other) = (fullResolvers.value.toVector, otherResolvers.value.toVector)
val s = streams.value
warnResolversConflict(rs ++: other, s.log)
val resCacheDir = crossTarget.value / "resolution-cache"
new InlineIvyConfiguration(
ivyPaths.value,
rs,
other,
moduleConfigurations.value.toVector,
offline.value,
Option(lock(appConfiguration.value)),
(checksums in update).value.toVector,
Some(resCacheDir),
updateOptions.value,
s.log
)
}
import java.util.LinkedHashSet
import collection.JavaConversions.asScalaSet

View File

@ -137,6 +137,8 @@ object Keys {
val definedSbtPlugins = TaskKey[Set[String]]("defined-sbt-plugins", "The set of names of Plugin implementations defined by this project.", CTask)
val discoveredSbtPlugins = TaskKey[PluginDiscovery.DiscoveredNames]("discovered-sbt-plugins", "The names of sbt plugin-related modules (modules that extend Build, Plugin, AutoPlugin) defined by this project.", CTask)
val sbtPlugin = SettingKey[Boolean]("sbt-plugin", "If true, enables adding sbt as a dependency and auto-generation of the plugin descriptor file.", BMinusSetting)
val pluginCrossBuild = TaskKey[Unit]("pluginCrossBuild", "Dummy task to scope `sbtVersion in pluginCrossBuild`, which gets used for plugin compilation.")
val crossSbtVersions = SettingKey[Seq[String]]("crossSbtVersions", "The versions of Sbt used when cross-building an sbt plugin.")
val printWarnings = TaskKey[Unit]("print-warnings", "Shows warnings from compilation, including ones that weren't printed initially.", BPlusTask)
val fileInputOptions = SettingKey[Seq[String]]("file-input-options", "Options that take file input, which may invalidate the cache.", CSetting)
val scalaCompilerBridgeSource = SettingKey[ModuleID]("scala-compiler-bridge-source", "Configures the module ID of the sources of the compiler bridge.", CSetting)

View File

@ -140,7 +140,8 @@ object BuiltinCommands {
def ConsoleCommands: Seq[Command] = Seq(ignore, exit, IvyConsole.command, setLogLevel, early, act, nop)
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, templateCommand,
projects, project, reboot, read, history, set, sessionCommand, inspect, loadProjectImpl, loadFailed, Cross.crossBuild, Cross.switchVersion,
projects, project, reboot, read, history, set, sessionCommand, inspect, loadProjectImpl, loadFailed,
Cross.crossBuild, Cross.switchVersion, PluginCross.pluginCross, PluginCross.pluginSwitch,
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

View File

@ -0,0 +1,72 @@
/* sbt -- Simple Build Tool
* Copyright 2011 Mark Harrah
* Copyright 2012 Johannes Rudolph
*
* This was basically copied from the sbt source code and then adapted to use
* `sbtVersion in pluginCrossBuild`.
*/
package sbt
import complete.DefaultParsers._
import complete.Parser
import sbt.Keys._
import Project._
import Scope.GlobalScope
import Def.ScopedKey
import CommandStrings._
import Cross.{ spacedFirst, requireSession }
/**
* Module responsible for plugin cross building.
*/
private[sbt] object PluginCross {
lazy val pluginSwitch: Command = {
def switchParser(state: State): Parser[(String, String)] = {
val knownVersions = Nil
lazy val switchArgs = token(NotSpace.examples(knownVersions: _*)) ~ (token(Space ~> matched(state.combinedParser)) ?? "")
lazy val nextSpaced = spacedFirst(PluginSwitchCommand)
token(PluginSwitchCommand ~ OptSpace) flatMap { _ =>
switchArgs & nextSpaced
}
}
def crossExclude(s: Def.Setting[_]): Boolean =
s.key match {
case ScopedKey(Scope(_, _, pluginCrossBuild.key, _), sbtVersion.key) => true
case _ => false
}
Command.arb(requireSession(switchParser), pluginSwitchHelp) {
case (state, (version, command)) =>
val x = Project.extract(state)
import x._
state.log.info(s"Setting `sbtVersion in pluginCrossBuild` to $version")
val add = (sbtVersion in GlobalScope in pluginCrossBuild :== version) :: Nil
val cleared = session.mergeSettings.filterNot(crossExclude)
val newStructure = Load.reapply(cleared ++ add, structure)
Project.setProject(session, newStructure, command :: state)
}
}
lazy val pluginCross: Command = {
def crossParser(state: State): Parser[String] =
token(PluginCrossCommand <~ OptSpace) flatMap { _ =>
token(matched(state.combinedParser &
spacedFirst(PluginCrossCommand)))
}
def crossVersions(state: State): Seq[String] = {
val x = Project.extract(state)
import x._
crossSbtVersions in currentRef get structure.data getOrElse Nil
}
Command.arb(requireSession(crossParser), pluginCrossHelp) {
case (state, command) =>
val x = Project.extract(state)
import x._
val versions = crossVersions(state)
val current = (sbtVersion in pluginCrossBuild)
.get(structure.data)
.map(PluginSwitchCommand + " " + _).toList
if (versions.isEmpty) command :: state
else versions.map(PluginSwitchCommand + " " + _ + " " + command) ::: current ::: state
}
}
}

View File

@ -0,0 +1,37 @@
### Improvements
- Ports sbt-cross-building's `^` and `^^` commands for plugin cross building. See below.
### sbt-cross-building
[@jrudolph][@jrudolph]'s sbt-cross-building is a plugin author's plugin.
It adds cross command `^` and sbtVersion switch command `^^`, similar to `+` and `++`,
but for switching between multiple sbt versions across major versions.
sbt 0.13.16 merges these commands into sbt because the feature it provides is useful as we migrate plugins to sbt 1.0.
To switch the `sbtVersion in pluginCrossBuild` from the shell use:
```
^^ 1.0.0-M5
```
Your plugin will now build with sbt 1.0.0-M5 (and its Scala version 2.12.2).
If you need to make changes specific to a sbt version, you can now include them into `src/main/scala-sbt-0.13`,
and `src/main/scala-sbt-1.0.0-M5`, where the binary sbt version number is used as postfix.
To run a command across multiple sbt versions, set:
```scala
crossSbtVersions := Vector("0.13.15", "1.0.0-M5")
```
Then, run:
```
^ compile
```
[@jrudolph]: https://github.com/jrudolph
[@eed3si9n]: https://github.com/eed3si9n

View File

@ -2,6 +2,8 @@ libraryDependencies += "log4j" % "log4j" % "1.2.16" % "compile"
autoScalaLibrary := false
crossPaths := false
TaskKey[Unit]("check-last-update-time") := (streams map { (s) =>
val fullUpdateOutput = s.cacheDirectory / "out"
val timeDiff = System.currentTimeMillis()-fullUpdateOutput.lastModified()

View File

@ -0,0 +1,24 @@
val baseSbt = "0.13"
lazy val root = (project in file("."))
.settings(
sbtPlugin := true,
TaskKey[Unit]("check") := {
val crossV = (sbtVersion in pluginCrossBuild).value
val sv = projectID.value.extraAttributes("e:scalaVersion")
assert(sbtVersion.value startsWith baseSbt, s"Wrong sbt version: ${sbtVersion.value}")
assert(sv == "2.10", s"Wrong e:scalaVersion: $sv")
assert(scalaBinaryVersion.value == "2.10", s"Wrong Scala binary version: ${scalaBinaryVersion.value}")
assert(crossV startsWith "0.13", s"Wrong `sbtVersion in pluginCrossBuild`: $crossV")
},
TaskKey[Unit]("check2") := {
val crossV = (sbtVersion in pluginCrossBuild).value
val sv = projectID.value.extraAttributes("e:scalaVersion")
assert(sbtVersion.value startsWith baseSbt, s"Wrong sbt version: ${sbtVersion.value}")
assert(sv == "2.12", s"Wrong e:scalaVersion: $sv")
assert(scalaBinaryVersion.value == "2.12", s"Wrong Scala binary version: ${scalaBinaryVersion.value}")
assert(crossV startsWith "1.0.", s"Wrong `sbtVersion in pluginCrossBuild`: $crossV")
}
)

View File

@ -0,0 +1,5 @@
> check
> ^^ 1.0.0-M5
> check2

View File

@ -0,0 +1,6 @@
lazy val root = (project in file("."))
.settings(
sbtPlugin := true,
sbtVersion in pluginCrossBuild := "0.12.4",
resolvers += Resolver.typesafeIvyRepo("releases")
)

View File

@ -0,0 +1,2 @@
// folder mustn't be included
trait B

View File

@ -0,0 +1 @@
object Test extends A with B

View File

@ -0,0 +1,3 @@
// should fail because the project is building for
// 0.12.4 where E is not included
object ErrorTest extends E

View File

@ -0,0 +1,3 @@
> show pluginCrossBuild::sbtDependency
> compile
-> test:compile

View File

@ -0,0 +1,6 @@
lazy val root = (project in file("."))
.settings(
scriptedSettings,
sbtPlugin := true,
resolvers += Resolver.typesafeIvyRepo("releases")
)

View File

@ -1,5 +1,7 @@
# This tests that this sbt scripted plugin can launch the next one
# It is currently disabled because there is no next plugin version
# It is currently disabled because it requires JDK8 to run sbt 1.0.
> ^^1.0.0-M5
$ copy-file changes/A.scala src/sbt-test/a/b/A.scala
> scripted

View File

@ -0,0 +1,5 @@
libraryDependencies += {
"org.scala-sbt" % "scripted-plugin" % sbtVersion.value
}
offline := true

View File

@ -1,11 +0,0 @@
scriptedSettings
scriptedSbt := sbtVersion.value
sbtPlugin := true
sbtVersion in Global := "0.13.0-Beta2"
scalaVersion in Global := "2.10.2-RC2"
offline := true

View File

@ -1,5 +0,0 @@
libraryDependencies += {
"org.scala-sbt" %% "scripted-plugin" % sbtVersion.value
}
offline := true

View File

@ -9,6 +9,7 @@ import complete.{ Parser, DefaultParsers }
import classpath.ClasspathUtilities
import java.lang.reflect.{ InvocationTargetException, Method }
import java.util.Properties
import CrossVersion.binarySbtVersion
object ScriptedPlugin extends Plugin {
def scriptedConf = config("scripted-sbt") hide
@ -97,13 +98,23 @@ object ScriptedPlugin extends Plugin {
val scriptedSettings = Seq(
ivyConfigurations ++= Seq(scriptedConf, scriptedLaunchConf),
scriptedSbt := sbtVersion.value,
scriptedSbt := (sbtVersion in pluginCrossBuild).value,
sbtLauncher <<= getJars(scriptedLaunchConf).map(_.get.head),
sbtTestDirectory := sourceDirectory.value / "sbt-test",
libraryDependencies ++= Seq(
"org.scala-sbt" % "scripted-sbt" % scriptedSbt.value % scriptedConf.toString,
"org.scala-sbt" % "sbt-launch" % scriptedSbt.value % scriptedLaunchConf.toString
),
libraryDependencies ++= {
binarySbtVersion(scriptedSbt.value) match {
case "0.13" =>
Seq(
"org.scala-sbt" % "scripted-sbt" % scriptedSbt.value % scriptedConf.toString,
"org.scala-sbt" % "sbt-launch" % scriptedSbt.value % scriptedLaunchConf.toString
)
case sv if sv startsWith "1.0." =>
Seq(
"org.scala-sbt" %% "scripted-sbt" % scriptedSbt.value % scriptedConf.toString,
"org.scala-sbt" % "sbt-launch" % scriptedSbt.value % scriptedLaunchConf.toString
)
}
},
scriptedBufferLog := true,
scriptedClasspath := getJars(scriptedConf).value,
scriptedTests <<= scriptedTestsTask,