Interpret bare settings are common settings

See https://eed3si9n.com/simplifying-sbt-with-common-settings/

Problem
-------
The behavior of bare settings is confusing in a multi-project build.
This is partly due to the fact that to use `ThisBuild` scoping
the build user needs to be aware of the task implementation,
and know if the task is already defined at project level.

Solution
--------
This changes the interpretation of the baresettings to be common
settings, which works similar to the way `ThisBuild` behaves in sbt 1.x,
but since this would be a simple append at project-level, it should
work for any tasks or settings.
This commit is contained in:
Eugene Yokota 2023-01-26 15:12:59 -05:00
parent 32ac1ef7da
commit dbaa34bdac
3 changed files with 93 additions and 35 deletions

View File

@ -65,6 +65,8 @@ sealed trait ProjectDefinition[PR <: ProjectReference] {
/** The [[AutoPlugin]]s enabled for this project. This value is only available on a loaded Project. */ /** The [[AutoPlugin]]s enabled for this project. This value is only available on a loaded Project. */
private[sbt] def autoPlugins: Seq[AutoPlugin] private[sbt] def autoPlugins: Seq[AutoPlugin]
private[sbt] def commonSettings: Seq[Setting[_]]
override final def hashCode: Int = id.hashCode ^ base.hashCode ^ getClass.hashCode override final def hashCode: Int = id.hashCode ^ base.hashCode ^ getClass.hashCode
override final def equals(o: Any) = o match { override final def equals(o: Any) = o match {
@ -161,6 +163,9 @@ sealed trait Project extends ProjectDefinition[ProjectReference] with CompositeP
/** Definitively set the [[ProjectOrigin]] for this project. */ /** Definitively set the [[ProjectOrigin]] for this project. */
private[sbt] def setProjectOrigin(origin: ProjectOrigin): Project = copy(projectOrigin = origin) private[sbt] def setProjectOrigin(origin: ProjectOrigin): Project = copy(projectOrigin = origin)
private[sbt] def setCommonSettings(settings: Seq[Setting[_]]): Project =
copy(commonSettings = settings)
/** /**
* Applies the given functions to this Project. * Applies the given functions to this Project.
* The second function is applied to the result of applying the first to this Project and so on. * The second function is applied to the result of applying the first to this Project and so on.
@ -180,6 +185,7 @@ sealed trait Project extends ProjectDefinition[ProjectReference] with CompositeP
aggregate: Seq[ProjectReference] = aggregate, aggregate: Seq[ProjectReference] = aggregate,
dependencies: Seq[ClasspathDep[ProjectReference]] = dependencies, dependencies: Seq[ClasspathDep[ProjectReference]] = dependencies,
settings: Seq[Setting[_]] = settings, settings: Seq[Setting[_]] = settings,
commonSettings: Seq[Setting[_]] = commonSettings,
configurations: Seq[Configuration] = configurations, configurations: Seq[Configuration] = configurations,
plugins: Plugins = plugins, plugins: Plugins = plugins,
autoPlugins: Seq[AutoPlugin] = autoPlugins, autoPlugins: Seq[AutoPlugin] = autoPlugins,
@ -191,6 +197,7 @@ sealed trait Project extends ProjectDefinition[ProjectReference] with CompositeP
aggregate = aggregate, aggregate = aggregate,
dependencies = dependencies, dependencies = dependencies,
settings = settings, settings = settings,
commonSettings = commonSettings,
configurations, configurations,
plugins, plugins,
autoPlugins, autoPlugins,
@ -218,6 +225,7 @@ sealed trait Project extends ProjectDefinition[ProjectReference] with CompositeP
aggregate = resolveRefs(aggregate), aggregate = resolveRefs(aggregate),
dependencies = resolveDeps(dependencies), dependencies = resolveDeps(dependencies),
settings, settings,
commonSettings,
configurations, configurations,
plugins, plugins,
autoPlugins, autoPlugins,
@ -227,7 +235,7 @@ end Project
object Project: object Project:
def apply(id: String, base: File): Project = def apply(id: String, base: File): Project =
unresolved(id, base, Nil, Nil, Nil, Nil, Plugins.empty, Nil, ProjectOrigin.Organic) unresolved(id, base, Nil, Nil, Nil, Nil, Nil, Plugins.empty, Nil, ProjectOrigin.Organic)
/** This is a variation of def apply that mixes in GeneratedRootProject. */ /** This is a variation of def apply that mixes in GeneratedRootProject. */
private[sbt] def mkGeneratedRoot( private[sbt] def mkGeneratedRoot(
@ -238,7 +246,7 @@ object Project:
validProjectID(id).foreach(errMsg => sys.error(s"Invalid project ID: $errMsg")) validProjectID(id).foreach(errMsg => sys.error(s"Invalid project ID: $errMsg"))
val plugins = Plugins.empty val plugins = Plugins.empty
val origin = ProjectOrigin.GenericRoot val origin = ProjectOrigin.GenericRoot
new ProjectDef(id, base, aggregate, Nil, Nil, Nil, plugins, Nil, origin) new ProjectDef(id, base, aggregate, Nil, Nil, Nil, Nil, plugins, Nil, origin)
with Project with Project
with GeneratedRootProject with GeneratedRootProject
@ -248,6 +256,7 @@ object Project:
val aggregate: Seq[PR], val aggregate: Seq[PR],
val dependencies: Seq[ClasspathDep[PR]], val dependencies: Seq[ClasspathDep[PR]],
val settings: Seq[Def.Setting[_]], val settings: Seq[Def.Setting[_]],
val commonSettings: Seq[Def.Setting[_]],
val configurations: Seq[Configuration], val configurations: Seq[Configuration],
val plugins: Plugins, val plugins: Plugins,
val autoPlugins: Seq[AutoPlugin], val autoPlugins: Seq[AutoPlugin],
@ -265,6 +274,7 @@ object Project:
aggregate: Seq[ProjectReference], aggregate: Seq[ProjectReference],
dependencies: Seq[ClasspathDep[ProjectReference]], dependencies: Seq[ClasspathDep[ProjectReference]],
settings: Seq[Def.Setting[_]], settings: Seq[Def.Setting[_]],
commonSettings: Seq[Def.Setting[_]],
configurations: Seq[Configuration], configurations: Seq[Configuration],
plugins: Plugins, plugins: Plugins,
autoPlugins: Seq[AutoPlugin], autoPlugins: Seq[AutoPlugin],
@ -277,6 +287,7 @@ object Project:
aggregate, aggregate,
dependencies, dependencies,
settings, settings,
commonSettings,
configurations, configurations,
plugins, plugins,
autoPlugins, autoPlugins,
@ -291,6 +302,7 @@ object Project:
aggregate: Seq[ProjectRef], aggregate: Seq[ProjectRef],
dependencies: Seq[ClasspathDep[ProjectRef]], dependencies: Seq[ClasspathDep[ProjectRef]],
settings: Seq[Def.Setting[_]], settings: Seq[Def.Setting[_]],
commonSettings: Seq[Def.Setting[_]],
configurations: Seq[Configuration], configurations: Seq[Configuration],
plugins: Plugins, plugins: Plugins,
autoPlugins: Seq[AutoPlugin], autoPlugins: Seq[AutoPlugin],
@ -302,6 +314,7 @@ object Project:
aggregate, aggregate,
dependencies, dependencies,
settings, settings,
commonSettings,
configurations, configurations,
plugins, plugins,
autoPlugins, autoPlugins,

View File

@ -78,7 +78,8 @@ private[sbt] object Load {
"JAVA_HOME" -> javaHome, "JAVA_HOME" -> javaHome,
) )
val loader = getClass.getClassLoader val loader = getClass.getClassLoader
val classpath = Attributed.blankSeq(provider.mainClasspath ++ scalaProvider.jars) val classpath =
Attributed.blankSeq(provider.mainClasspath.toIndexedSeq ++ scalaProvider.jars.toIndexedSeq)
val ivyConfiguration = val ivyConfiguration =
InlineIvyConfiguration() InlineIvyConfiguration()
.withPaths(IvyPaths(baseDirectory, bootIvyHome(state.configuration))) .withPaths(IvyPaths(baseDirectory, bootIvyHome(state.configuration)))
@ -339,7 +340,7 @@ private[sbt] object Load {
val keys = Index.allKeys(settings) val keys = Index.allKeys(settings)
val attributeKeys = Index.attributeKeys(data) ++ keys.map(_.key) val attributeKeys = Index.attributeKeys(data) ++ keys.map(_.key)
val scopedKeys = keys ++ data.allKeys((s, k) => ScopedKey(s, k)).toVector val scopedKeys = keys ++ data.allKeys((s, k) => ScopedKey(s, k)).toVector
val projectsMap = projects.mapValues(_.defined.keySet).toMap val projectsMap = projects.view.mapValues(_.defined.keySet).toMap
val configsMap: Map[String, Seq[Configuration]] = val configsMap: Map[String, Seq[Configuration]] =
projects.values.flatMap(bu => bu.defined map { case (k, v) => (k, v.configurations) }).toMap projects.values.flatMap(bu => bu.defined map { case (k, v) => (k, v.configurations) }).toMap
val keyIndex = KeyIndex(scopedKeys.toVector, projectsMap, configsMap) val keyIndex = KeyIndex(scopedKeys.toVector, projectsMap, configsMap)
@ -403,7 +404,7 @@ private[sbt] object Load {
yield ((ref / ConfigKey(c.name) / configuration) :== c) yield ((ref / ConfigKey(c.name) / configuration) :== c)
val builtin: Seq[Setting[_]] = val builtin: Seq[Setting[_]] =
(thisProject :== project) +: (thisProjectRef :== ref) +: defineConfig (thisProject :== project) +: (thisProjectRef :== ref) +: defineConfig
val settings = builtin ++ project.settings ++ injectSettings.project val settings = builtin ++ injectSettings.project ++ project.settings
// map This to thisScope, Select(p) to mapRef(uri, rootProject, p) // map This to thisScope, Select(p) to mapRef(uri, rootProject, p)
transformSettings(projectScope(ref), uri, rootProject, settings) transformSettings(projectScope(ref), uri, rootProject, settings)
} }
@ -769,6 +770,7 @@ private[sbt] object Load {
() => eval, () => eval,
config.injectSettings, config.injectSettings,
Nil, Nil,
Nil,
memoSettings, memoSettings,
config.log, config.log,
createRoot, createRoot,
@ -886,7 +888,7 @@ private[sbt] object Load {
* @param buildBase The `baseDirectory` for the entire build. * @param buildBase The `baseDirectory` for the entire build.
* @param plugins A misnomer, this is actually the compiled BuildDefinition (classpath and such) for this project. * @param plugins A misnomer, this is actually the compiled BuildDefinition (classpath and such) for this project.
* @param eval A mechanism of generating an "Eval" which can compile scala code for us. * @param eval A mechanism of generating an "Eval" which can compile scala code for us.
* @param injectSettings Settings we need to inject into projects. * @param machineWideUserSettings Settings we need to inject into projects.
* @param acc An accumulated list of loaded projects, originally in newProjects. * @param acc An accumulated list of loaded projects, originally in newProjects.
* @param memoSettings A recording of all sbt files that have been loaded so far. * @param memoSettings A recording of all sbt files that have been loaded so far.
* @param log The logger used for this project. * @param log The logger used for this project.
@ -902,7 +904,8 @@ private[sbt] object Load {
buildBase: File, buildBase: File,
plugins: LoadedPlugins, plugins: LoadedPlugins,
eval: () => Eval, eval: () => Eval,
injectSettings: InjectSettings, machineWideUserSettings: InjectSettings,
commonSettings: Seq[Setting[_]],
acc: Seq[Project], acc: Seq[Project],
memoSettings: mutable.Map[VirtualFile, LoadedSbtFile], memoSettings: mutable.Map[VirtualFile, LoadedSbtFile],
log: Logger, log: Logger,
@ -914,14 +917,19 @@ private[sbt] object Load {
converter: MappedFileConverter, converter: MappedFileConverter,
): LoadedProjects = ): LoadedProjects =
/*timed(s"Load.loadTransitive(${ newProjects.map(_.id) })", log)*/ { /*timed(s"Load.loadTransitive(${ newProjects.map(_.id) })", log)*/ {
def load(
def load(newProjects: Seq[Project], acc: Seq[Project], generated: Seq[Path]) = newProjects: Seq[Project],
acc: Seq[Project],
generated: Seq[Path],
commonSettings0: Seq[Setting[_]],
) =
loadTransitive( loadTransitive(
newProjects, newProjects,
buildBase, buildBase,
plugins, plugins,
eval, eval,
injectSettings, machineWideUserSettings,
commonSettings0,
acc, acc,
memoSettings, memoSettings,
log, log,
@ -966,7 +974,8 @@ private[sbt] object Load {
p1, p1,
autoPlugins, autoPlugins,
plugins, plugins,
injectSettings, commonSettings,
machineWideUserSettings,
memoSettings, memoSettings,
extraFiles, extraFiles,
converter, converter,
@ -995,12 +1004,12 @@ 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) load(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.
// Continue loading if we find any more. // Continue loading if we find any more.
newProjects match { newProjects match
case Seq(next, rest @ _*) => case Seq(next, rest @ _*) =>
log.debug(s"[Loading] Loading project ${next.id} @ ${next.base}") log.debug(s"[Loading] Loading project ${next.id} @ ${next.base}")
discoverAndLoad(next, rest) discoverAndLoad(next, rest)
@ -1018,7 +1027,7 @@ 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) val otherProjects = load(discovered, acc, Nil, Nil)
val root = { val root = {
val existingIds = otherProjects.projects.map(_.id) val existingIds = otherProjects.projects.map(_.id)
val defaultID = autoID(buildBase, context, existingIds) val defaultID = autoID(buildBase, context, existingIds)
@ -1036,12 +1045,11 @@ 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) load(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)
}
} }
private[this] def translateAutoPluginException( private[this] def translateAutoPluginException(
@ -1085,7 +1093,8 @@ private[sbt] object Load {
p: Project, p: Project,
projectPlugins: Seq[AutoPlugin], projectPlugins: Seq[AutoPlugin],
loadedPlugins: LoadedPlugins, loadedPlugins: LoadedPlugins,
globalUserSettings: InjectSettings, commonSettings0: Seq[Setting[_]],
machineWideUserSettings: InjectSettings,
memoSettings: mutable.Map[VirtualFile, LoadedSbtFile], memoSettings: mutable.Map[VirtualFile, LoadedSbtFile],
extraSbtFiles: Seq[VirtualFile], extraSbtFiles: Seq[VirtualFile],
converter: MappedFileConverter, converter: MappedFileConverter,
@ -1094,47 +1103,70 @@ private[sbt] object Load {
timed(s"Load.resolveProject(${p.id})", log) { timed(s"Load.resolveProject(${p.id})", log) {
import AddSettings._ import AddSettings._
val autoConfigs = projectPlugins.flatMap(_.projectConfigurations) val autoConfigs = projectPlugins.flatMap(_.projectConfigurations)
val auto = AddSettings.allDefaults
// 3. Use AddSettings instance to order all Setting[_]s appropriately // 3. Use AddSettings instance to order all Setting[_]s appropriately
val allSettings = { // Settings are ordered as:
// AutoPlugin settings, common settings, machine-wide settings + project.settings(...)
def allAutoPluginSettings: Seq[Setting[_]] = {
// Filter the AutoPlugin settings we included based on which ones are
// intended in the AddSettings.AutoPlugins filter.
def autoPluginSettings(f: AutoPlugins) =
projectPlugins.filter(f.include).flatMap(_.projectSettings)
// Expand the AddSettings instance into a real Seq[Setting[_]] we'll use on the project
def expandPluginSettings(auto: AddSettings): Seq[Setting[_]] =
auto match
case p: AutoPlugins => autoPluginSettings(p)
case q: Sequence =>
q.sequence.foldLeft(Seq.empty[Setting[_]]) { (b, add) =>
b ++ expandPluginSettings(add)
}
case _ => Nil
expandPluginSettings(auto)
}
def buildWideCommonSettings: Seq[Setting[_]] = {
// TODO - This mechanism of applying settings could be off... It's in two places now... // TODO - This mechanism of applying settings could be off... It's in two places now...
lazy val defaultSbtFiles = configurationSources(p.base.getCanonicalFile()) lazy val defaultSbtFiles = configurationSources(p.base.getCanonicalFile())
.map(_.getAbsoluteFile().toPath()) .map(_.getAbsoluteFile().toPath())
.map(converter.toVirtualFile) .map(converter.toVirtualFile)
lazy val sbtFiles: Seq[VirtualFile] = defaultSbtFiles ++ extraSbtFiles lazy val sbtFiles: Seq[VirtualFile] = defaultSbtFiles ++ extraSbtFiles
// Filter the AutoPlugin settings we included based on which ones are
// intended in the AddSettings.AutoPlugins filter.
def autoPluginSettings(f: AutoPlugins) =
projectPlugins.filter(f.include).flatMap(_.projectSettings)
// Grab all the settings we already loaded from sbt files // Grab all the settings we already loaded from sbt files
def settings(files: Seq[VirtualFile]): Seq[Setting[_]] = { def settings(files: Seq[VirtualFile]): Seq[Setting[_]] =
if (files.nonEmpty) if files.nonEmpty then
log.info( log.info(
s"${files.map(_.name()).mkString(s"loading settings for project ${p.id} from ", ",", " ...")}" s"${files.map(_.name()).mkString(s"loading settings for project ${p.id} from ", ",", " ...")}"
) )
for { else ()
for
file <- files file <- files
config <- memoSettings.get(file).toSeq config <- memoSettings.get(file).toSeq
setting <- config.settings setting <- config.settings
} yield setting 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
case User => machineWideUserSettings.cachedProjectLoaded(loadedPlugins.loader)
case BuildScalaFiles => p.settings case BuildScalaFiles => p.settings
case User => globalUserSettings.cachedProjectLoaded(loadedPlugins.loader)
// case sf: SbtFiles => settings(sf.files.map(f => IO.resolve(p.base, f)))
case sf: DefaultSbtFiles => settings(sbtFiles.filter(sf.include))
case p: AutoPlugins => autoPluginSettings(p)
case q: Sequence => case q: Sequence =>
q.sequence.foldLeft(Seq.empty[Setting[_]]) { (b, add) => q.sequence.foldLeft(Seq.empty[Setting[_]]) { (b, add) =>
b ++ expandSettings(add) b ++ expandSettings(add)
} }
val auto = AddSettings.allDefaults case _ => Nil
expandSettings(auto) expandSettings(auto)
} }
// Finally, a project we can use in buildStructure. // Finally, a project we can use in buildStructure.
p.copy(settings = allSettings) p.copy(settings = allAutoPluginSettings ++ buildWideCommonSettings ++ allProjectSettings)
.setCommonSettings(buildWideCommonSettings)
.setAutoPlugins(projectPlugins) .setAutoPlugins(projectPlugins)
.prefixConfigs(autoConfigs: _*) .prefixConfigs(autoConfigs: _*)
} }
@ -1448,7 +1480,7 @@ private[sbt] object Load {
final class EvaluatedConfigurations(val eval: Eval, val settings: Seq[Setting[_]]) final class EvaluatedConfigurations(val eval: Eval, val settings: Seq[Setting[_]])
final case class InjectSettings( case class InjectSettings(
global: Seq[Setting[_]], global: Seq[Setting[_]],
project: Seq[Setting[_]], project: Seq[Setting[_]],
projectLoaded: ClassLoader => Seq[Setting[_]] projectLoaded: ClassLoader => Seq[Setting[_]]

View File

@ -0,0 +1,13 @@
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)
}