[2.x] fix: derivedProjects not invoked when no explicit root project is defined (#9012)

**Problem**
When build.sbt contains no explicit project definition (e.g. just bare settings with no lazy val root = project.in(file("."))), sbt creates a synthetic root project. In Load.loadTransitive, this synthetic root was processed with expand = false, which prevented AutoPlugin.derivedProjects from being called.
Any plugin relying on derivedProjects to inject projects would fail with "Reference to undefined setting" errors.

The workaround was to add an explicit root project in build.sbt (val root = project.in(file("."))), which caused the Some(root) branch to execute with expand = true.

**Solution**

Removed the expand variable from loadTransitive and pass true directly to processProject. Previously, the Some(root) branch set expand = true while the None branch set expand = false. Since derivedProjects should always be invoked for the root project regardless of whether it was explicitly defined or auto-generated, both branches should behave the same way. Eliminating the variable makes this intent clear.
This commit is contained in:
BitCompass 2026-04-04 14:55:42 -04:00 committed by GitHub
parent 4d71c15e87
commit 1f2d14aa3e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 34 additions and 4 deletions

View File

@ -1120,11 +1120,11 @@ private[sbt] object Load {
buildBase
)
val discoveredIdsStr = discovered.map(_.id).mkString(",")
val (root, expand, moreProjects, otherProjects) =
val (root, moreProjects, otherProjects) =
rootOpt match
case Some(root) =>
log.debug(s"[Loading] Found root project ${root.id} w/ remaining $discoveredIdsStr")
(root, true, discovered, LoadedProjects(Nil, Nil))
(root, discovered, LoadedProjects(Nil, Nil))
case None =>
log.debug(s"[Loading] Found non-root projects $discoveredIdsStr")
// Here we do something interesting... We need to create an aggregate root project
@ -1144,10 +1144,10 @@ private[sbt] object Load {
)
val existingIds = otherProjects.projects.map(_.id)
val refs = existingIds.map(id => ProjectRef(buildUri, id))
(root.aggregate(refs*), false, Nil, otherProjects)
(root.aggregate(refs*), Nil, otherProjects)
val (finalRoot, projectLevelExtra) =
timed(s"Load.loadTransitive: processProject($root)", log) {
processProject(root, files, extraFiles, expand)
processProject(root, files, extraFiles, true)
}
val newProjects = moreProjects ++ projectLevelExtra
val newAcc = finalRoot +: (acc ++ otherProjects.projects)

View File

@ -0,0 +1,7 @@
@transient
lazy val check = taskKey[Unit]("check")
check := {
val v = (LocalProject("foo") / DerivedProjectPlugin.value1).value
assert(v == 3, s"Expected 3 but got $v")
}

View File

@ -0,0 +1,21 @@
import sbt._
import sbt.Keys._
object DerivedProjectPlugin extends AutoPlugin {
val value1 = settingKey[Int]("value1")
override def derivedProjects(proj: ProjectDefinition[?]) =
proj.projectOrigin match {
case ProjectOrigin.DerivedProject =>
Nil
case _ =>
Seq(
Project("foo", file("foo")).settings(
value1 := 3,
name := "foo",
)
)
}
override def trigger = allRequirements
}

View File

@ -0,0 +1,2 @@
# derivedProjects should work even without an explicit root project
> check