Adds templateResolvers and `new` command

This adds `new` command, which helps create a new build definition. The
`new` command is extensible via a mechanism called the template
resolver,
which evaluates the arbitrary arguments passed to the command to find
and run a template.

As a reference implementation [Giter8][g8] is provided as follows:

    sbt new eed3si9n/hello.g8

This will run eed3si9n/hello.g8 using Giter8.

  [g8]: http://www.foundweekends.org/giter8/
This commit is contained in:
Eugene Yokota 2016-08-22 02:38:46 -04:00
parent e54d4ed8fd
commit 73a427c0b8
13 changed files with 116 additions and 6 deletions

View File

@ -192,7 +192,7 @@ lazy val commandProj = (project in file("main-command")).
settings(
testedBaseSettings,
name := "Command",
libraryDependencies ++= Seq(launcherInterface, sjsonNewScalaJson),
libraryDependencies ++= Seq(launcherInterface, sjsonNewScalaJson, templateResolverApi, giter8),
sourceManaged in (Compile, generateContrabands) := baseDirectory.value / "src" / "main" / "contraband-scala",
contrabandFormatsForType in generateContrabands in Compile := ContrabandConfig.getFormats
).

View File

@ -11,6 +11,7 @@ object BasicCommandStrings {
val CompletionsCommand = "completions"
val Exit = "exit"
val Quit = "quit"
val TemplateCommand = "new"
/** The command name to terminate the program.*/
val TerminateAction: String = Exit
@ -32,6 +33,10 @@ object BasicCommandStrings {
def CompletionsDetailed = "Displays a list of completions for the given argument string (run 'completions <string>')."
def CompletionsBrief = (CompletionsCommand, CompletionsDetailed)
def templateBrief = (TemplateCommand, "Creates a new sbt build.")
def templateDetailed = TemplateCommand + """ [--options] <template>
Create a new sbt build based on the given template."""
def HistoryHelpBrief = (HistoryCommands.Start -> "History command help. Lists and describes all history commands.")
def historyHelp = Help(Nil, (HistoryHelpBrief +: HistoryCommands.descriptions).toMap, Set(HistoryCommands.Start))

View File

@ -20,7 +20,7 @@ import sbt.io.IO
import scala.util.control.NonFatal
object BasicCommands {
lazy val allBasicCommands = Seq(nop, ignore, help, completionsCommand, multi, ifLast, append, setOnFailure, clearOnFailure,
lazy val allBasicCommands = Seq(nop, ignore, help, completionsCommand, templateCommand, multi, ifLast, append, setOnFailure, clearOnFailure,
stashOnFailure, popOnFailure, reboot, call, early, exit, continuous, history, shell, server, client, read, alias) ++ compatCommands
def nop = Command.custom(s => success(() => s))
@ -82,7 +82,32 @@ object BasicCommands {
state
}
<<<<<<< HEAD:main-command/src/main/scala/sbt/BasicCommands.scala
def multiParser(s: State): Parser[List[String]] =
=======
def templateCommand = Command.make(TemplateCommand, templateBrief, templateDetailed)(templateCommandParser)
def templateCommandParser(state: State) =
{
val p = token(Space).* ~> repsep(StringBasic, token(Space))
val trs = (state get templateResolvers) match {
case Some(trs) => trs.toList
case None => Nil
}
applyEffect(p)({ inputArg =>
val arguments = inputArg.toList ++ state.remainingCommands
trs find { tr =>
tr.isDefined(arguments.toArray)
} match {
case Some(tr) => tr.run(arguments.toArray)
case None =>
System.err.println("Template not found for: " + arguments.mkString(" "))
}
"exit" :: state.copy(remainingCommands = Nil)
})
}
def multiParser(s: State): Parser[Seq[String]] =
>>>>>>> 954e744... Adds templateResolvers and `new` command:main/command/src/main/scala/sbt/BasicCommands.scala
{
val nonSemi = token(charClass(_ != ';').+, hide = const(true))
(token(';' ~> OptSpace) flatMap { _ => matched((s.combinedParser & nonSemi) | nonSemi) <~ token(OptSpace) } map (_.trim)).+ map { _.toList }

View File

@ -3,6 +3,7 @@ package sbt
import java.io.File
import sbt.internal.util.AttributeKey
import sbt.internal.inc.classpath.ClassLoaderCache
import sbt.template.TemplateResolver
object BasicKeys {
val historyPath = AttributeKey[Option[File]]("history", "The location where command line history is persisted.", 40)
@ -13,4 +14,5 @@ object BasicKeys {
private[sbt] val classLoaderCache = AttributeKey[ClassLoaderCache]("class-loader-cache", "Caches class loaders based on the classpath entries and last modified times.", 10)
private[sbt] val OnFailureStack = AttributeKey[List[Option[Exec]]]("on-failure-stack", "Stack that remembers on-failure handlers.", 10)
private[sbt] val explicitGlobalLogLevels = AttributeKey[Boolean]("explicit-global-log-levels", "True if the global logging levels were explicitly set by the user.", 10)
private[sbt] val templateResolvers = AttributeKey[Seq[TemplateResolver]]("templateResolvers", "List of template resolvers.", 1000)
}

View File

@ -203,6 +203,7 @@ object Defaults extends BuildCommon {
maxErrors :== 100,
fork :== false,
initialize :== {},
templateResolvers :== Nil,
forcegc :== sys.props.get("sbt.task.forcegc").map(java.lang.Boolean.parseBoolean).getOrElse(GCUtil.defaultForceGarbageCollection),
minForcegcInterval :== GCUtil.defaultMinForcegcInterval,
serverPort := 5000 + (Hash.toHex(Hash(appConfiguration.value.baseDirectory.toString)).## % 1000)

View File

@ -0,0 +1,32 @@
package sbt
import sbt.template.TemplateResolver
object Giter8TemplateResolver extends TemplateResolver {
def isDefined(args0: Array[String]): Boolean =
{
val args = args0.toList filterNot { _.startsWith("-") }
// Mandate .g8
val Github = """^([^\s/]+)/([^\s/]+?)(?:\.g8)$""".r
val Local = """^file://(\S+)(?:\.g8)(?:/)?$""".r
object GitUrl {
val NativeUrl = "^(git[@|://].*)$".r
val HttpsUrl = "^(https://.*)$".r
val HttpUrl = "^(http://.*)$".r
val SshUrl = "^(ssh://.*)$".r
def unapplySeq(s: Any): Option[List[String]] =
NativeUrl.unapplySeq(s) orElse
HttpsUrl.unapplySeq(s) orElse
HttpUrl.unapplySeq(s) orElse
SshUrl.unapplySeq(s)
}
args.headOption match {
case Some(Github(_, _)) => true
case Some(Local(_)) => true
case GitUrl(uri) => uri contains (".g8")
case _ => false
}
}
def run(args: Array[String]): Unit =
giter8.Giter8.run(args)
}

View File

@ -27,6 +27,7 @@ import org.apache.ivy.core.module.{ descriptor, id }
import descriptor.ModuleDescriptor, id.ModuleRevisionId
import testing.Framework
import KeyRanks._
import sbt.template.TemplateResolver
import sbt.internal.{ BuildStructure, LoadedBuild, PluginDiscovery, BuildDependencies, SessionSettings, LogManager }
import sbt.io.FileFilter
@ -403,6 +404,7 @@ object Keys {
val sbtVersion = SettingKey[String]("sbt-version", "Provides the version of sbt. This setting should be not be modified.", AMinusSetting)
val sbtBinaryVersion = SettingKey[String]("sbt-binary-version", "Defines the binary compatibility version substring.", BPlusSetting)
val skip = TaskKey[Boolean]("skip", "For tasks that support it (currently only 'compile' and 'update'), setting skip to true will force the task to not to do its work. This exact semantics may vary by task.", BSetting)
val templateResolvers = SettingKey[Seq[TemplateResolver]]("templateResolvers", "Template resolvers used for 'new'.", BSetting)
// special
val sessionVars = AttributeKey[SessionVar.Map]("session-vars", "Bindings that exist for the duration of the session.", Invisible)

View File

@ -113,7 +113,7 @@ 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,
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,
Cross.crossRestoreSession, setOnFailure, clearOnFailure, stashOnFailure, popOnFailure, setLogLevel, plugin, plugins,
ifLast, multi, shell, BasicCommands.server, BasicCommands.client, continuous, eval, alias, append, last, lastGrep, export, boot, nop, call, exit, early, initialize, act) ++

View File

@ -6,8 +6,8 @@ package sbt
import java.io.File
import java.net.URI
import java.util.Locale
import Project._
import Keys.{ appConfiguration, stateBuildStructure, commands, configuration, historyPath, projectCommand, sessionSettings, shellPrompt, serverPort, thisProject, thisProjectRef, watch }
import Project.{ Initialize => _, Setting => _, _ }
import Keys.{ appConfiguration, stateBuildStructure, commands, configuration, historyPath, projectCommand, sessionSettings, shellPrompt, templateResolvers, serverPort, thisProject, thisProjectRef, watch }
import Scope.{ GlobalScope, ThisScope }
import Def.{ Flattened, Initialize, ScopedKey, Setting }
import sbt.internal.{ Load, BuildStructure, LoadedBuild, LoadedBuildUnit, SettingGraph, SettingCompletions, AddSettings, SessionSettings, LogManager }
@ -17,6 +17,7 @@ import sbt.internal.util.complete.DefaultParsers
import sbt.librarymanagement.Configuration
import sbt.util.Eval
import sjsonnew.JsonFormat
import sbt.template.TemplateResolver
import language.experimental.macros
@ -420,12 +421,19 @@ object Project extends ProjectExtra {
val allCommands = commandsIn(ref) ++ commandsIn(BuildRef(ref.build)) ++ (commands in Global get structure.data toList)
val history = get(historyPath) flatMap idFun
val prompt = get(shellPrompt)
val trs = (templateResolvers in Global get structure.data).toList.flatten
val watched = get(watch)
val port: Option[Int] = get(serverPort)
val commandDefs = allCommands.distinct.flatten[Command].map(_ tag (projectCommand, true))
val newDefinedCommands = commandDefs ++ BasicCommands.removeTagged(s.definedCommands, projectCommand)
<<<<<<< HEAD
val newAttrs0 = setCond(Watched.Configuration, watched, s.attributes).put(historyPath.key, history)
val newAttrs = setCond(serverPort.key, port, newAttrs0)
=======
val newAttrs = setCond(Watched.Configuration, watched, s.attributes).
put(historyPath.key, history).
put(templateResolvers.key, trs)
>>>>>>> 954e744... Adds templateResolvers and `new` command
s.copy(attributes = setCond(shellPrompt.key, prompt, newAttrs), definedCommands = newDefinedCommands)
}
def setCond[T](key: AttributeKey[T], vopt: Option[T], attributes: AttributeMap): AttributeMap =

View File

@ -39,7 +39,8 @@ object PluginDiscovery {
"sbt.plugins.IvyPlugin" -> sbt.plugins.IvyPlugin,
"sbt.plugins.JvmPlugin" -> sbt.plugins.JvmPlugin,
"sbt.plugins.CorePlugin" -> sbt.plugins.CorePlugin,
"sbt.plugins.JUnitXmlReportPlugin" -> sbt.plugins.JUnitXmlReportPlugin
"sbt.plugins.JUnitXmlReportPlugin" -> sbt.plugins.JUnitXmlReportPlugin,
"sbt.plugins.Giter8TemplatePlugin" -> sbt.plugins.Giter8TemplatePlugin
)
val detectedAutoPugins = discover[AutoPlugin](AutoPlugins)
val allAutoPlugins = (defaultAutoPlugins ++ detectedAutoPugins.modules) map {

View File

@ -0,0 +1,18 @@
package sbt
package plugins
import Def.Setting
import Keys._
/**
* An experimental plugin that adds the ability for Giter8 templates to be resolved
*/
object Giter8TemplatePlugin extends AutoPlugin {
override def requires = CorePlugin
override def trigger = allRequirements
override lazy val globalSettings: Seq[Setting[_]] =
Seq(
templateResolvers += Giter8TemplateResolver
)
}

View File

@ -0,0 +1,14 @@
### new command and templateResolvers
sbt 0.13.13 adds `new` command, which helps create a new build definition.
The `new` command is extensible via a mechanism called the template resolver,
which evaluates the arguments passed to the command to find and run a template.
As a reference implementation [Giter8][g8] is provided as follows:
sbt new eed3si9n/hello.g8
This will run eed3si9n/hello.g8 using Giter8.
[@eed3si9n]: https://github.com/eed3si9n
[g8]: http://www.foundweekends.org/giter8/

View File

@ -92,6 +92,8 @@ object Dependencies {
val scalaCheck = "org.scalacheck" %% "scalacheck" % "1.13.4"
val specs2 = "org.specs2" %% "specs2" % "2.4.17"
val junit = "junit" % "junit" % "4.11"
val templateResolverApi = "org.scala-sbt" % "template-resolver" % "0.1"
val giter8 = "org.foundweekends.giter8" %% "giter8" % "0.7.0"
private def scala211Module(name: String, moduleVersion: String) = Def setting (
scalaBinaryVersion.value match {