From 1b79cb85b69f712b80c1af716c0825b4f383a82b Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Wed, 12 Oct 2016 20:06:10 -0400 Subject: [PATCH] Safer template resolver Fixes #2761 With sbt 0.13.13-RC1 rediscovered that the dependency pulled in from Giter8 was affecting the plugins. To avoid this, this change splits up the template resolver implementation to another module called sbt-giter8-resolver, and it will be downloaded using Ivy into `~/.sbt/0.13/templates/`, and then launched reflectively using Java as the interface. --- build.sbt | 2 +- .../src/main/scala/sbt/BasicCommands.scala | 8 +- .../src/main/scala/sbt/BasicKeys.scala | 2 +- main-command/src/main/scala/sbt/State.scala | 2 + main/src/main/scala/sbt/Defaults.scala | 2 +- .../scala/sbt/Giter8TemplateResolver.scala | 32 ------ main/src/main/scala/sbt/Keys.scala | 2 +- main/src/main/scala/sbt/Main.scala | 1 + main/src/main/scala/sbt/Project.scala | 12 +-- main/src/main/scala/sbt/TemplateCommand.scala | 97 +++++++++++++++++++ .../sbt/plugins/Giter8ResolverPlugin.scala | 6 +- project/Dependencies.scala | 1 - 12 files changed, 115 insertions(+), 52 deletions(-) delete mode 100644 main/src/main/scala/sbt/Giter8TemplateResolver.scala create mode 100644 main/src/main/scala/sbt/TemplateCommand.scala diff --git a/build.sbt b/build.sbt index 103ee3ded..9e275a91a 100644 --- a/build.sbt +++ b/build.sbt @@ -192,7 +192,7 @@ lazy val commandProj = (project in file("main-command")). settings( testedBaseSettings, name := "Command", - libraryDependencies ++= Seq(launcherInterface, sjsonNewScalaJson, templateResolverApi, giter8), + libraryDependencies ++= Seq(launcherInterface, sjsonNewScalaJson, templateResolverApi), sourceManaged in (Compile, generateContrabands) := baseDirectory.value / "src" / "main" / "contraband-scala", contrabandFormatsForType in generateContrabands in Compile := ContrabandConfig.getFormats ). diff --git a/main-command/src/main/scala/sbt/BasicCommands.scala b/main-command/src/main/scala/sbt/BasicCommands.scala index 4be061c7e..7fb320d98 100644 --- a/main-command/src/main/scala/sbt/BasicCommands.scala +++ b/main-command/src/main/scala/sbt/BasicCommands.scala @@ -20,7 +20,7 @@ import sbt.io.IO import scala.util.control.NonFatal object BasicCommands { - lazy val allBasicCommands = Seq(nop, ignore, help, completionsCommand, templateCommand, multi, ifLast, append, setOnFailure, clearOnFailure, + lazy val allBasicCommands = Seq(nop, ignore, help, completionsCommand, 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,9 +82,6 @@ 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) = { @@ -110,8 +107,7 @@ object BasicCommands { }) } - def multiParser(s: State): Parser[Seq[String]] = ->>>>>>> 954e744... Adds templateResolvers and `new` command:main/command/src/main/scala/sbt/BasicCommands.scala + def multiParser(s: State): Parser[List[String]] = { val nonSemi = token(charClass(_ != ';').+, hide = const(true)) (token(';' ~> OptSpace) flatMap { _ => matched((s.combinedParser & nonSemi) | nonSemi) <~ token(OptSpace) } map (_.trim)).+ map { _.toList } diff --git a/main-command/src/main/scala/sbt/BasicKeys.scala b/main-command/src/main/scala/sbt/BasicKeys.scala index cfaa5ae37..f5a248414 100644 --- a/main-command/src/main/scala/sbt/BasicKeys.scala +++ b/main-command/src/main/scala/sbt/BasicKeys.scala @@ -14,5 +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) + private[sbt] val templateResolverInfos = AttributeKey[Seq[TemplateResolverInfo]]("templateResolverInfos", "List of template resolver infos.", 1000) } diff --git a/main-command/src/main/scala/sbt/State.scala b/main-command/src/main/scala/sbt/State.scala index e08bbc5b6..ac14ebaab 100644 --- a/main-command/src/main/scala/sbt/State.scala +++ b/main-command/src/main/scala/sbt/State.scala @@ -279,3 +279,5 @@ object State { private[sbt] def getBoolean(s: State, key: AttributeKey[Boolean], default: Boolean): Boolean = s.get(key) getOrElse default } + +case class TemplateResolverInfo(module: ModuleID, implementationClass: String) diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index c140ce17c..21d986c6f 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -203,7 +203,7 @@ object Defaults extends BuildCommon { maxErrors :== 100, fork :== false, initialize :== {}, - templateResolvers :== Nil, + 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) diff --git a/main/src/main/scala/sbt/Giter8TemplateResolver.scala b/main/src/main/scala/sbt/Giter8TemplateResolver.scala deleted file mode 100644 index 463ca15cb..000000000 --- a/main/src/main/scala/sbt/Giter8TemplateResolver.scala +++ /dev/null @@ -1,32 +0,0 @@ -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) -} diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index 54d737026..752e5c340 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -404,7 +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) + 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) diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index 5acf6f122..3e02a4b2f 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -107,6 +107,7 @@ import sbt.internal.CommandStrings._ import BasicCommandStrings._ import BasicCommands._ import CommandUtil._ +import TemplateCommandUtil.templateCommand object BuiltinCommands { def initialAttributes = AttributeMap.empty diff --git a/main/src/main/scala/sbt/Project.scala b/main/src/main/scala/sbt/Project.scala index 7e154f83d..a630c2b23 100755 --- a/main/src/main/scala/sbt/Project.scala +++ b/main/src/main/scala/sbt/Project.scala @@ -7,7 +7,7 @@ import java.io.File import java.net.URI import java.util.Locale import Project.{ Initialize => _, Setting => _, _ } -import Keys.{ appConfiguration, stateBuildStructure, commands, configuration, historyPath, projectCommand, sessionSettings, shellPrompt, templateResolvers, 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 } @@ -421,19 +421,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 = (templateResolvers in Global get structure.data).toList.flatten + 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) -<<<<<<< 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 + .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 = diff --git a/main/src/main/scala/sbt/TemplateCommand.scala b/main/src/main/scala/sbt/TemplateCommand.scala new file mode 100644 index 000000000..c99fac41a --- /dev/null +++ b/main/src/main/scala/sbt/TemplateCommand.scala @@ -0,0 +1,97 @@ +package sbt + +import java.io.File +import xsbti.AppConfiguration +import sbt.classpath.ClasspathUtilities +import BasicCommandStrings._ +import BasicKeys._ +import complete.{ Parser, DefaultParsers } +import DefaultParsers._ +import Command.applyEffect +import Path._ + +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 "shell" :: Nil => Nil + case xs => xs + }) + 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: java.lang.reflect.InvocationTargetException => + e.getCause match { + case t => throw t + } + } + } + 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 updateUtil = new UpdateUtil(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 = updateUtil.getModule(info.module.copy(configurations = Some("component")), ivyScala) + val xs = updateUtil.update(m, templateDirectory)(_ => true) match { + case Some(xs) => xs.toList + case None => Nil + } + xs + } + } +} diff --git a/main/src/main/scala/sbt/plugins/Giter8ResolverPlugin.scala b/main/src/main/scala/sbt/plugins/Giter8ResolverPlugin.scala index 03c0db93d..56586fc1b 100644 --- a/main/src/main/scala/sbt/plugins/Giter8ResolverPlugin.scala +++ b/main/src/main/scala/sbt/plugins/Giter8ResolverPlugin.scala @@ -13,6 +13,10 @@ object Giter8TemplatePlugin extends AutoPlugin { override lazy val globalSettings: Seq[Setting[_]] = Seq( - templateResolvers += Giter8TemplateResolver + templateResolverInfos += + TemplateResolverInfo( + ModuleID("org.scala-sbt.sbt-giter8-resolver", "sbt-giter8-resolver", "0.1.0") cross CrossVersion.binary, + "sbtgiter8resolver.Giter8TemplateResolver" + ) ) } diff --git a/project/Dependencies.scala b/project/Dependencies.scala index f17a29e26..9a00ddc0a 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -93,7 +93,6 @@ object Dependencies { 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 {