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.nio.Settings
import sbt.util.{ Logger, Show }
import xsbti.{ HashedVirtualFileRef, VirtualFile }
import xsbti.{ FileConverter, HashedVirtualFileRef, VirtualFile }
import xsbti.compile.{ ClasspathOptionsUtil, Compilers }
import java.io.File
import java.net.URI
@ -943,142 +943,160 @@ private[sbt] object Load {
extraSbtFiles: Seq[VirtualFile],
converter: MappedFileConverter,
): LoadedProjects =
/*timed(s"Load.loadTransitive(${ newProjects.map(_.id) })", log)*/ {
def load(
newProjects: Seq[Project],
acc: Seq[Project],
generated: Seq[Path],
commonSettings0: Seq[Setting[_]],
) =
loadTransitive(
newProjects,
buildBase,
plugins,
eval,
machineWideUserSettings,
commonSettings0,
acc,
memoSettings,
log,
makeOrDiscoverRoot = false,
buildUri,
context,
generated,
Nil,
converter,
)
// alias for parameter forwarding
def loadTransitive1(
newProjects: Seq[Project],
acc: Seq[Project],
generated: Seq[Path],
commonSettings0: Seq[Setting[_]],
): LoadedProjects =
loadTransitive(
newProjects,
buildBase,
plugins,
eval,
machineWideUserSettings,
commonSettings0,
acc,
memoSettings,
log,
makeOrDiscoverRoot = false,
buildUri,
context,
generated,
Nil,
converter,
)
// load all relevant configuration files (.sbt, as .scala already exists at this point)
def discover(base: File): DiscoveredProjects = {
val auto =
if (base.getCanonicalFile() == buildBase.getCanonicalFile()) AddSettings.allDefaults
else AddSettings.defaultSbtFiles
// alias for parameter forwarding
def expandCommonSettingsPerBase1(directory: File): Seq[Setting[?]] =
expandCommonSettingsPerBase(
directory = directory,
memoSettings = memoSettings,
extraSbtFiles = extraSbtFiles,
converter = converter,
log = log,
)
val extraFiles =
if base.getCanonicalFile() == buildBase.getCanonicalFile() && isMetaBuildContext(context)
then extraSbtFiles
else Nil
discoverProjects(auto, base, extraFiles, plugins, eval, memoSettings, converter)
}
// load all relevant configuration files (.sbt, as .scala already exists at this point)
def discover(base: File): DiscoveredProjects = {
val auto =
if (base.getCanonicalFile() == buildBase.getCanonicalFile()) AddSettings.allDefaults
else AddSettings.defaultSbtFiles
// 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[VirtualFile],
extraFiles: Seq[VirtualFile],
expand: Boolean
): (Project, Seq[Project]) = {
val configFiles = files.flatMap(f => memoSettings.get(f))
val p1: Project = Function.chain(configFiles.flatMap(_.manipulations))(p)
val autoPlugins: Seq[AutoPlugin] =
try plugins.detected.deducePluginsFromProject(p1, log)
catch { case e: AutoPluginException => throw translateAutoPluginException(e, p) }
val p2 =
resolveProject(
p1,
autoPlugins,
plugins,
commonSettings,
machineWideUserSettings,
memoSettings,
extraFiles,
converter,
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.
def discoverAndLoad(p: Project, rest: Seq[Project]): LoadedProjects = {
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 (finalRoot, projectLevelExtra) = finalizeProject(root, files, extraFiles, true)
val newProjects = rest ++ discovered ++ projectLevelExtra
val newAcc = acc :+ finalRoot
val newGenerated = generated ++ generatedConfigClassFiles
load(newProjects, newAcc, newGenerated, finalRoot.commonSettings)
}
// Load all config files AND finalize the project at the root directory, if it exists.
// Continue loading if we find any more.
newProjects match
case Seq(next, rest @ _*) =>
log.debug(s"[Loading] Loading project ${next.id} @ ${next.base}")
discoverAndLoad(next, rest)
case Nil if makeOrDiscoverRoot =>
log.debug(s"[Loading] Scanning directory $buildBase")
val DiscoveredProjects(rootOpt, discovered, files, extraFiles, generated) = discover(
buildBase
)
val discoveredIdsStr = discovered.map(_.id).mkString(",")
val (root, expand, 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))
case None =>
log.debug(s"[Loading] Found non-root projects $discoveredIdsStr")
// Here we do something interesting... We need to create an aggregate root project
val otherProjects = load(discovered, acc, Nil, Nil)
val root = {
val existingIds = otherProjects.projects.map(_.id)
val defaultID = autoID(buildBase, context, existingIds)
val refs = existingIds.map(id => ProjectRef(buildUri, id))
if (discovered.isEmpty || java.lang.Boolean.getBoolean("sbt.root.ivyplugin"))
BuildDef.defaultAggregatedProject(defaultID, buildBase, refs)
else BuildDef.generatedRootSkipPublish(defaultID, buildBase, refs)
}
(root, false, Nil, otherProjects)
val (finalRoot, projectLevelExtra) =
timed(s"Load.loadTransitive: finalizeProject($root)", log) {
finalizeProject(root, files, extraFiles, expand)
}
val newProjects = moreProjects ++ projectLevelExtra
val newAcc = finalRoot +: (acc ++ otherProjects.projects)
val newGenerated =
generated ++ otherProjects.generatedConfigClassFiles ++ generatedConfigClassFiles
load(newProjects, newAcc, newGenerated, finalRoot.commonSettings)
case Nil =>
val projectIds = acc.map(_.id).mkString("(", ", ", ")")
log.debug(s"[Loading] Done in $buildBase, returning: $projectIds")
LoadedProjects(acc, generatedConfigClassFiles)
val extraFiles =
if base.getCanonicalFile() == buildBase.getCanonicalFile() && isMetaBuildContext(context)
then extraSbtFiles
else Nil
discoverProjects(auto, base, extraFiles, plugins, eval, memoSettings, converter)
}
// 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[VirtualFile],
extraFiles: Seq[VirtualFile],
expand: Boolean
): (Project, Seq[Project]) = {
val configFiles = files.flatMap(f => memoSettings.get(f))
val p1: Project = Function.chain(configFiles.flatMap(_.manipulations))(p)
val autoPlugins: Seq[AutoPlugin] =
try plugins.detected.deducePluginsFromProject(p1, log)
catch { case e: AutoPluginException => throw translateAutoPluginException(e, p) }
val p2 =
resolveProjectSettings(
p = p1,
projectPlugins = autoPlugins,
loadedPlugins = plugins,
commonSettings =
commonSettings ++ expandCommonSettingsPerBase1(p1.base.getCanonicalFile()),
machineWideUserSettings = machineWideUserSettings,
memoSettings = memoSettings,
extraSbtFiles = extraFiles,
converter = converter,
log = 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.
def discoverAndLoad(p: Project, rest: Seq[Project]): LoadedProjects = {
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 (finalRoot, projectLevelExtra) = finalizeProject(root, files, extraFiles, true)
val newProjects = rest ++ discovered ++ projectLevelExtra
val newAcc = acc :+ finalRoot
val newGenerated = generated ++ generatedConfigClassFiles
loadTransitive1(newProjects, newAcc, newGenerated, finalRoot.commonSettings)
}
// Load all config files AND finalize the project at the root directory, if it exists.
// Continue loading if we find any more.
newProjects match
case Seq(next, rest @ _*) =>
log.debug(s"[Loading] Loading project ${next.id} @ ${next.base}")
discoverAndLoad(next, rest)
case Nil if makeOrDiscoverRoot =>
log.debug(s"[Loading] Scanning directory $buildBase")
val DiscoveredProjects(rootOpt, discovered, files, extraFiles, generated) = discover(
buildBase
)
val discoveredIdsStr = discovered.map(_.id).mkString(",")
val (root, expand, 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))
case None =>
log.debug(s"[Loading] Found non-root projects $discoveredIdsStr")
// Here we do something interesting... We need to create an aggregate root project
val root = {
val defaultID = autoID(buildBase, context, Nil)
if discovered.isEmpty || java.lang.Boolean.getBoolean("sbt.root.ivyplugin") then
BuildDef.defaultProject(defaultID, buildBase)
else BuildDef.generatedRootSkipPublish(defaultID, buildBase, Nil)
}
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) =
timed(s"Load.loadTransitive: finalizeProject($root)", log) {
finalizeProject(root, files, extraFiles, expand)
}
val newProjects = moreProjects ++ projectLevelExtra
val newAcc = finalRoot +: (acc ++ otherProjects.projects)
val newGenerated =
generated ++ otherProjects.generatedConfigClassFiles ++ generatedConfigClassFiles
loadTransitive1(newProjects, newAcc, newGenerated, finalRoot.commonSettings)
case Nil =>
val projectIds = acc.map(_.id).mkString("(", ", ", ")")
log.debug(s"[Loading] Done in $buildBase, returning: $projectIds")
LoadedProjects(acc, generatedConfigClassFiles)
end loadTransitive
private[this] def translateAutoPluginException(
e: AutoPluginException,
project: Project
@ -1116,18 +1134,18 @@ private[sbt] object Load {
* @param extraSbtFiles Extra *.sbt files.
* @param log A logger to report auto-plugin issues to.
*/
private[sbt] def resolveProject(
private[sbt] def resolveProjectSettings(
p: Project,
projectPlugins: Seq[AutoPlugin],
loadedPlugins: LoadedPlugins,
commonSettings0: Seq[Setting[_]],
commonSettings: Seq[Setting[_]],
machineWideUserSettings: InjectSettings,
memoSettings: mutable.Map[VirtualFile, LoadedSbtFile],
extraSbtFiles: Seq[VirtualFile],
converter: MappedFileConverter,
log: Logger
): Project =
timed(s"Load.resolveProject(${p.id})", log) {
timed(s"Load.resolveProjectSettings(${p.id})", log) {
import AddSettings._
val autoConfigs = projectPlugins.flatMap(_.projectConfigurations)
val auto = AddSettings.allDefaults
@ -1150,35 +1168,7 @@ private[sbt] object Load {
case _ => Nil
expandPluginSettings(auto)
}
def buildWideCommonSettings: 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[_]] = {
def allProjectSettings: Seq[Setting[_]] =
// Expand the AddSettings instance into a real Seq[Setting[_]] we'll use on the project
def expandSettings(auto: AddSettings): Seq[Setting[_]] =
auto match
@ -1190,14 +1180,46 @@ private[sbt] object Load {
}
case _ => Nil
expandSettings(auto)
}
end allProjectSettings
// Finally, a project we can use in buildStructure.
p.copy(settings = allAutoPluginSettings ++ buildWideCommonSettings ++ allProjectSettings)
.setCommonSettings(buildWideCommonSettings)
p.copy(settings = allAutoPluginSettings ++ commonSettings ++ allProjectSettings)
.setCommonSettings(commonSettings)
.setAutoPlugins(projectPlugins)
.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.
*

View File

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

View File

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

View File

@ -1,12 +1,11 @@
ThisBuild / organization := "com.example"
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / publishMavenStyle := true
ThisBuild / ivyPaths := {
val base = (ThisBuild / baseDirectory).value
IvyPaths(base.toString, s"$base/ivy-cache")
organization := "com.example"
version := "0.1.0-SNAPSHOT"
publishMavenStyle := true
ivyPaths := {
val base = baseDirectory.value
val thisBuildBase = (ThisBuild / baseDirectory).value
IvyPaths(base.toString, s"$thisBuildBase/ivy-cache")
}
publish / skip := true
lazy val config12 = ConfigAxis("Config1_2", "-config1.2")
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"))
.settings(
name := "app",
ivyPaths := (ThisBuild / ivyPaths).value
)
.customRow(
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 sub2 = project
val assertNoWarning = taskKey[Unit]("checks warning *is not* 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] {
val lastLog: File = BuiltinCommands.lastLogFile(state.value).get
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'.
}
}
assertWarning := check(true).value
assertNoWarning := check(false).value