From 0321f0cd48458c05f6c16e94dc742811aa983bee Mon Sep 17 00:00:00 2001 From: Eugene Yokota Date: Sat, 27 Aug 2016 05:40:55 -0400 Subject: [PATCH] Add extraProjects adn derivedProjects. Fixes #2532 This adds support to generate synthetic subprojects from an auto plugin. In addition, a method called `projectOrigin` is added to distinguish Organic, BuildExtra, ProjectExtra, and GenericRoot. Forward-port of #2717 and #2738 --- main/src/main/scala/sbt/Plugins.scala | 6 ++ main/src/main/scala/sbt/Project.scala | 48 ++++++++---- main/src/main/scala/sbt/internal/Load.scala | 73 ++++++++++--------- .../project/DatabasePlugin.scala | 15 ++++ .../project/ExtraProjectPluginExample.scala | 12 +++ .../project/ExtraProjectPluginExample2.scala | 18 +++++ sbt/src/sbt-test/project/extra-projects/test | 5 ++ 7 files changed, 131 insertions(+), 46 deletions(-) create mode 100644 sbt/src/sbt-test/project/extra-projects/project/DatabasePlugin.scala create mode 100644 sbt/src/sbt-test/project/extra-projects/project/ExtraProjectPluginExample.scala create mode 100644 sbt/src/sbt-test/project/extra-projects/project/ExtraProjectPluginExample2.scala create mode 100644 sbt/src/sbt-test/project/extra-projects/test diff --git a/main/src/main/scala/sbt/Plugins.scala b/main/src/main/scala/sbt/Plugins.scala index 7a59849d3..64d12723f 100644 --- a/main/src/main/scala/sbt/Plugins.scala +++ b/main/src/main/scala/sbt/Plugins.scala @@ -93,6 +93,12 @@ abstract class AutoPlugin extends Plugins.Basic with PluginsFunctions { // TODO?: def commands: Seq[Command] + /** The [[Project]]s to add to the current build. */ + def extraProjects: Seq[Project] = Nil + + /** The [[Project]]s to add to the current build based on an existing project. */ + def derivedProjects(proj: ProjectDefinition[_]): Seq[Project] = Nil + private[sbt] def unary_! : Exclude = Exclude(this) diff --git a/main/src/main/scala/sbt/Project.scala b/main/src/main/scala/sbt/Project.scala index 21cb25a28..f02fec3c2 100755 --- a/main/src/main/scala/sbt/Project.scala +++ b/main/src/main/scala/sbt/Project.scala @@ -67,6 +67,9 @@ sealed trait ProjectDefinition[PR <: ProjectReference] { */ def plugins: Plugins + /** Indicates whether the project was created organically, or was generated synthetically. */ + def projectOrigin: ProjectOrigin + /** The [[AutoPlugin]]s enabled for this project. This value is only available on a loaded Project. */ private[sbt] def autoPlugins: Seq[AutoPlugin] @@ -107,7 +110,7 @@ sealed trait Project extends ProjectDefinition[ProjectReference] { dependenciesEval = dependenciesEval, delegatesEval = delegatesEval, settingsEval = settingsEval, - configurations, auto, plugins, autoPlugins) + configurations, auto, plugins, autoPlugins, projectOrigin) def resolve(resolveRef: ProjectReference => ProjectRef): ResolvedProject = { @@ -119,7 +122,7 @@ sealed trait Project extends ProjectDefinition[ProjectReference] { dependenciesEval = dependenciesEval map resolveDeps, delegatesEval = delegatesEval map resolveRefs, settingsEval, - configurations, auto, plugins, autoPlugins) + configurations, auto, plugins, autoPlugins, projectOrigin) } def resolveBuild(resolveRef: ProjectReference => ProjectReference): Project = { @@ -131,7 +134,7 @@ sealed trait Project extends ProjectDefinition[ProjectReference] { dependenciesEval = dependenciesEval map resolveDeps, delegatesEval = delegatesEval map resolveRefs, settingsEval, - configurations, auto, plugins, autoPlugins) + configurations, auto, plugins, autoPlugins, projectOrigin) } /** @@ -229,13 +232,19 @@ sealed trait Project extends ProjectDefinition[ProjectReference] { private[this] def setPlugins(ns: Plugins): Project = { // TODO: for 0.14.0, use copy when it has the additional `plugins` parameter - unresolved(id, base, aggregateEval = aggregateEval, dependenciesEval = dependenciesEval, delegatesEval = delegatesEval, settingsEval, configurations, auto, ns, autoPlugins) + unresolved(id, base, aggregateEval = aggregateEval, dependenciesEval = dependenciesEval, delegatesEval = delegatesEval, settingsEval, configurations, auto, ns, autoPlugins, projectOrigin) } /** Definitively set the [[AutoPlugin]]s for this project. */ private[sbt] def setAutoPlugins(autos: Seq[AutoPlugin]): Project = { // TODO: for 0.14.0, use copy when it has the additional `autoPlugins` parameter - unresolved(id, base, aggregateEval = aggregateEval, dependenciesEval = dependenciesEval, delegatesEval = delegatesEval, settingsEval, configurations, auto, plugins, autos) + unresolved(id, base, aggregateEval = aggregateEval, dependenciesEval = dependenciesEval, delegatesEval = delegatesEval, settingsEval, configurations, auto, plugins, autos, projectOrigin) + } + + /** Definitively set the [[ProjectOrigin]] for this project. */ + private[sbt] def setProjectOrigin(origin: ProjectOrigin): Project = { + // TODO: for 1.0.x, use withProjectOrigin. + unresolved(id, base, aggregateEval = aggregateEval, dependenciesEval = dependenciesEval, delegatesEval = delegatesEval, settingsEval, configurations, auto, plugins, autoPlugins, origin) } } sealed trait ResolvedProject extends ProjectDefinition[ProjectRef] { @@ -247,6 +256,18 @@ sealed trait ClasspathDep[PR <: ProjectReference] { def project: PR; def configu final case class ResolvedClasspathDependency(project: ProjectRef, configuration: Option[String]) extends ClasspathDep[ProjectRef] final case class ClasspathDependency(project: ProjectReference, configuration: Option[String]) extends ClasspathDep[ProjectReference] +/** + * Indicate whether the project was created organically, synthesized by a plugin, + * or is a "generic root" project supplied by sbt when a project doesn't exist for `file(".")`. + */ +sealed trait ProjectOrigin +object ProjectOrigin { + case object Organic extends ProjectOrigin + case object ExtraProject extends ProjectOrigin + case object DerivedProject extends ProjectOrigin + case object GenericRoot extends ProjectOrigin +} + object Project extends ProjectExtra { private abstract class ProjectDef[PR <: ProjectReference]( @@ -259,7 +280,8 @@ object Project extends ProjectExtra { val configurations: Seq[Configuration], val auto: AddSettings, val plugins: Plugins, - val autoPlugins: Seq[AutoPlugin]) extends ProjectDefinition[PR] { + val autoPlugins: Seq[AutoPlugin], + val projectOrigin: ProjectOrigin) extends ProjectDefinition[PR] { def aggregate: Seq[PR] = aggregateEval.get def dependencies: Seq[ClasspathDep[PR]] = dependenciesEval.get def delegates: Seq[PR] = delegatesEval.get @@ -278,9 +300,9 @@ object Project extends ProjectExtra { } def apply(id: String, base: File): Project = - unresolved(id, base, evalNil, evalNil, evalNil, evalNil, Nil, AddSettings.allDefaults, Plugins.empty, Nil) + unresolved(id, base, evalNil, evalNil, evalNil, evalNil, Nil, AddSettings.allDefaults, Plugins.empty, Nil, ProjectOrigin.Organic) - // TODO: add parameter for plugins in 0.14.0 + // TODO: add parameter for plugins and projectOrigin in 1.0 // TODO: Modify default settings to be the core settings, and automatically add the IvyModule + JvmPlugins. // def apply(id: String, base: File, aggregate: => Seq[ProjectReference] = Nil, dependencies: => Seq[ClasspathDep[ProjectReference]] = Nil, // delegates: => Seq[ProjectReference] = Nil, settings: => Seq[Def.Setting[_]] = Nil, configurations: Seq[Configuration] = Nil, @@ -303,7 +325,7 @@ object Project extends ProjectExtra { private[sbt] def mkGeneratedRoot(id: String, base: File, aggregate: Eval[Seq[ProjectReference]]): Project = { validProjectID(id).foreach(errMsg => sys.error("Invalid project ID: " + errMsg)) - new ProjectDef[ProjectReference](id, base, aggregate, evalNil, evalNil, evalNil, Nil, AddSettings.allDefaults, Plugins.empty, Nil) with Project with GeneratedRootProject + new ProjectDef[ProjectReference](id, base, aggregate, evalNil, evalNil, evalNil, Nil, AddSettings.allDefaults, Plugins.empty, Nil, ProjectOrigin.GenericRoot) with Project with GeneratedRootProject } /** Returns None if `id` is a valid Project ID or Some containing the parser error message if it is not.*/ @@ -330,15 +352,15 @@ object Project extends ProjectExtra { private def resolved(id: String, base: File, aggregateEval: Eval[Seq[ProjectRef]], dependenciesEval: Eval[Seq[ClasspathDep[ProjectRef]]], delegatesEval: Eval[Seq[ProjectRef]], settingsEval: Eval[Seq[Def.Setting[_]]], configurations: Seq[Configuration], auto: AddSettings, - plugins: Plugins, autoPlugins: Seq[AutoPlugin]): ResolvedProject = - new ProjectDef[ProjectRef](id, base, aggregateEval, dependenciesEval, delegatesEval, settingsEval, configurations, auto, plugins, autoPlugins) with ResolvedProject + plugins: Plugins, autoPlugins: Seq[AutoPlugin], origin: ProjectOrigin): ResolvedProject = + new ProjectDef[ProjectRef](id, base, aggregateEval, dependenciesEval, delegatesEval, settingsEval, configurations, auto, plugins, autoPlugins, origin) with ResolvedProject private def unresolved(id: String, base: File, aggregateEval: Eval[Seq[ProjectReference]], dependenciesEval: Eval[Seq[ClasspathDep[ProjectReference]]], delegatesEval: Eval[Seq[ProjectReference]], settingsEval: Eval[Seq[Def.Setting[_]]], configurations: Seq[Configuration], auto: AddSettings, - plugins: Plugins, autoPlugins: Seq[AutoPlugin]): Project = + plugins: Plugins, autoPlugins: Seq[AutoPlugin], origin: ProjectOrigin): Project = { validProjectID(id).foreach(errMsg => sys.error("Invalid project ID: " + errMsg)) - new ProjectDef[ProjectReference](id, base, aggregateEval, dependenciesEval, delegatesEval, settingsEval, configurations, auto, plugins, autoPlugins) with Project + new ProjectDef[ProjectReference](id, base, aggregateEval, dependenciesEval, delegatesEval, settingsEval, configurations, auto, plugins, autoPlugins, origin) with Project } final class Constructor(p: ProjectReference) { diff --git a/main/src/main/scala/sbt/internal/Load.scala b/main/src/main/scala/sbt/internal/Load.scala index b79bc751b..7d8b22e3b 100755 --- a/main/src/main/scala/sbt/internal/Load.scala +++ b/main/src/main/scala/sbt/internal/Load.scala @@ -436,10 +436,13 @@ private[sbt] object Load { val plugs = plugins(defDir, s, config.copy(pluginManagement = config.pluginManagement.forPlugin)) val defsScala = plugs.detected.builds.values + val buildLevelExtraProjects = plugs.detected.autoPlugins flatMap { d => + d.value.extraProjects map {_.setProjectOrigin(ProjectOrigin.ExtraProject)} + } // NOTE - because we create an eval here, we need a clean-eval later for this URI. lazy val eval = mkEval(plugs.classpath, defDir, plugs.pluginData.scalacOptions) - val initialProjects = defsScala.flatMap(b => projectsFromBuild(b, normBase)) + val initialProjects = defsScala.flatMap(b => projectsFromBuild(b, normBase)) ++ buildLevelExtraProjects val hasRootAlreadyDefined = defsScala.exists(_.rootProject.isDefined) @@ -549,10 +552,24 @@ private[sbt] object Load { // load all relevant configuration files (.sbt, as .scala already exists at this point) def discover(auto: AddSettings, base: File): DiscoveredProjects = discoverProjects(auto, base, plugins, eval, memoSettings) - // Step two, Finalize a project with all its settings/configuration. - def finalizeProject(p: Project, configFiles: Seq[File]): Project = { - val loadedFiles = configFiles flatMap { f => memoSettings.get(f) } - resolveProject(p, loadedFiles, plugins, injectSettings, memoSettings, log) + // Step two: + // a. Apply all the project manipulations from .sbt files in order + // b. Deduce the auto plugins for the project + // c. Finalize a project with all its settings/configuration. + def finalizeProject(p: Project, files: Seq[File], expand: Boolean): (Project, Seq[Project]) = { + val configFiles = files flatMap { f => memoSettings.get(f) } + val p1: Project = + configFiles.flatMap(_.manipulations).foldLeft(p) { (prev, t) => + t(prev) + } + val autoPlugins: Seq[AutoPlugin] = + try plugins.detected.deducePluginsFromProject(p1, log) + catch { case e: AutoPluginException => throw translateAutoPluginException(e, p) } + val p2 = this.resolveProject(p1, autoPlugins, plugins, injectSettings, memoSettings, log) + val projectLevelExtra = + if (expand) autoPlugins flatMap { _.derivedProjects(p2) map {_.setProjectOrigin(ProjectOrigin.DerivedProject)} } + else Nil + (p2, projectLevelExtra) } // Discover any new project definition for the base directory of this project, and load all settings. // Also return any newly discovered project instances. @@ -565,9 +582,10 @@ private[sbt] object Load { (root, rest, files, generated) case DiscoveredProjects(None, rest, files, generated) => (p, rest, files, generated) } - val finalRoot = finalizeProject(root, files) - (finalRoot, discovered, generated) + val (finalRoot, projectLevelExtra) = finalizeProject(root, files, true) + (finalRoot, discovered ++ projectLevelExtra, generated) } + // Load all config files AND finalize the project at the root directory, if it exists. // Continue loading if we find any more. newProjects match { @@ -580,8 +598,8 @@ private[sbt] object Load { discover(AddSettings.defaultSbtFiles, buildBase) match { case DiscoveredProjects(Some(root), discovered, files, generated) => log.debug(s"[Loading] Found root project ${root.id} w/ remaining ${discovered.map(_.id).mkString(",")}") - val finalRoot = finalizeProject(root, files) - loadTransitive(discovered, buildBase, plugins, eval, injectSettings, finalRoot +: acc, memoSettings, log, false, buildUri, context, generated ++ generatedConfigClassFiles) + val (finalRoot, projectLevelExtra) = finalizeProject(root, files, true) + loadTransitive(discovered ++ projectLevelExtra, buildBase, plugins, eval, injectSettings, finalRoot +: acc, memoSettings, log, false, buildUri, context, generated ++ generatedConfigClassFiles) // Here we need to create a root project... case DiscoveredProjects(None, discovered, files, generated) => log.debug(s"[Loading] Found non-root projects ${discovered.map(_.id).mkString(",")}") @@ -593,7 +611,7 @@ private[sbt] object Load { val defaultID = autoID(buildBase, context, existingIds) val root0 = if (discovered.isEmpty || java.lang.Boolean.getBoolean("sbt.root.ivyplugin")) BuildDef.defaultAggregatedProject(defaultID, buildBase, refs) else BuildDef.generatedRootWithoutIvyPlugin(defaultID, buildBase, refs) - val root = finalizeProject(root0, files) + val (root, _) = finalizeProject(root0, files, false) val result = root +: (acc ++ otherProjects.projects) log.debug(s"[Loading] Done in ${buildBase}, returning: ${result.map(_.id).mkString("(", ", ", ")")}") LoadedProjects(result, generated ++ otherGenerated ++ generatedConfigClassFiles) @@ -625,13 +643,11 @@ private[sbt] object Load { /** * This method attempts to resolve/apply all configuration loaded for a project. It is responsible for the following: * - * 1. Apply any manipulations defined in .sbt files. - * 2. Detecting which autoPlugins are enabled for the project. - * 3. Ordering all Setting[_]s for the project + * Ordering all Setting[_]s for the project * * - * @param rawProject The original project, with nothing manipulated since it was evaluated/discovered. - * @param configFiles All configuration files loaded for this project. Used to discover project manipulations + * @param transformedProject The project with manipulation. + * @param projectPlugins The deduced list of plugins for the given project. * @param loadedPlugins The project definition (and classloader) of the build. * @param globalUserSettings All the settings contributed from the ~/.sbt/ directory * @param memoSettings A recording of all loaded files (our files should reside in there). We should need not load any @@ -639,32 +655,23 @@ private[sbt] object Load { * @param log A logger to report auto-plugin issues to. */ private[this] def resolveProject( - rawProject: Project, - configFiles: Seq[LoadedSbtFile], + p: Project, + projectPlugins: Seq[AutoPlugin], loadedPlugins: LoadedPlugins, globalUserSettings: InjectSettings, memoSettings: mutable.Map[File, LoadedSbtFile], log: Logger): Project = { import AddSettings._ - // 1. Apply all the project manipulations from .sbt files in order - val transformedProject = - configFiles.flatMap(_.manipulations).foldLeft(rawProject) { (prev, t) => - t(prev) - } - // 2. Discover all the autoplugins and contributed configurations. - val autoPlugins = - try loadedPlugins.detected.deducePluginsFromProject(transformedProject, log) - catch { case e: AutoPluginException => throw translateAutoPluginException(e, transformedProject) } - val autoConfigs = autoPlugins.flatMap(_.projectConfigurations) + val autoConfigs = projectPlugins.flatMap(_.projectConfigurations) // 3. Use AddSettings instance to order all Setting[_]s appropriately val allSettings = { // TODO - This mechanism of applying settings could be off... It's in two places now... - lazy val defaultSbtFiles = configurationSources(transformedProject.base) + lazy val defaultSbtFiles = configurationSources(p.base) // Filter the AutoPlugin settings we included based on which ones are // intended in the AddSettings.AutoPlugins filter. def autoPluginSettings(f: AutoPlugins) = - autoPlugins.filter(f.include).flatMap(_.projectSettings) + projectPlugins.filter(f.include).flatMap(_.projectSettings) // Grab all the settigns we already loaded from sbt files def settings(files: Seq[File]): Seq[Setting[_]] = for { @@ -674,17 +681,17 @@ private[sbt] object Load { } yield setting // Expand the AddSettings instance into a real Seq[Setting[_]] we'll use on the project def expandSettings(auto: AddSettings): Seq[Setting[_]] = auto match { - case BuildScalaFiles => rawProject.settings + case BuildScalaFiles => p.settings case User => globalUserSettings.projectLoaded(loadedPlugins.loader) - case sf: SbtFiles => settings(sf.files.map(f => IO.resolve(rawProject.base, f))) + case sf: SbtFiles => settings(sf.files.map(f => IO.resolve(p.base, f))) case sf: DefaultSbtFiles => settings(defaultSbtFiles.filter(sf.include)) case p: AutoPlugins => autoPluginSettings(p) case q: Sequence => (Seq.empty[Setting[_]] /: q.sequence) { (b, add) => b ++ expandSettings(add) } } - expandSettings(transformedProject.auto) + expandSettings(p.auto) } // Finally, a project we can use in buildStructure. - transformedProject.copy(settingsEval = Ev.later(allSettings)).setAutoPlugins(autoPlugins).prefixConfigs(autoConfigs: _*) + p.copy(settingsEval = Ev.later(allSettings)).setAutoPlugins(projectPlugins).prefixConfigs(autoConfigs: _*) } /** diff --git a/sbt/src/sbt-test/project/extra-projects/project/DatabasePlugin.scala b/sbt/src/sbt-test/project/extra-projects/project/DatabasePlugin.scala new file mode 100644 index 000000000..0febc622b --- /dev/null +++ b/sbt/src/sbt-test/project/extra-projects/project/DatabasePlugin.scala @@ -0,0 +1,15 @@ +import sbt._, syntax._, Keys._ + +object DatabasePlugin extends AutoPlugin { + override def requires: Plugins = sbt.plugins.JvmPlugin + override def trigger = noTrigger + + object autoImport { + lazy val databaseName = settingKey[String]("name of the database") + } + import autoImport._ + override def projectSettings: Seq[Setting[_]] = + Seq( + databaseName := "something" + ) +} diff --git a/sbt/src/sbt-test/project/extra-projects/project/ExtraProjectPluginExample.scala b/sbt/src/sbt-test/project/extra-projects/project/ExtraProjectPluginExample.scala new file mode 100644 index 000000000..1ebb3d494 --- /dev/null +++ b/sbt/src/sbt-test/project/extra-projects/project/ExtraProjectPluginExample.scala @@ -0,0 +1,12 @@ +import sbt._, syntax._, Keys._ + +object ExtraProjectPluginExample extends AutoPlugin { + override def extraProjects: Seq[Project] = + List("foo", "bar", "baz") map generateProject + + def generateProject(id: String): Project = + Project(id, file(id)). + settings( + name := id + ) +} diff --git a/sbt/src/sbt-test/project/extra-projects/project/ExtraProjectPluginExample2.scala b/sbt/src/sbt-test/project/extra-projects/project/ExtraProjectPluginExample2.scala new file mode 100644 index 000000000..e74064a4a --- /dev/null +++ b/sbt/src/sbt-test/project/extra-projects/project/ExtraProjectPluginExample2.scala @@ -0,0 +1,18 @@ +import sbt._, syntax._, Keys._ + +object ExtraProjectPluginExample2 extends AutoPlugin { + // Enable this plugin by default + override def requires: Plugins = sbt.plugins.CorePlugin + override def trigger = allRequirements + + override def derivedProjects(proj: ProjectDefinition[_]): Seq[Project] = + // Make sure to exclude project extras to avoid recursive generation + if (proj.projectOrigin != ProjectOrigin.DerivedProject) { + val id = proj.id + "1" + Seq( + Project(id, file(id)). + enablePlugins(DatabasePlugin) + ) + } + else Nil +} diff --git a/sbt/src/sbt-test/project/extra-projects/test b/sbt/src/sbt-test/project/extra-projects/test new file mode 100644 index 000000000..63a323084 --- /dev/null +++ b/sbt/src/sbt-test/project/extra-projects/test @@ -0,0 +1,5 @@ +> bar/compile + +> foo1/compile + +> foo1/databaseName