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( settings(
testedBaseSettings, testedBaseSettings,
name := "Command", name := "Command",
libraryDependencies ++= Seq(launcherInterface, sjsonNewScalaJson), libraryDependencies ++= Seq(launcherInterface, sjsonNewScalaJson, templateResolverApi),
sourceManaged in (Compile, generateContrabands) := baseDirectory.value / "src" / "main" / "contraband-scala", sourceManaged in (Compile, generateContrabands) := baseDirectory.value / "src" / "main" / "contraband-scala",
contrabandFormatsForType in generateContrabands in Compile := ContrabandConfig.getFormats 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 // 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")). lazy val mainSettingsProj = (project in file("main-settings")).

View File

@ -11,6 +11,7 @@ object BasicCommandStrings {
val CompletionsCommand = "completions" val CompletionsCommand = "completions"
val Exit = "exit" val Exit = "exit"
val Quit = "quit" val Quit = "quit"
val TemplateCommand = "new"
/** The command name to terminate the program.*/ /** The command name to terminate the program.*/
val TerminateAction: String = Exit 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 CompletionsDetailed = "Displays a list of completions for the given argument string (run 'completions <string>')."
def CompletionsBrief = (CompletionsCommand, CompletionsDetailed) 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 HistoryHelpBrief = (HistoryCommands.Start -> "History command help. Lists and describes all history commands.")
def historyHelp = Help(Nil, (HistoryHelpBrief +: HistoryCommands.descriptions).toMap, Set(HistoryCommands.Start)) def historyHelp = Help(Nil, (HistoryHelpBrief +: HistoryCommands.descriptions).toMap, Set(HistoryCommands.Start))

View File

@ -3,6 +3,7 @@ package sbt
import java.io.File import java.io.File
import sbt.internal.util.AttributeKey import sbt.internal.util.AttributeKey
import sbt.internal.inc.classpath.ClassLoaderCache import sbt.internal.inc.classpath.ClassLoaderCache
import sbt.librarymanagement.ModuleID
object BasicKeys { object BasicKeys {
val historyPath = AttributeKey[Option[File]]("history", "The location where command line history is persisted.", 40) 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 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 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 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, maxErrors :== 100,
fork :== false, fork :== false,
initialize :== {}, initialize :== {},
templateResolverInfos :== Nil,
forcegc :== sys.props.get("sbt.task.forcegc").map(java.lang.Boolean.parseBoolean).getOrElse(GCUtil.defaultForceGarbageCollection), forcegc :== sys.props.get("sbt.task.forcegc").map(java.lang.Boolean.parseBoolean).getOrElse(GCUtil.defaultForceGarbageCollection),
minForcegcInterval :== GCUtil.defaultMinForcegcInterval, minForcegcInterval :== GCUtil.defaultMinForcegcInterval,
serverPort := 5000 + (Hash.toHex(Hash(appConfiguration.value.baseDirectory.toString)).## % 1000) 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 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 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 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 // special
val sessionVars = AttributeKey[SessionVar.Map]("session-vars", "Bindings that exist for the duration of the session.", Invisible) 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 BasicCommandStrings._
import BasicCommands._ import BasicCommands._
import CommandUtil._ import CommandUtil._
import TemplateCommandUtil.templateCommand
object BuiltinCommands { object BuiltinCommands {
def initialAttributes = AttributeMap.empty def initialAttributes = AttributeMap.empty
def ConsoleCommands: Seq[Command] = Seq(ignore, exit, IvyConsole.command, setLogLevel, early, act, nop) 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 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, projects, project, reboot, read, history, set, sessionCommand, inspect, loadProjectImpl, loadFailed, Cross.crossBuild, Cross.switchVersion,
Cross.crossRestoreSession, setOnFailure, clearOnFailure, stashOnFailure, popOnFailure, setLogLevel, plugin, plugins, 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) ++ 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.net.URI
import java.util.Locale import java.util.Locale
import Project._ 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 Scope.{ GlobalScope, ThisScope }
import Def.{ Flattened, Initialize, ScopedKey, Setting } import Def.{ Flattened, Initialize, ScopedKey, Setting }
import sbt.internal.{ Load, BuildStructure, LoadedBuild, LoadedBuildUnit, SettingGraph, SettingCompletions, AddSettings, SessionSettings, LogManager } 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 allCommands = commandsIn(ref) ++ commandsIn(BuildRef(ref.build)) ++ (commands in Global get structure.data toList)
val history = get(historyPath) flatMap idFun val history = get(historyPath) flatMap idFun
val prompt = get(shellPrompt) val prompt = get(shellPrompt)
val trs = (templateResolverInfos in Global get structure.data).toList.flatten
val watched = get(watch) val watched = get(watch)
val port: Option[Int] = get(serverPort) val port: Option[Int] = get(serverPort)
val commandDefs = allCommands.distinct.flatten[Command].map(_ tag (projectCommand, true)) val commandDefs = allCommands.distinct.flatten[Command].map(_ tag (projectCommand, true))
val newDefinedCommands = commandDefs ++ BasicCommands.removeTagged(s.definedCommands, projectCommand) val newDefinedCommands = commandDefs ++ BasicCommands.removeTagged(s.definedCommands, projectCommand)
val newAttrs0 = setCond(Watched.Configuration, watched, s.attributes).put(historyPath.key, history) val newAttrs0 = setCond(Watched.Configuration, watched, s.attributes).put(historyPath.key, history)
val newAttrs = setCond(serverPort.key, port, newAttrs0) 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) s.copy(attributes = setCond(shellPrompt.key, prompt, newAttrs), definedCommands = newDefinedCommands)
} }
def setCond[T](key: AttributeKey[T], vopt: Option[T], attributes: AttributeMap): AttributeMap = 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.IvyPlugin" -> sbt.plugins.IvyPlugin,
"sbt.plugins.JvmPlugin" -> sbt.plugins.JvmPlugin, "sbt.plugins.JvmPlugin" -> sbt.plugins.JvmPlugin,
"sbt.plugins.CorePlugin" -> sbt.plugins.CorePlugin, "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 detectedAutoPugins = discover[AutoPlugin](AutoPlugins)
val allAutoPlugins = (defaultAutoPlugins ++ detectedAutoPugins.modules) map { 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 // sbt modules
private val ioVersion = "1.0.0-M9" private val ioVersion = "1.0.0-M9"
private val utilVersion = "1.0.0-M18" 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 zincVersion = "1.0.0-X8"
private val sbtIO = "org.scala-sbt" %% "io" % ioVersion private val sbtIO = "org.scala-sbt" %% "io" % ioVersion
@ -92,6 +92,7 @@ object Dependencies {
val scalaCheck = "org.scalacheck" %% "scalacheck" % "1.13.4" val scalaCheck = "org.scalacheck" %% "scalacheck" % "1.13.4"
val specs2 = "org.specs2" %% "specs2" % "2.4.17" val specs2 = "org.specs2" %% "specs2" % "2.4.17"
val junit = "junit" % "junit" % "4.11" val junit = "junit" % "junit" % "4.11"
val templateResolverApi = "org.scala-sbt" % "template-resolver" % "0.1"
private def scala211Module(name: String, moduleVersion: String) = Def setting ( private def scala211Module(name: String, moduleVersion: String) = Def setting (
scalaBinaryVersion.value match { scalaBinaryVersion.value match {