From 52bc35e3a98e79625e6edeb92f29540448b24d2b Mon Sep 17 00:00:00 2001 From: bitloi <89318445+bitloi@users.noreply.github.com> Date: Thu, 5 Feb 2026 19:07:30 -0500 Subject: [PATCH] [2.x] fix: allow defining root project with extraProjects (#4976) (#8694) When an AutoPlugin adds a project at the build root via extraProjects, avoid creating a second root from the build definition so both do not share the same target directory. Treat extraProjects root as 'root already defined' and exclude the build-defined root from initialProjects when both would be at the same base. --- main/src/main/scala/sbt/internal/Load.scala | 21 ++++++++++++------- .../project/extra-projects-root/build.sbt | 2 ++ .../project/RootFromExtraPlugin.scala | 19 +++++++++++++++++ .../sbt-test/project/extra-projects-root/test | 6 ++++++ 4 files changed, 40 insertions(+), 8 deletions(-) create mode 100644 sbt-app/src/sbt-test/project/extra-projects-root/build.sbt create mode 100644 sbt-app/src/sbt-test/project/extra-projects-root/project/RootFromExtraPlugin.scala create mode 100644 sbt-app/src/sbt-test/project/extra-projects-root/test diff --git a/main/src/main/scala/sbt/internal/Load.scala b/main/src/main/scala/sbt/internal/Load.scala index 83623cf3b..88da9128f 100755 --- a/main/src/main/scala/sbt/internal/Load.scala +++ b/main/src/main/scala/sbt/internal/Load.scala @@ -812,10 +812,17 @@ private[sbt] object Load { mkReporter, ) } + val projectsFromBuildDef = defsScala.flatMap(b => projectsFromBuild(b, normBase)) + val rootFromExtra = + buildLevelExtraProjects.filter(p => isRootPath(p.base, normBase)) val initialProjects = - defsScala.flatMap(b => projectsFromBuild(b, normBase)) ++ buildLevelExtraProjects - - val hasRootAlreadyDefined = defsScala.exists(_.rootProject.isDefined) + if rootFromExtra.nonEmpty then + projectsFromBuildDef.filterNot(p => + isRootPath(p.base, normBase) + ) ++ buildLevelExtraProjects + else projectsFromBuildDef ++ buildLevelExtraProjects + val hasRootAlreadyDefined = + defsScala.exists(_.rootProject.isDefined) || rootFromExtra.nonEmpty val memoSettings = new mutable.HashMap[VirtualFile, LoadedSbtFile] def loadProjects(ps: Seq[Project], createRoot: Boolean) = @@ -1086,11 +1093,9 @@ private[sbt] object Load { val DiscoveredProjects(rootOpt, discovered, files, extraFiles, generated) = discover( p.base ) - - // TODO: We assume here the project defined in a build.sbt WINS because the original was a - // phony. However, we may want to 'merge' the two, or only do this if the original was a - // default generated project. - val root = rootOpt.getOrElse(p) + val root = + if p.projectOrigin == ProjectOrigin.ExtraProject && isRootPath(p.base, buildBase) then p + else rootOpt.getOrElse(p) val (finalRoot, projectLevelExtra) = processProject(root, files, extraFiles, true) val newProjects = rest ++ discovered ++ projectLevelExtra val newAcc = acc :+ finalRoot diff --git a/sbt-app/src/sbt-test/project/extra-projects-root/build.sbt b/sbt-app/src/sbt-test/project/extra-projects-root/build.sbt new file mode 100644 index 000000000..bc4ae9068 --- /dev/null +++ b/sbt-app/src/sbt-test/project/extra-projects-root/build.sbt @@ -0,0 +1,2 @@ +// Minimal build so the build definition contributes a root; the plugin's extraProjects +// root (foo at file(".")) should win and be the only root (#4976). diff --git a/sbt-app/src/sbt-test/project/extra-projects-root/project/RootFromExtraPlugin.scala b/sbt-app/src/sbt-test/project/extra-projects-root/project/RootFromExtraPlugin.scala new file mode 100644 index 000000000..7e5b43bfc --- /dev/null +++ b/sbt-app/src/sbt-test/project/extra-projects-root/project/RootFromExtraPlugin.scala @@ -0,0 +1,19 @@ +/* + * sbt + * Copyright 2023, Scala center + * Copyright 2011 - 2022, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +import sbt._, Keys._ + +/** + * Reproduces #4976 / Dale's use case: plugin defines the root project via extraProjects. + * Without the fix, loading fails with "Overlapping output directories" (build root + foo). + */ +object RootFromExtraPlugin extends AutoPlugin { + override def trigger = allRequirements + override def extraProjects: Seq[Project] = + Seq(Project("foo", file(".")).settings(name := "foo")) +} diff --git a/sbt-app/src/sbt-test/project/extra-projects-root/test b/sbt-app/src/sbt-test/project/extra-projects-root/test new file mode 100644 index 000000000..3cf7fe2ae --- /dev/null +++ b/sbt-app/src/sbt-test/project/extra-projects-root/test @@ -0,0 +1,6 @@ +# Root project is defined by plugin via extraProjects (#4976). Without the fix, +# loading fails with "Overlapping output directories" (build root + foo). + +> compile + +> show name