fix: Fix common settings loading when root is synthetic

**Problem**
The new common settings feature doesn't work when the root isn't created by the user.

**Solution**
This fixes common settings by calling `expandCommonSettingsPerBase(...)` on
the synthetic root's base first.
This commit is contained in:
Eugene Yokota 2024-09-22 15:56:32 -04:00
parent 864da879c6
commit 354dd25988
13 changed files with 278 additions and 209 deletions

View File

@ -28,7 +28,7 @@ import sbt.librarymanagement.ivy.{ InlineIvyConfiguration, IvyDependencyResoluti
import sbt.librarymanagement.{ Configuration, Configurations, Resolver } import sbt.librarymanagement.{ Configuration, Configurations, Resolver }
import sbt.nio.Settings import sbt.nio.Settings
import sbt.util.{ Logger, Show } import sbt.util.{ Logger, Show }
import xsbti.{ HashedVirtualFileRef, VirtualFile } import xsbti.{ FileConverter, HashedVirtualFileRef, VirtualFile }
import xsbti.compile.{ ClasspathOptionsUtil, Compilers } import xsbti.compile.{ ClasspathOptionsUtil, Compilers }
import java.io.File import java.io.File
import java.net.URI import java.net.URI
@ -943,13 +943,13 @@ private[sbt] object Load {
extraSbtFiles: Seq[VirtualFile], extraSbtFiles: Seq[VirtualFile],
converter: MappedFileConverter, converter: MappedFileConverter,
): LoadedProjects = ): LoadedProjects =
/*timed(s"Load.loadTransitive(${ newProjects.map(_.id) })", log)*/ { // alias for parameter forwarding
def load( def loadTransitive1(
newProjects: Seq[Project], newProjects: Seq[Project],
acc: Seq[Project], acc: Seq[Project],
generated: Seq[Path], generated: Seq[Path],
commonSettings0: Seq[Setting[_]], commonSettings0: Seq[Setting[_]],
) = ): LoadedProjects =
loadTransitive( loadTransitive(
newProjects, newProjects,
buildBase, buildBase,
@ -968,6 +968,16 @@ private[sbt] object Load {
converter, converter,
) )
// alias for parameter forwarding
def expandCommonSettingsPerBase1(directory: File): Seq[Setting[?]] =
expandCommonSettingsPerBase(
directory = directory,
memoSettings = memoSettings,
extraSbtFiles = extraSbtFiles,
converter = converter,
log = log,
)
// load all relevant configuration files (.sbt, as .scala already exists at this point) // load all relevant configuration files (.sbt, as .scala already exists at this point)
def discover(base: File): DiscoveredProjects = { def discover(base: File): DiscoveredProjects = {
val auto = val auto =
@ -997,16 +1007,17 @@ private[sbt] object Load {
try plugins.detected.deducePluginsFromProject(p1, log) try plugins.detected.deducePluginsFromProject(p1, log)
catch { case e: AutoPluginException => throw translateAutoPluginException(e, p) } catch { case e: AutoPluginException => throw translateAutoPluginException(e, p) }
val p2 = val p2 =
resolveProject( resolveProjectSettings(
p1, p = p1,
autoPlugins, projectPlugins = autoPlugins,
plugins, loadedPlugins = plugins,
commonSettings, commonSettings =
machineWideUserSettings, commonSettings ++ expandCommonSettingsPerBase1(p1.base.getCanonicalFile()),
memoSettings, machineWideUserSettings = machineWideUserSettings,
extraFiles, memoSettings = memoSettings,
converter, extraSbtFiles = extraFiles,
log converter = converter,
log = log,
) )
val projectLevelExtra = val projectLevelExtra =
if (expand) { if (expand) {
@ -1031,7 +1042,7 @@ private[sbt] object Load {
val newProjects = rest ++ discovered ++ projectLevelExtra val newProjects = rest ++ discovered ++ projectLevelExtra
val newAcc = acc :+ finalRoot val newAcc = acc :+ finalRoot
val newGenerated = generated ++ generatedConfigClassFiles val newGenerated = generated ++ generatedConfigClassFiles
load(newProjects, newAcc, newGenerated, finalRoot.commonSettings) loadTransitive1(newProjects, newAcc, newGenerated, finalRoot.commonSettings)
} }
// Load all config files AND finalize the project at the root directory, if it exists. // Load all config files AND finalize the project at the root directory, if it exists.
@ -1054,16 +1065,23 @@ private[sbt] object Load {
case None => case None =>
log.debug(s"[Loading] Found non-root projects $discoveredIdsStr") log.debug(s"[Loading] Found non-root projects $discoveredIdsStr")
// Here we do something interesting... We need to create an aggregate root project // Here we do something interesting... We need to create an aggregate root project
val otherProjects = load(discovered, acc, Nil, Nil)
val root = { val root = {
val existingIds = otherProjects.projects.map(_.id) val defaultID = autoID(buildBase, context, Nil)
val defaultID = autoID(buildBase, context, existingIds) if discovered.isEmpty || java.lang.Boolean.getBoolean("sbt.root.ivyplugin") then
val refs = existingIds.map(id => ProjectRef(buildUri, id)) BuildDef.defaultProject(defaultID, buildBase)
if (discovered.isEmpty || java.lang.Boolean.getBoolean("sbt.root.ivyplugin")) else BuildDef.generatedRootSkipPublish(defaultID, buildBase, Nil)
BuildDef.defaultAggregatedProject(defaultID, buildBase, refs)
else BuildDef.generatedRootSkipPublish(defaultID, buildBase, refs)
} }
(root, false, Nil, otherProjects) val otherProjects =
loadTransitive1(
newProjects = discovered,
acc = acc,
generated = Nil,
commonSettings0 = commonSettings
++ expandCommonSettingsPerBase1(buildBase.getCanonicalFile()),
)
val existingIds = otherProjects.projects.map(_.id)
val refs = existingIds.map(id => ProjectRef(buildUri, id))
(root.aggregate(refs: _*), false, Nil, otherProjects)
val (finalRoot, projectLevelExtra) = val (finalRoot, projectLevelExtra) =
timed(s"Load.loadTransitive: finalizeProject($root)", log) { timed(s"Load.loadTransitive: finalizeProject($root)", log) {
finalizeProject(root, files, extraFiles, expand) finalizeProject(root, files, extraFiles, expand)
@ -1072,12 +1090,12 @@ private[sbt] object Load {
val newAcc = finalRoot +: (acc ++ otherProjects.projects) val newAcc = finalRoot +: (acc ++ otherProjects.projects)
val newGenerated = val newGenerated =
generated ++ otherProjects.generatedConfigClassFiles ++ generatedConfigClassFiles generated ++ otherProjects.generatedConfigClassFiles ++ generatedConfigClassFiles
load(newProjects, newAcc, newGenerated, finalRoot.commonSettings) loadTransitive1(newProjects, newAcc, newGenerated, finalRoot.commonSettings)
case Nil => case Nil =>
val projectIds = acc.map(_.id).mkString("(", ", ", ")") val projectIds = acc.map(_.id).mkString("(", ", ", ")")
log.debug(s"[Loading] Done in $buildBase, returning: $projectIds") log.debug(s"[Loading] Done in $buildBase, returning: $projectIds")
LoadedProjects(acc, generatedConfigClassFiles) LoadedProjects(acc, generatedConfigClassFiles)
} end loadTransitive
private[this] def translateAutoPluginException( private[this] def translateAutoPluginException(
e: AutoPluginException, e: AutoPluginException,
@ -1116,18 +1134,18 @@ private[sbt] object Load {
* @param extraSbtFiles Extra *.sbt files. * @param extraSbtFiles Extra *.sbt files.
* @param log A logger to report auto-plugin issues to. * @param log A logger to report auto-plugin issues to.
*/ */
private[sbt] def resolveProject( private[sbt] def resolveProjectSettings(
p: Project, p: Project,
projectPlugins: Seq[AutoPlugin], projectPlugins: Seq[AutoPlugin],
loadedPlugins: LoadedPlugins, loadedPlugins: LoadedPlugins,
commonSettings0: Seq[Setting[_]], commonSettings: Seq[Setting[_]],
machineWideUserSettings: InjectSettings, machineWideUserSettings: InjectSettings,
memoSettings: mutable.Map[VirtualFile, LoadedSbtFile], memoSettings: mutable.Map[VirtualFile, LoadedSbtFile],
extraSbtFiles: Seq[VirtualFile], extraSbtFiles: Seq[VirtualFile],
converter: MappedFileConverter, converter: MappedFileConverter,
log: Logger log: Logger
): Project = ): Project =
timed(s"Load.resolveProject(${p.id})", log) { timed(s"Load.resolveProjectSettings(${p.id})", log) {
import AddSettings._ import AddSettings._
val autoConfigs = projectPlugins.flatMap(_.projectConfigurations) val autoConfigs = projectPlugins.flatMap(_.projectConfigurations)
val auto = AddSettings.allDefaults val auto = AddSettings.allDefaults
@ -1150,35 +1168,7 @@ private[sbt] object Load {
case _ => Nil case _ => Nil
expandPluginSettings(auto) expandPluginSettings(auto)
} }
def buildWideCommonSettings: Seq[Setting[_]] = { def allProjectSettings: Seq[Setting[_]] =
// TODO - This mechanism of applying settings could be off... It's in two places now...
lazy val defaultSbtFiles = configurationSources(p.base.getCanonicalFile())
.map(_.getAbsoluteFile().toPath())
.map(converter.toVirtualFile)
lazy val sbtFiles: Seq[VirtualFile] = defaultSbtFiles ++ extraSbtFiles
// Grab all the settings we already loaded from sbt files
def settings(files: Seq[VirtualFile]): Seq[Setting[_]] =
if files.nonEmpty then
log.info(
s"${files.map(_.name()).mkString(s"loading settings for project ${p.id} from ", ",", " ...")}"
)
else ()
for
file <- files
config <- memoSettings.get(file).toSeq
setting <- config.settings
yield setting
def expandCommonSettings(auto: AddSettings): Seq[Setting[_]] =
auto match
case sf: DefaultSbtFiles => settings(sbtFiles.filter(sf.include))
case q: Sequence =>
q.sequence.foldLeft(Seq.empty[Setting[_]]) { (b, add) =>
b ++ expandCommonSettings(add)
}
case _ => Nil
commonSettings0 ++ expandCommonSettings(auto)
}
def allProjectSettings: Seq[Setting[_]] = {
// Expand the AddSettings instance into a real Seq[Setting[_]] we'll use on the project // Expand the AddSettings instance into a real Seq[Setting[_]] we'll use on the project
def expandSettings(auto: AddSettings): Seq[Setting[_]] = def expandSettings(auto: AddSettings): Seq[Setting[_]] =
auto match auto match
@ -1190,14 +1180,46 @@ private[sbt] object Load {
} }
case _ => Nil case _ => Nil
expandSettings(auto) expandSettings(auto)
} end allProjectSettings
// Finally, a project we can use in buildStructure. // Finally, a project we can use in buildStructure.
p.copy(settings = allAutoPluginSettings ++ buildWideCommonSettings ++ allProjectSettings) p.copy(settings = allAutoPluginSettings ++ commonSettings ++ allProjectSettings)
.setCommonSettings(buildWideCommonSettings) .setCommonSettings(commonSettings)
.setAutoPlugins(projectPlugins) .setAutoPlugins(projectPlugins)
.prefixConfigs(autoConfigs: _*) .prefixConfigs(autoConfigs: _*)
} }
private[this] def expandCommonSettingsPerBase(
directory: File,
memoSettings: mutable.Map[VirtualFile, LoadedSbtFile],
extraSbtFiles: Seq[VirtualFile],
converter: MappedFileConverter,
log: Logger
): Seq[Setting[_]] =
val defaultSbtFiles = configurationSources(directory)
.map(_.getAbsoluteFile().toPath())
.map(converter.toVirtualFile)
.toVector
val sbtFiles = defaultSbtFiles ++ extraSbtFiles.toVector
// Grab all the settings we already loaded from sbt files
def settings(files: Vector[VirtualFile]): Vector[Setting[_]] =
for
file <- files
config <- memoSettings.get(file).toSeq
setting <- config.settings
yield setting
import AddSettings.*
def expandCommonSettings(auto: AddSettings): Vector[Setting[_]] =
auto match
case sf: DefaultSbtFiles => settings(sbtFiles.filter(sf.include))
case q: Sequence =>
q.sequence.foldLeft(Vector.empty[Setting[_]]) { (b, add) =>
b ++ expandCommonSettings(add)
}
case _ => Vector.empty
expandCommonSettings(AddSettings.allDefaults).distinct
end expandCommonSettingsPerBase
/** /**
* This method attempts to discover all Project/settings it can using the configured AddSettings and project base. * This method attempts to discover all Project/settings it can using the configured AddSettings and project base.
* *

View File

@ -1,5 +1,4 @@
scalaVersion := "2.12.19" scalaVersion := "2.12.19"
name := "root"
lazy val core = project lazy val core = project
.settings( .settings(

View File

@ -1,11 +1,9 @@
ThisBuild / organization := "com.example" organization := "com.example"
ThisBuild / ivyPaths := IvyPaths((ThisBuild / baseDirectory).value.toString, Some(((ThisBuild / baseDirectory).value / "ivy" / "cache").toString)) ivyPaths := IvyPaths((ThisBuild / baseDirectory).value.toString, Some(((ThisBuild / baseDirectory).value / "ivy" / "cache").toString))
name := "root" name := "root"
lazy val core = project lazy val core = project
.settings( .settings(
name := "core", name := "core",
// organization := "com.example",
ivyPaths := IvyPaths((ThisBuild / baseDirectory).value.toString, Some(((ThisBuild / baseDirectory).value / "ivy" / "cache").toString))
) )

View File

@ -1,12 +1,11 @@
ThisBuild / organization := "com.example" organization := "com.example"
ThisBuild / version := "0.1.0-SNAPSHOT" version := "0.1.0-SNAPSHOT"
ThisBuild / publishMavenStyle := true publishMavenStyle := true
ivyPaths := {
ThisBuild / ivyPaths := { val base = baseDirectory.value
val base = (ThisBuild / baseDirectory).value val thisBuildBase = (ThisBuild / baseDirectory).value
IvyPaths(base.toString, s"$base/ivy-cache") IvyPaths(base.toString, s"$thisBuildBase/ivy-cache")
} }
publish / skip := true
lazy val config12 = ConfigAxis("Config1_2", "config1.2") lazy val config12 = ConfigAxis("Config1_2", "config1.2")
lazy val config13 = ConfigAxis("Config1_3", "config1.3") lazy val config13 = ConfigAxis("Config1_3", "config1.3")
@ -21,7 +20,6 @@ lazy val app = (projectMatrix in file("app"))
.dependsOn(core) .dependsOn(core)
.settings( .settings(
name := "app", name := "app",
ivyPaths := (ThisBuild / ivyPaths).value
) )
.customRow( .customRow(
scalaVersions = Seq(scala212, scala211), scalaVersions = Seq(scala212, scala211),

View File

@ -1,12 +1,11 @@
ThisBuild / organization := "com.example" organization := "com.example"
ThisBuild / version := "0.1.0-SNAPSHOT" version := "0.1.0-SNAPSHOT"
ThisBuild / publishMavenStyle := true publishMavenStyle := true
ivyPaths := {
ThisBuild / ivyPaths := { val base = baseDirectory.value
val base = (ThisBuild / baseDirectory).value val thisBuildBase = (ThisBuild / baseDirectory).value
IvyPaths(base.toString, s"$base/ivy-cache") IvyPaths(base.toString, s"$thisBuildBase/ivy-cache")
} }
publish / skip := true
lazy val config12 = ConfigAxis("Config1_2", "-config1.2") lazy val config12 = ConfigAxis("Config1_2", "-config1.2")
lazy val config13 = ConfigAxis("Config1_3", "-config1.3") lazy val config13 = ConfigAxis("Config1_3", "-config1.3")
@ -16,7 +15,6 @@ lazy val scala212 = "2.12.10"
lazy val app = (projectMatrix in file("app")) lazy val app = (projectMatrix in file("app"))
.settings( .settings(
name := "app", name := "app",
ivyPaths := (ThisBuild / ivyPaths).value
) )
.customRow( .customRow(
scalaVersions = Seq(scala212), scalaVersions = Seq(scala212),

View File

@ -1,13 +0,0 @@
lazy val check = taskKey[Unit]("")
lazy val root = (project in file("."))
lazy val foo = project
lazy val bar = project
def scala212 = "2.12.17"
scalaVersion := scala212
check := {
assert((root / scalaVersion).value == scala212)
assert((foo / scalaVersion).value == scala212)
assert((bar / scalaVersion).value == scala212)
}

View File

@ -0,0 +1 @@
organization := "com.example.baz"

View File

@ -0,0 +1,28 @@
lazy val check = taskKey[Unit]("")
def scala212 = "2.12.19"
scalaVersion := scala212
val o = "com.example"
organization := o
lazy val foo = project
lazy val bar = project
.settings(
name := "bar",
organization := "com.example.bar",
)
lazy val baz = project
check := {
assert((foo / scalaVersion).value == scala212)
assert((bar / scalaVersion).value == scala212)
assert((baz / scalaVersion).value == scala212)
assert((foo / organization).value == o)
// Test that bar can override common setting in settings(...)
assert((bar / organization).value == "com.example.bar", s"unexpected bar / organization = {(bar / organization).value}")
// Test that baz/build.sbt bare settings get loaded
assert((baz / organization).value == "com.example.baz", s"unexpected baz/organization")
}
check / aggregate := false

View File

@ -0,0 +1 @@
> check

View File

@ -0,0 +1 @@
organization := "com.example.baz"

View File

@ -0,0 +1,33 @@
lazy val check = taskKey[Unit]("")
def scala212 = "2.12.19"
scalaVersion := scala212
val o = "com.example"
organization := o
lazy val root = (project in file("."))
.aggregate(foo, bar, baz)
lazy val foo = project
lazy val bar = project
.settings(
name := "bar",
organization := "com.example.bar",
)
lazy val baz = project
check := {
assert((root / scalaVersion).value == scala212)
assert((foo / scalaVersion).value == scala212)
assert((bar / scalaVersion).value == scala212)
assert((baz / scalaVersion).value == scala212)
assert((root / organization).value == o)
assert((foo / organization).value == o)
// Test that bar can override common setting in settings(...)
assert((bar / organization).value == "com.example.bar")
// Test that baz/build.sbt bare settings get loaded
assert((baz / organization).value == "com.example.baz")
}
check / aggregate := false

View File

@ -0,0 +1 @@
> check

View File

@ -1,13 +1,19 @@
ThisBuild / scalaVersion := "2.12.17" scalaVersion := "2.12.19"
lazy val sub1 = project lazy val sub1 = project
lazy val sub2 = project lazy val sub2 = project
val assertNoWarning = taskKey[Unit]("checks warning *is not* emitted") val assertNoWarning = taskKey[Unit]("checks warning *is not* emitted")
val assertWarning = taskKey[Unit]("checks warning *is* emitted") val assertWarning = taskKey[Unit]("checks warning *is* emitted")
lazy val root = (project in file("."))
.aggregate(sub1, sub2)
.settings(
assertWarning := check(true).value,
assertNoWarning := check(false).value,
)
def check(expectation: Boolean) = Def.task[Unit] { def check(expectation: Boolean) = Def.task[Unit] {
val lastLog: File = BuiltinCommands.lastLogFile(state.value).get val lastLog: File = BuiltinCommands.lastLogFile(state.value).get
val last: String = IO.read(lastLog) val last: String = IO.read(lastLog)
@ -20,7 +26,3 @@ def check(expectation: Boolean) = Def.task[Unit] {
IO.write(lastLog, "") // clear the backing log for for 'last'. IO.write(lastLog, "") // clear the backing log for for 'last'.
} }
} }
assertWarning := check(true).value
assertNoWarning := check(false).value