Merge pull request #2916 from eed3si9n/wip/new_command

[fport] Safer template resolver
This commit is contained in:
eugene yokota 2017-01-16 11:05:46 -05:00 committed by GitHub
commit a52a95f67e
12 changed files with 157 additions and 6 deletions

View File

@ -192,11 +192,11 @@ lazy val commandProj = (project in file("main-command")).
settings(
testedBaseSettings,
name := "Command",
libraryDependencies ++= Seq(launcherInterface, sjsonNewScalaJson),
libraryDependencies ++= Seq(launcherInterface, sjsonNewScalaJson, templateResolverApi),
sourceManaged in (Compile, generateContrabands) := baseDirectory.value / "src" / "main" / "contraband-scala",
contrabandFormatsForType in generateContrabands in Compile := ContrabandConfig.getFormats
).
configure(addSbtCompilerInterface, addSbtIO, addSbtUtilLogging, addSbtUtilCompletion, addSbtCompilerClasspath)
configure(addSbtCompilerInterface, addSbtIO, addSbtUtilLogging, addSbtUtilCompletion, addSbtCompilerClasspath, addSbtLm)
// Fixes scope=Scope for Setting (core defined in collectionProj) to define the settings system used in build definitions
lazy val mainSettingsProj = (project in file("main-settings")).

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

@ -3,6 +3,7 @@ package sbt
import java.io.File
import sbt.internal.util.AttributeKey
import sbt.internal.inc.classpath.ClassLoaderCache
import sbt.librarymanagement.ModuleID
object BasicKeys {
val historyPath = AttributeKey[Option[File]]("history", "The location where command line history is persisted.", 40)
@ -13,4 +14,7 @@ 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 templateResolverInfos = AttributeKey[Seq[TemplateResolverInfo]]("templateResolverInfos", "List of template resolver infos.", 1000)
}
case class TemplateResolverInfo(module: ModuleID, implementationClass: String)

View File

@ -203,6 +203,7 @@ object Defaults extends BuildCommon {
maxErrors :== 100,
fork :== false,
initialize :== {},
templateResolverInfos :== 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

@ -403,6 +403,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 templateResolverInfos = SettingKey[Seq[TemplateResolverInfo]]("templateResolverInfos", "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

@ -107,13 +107,14 @@ import sbt.internal.CommandStrings._
import BasicCommandStrings._
import BasicCommands._
import CommandUtil._
import TemplateCommandUtil.templateCommand
object BuiltinCommands {
def initialAttributes = AttributeMap.empty
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

@ -7,7 +7,7 @@ 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 Keys.{ appConfiguration, stateBuildStructure, commands, configuration, historyPath, projectCommand, sessionSettings, shellPrompt, templateResolverInfos, 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 }
@ -420,12 +420,15 @@ 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 = (templateResolverInfos 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)
val newAttrs0 = setCond(Watched.Configuration, watched, s.attributes).put(historyPath.key, history)
val newAttrs = setCond(serverPort.key, port, newAttrs0)
.put(historyPath.key, history)
.put(templateResolverInfos.key, trs)
s.copy(attributes = setCond(shellPrompt.key, prompt, newAttrs), definedCommands = newDefinedCommands)
}
def setCond[T](key: AttributeKey[T], vopt: Option[T], attributes: AttributeMap): AttributeMap =

View File

@ -0,0 +1,97 @@
package sbt
import java.lang.reflect.InvocationTargetException
import java.io.File
import sbt.util._
import sbt.internal.util._
import xsbti.AppConfiguration
import sbt.internal.inc.classpath.ClasspathUtilities
import BasicCommandStrings._
import BasicKeys._
import complete.{ Parser, DefaultParsers }
import DefaultParsers._
import Command.applyEffect
import sbt.io._
import sbt.io.syntax._
import sbt.librarymanagement._
import sbt.internal.librarymanagement.IvyConfiguration
private[sbt] object TemplateCommandUtil {
def templateCommand = Command.make(TemplateCommand, templateBrief, templateDetailed)(templateCommandParser)
def templateCommandParser(state: State) =
{
val p = (token(Space) ~> repsep(StringBasic, token(Space))) | (token(EOF) map { case _ => Nil })
val infos = (state get templateResolverInfos) match {
case Some(infos) => infos.toList
case None => Nil
}
val log = state.globalLogging.full
val extracted = (Project extract state)
val (s2, ivyConf) = extracted.runTask(Keys.ivyConfiguration, state)
val globalBase = BuildPaths.getGlobalBase(state)
val ivyScala = extracted.get(Keys.ivyScala in Keys.updateSbtClassifiers)
applyEffect(p)({ inputArg =>
val arguments = inputArg.toList ++
(state.remainingCommands.toList match {
case exec :: Nil if exec.commandLine == "shell" => Nil
case xs => xs map { _.commandLine }
})
run(infos, arguments, state.configuration, ivyConf, globalBase, ivyScala, log)
"exit" :: s2.copy(remainingCommands = Nil)
})
}
private def run(infos: List[TemplateResolverInfo], arguments: List[String], config: AppConfiguration,
ivyConf: IvyConfiguration, globalBase: File, ivyScala: Option[IvyScala], log: Logger): Unit =
infos find { info =>
val loader = infoLoader(info, config, ivyConf, globalBase, ivyScala, log)
val hit = tryTemplate(info, arguments, loader)
if (hit) {
runTemplate(info, arguments, loader)
}
hit
} match {
case Some(_) => // do nothing
case None => System.err.println("Template not found for: " + arguments.mkString(" "))
}
private def tryTemplate(info: TemplateResolverInfo, arguments: List[String], loader: ClassLoader): Boolean =
{
val resultObj = call(info.implementationClass, "isDefined", loader)(
classOf[Array[String]]
)(arguments.toArray)
resultObj.asInstanceOf[Boolean]
}
private def runTemplate(info: TemplateResolverInfo, arguments: List[String], loader: ClassLoader): Unit =
call(info.implementationClass, "run", loader)(classOf[Array[String]])(arguments.toArray)
private def infoLoader(info: TemplateResolverInfo, config: AppConfiguration,
ivyConf: IvyConfiguration, globalBase: File, ivyScala: Option[IvyScala], log: Logger): ClassLoader =
ClasspathUtilities.toLoader(classpathForInfo(info, ivyConf, globalBase, ivyScala, log), config.provider.loader)
private def call(interfaceClassName: String, methodName: String, loader: ClassLoader)(argTypes: Class[_]*)(args: AnyRef*): AnyRef =
{
val interfaceClass = getInterfaceClass(interfaceClassName, loader)
val interface = interfaceClass.newInstance.asInstanceOf[AnyRef]
val method = interfaceClass.getMethod(methodName, argTypes: _*)
try { method.invoke(interface, args: _*) }
catch {
case e: InvocationTargetException => throw e.getCause
}
}
private def getInterfaceClass(name: String, loader: ClassLoader) = Class.forName(name, true, loader)
// Cache files under ~/.sbt/0.13/templates/org_name_version
private def classpathForInfo(info: TemplateResolverInfo, ivyConf: IvyConfiguration, globalBase: File, ivyScala: Option[IvyScala], log: Logger): List[File] =
{
val lm = new DefaultLibraryManagement(ivyConf, log)
val templatesBaseDirectory = new File(globalBase, "templates")
val templateId = s"${info.module.organization}_${info.module.name}_${info.module.revision}"
val templateDirectory = new File(templatesBaseDirectory, templateId)
def jars = (templateDirectory ** -DirectoryFilter).get
if (!(info.module.revision endsWith "-SNAPSHOT") && jars.nonEmpty) jars.toList
else {
IO.createDirectory(templateDirectory)
val m = lm.getModule(info.module.withConfigurations(Some("component")), ivyScala)
val xs = lm.update(m, templateDirectory)(_ => true).toList.flatten
xs
}
}
}

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,23 @@
package sbt
package plugins
import Def.Setting
import Keys._
import librarymanagement._
/**
* 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(
templateResolverInfos +=
TemplateResolverInfo(
ModuleID("org.scala-sbt.sbt-giter8-resolver", "sbt-giter8-resolver", "0.1.3") cross CrossVersion.binary,
"sbtgiter8resolver.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

@ -13,7 +13,7 @@ object Dependencies {
// sbt modules
private val ioVersion = "1.0.0-M9"
private val utilVersion = "1.0.0-M18"
private val lmVersion = "1.0.0-X4"
private val lmVersion = "1.0.0-X5"
private val zincVersion = "1.0.0-X8"
private val sbtIO = "org.scala-sbt" %% "io" % ioVersion
@ -92,6 +92,7 @@ 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"
private def scala211Module(name: String, moduleVersion: String) = Def setting (
scalaBinaryVersion.value match {