mirror of https://github.com/sbt/sbt.git
Merge pull request #2916 from eed3si9n/wip/new_command
[fport] Safer template resolver
This commit is contained in:
commit
a52a95f67e
|
|
@ -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")).
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) ++
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
@ -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/
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue