mirror of
https://github.com/sbt/sbt.git
synced 2026-08-22 06:07:24 +02:00
[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:
@@ -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.boot.directory=project/.boot"))
|
||||||
assert(out.contains[String]("-Dsbt.ivy.home=project/.ivy"))
|
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"):
|
testOutput("accept `--ivy` in `SBT_OPTS`", sbtOpts = "--ivy /ivy/dir")("-v"):
|
||||||
(out: List[String]) =>
|
(out: List[String]) =>
|
||||||
if (isWindows) cancel("Test not supported on windows")
|
if (isWindows) cancel("Test not supported on windows")
|
||||||
|
|||||||
@@ -675,6 +675,8 @@ if defined sbt_args_sbt_version (
|
|||||||
|
|
||||||
if defined sbt_args_sbt_dir (
|
if defined sbt_args_sbt_dir (
|
||||||
set _SBT_OPTS=-Dsbt.global.base=!sbt_args_sbt_dir! !_SBT_OPTS!
|
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 (
|
if defined sbt_args_sbt_boot (
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ import java.io.File
|
|||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import KeyRanks.DSetting
|
import KeyRanks.DSetting
|
||||||
|
|
||||||
import sbt.io.{ GlobFilter, Path }
|
import sbt.io.GlobFilter
|
||||||
import sbt.internal.util.AttributeKey
|
import sbt.internal.util.AttributeKey
|
||||||
|
|
||||||
object BuildPaths {
|
object BuildPaths:
|
||||||
val globalBaseDirectory = AttributeKey[File](
|
val globalBaseDirectory = AttributeKey[File](
|
||||||
"global-base-directory",
|
"global-base-directory",
|
||||||
"The base directory for global sbt configuration and staging.",
|
"The base directory for global sbt configuration and staging.",
|
||||||
@@ -106,7 +106,7 @@ object BuildPaths {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def defaultVersionedGlobalBase(sbtVersion: String): File = defaultGlobalBase / sbtVersion
|
def defaultVersionedGlobalBase(sbtVersion: String): File = defaultGlobalBase / sbtVersion
|
||||||
def defaultGlobalBase = Path.userHome / ConfigDirectoryName
|
def defaultGlobalBase: File = internal.SysProp.defaultGlobalBaseDirectory
|
||||||
|
|
||||||
private def binarySbtVersion(state: State): String =
|
private def binarySbtVersion(state: State): String =
|
||||||
sbt.internal.librarymanagement.cross.CrossVersionUtil
|
sbt.internal.librarymanagement.cross.CrossVersionUtil
|
||||||
@@ -145,4 +145,4 @@ object BuildPaths {
|
|||||||
|
|
||||||
def crossPath(base: File, instance: xsbti.compile.ScalaInstance): File =
|
def crossPath(base: File, instance: xsbti.compile.ScalaInstance): File =
|
||||||
base / ("scala_" + instance.version)
|
base / ("scala_" + instance.version)
|
||||||
}
|
end BuildPaths
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import sbt.nio.Keys.*
|
|||||||
|
|
||||||
// See also BuildPaths.scala
|
// See also BuildPaths.scala
|
||||||
// See also LineReader.scala
|
// See also LineReader.scala
|
||||||
object SysProp {
|
object SysProp:
|
||||||
def booleanOpt(name: String): Option[Boolean] =
|
def booleanOpt(name: String): Option[Boolean] =
|
||||||
sys.props.get(name) match {
|
sys.props.get(name) match {
|
||||||
case Some(x) => parseBoolean(x)
|
case Some(x) => parseBoolean(x)
|
||||||
@@ -204,6 +204,25 @@ object SysProp {
|
|||||||
private def file(value: String): File = new File(value)
|
private def file(value: String): File = new File(value)
|
||||||
private def home: File = file(sys.props("user.home"))
|
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.
|
* 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")))
|
.get(sys.env.getOrElse("XDG_RUNTIME_DIR", sys.props("java.io.tmpdir")))
|
||||||
.resolve(s".sbt$halfhash")
|
.resolve(s".sbt$halfhash")
|
||||||
}
|
}
|
||||||
}
|
end SysProp
|
||||||
|
|||||||
@@ -8,43 +8,50 @@
|
|||||||
|
|
||||||
package sbt
|
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(""))
|
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"))
|
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/"))
|
assertEquals("/~/foo/", expandTildePrefix("/~/foo/"))
|
||||||
}
|
|
||||||
|
|
||||||
test("it should expand ~ to $HOME") {
|
test("it should expand ~ to $HOME"):
|
||||||
assertEquals(sys.env.getOrElse("HOME", ""), expandTildePrefix("~"))
|
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"))
|
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("~+"))
|
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"))
|
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("~-"))
|
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"))
|
assertEquals(sys.env.getOrElse("OLDPWD", "") + "/foo/bar", expandTildePrefix("~-/foo/bar"))
|
||||||
}
|
end BuildPathsTest
|
||||||
}
|
|
||||||
|
|||||||
@@ -448,8 +448,8 @@ findProperty() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
# Extracts the preloaded directory from either -Dsbt.preloaded, -Dsbt.global.base or -Duser.home
|
# Extracts the preloaded directory from -Dsbt.preloaded, -Dsbt.global.base, SBT_CONFIG_HOME,
|
||||||
# in that order.
|
# XDG_CONFIG_HOME/sbt, or user.home/.sbt in that order.
|
||||||
getPreloaded() {
|
getPreloaded() {
|
||||||
local preloaded && preloaded=$(findProperty sbt.preloaded)
|
local preloaded && preloaded=$(findProperty sbt.preloaded)
|
||||||
[ "$preloaded" ] && echo "$preloaded" && return
|
[ "$preloaded" ] && echo "$preloaded" && return
|
||||||
@@ -457,6 +457,9 @@ getPreloaded() {
|
|||||||
local global_base && global_base=$(findProperty sbt.global.base)
|
local global_base && global_base=$(findProperty sbt.global.base)
|
||||||
[ "$global_base" ] && echo "$global_base/preloaded" && return
|
[ "$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)
|
local user_home && user_home=$(findProperty user.home)
|
||||||
echo "${user_home:-$HOME}/.sbt/preloaded"
|
echo "${user_home:-$HOME}/.sbt/preloaded"
|
||||||
}
|
}
|
||||||
@@ -642,7 +645,7 @@ Usage: `basename "$0"` [options]
|
|||||||
--jvm-client run JVM client
|
--jvm-client run JVM client
|
||||||
--timings display task timings report on shutdown
|
--timings display task timings report on shutdown
|
||||||
--allow-empty start sbt even if current directory contains no sbt project
|
--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-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)
|
--sbt-cache <path> path to global cache directory (default: operating system specific)
|
||||||
--ivy <path> path to local Ivy repository (default: ~/.ivy2)
|
--ivy <path> path to local Ivy repository (default: ~/.ivy2)
|
||||||
@@ -973,6 +976,13 @@ else
|
|||||||
vlog "[process_args] java_version = '$java_version'"
|
vlog "[process_args] java_version = '$java_version'"
|
||||||
addDefaultMemory
|
addDefaultMemory
|
||||||
addSbtScriptProperty
|
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
|
addJdkWorkaround
|
||||||
set -- "${residual_args[@]}"
|
set -- "${residual_args[@]}"
|
||||||
argumentCount=$#
|
argumentCount=$#
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Closes #3681: verify global base (XDG / sbt.global.base) is resolvable and absolute
|
||||||
|
> checkGlobalBase
|
||||||
Reference in New Issue
Block a user