[2.x] feat: XDG directory standard (#8769)

- Add SysProp.defaultGlobalBaseDirectory: uses SBT_CONFIG_HOME,
  XDG_CONFIG_HOME/sbt (Unix), LOCALAPPDATA/sbt (Windows), else ~/.sbt
- BuildPaths.defaultGlobalBase delegates to SysProp for consistent default
- sbt script: inject -Dsbt.global.base from XDG when not already set;
  getPreloaded() fallback uses SBT_CONFIG_HOME/XDG_CONFIG_HOME
- sbt.bat: use LOCALAPPDATA/sbt when --sbt-dir not set
- Tests: BuildPathsTest (property + absolute path), RunnerScriptTest (XDG)
This commit is contained in:
bitloi 2026-02-20 23:32:48 -05:00 committed by GitHub
parent 2f14fd8bb1
commit c6f67d706f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 88 additions and 30 deletions

View File

@ -122,6 +122,14 @@ object RunnerScriptTest extends verify.BasicTestSuite with ShellScriptUtil:
assert(out.contains[String]("-Dsbt.boot.directory=project/.boot"))
assert(out.contains[String]("-Dsbt.ivy.home=project/.ivy"))
testOutput(
"sbt with XDG_CONFIG_HOME set uses it for sbt.global.base (Closes #3681)",
machineSbtoptsContents = "-v",
windowsSupport = false,
)("compile", "-v"): (out: List[String]) =>
val hasGlobalBase = out.exists(s => s.contains("-Dsbt.global.base=") && s.contains("/sbt"))
assert(hasGlobalBase, s"Expected -Dsbt.global.base=.../sbt in output (XDG): ${out.mkString(System.lineSeparator())}")
testOutput("accept `--ivy` in `SBT_OPTS`", sbtOpts = "--ivy /ivy/dir")("-v"):
(out: List[String]) =>
if (isWindows) cancel("Test not supported on windows")

View File

@ -675,6 +675,8 @@ if defined sbt_args_sbt_version (
if defined sbt_args_sbt_dir (
set _SBT_OPTS=-Dsbt.global.base=!sbt_args_sbt_dir! !_SBT_OPTS!
) else if defined LOCALAPPDATA (
set _SBT_OPTS=-Dsbt.global.base=!LOCALAPPDATA!\sbt !_SBT_OPTS!
)
if defined sbt_args_sbt_boot (

View File

@ -12,10 +12,10 @@ import java.io.File
import java.util.Locale
import KeyRanks.DSetting
import sbt.io.{ GlobFilter, Path }
import sbt.io.GlobFilter
import sbt.internal.util.AttributeKey
object BuildPaths {
object BuildPaths:
val globalBaseDirectory = AttributeKey[File](
"global-base-directory",
"The base directory for global sbt configuration and staging.",
@ -106,7 +106,7 @@ object BuildPaths {
}
def defaultVersionedGlobalBase(sbtVersion: String): File = defaultGlobalBase / sbtVersion
def defaultGlobalBase = Path.userHome / ConfigDirectoryName
def defaultGlobalBase: File = internal.SysProp.defaultGlobalBaseDirectory
private def binarySbtVersion(state: State): String =
sbt.internal.librarymanagement.cross.CrossVersionUtil
@ -145,4 +145,4 @@ object BuildPaths {
def crossPath(base: File, instance: xsbti.compile.ScalaInstance): File =
base / ("scala_" + instance.version)
}
end BuildPaths

View File

@ -25,7 +25,7 @@ import sbt.nio.Keys.*
// See also BuildPaths.scala
// See also LineReader.scala
object SysProp {
object SysProp:
def booleanOpt(name: String): Option[Boolean] =
sys.props.get(name) match {
case Some(x) => parseBoolean(x)
@ -204,6 +204,25 @@ object SysProp {
private def file(value: String): File = new File(value)
private def home: File = file(sys.props("user.home"))
/**
* Default directory for global sbt config (plugins, settings). Respects XDG Base Directory
* and platform conventions: SBT_CONFIG_HOME, then XDG_CONFIG_HOME/sbt (Unix), then
* LOCALAPPDATA/sbt (Windows), else user.home/.sbt.
*/
def defaultGlobalBaseDirectory: File =
def fromEnv(name: String): Option[File] =
sys.env.get(name).filter(_.nonEmpty).map(p => file(p.trim))
val propBase =
sys.props.get(BuildPaths.GlobalBaseProperty).filter(_.nonEmpty).map(p => file(p.trim))
propBase
.orElse(fromEnv("SBT_CONFIG_HOME"))
.orElse(
if Util.isWindows then fromEnv("LOCALAPPDATA").map(_ / "sbt")
else fromEnv("XDG_CONFIG_HOME").map(_ / "sbt")
)
.getOrElse(home / BuildPaths.ConfigDirectoryName)
.getAbsoluteFile
/**
* Operating system specific cache directory, similar to Coursier cache.
*/
@ -289,4 +308,4 @@ object SysProp {
.get(sys.env.getOrElse("XDG_RUNTIME_DIR", sys.props("java.io.tmpdir")))
.resolve(s".sbt$halfhash")
}
}
end SysProp

View File

@ -8,43 +8,50 @@
package sbt
import BuildPaths.expandTildePrefix
import java.io.File
import BuildPaths.{ expandTildePrefix, defaultGlobalBase, GlobalBaseProperty }
object BuildPathsTest extends verify.BasicTestSuite {
object BuildPathsTest extends verify.BasicTestSuite:
test("expandTildePrefix should expand empty path to itself") {
test("defaultGlobalBase respects sbt.global.base system property"):
val custom = new File(System.getProperty("java.io.tmpdir"), "sbt-test-3681").getAbsolutePath
val prev = sys.props.get(GlobalBaseProperty)
try
sys.props(GlobalBaseProperty) = custom
assert(defaultGlobalBase.getAbsolutePath == new File(custom).getAbsolutePath)
finally
prev match
case Some(v) => sys.props(GlobalBaseProperty) = v
case None => sys.props.remove(GlobalBaseProperty)
test("defaultGlobalBase returns absolute path"):
assert(defaultGlobalBase.getAbsolutePath.nonEmpty)
assert(defaultGlobalBase.isAbsolute)
test("expandTildePrefix should expand empty path to itself"):
assertEquals("", expandTildePrefix(""))
}
test("it should expand /home/user/path to itself") {
test("it should expand /home/user/path to itself"):
assertEquals("/home/user/path", expandTildePrefix("/home/user/path"))
}
test("it should expand /~/foo/ to itself") {
test("it should expand /~/foo/ to itself"):
assertEquals("/~/foo/", expandTildePrefix("/~/foo/"))
}
test("it should expand ~ to $HOME") {
test("it should expand ~ to $HOME"):
assertEquals(sys.env.getOrElse("HOME", ""), expandTildePrefix("~"))
}
test("it should expand ~/foo/bar to $HOME/foo/bar") {
test("it should expand ~/foo/bar to $HOME/foo/bar"):
assertEquals(sys.env.getOrElse("HOME", "") + "/foo/bar", expandTildePrefix("~/foo/bar"))
}
test("it should expand ~+ to $PWD") {
test("it should expand ~+ to $PWD"):
assertEquals(sys.env.getOrElse("PWD", ""), expandTildePrefix("~+"))
}
test("it should expand ~+/foo/bar to $PWD/foo/bar") {
test("it should expand ~+/foo/bar to $PWD/foo/bar"):
assertEquals(sys.env.getOrElse("PWD", "") + "/foo/bar", expandTildePrefix("~+/foo/bar"))
}
test("it should expand ~- to $OLDPWD") {
test("it should expand ~- to $OLDPWD"):
assertEquals(sys.env.getOrElse("OLDPWD", ""), expandTildePrefix("~-"))
}
test("it should expand ~-/foo/bar to $OLDPWD/foo/bar") {
test("it should expand ~-/foo/bar to $OLDPWD/foo/bar"):
assertEquals(sys.env.getOrElse("OLDPWD", "") + "/foo/bar", expandTildePrefix("~-/foo/bar"))
}
}
end BuildPathsTest

16
sbt
View File

@ -448,8 +448,8 @@ findProperty() {
done
}
# Extracts the preloaded directory from either -Dsbt.preloaded, -Dsbt.global.base or -Duser.home
# in that order.
# Extracts the preloaded directory from -Dsbt.preloaded, -Dsbt.global.base, SBT_CONFIG_HOME,
# XDG_CONFIG_HOME/sbt, or user.home/.sbt in that order.
getPreloaded() {
local preloaded && preloaded=$(findProperty sbt.preloaded)
[ "$preloaded" ] && echo "$preloaded" && return
@ -457,6 +457,9 @@ getPreloaded() {
local global_base && global_base=$(findProperty sbt.global.base)
[ "$global_base" ] && echo "$global_base/preloaded" && return
[ -n "${SBT_CONFIG_HOME}" ] && echo "${SBT_CONFIG_HOME}/preloaded" && return
[ -n "${XDG_CONFIG_HOME}" ] && echo "${XDG_CONFIG_HOME}/sbt/preloaded" && return
local user_home && user_home=$(findProperty user.home)
echo "${user_home:-$HOME}/.sbt/preloaded"
}
@ -642,7 +645,7 @@ Usage: `basename "$0"` [options]
--jvm-client run JVM client
--timings display task timings report on shutdown
--allow-empty start sbt even if current directory contains no sbt project
--sbt-dir <path> path to global settings/plugins directory (default: ~/.sbt)
--sbt-dir <path> path to global settings/plugins directory (default: \$XDG_CONFIG_HOME/sbt or ~/.sbt)
--sbt-boot <path> path to shared boot directory (default: ~/.sbt/boot in 0.11 series)
--sbt-cache <path> path to global cache directory (default: operating system specific)
--ivy <path> path to local Ivy repository (default: ~/.ivy2)
@ -973,6 +976,13 @@ else
vlog "[process_args] java_version = '$java_version'"
addDefaultMemory
addSbtScriptProperty
if ! printf '%s\n' "${sbt_options[@]}" | grep -q '^-Dsbt\.global\.base='; then
if [[ -n "${SBT_CONFIG_HOME}" ]]; then
addSbt "-Dsbt.global.base=$SBT_CONFIG_HOME"
elif [[ -n "${XDG_CONFIG_HOME}" ]]; then
addSbt "-Dsbt.global.base=${XDG_CONFIG_HOME}/sbt"
fi
fi
addJdkWorkaround
set -- "${residual_args[@]}"
argumentCount=$#

View File

@ -0,0 +1,10 @@
// Scripted test for #3681: default global base (XDG / sbt.global.base) is used correctly.
lazy val checkGlobalBase = taskKey[Unit]("Verifies global base is absolute and non-empty")
lazy val root = (project in file(".")).settings(
checkGlobalBase := {
val g = BuildPaths.getGlobalBase(state.value)
assert(g.isAbsolute, s"expected absolute path: $g")
assert(g.getAbsolutePath.nonEmpty, "global base path must be non-empty")
}
)

View File

@ -0,0 +1,2 @@
# Closes #3681: verify global base (XDG / sbt.global.base) is resolvable and absolute
> checkGlobalBase