From 90134b3af094db9a1d1133664df095fd07c8e847 Mon Sep 17 00:00:00 2001 From: Mark Harrah Date: Thu, 20 Feb 2014 13:51:17 -0500 Subject: [PATCH] 'plugins' and 'plugin ' commands to inspect plugins * 'plugins' displays the list of plugins available for each build along with the project IDs each is enabled on * 'plugin ' displays information about a specific plugin in the context of the current project - if the plugin is activated on the current project and if so, information about the keys/configurations it provides - how the plugin could be activated if possible * tries to detect when it is run on an aggregating project and adjusts accordingly - indicates if an aggregated project has the plugin activated - indicates to change to the specific project to get the right context This is a rough implementation and needs lots of polishing and deduplicating. The help for the commands needs to be added/expanded. --- main/src/main/scala/sbt/BuildStructure.scala | 1 + main/src/main/scala/sbt/CommandStrings.scala | 5 ++ main/src/main/scala/sbt/Main.scala | 16 +++- main/src/main/scala/sbt/NaturesDebug.scala | 89 +++++++++++++++++--- 4 files changed, 97 insertions(+), 14 deletions(-) diff --git a/main/src/main/scala/sbt/BuildStructure.scala b/main/src/main/scala/sbt/BuildStructure.scala index 0ae96b923..1fddbf2a0 100644 --- a/main/src/main/scala/sbt/BuildStructure.scala +++ b/main/src/main/scala/sbt/BuildStructure.scala @@ -142,6 +142,7 @@ final class LoadedBuild(val root: URI, val units: Map[URI, LoadedBuildUnit]) BuildUtil.checkCycles(units) def allProjectRefs: Seq[(ProjectRef, ResolvedProject)] = for( (uri, unit) <- units.toSeq; (id, proj) <- unit.defined ) yield ProjectRef(uri, id) -> proj def extra(data: Settings[Scope])(keyIndex: KeyIndex): BuildUtil[ResolvedProject] = BuildUtil(root, units, keyIndex, data) + private[sbt] def autos = GroupedAutoPlugins(units) } final class PartBuild(val root: URI, val units: Map[URI, PartBuildUnit]) diff --git a/main/src/main/scala/sbt/CommandStrings.scala b/main/src/main/scala/sbt/CommandStrings.scala index 9baf0e7d1..14cc6fee9 100644 --- a/main/src/main/scala/sbt/CommandStrings.scala +++ b/main/src/main/scala/sbt/CommandStrings.scala @@ -49,6 +49,11 @@ $ShowCommand Evaluates the specified task and display the value returned by the task.""" + val PluginsCommand = "plugins" + val PluginCommand = "plugin" + def pluginsBrief = "Lists currently available plugins." + def pluginsDetailed = pluginsBrief // TODO: expand + val LastCommand = "last" val LastGrepCommand = "last-grep" val ExportCommand = "export" diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index c582426ae..ad5291ec2 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -89,7 +89,7 @@ object BuiltinCommands 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, projects, project, reboot, read, history, set, sessionCommand, inspect, loadProjectImpl, loadFailed, Cross.crossBuild, Cross.switchVersion, - setOnFailure, clearOnFailure, stashOnFailure, popOnFailure, setLogLevel, + setOnFailure, clearOnFailure, stashOnFailure, popOnFailure, setLogLevel, plugin, plugins, ifLast, multi, shell, continuous, eval, alias, append, last, lastGrep, export, boot, nop, call, exit, early, initialize, act) ++ compatCommands def DefaultBootCommands: Seq[String] = LoadProject :: (IfLast + " " + Shell) :: Nil @@ -375,6 +375,20 @@ object BuiltinCommands Help.detailOnly(taskDetail(allTaskAndSettingKeys(s))) else Help.empty + def plugins = Command.command(PluginsCommand, pluginsBrief, pluginsDetailed) { s => + val helpString = NaturesDebug.helpAll(s) + System.out.println(helpString) + s + } + val pluginParser: State => Parser[AutoPlugin] = s => { + val autoPlugins: Map[String, AutoPlugin] = NaturesDebug.autoPluginMap(s) + token(Space) ~> Act.knownIDParser(autoPlugins, "plugin") + } + def plugin = Command(PluginCommand)(pluginParser) { (s, plugin) => + val helpString = NaturesDebug.help(plugin, s) + System.out.println(helpString) + s + } def projects = Command(ProjectsCommand, (ProjectsCommand, projectsBrief), projectsDetailed )(s => projectsParser(s).?) { case (s, Some(modifyBuilds)) => transformExtraBuilds(s, modifyBuilds) diff --git a/main/src/main/scala/sbt/NaturesDebug.scala b/main/src/main/scala/sbt/NaturesDebug.scala index 8b3bc3595..d0e27a9dd 100644 --- a/main/src/main/scala/sbt/NaturesDebug.scala +++ b/main/src/main/scala/sbt/NaturesDebug.scala @@ -3,6 +3,7 @@ package sbt import Def.Setting import Natures._ import NaturesDebug._ + import java.net.URI private[sbt] class NaturesDebug(val available: List[AutoPlugin], val nameToKey: Map[String, AttributeKey[_]], val provided: Relation[AutoPlugin, AttributeKey[_]]) { @@ -55,7 +56,7 @@ private[sbt] class NaturesDebug(val available: List[AutoPlugin], val nameToKey: activatedHelp(plugin) else deactivatedHelp(plugin, context) - private[this] def activatedHelp(plugin: AutoPlugin): String = + private def activatedHelp(plugin: AutoPlugin): String = { val prefix = s"${plugin.label} is activated." val keys = provided.forward(plugin) @@ -64,9 +65,9 @@ private[sbt] class NaturesDebug(val available: List[AutoPlugin], val nameToKey: val confsString = if(configs.isEmpty) "" else s"\nIt defines these configurations: ${multi(configs.map(_.name))}" prefix + keysString + confsString } - private[this] def deactivatedHelp(plugin: AutoPlugin, context: Context): String = + private def deactivatedHelp(plugin: AutoPlugin, context: Context): String = { - val prefix = s"${plugin.label} is not activated." + val prefix = s"${plugin.label} is NOT activated." val keys = provided.forward(plugin) val keysString = if(keys.isEmpty) "" else s"\nActivating it may affect these keys: ${multi(keys.toList.map(_.label))}" val configs = plugin.projectConfigurations @@ -80,6 +81,66 @@ private[sbt] class NaturesDebug(val available: List[AutoPlugin], val nameToKey: private[sbt] object NaturesDebug { + def helpAll(s: State): String = + if(Project.isProjectLoaded(s)) + { + val extracted = Project.extract(s) + import extracted._ + def helpBuild(uri: URI, build: LoadedBuildUnit): String = + { + val pluginStrings = for(plugin <- availableAutoPlugins(build)) yield { + val activatedIn = build.defined.values.toList.filter(_.autoPlugins.contains(plugin)).map(_.id) + val actString = if(activatedIn.nonEmpty) activatedIn.mkString(": enabled in ", ", ", "") else "" // TODO: deal with large builds + s"\n\t${plugin.label}$actString" + } + s"In $uri${pluginStrings.mkString}" + } + val buildStrings = for((uri, build) <- structure.units) yield helpBuild(uri, build) + buildStrings.mkString("\n") + } + else + "No project is currently loaded." + + def autoPluginMap(s: State): Map[String, AutoPlugin] = + { + val extracted = Project.extract(s) + import extracted._ + structure.units.values.toList.flatMap(availableAutoPlugins).map(plugin => (plugin.label, plugin)).toMap + } + private[this] def availableAutoPlugins(build: LoadedBuildUnit): Seq[AutoPlugin] = + build.unit.plugins.detected.autoPlugins.values + + def help(plugin: AutoPlugin, s: State): String = + { + val extracted = Project.extract(s) + import extracted._ + def definesPlugin(p: ResolvedProject): Boolean = p.autoPlugins.contains(plugin) + def projectForRef(ref: ProjectRef): ResolvedProject = get(Keys.thisProject in ref) + val perBuild: Map[URI, Set[AutoPlugin]] = structure.units.mapValues(unit => availableAutoPlugins(unit).toSet) + val pluginsThisBuild = perBuild.getOrElse(currentRef.build, Set.empty).toList + lazy val context = Context(currentProject.natures, currentProject.autoPlugins, Natures.compile(pluginsThisBuild), pluginsThisBuild) + lazy val debug = NaturesDebug(context.available) + if(!pluginsThisBuild.contains(plugin)) { + val availableInBuilds: List[URI] = perBuild.toList.filter(_._2(plugin)).map(_._1) + s"Plugin ${plugin.label} is only available in builds:\n\t${availableInBuilds.mkString("\n\t")}\nSwitch to a project in one of those builds using `project` and rerun this command for more information." + } else if(definesPlugin(currentProject)) + debug.activatedHelp(plugin) + else { + val thisAggregated = BuildUtil.dependencies(structure.units).aggregateTransitive.getOrElse(currentRef, Nil) + val definedInAggregated = thisAggregated.filter(ref => definesPlugin(projectForRef(ref))) + if(definedInAggregated.nonEmpty) { + val projectNames = definedInAggregated.map(_.project) // TODO: usually in this build, but could technically require the build to be qualified + s"Plugin ${plugin.label} is not activated on this project, but this project aggregates projects where it is activated:\n\t${projectNames.mkString("\n\t")}" + } else { + val base = debug.deactivatedHelp(plugin, context) + val aggNote = if(thisAggregated.nonEmpty) "Note: This project aggregates other projects and this" else "Note: This" + val common = " information is for this project only." + val helpOther = "To see how to activate this plugin for another project, change to the project using `project ` and rerun this command." + s"$base\n$aggNote$common\n$helpOther" + } + } + } + /** Precomputes information for debugging natures and plugins. */ def apply(available: List[AutoPlugin]): NaturesDebug = { @@ -230,14 +291,16 @@ private[sbt] object NaturesDebug def explainPluginEnable(ps: PluginEnable): String = ps match { case PluginRequirements(plugin, context, blockingExcludes, enablingNatures, extraEnabledPlugins, toBeRemoved, deactivate) => + def indent(str: String) = if(str.isEmpty) "" else s"\t$str" + def note(str: String) = if(str.isEmpty) "" else s"Note: $str" val parts = - excludedError(false /* TODO */, blockingExcludes.toList) :: - required(enablingNatures.toList) :: - willAdd(plugin, extraEnabledPlugins.toList) :: - willRemove(plugin, toBeRemoved.toList) :: - needToDeactivate(deactivate) :: + indent(excludedError(false /* TODO */, blockingExcludes.toList)) :: + indent(required(enablingNatures.toList)) :: + indent(needToDeactivate(deactivate)) :: + note(willAdd(plugin, extraEnabledPlugins.toList)) :: + note(willRemove(plugin, toBeRemoved.toList)) :: Nil - parts.mkString("\n") + parts.filterNot(_.isEmpty).mkString("\n") case PluginImpossible(plugin, context, contradictions) => pluginImpossible(plugin, contradictions) case PluginActivated(plugin, context) => s"Plugin ${plugin.label} already activated." } @@ -299,16 +362,16 @@ private[sbt] object NaturesDebug private[this] def needToDeactivate(deactivate: List[DeactivatePlugin]): String = str(deactivate)(deactivate1, deactivateN) private[this] def deactivateN(plugins: List[DeactivatePlugin]): String = - plugins.map(deactivate1).mkString("These plugins need to be deactivated:\n\t", "\n\t", "") + plugins.map(deactivateString).mkString("These plugins need to be deactivated:\n\t", "\n\t", "") private[this] def deactivate1(deactivate: DeactivatePlugin): String = - s"Deactivate ${deactivateString(deactivate)}" + s"Need to deactivate ${deactivateString(deactivate)}" private[this] def deactivateString(d: DeactivatePlugin): String = { val removeNaturesString: String = d.removeOneOf.toList match { case Nil => "" - case x :: Nil => s"or no longer include $x" - case xs => s"or remove one of ${xs.mkString(", ")}" + case x :: Nil => s" or no longer include $x" + case xs => s" or remove one of ${xs.mkString(", ")}" } s"${d.plugin.label}: directly exclude it${removeNaturesString}" }