diff --git a/main/src/main/scala/sbt/Project.scala b/main/src/main/scala/sbt/Project.scala index ea3a6b942..bac52a877 100755 --- a/main/src/main/scala/sbt/Project.scala +++ b/main/src/main/scala/sbt/Project.scala @@ -121,7 +121,45 @@ sealed trait ProjectDefinition[PR <: ProjectReference] { if (ts.isEmpty) Nil else s"$label: $ts" :: Nil } -sealed trait Project extends ProjectDefinition[ProjectReference] { +trait CompositeProject { + def componentProjects: Seq[Project] +} + +private[sbt] object CompositeProject { + + /** + * Expand user defined projects with the component projects of `compositeProjects`. + * + * If two projects with the same id appear in the user defined projects and + * in `compositeProjects.componentProjects`, the user defined project wins. + * This is necessary for backward compatibility with the idioms: + * {{{ + * lazy val foo = crossProject + * lazy val fooJS = foo.js.settings(...) + * lazy val fooJVM = foo.jvm.settings(...) + * }}} + * and the rarer: + * {{{ + * lazy val fooJS = foo.js.settings(...) + * lazy val foo = crossProject + * lazy val fooJVM = foo.jvm.settings(...) + * }}} + */ + def expand(compositeProjects: Seq[CompositeProject]): Seq[Project] = { + val userProjects = compositeProjects.collect { case p: Project => p } + for (p <- compositeProjects.flatMap(_.componentProjects)) yield { + userProjects.find(_.id == p.id) match { + case Some(userProject) => userProject + case None => p + } + } + }.distinct + +} + +sealed trait Project extends ProjectDefinition[ProjectReference] with CompositeProject { + def componentProjects: Seq[Project] = this :: Nil + private[sbt] def copy( id: String = id, base: File = base, diff --git a/main/src/main/scala/sbt/internal/BuildDef.scala b/main/src/main/scala/sbt/internal/BuildDef.scala index dd1d11aec..d4a435ab5 100644 --- a/main/src/main/scala/sbt/internal/BuildDef.scala +++ b/main/src/main/scala/sbt/internal/BuildDef.scala @@ -17,7 +17,8 @@ import sbt.internal.inc.ReflectUtilities trait BuildDef { def projectDefinitions(@deprecated("unused", "") baseDirectory: File): Seq[Project] = projects - def projects: Seq[Project] = ReflectUtilities.allVals[Project](this).values.toSeq + def projects: Seq[Project] = + CompositeProject.expand(ReflectUtilities.allVals[CompositeProject](this).values.toSeq) // TODO: Should we grab the build core settings here or in a plugin? def settings: Seq[Setting[_]] = Defaults.buildCore def buildLoaders: Seq[BuildLoader.Components] = Nil diff --git a/main/src/main/scala/sbt/internal/EvaluateConfigurations.scala b/main/src/main/scala/sbt/internal/EvaluateConfigurations.scala index 85b40293c..26e5348ff 100644 --- a/main/src/main/scala/sbt/internal/EvaluateConfigurations.scala +++ b/main/src/main/scala/sbt/internal/EvaluateConfigurations.scala @@ -151,10 +151,12 @@ private[sbt] object EvaluateConfigurations { val allGeneratedFiles = (definitions.generated ++ dslEntries.flatMap(_.generated)) loader => { - val projects = - definitions.values(loader).collect { - case p: Project => resolveBase(file.getParentFile, p) + val projects = { + val compositeProjects = definitions.values(loader).collect { + case p: CompositeProject => p } + CompositeProject.expand(compositeProjects).map(resolveBase(file.getParentFile, _)) + } val (settingsRaw, manipulationsRaw) = dslEntries map (_.result apply loader) partition { case DslEntry.ProjectSettings(_) => true @@ -280,7 +282,10 @@ private[sbt] object EvaluateConfigurations { } private[this] def extractedValTypes: Seq[String] = - Seq(classOf[Project], classOf[InputKey[_]], classOf[TaskKey[_]], classOf[SettingKey[_]]) + Seq(classOf[CompositeProject], + classOf[InputKey[_]], + classOf[TaskKey[_]], + classOf[SettingKey[_]]) .map(_.getName) private[this] def evaluateDefinitions(eval: Eval, diff --git a/notes/1.2.0/introduce-CompositeProject.md b/notes/1.2.0/introduce-CompositeProject.md new file mode 100644 index 000000000..759d75a71 --- /dev/null +++ b/notes/1.2.0/introduce-CompositeProject.md @@ -0,0 +1,9 @@ + +[#3042]: https://github.com/sbt/sbt/issues/3042 +[#4056]: https://github.com/sbt/sbt/pull/4056 + +### Introduce CompositeProject + +### Improvements + +- Support for: `lazy val foo = someCompositeProject` (e.g.`CrossProject`) [#3042][]/[#4056][] diff --git a/sbt/src/sbt-test/project/sbt-composite-projects/build.sbt b/sbt/src/sbt-test/project/sbt-composite-projects/build.sbt new file mode 100644 index 000000000..1537b2610 --- /dev/null +++ b/sbt/src/sbt-test/project/sbt-composite-projects/build.sbt @@ -0,0 +1,36 @@ +import sbt.internal.AddSettings +import sbt.CompositeProject + +lazy val check = taskKey[Unit]("check") + +// Based on sbt-file-projects test +lazy val foo = new CompositeProject +{ + val jvm = Project.apply("jvm", new File("jvm")).settings(version := "0.1.0") // this one needs to win + val js = Project.apply("js", new File("js")).settings(version := "0.1.0") // this one needs to win + def componentProjects: Seq[Project] = Seq(jvm, js) +} + +lazy val fooJS = foo.js +lazy val fooJVM = foo.jvm + +lazy val bar = project + .dependsOn(foo.jvm) + +val g = taskKey[Unit]("A task in the root project") +g := println("Hello.") + + +check := { + val verJvm = (version in foo.jvm).?.value + assert (verJvm == Some("0.1.0")) + + val verFooJvm = (version in fooJVM).?.value + assert (verFooJvm == Some("0.1.0")) + + val verJs = (version in foo.js).?.value + assert (verJs == Some("0.1.0")) + + val verFooJs = (version in fooJS).?.value + assert (verFooJs == Some("0.1.0")) +} diff --git a/sbt/src/sbt-test/project/sbt-composite-projects/changes/basic.sbt b/sbt/src/sbt-test/project/sbt-composite-projects/changes/basic.sbt new file mode 100644 index 000000000..c128b140e --- /dev/null +++ b/sbt/src/sbt-test/project/sbt-composite-projects/changes/basic.sbt @@ -0,0 +1 @@ +lazy val root = (project in file(".")) diff --git a/sbt/src/sbt-test/project/sbt-composite-projects/changes/shadow.sbt b/sbt/src/sbt-test/project/sbt-composite-projects/changes/shadow.sbt new file mode 100644 index 000000000..0713e7f4b --- /dev/null +++ b/sbt/src/sbt-test/project/sbt-composite-projects/changes/shadow.sbt @@ -0,0 +1,36 @@ +import sbt.internal.AddSettings +import sbt.CompositeProject + +lazy val check = taskKey[Unit]("check") + +// Based on sbt-file-projects test +lazy val foo = new CompositeProject +{ + val jvm = Project.apply("jvm", new File("jvm")).settings(version := "0.1.0") + val js = Project.apply("js", new File("js")).settings(version := "0.1.0") // this one needs to win + def componentProjects: Seq[Project] = Seq(jvm, js) +} + +lazy val fooJS = foo.js +lazy val fooJVM = foo.jvm.settings(version := "0.2.0") // this one needs to win + +lazy val bar = project + .dependsOn(foo.jvm) + +val g = taskKey[Unit]("A task in the root project") +g := println("Hello.") + + +check := { + val verJvm = (version in foo.jvm).?.value + assert (verJvm == Some("0.2.0")) + + val verFooJvm = (version in fooJVM).?.value + assert (verFooJvm == Some("0.2.0")) + + val verJs = (version in foo.js).?.value + assert (verJs == Some("0.1.0")) + + val verFooJs = (version in fooJS).?.value + assert (verFooJs == Some("0.1.0")) +} diff --git a/sbt/src/sbt-test/project/sbt-composite-projects/changes/shadowLazy.sbt b/sbt/src/sbt-test/project/sbt-composite-projects/changes/shadowLazy.sbt new file mode 100644 index 000000000..492fbd621 --- /dev/null +++ b/sbt/src/sbt-test/project/sbt-composite-projects/changes/shadowLazy.sbt @@ -0,0 +1,37 @@ +import sbt.internal.AddSettings +import sbt.CompositeProject + +lazy val check = taskKey[Unit]("check") + +lazy val fooJS = foo.js.settings(version := "0.2.1") // this one needs to win + +// Based on sbt-file-projects test +lazy val foo = new CompositeProject +{ + val jvm = Project.apply("jvm", new File("jvm")).settings(version := "0.1.0") + val js = Project.apply("js", new File("js")).settings(version := "0.1.0") + def componentProjects: Seq[Project] = Seq(jvm, js) +} + +lazy val fooJVM = foo.jvm.settings(version := "0.2.0") // this one needs to win + +lazy val bar = project + .dependsOn(foo.jvm) + +val g = taskKey[Unit]("A task in the root project") +g := println("Hello.") + + +check := { + val verJvm = (version in foo.jvm).?.value + assert (verJvm == Some("0.2.0")) + + val verFooJvm = (version in fooJVM).?.value + assert (verFooJvm == Some("0.2.0")) + + val verJs = (version in foo.js).?.value + assert (verJs == Some("0.2.1")) + + val verFooJs = (version in fooJS).?.value + assert (verFooJs == Some("0.2.1")) +} diff --git a/sbt/src/sbt-test/project/sbt-composite-projects/js/build.sbt b/sbt/src/sbt-test/project/sbt-composite-projects/js/build.sbt new file mode 100644 index 000000000..0ddd49e30 --- /dev/null +++ b/sbt/src/sbt-test/project/sbt-composite-projects/js/build.sbt @@ -0,0 +1,2 @@ +val h = taskKey[Unit]("A task in project 'js'") +h := println("Hello.") diff --git a/sbt/src/sbt-test/project/sbt-composite-projects/jvm/A.scala b/sbt/src/sbt-test/project/sbt-composite-projects/jvm/A.scala new file mode 100644 index 000000000..528ffce71 --- /dev/null +++ b/sbt/src/sbt-test/project/sbt-composite-projects/jvm/A.scala @@ -0,0 +1 @@ +object A \ No newline at end of file diff --git a/sbt/src/sbt-test/project/sbt-composite-projects/jvm/a.sbt b/sbt/src/sbt-test/project/sbt-composite-projects/jvm/a.sbt new file mode 100644 index 000000000..9b73a55bf --- /dev/null +++ b/sbt/src/sbt-test/project/sbt-composite-projects/jvm/a.sbt @@ -0,0 +1,2 @@ +val aa = taskKey[Unit]("A task in the 'jvm' project") +aa := println("Hello.") diff --git a/sbt/src/sbt-test/project/sbt-composite-projects/other.sbt b/sbt/src/sbt-test/project/sbt-composite-projects/other.sbt new file mode 100644 index 000000000..94e5f2363 --- /dev/null +++ b/sbt/src/sbt-test/project/sbt-composite-projects/other.sbt @@ -0,0 +1 @@ +val c = project diff --git a/sbt/src/sbt-test/project/sbt-composite-projects/test b/sbt/src/sbt-test/project/sbt-composite-projects/test new file mode 100644 index 000000000..34bc8097d --- /dev/null +++ b/sbt/src/sbt-test/project/sbt-composite-projects/test @@ -0,0 +1,40 @@ +> g +-> root/compile +> jvm/compile +> jvm/aa +> js/compile +> js/h +> c/compile +> bar/compile + +$ copy-file changes/basic.sbt basic.sbt +> reload +> g +> root/compile +> jvm/compile +> jvm/aa +> js/compile +> js/h +> c/compile +> bar/compile +> check + +$ copy-file changes/shadow.sbt build.sbt +> reload +> jvm/compile +> jvm/aa +> js/compile +> js/h +> c/compile +> bar/compile +> check + +$ copy-file changes/shadowLazy.sbt build.sbt +> reload +> jvm/compile +> jvm/aa +> js/compile +> js/h +> c/compile +> bar/compile +> check \ No newline at end of file