diff --git a/launcher-package/integration-test/src/test/scala/RunnerScriptTest.scala b/launcher-package/integration-test/src/test/scala/RunnerScriptTest.scala index 328d9ec21..62e5ace14 100644 --- a/launcher-package/integration-test/src/test/scala/RunnerScriptTest.scala +++ b/launcher-package/integration-test/src/test/scala/RunnerScriptTest.scala @@ -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") diff --git a/launcher-package/src/universal/bin/sbt.bat b/launcher-package/src/universal/bin/sbt.bat index b694ca349..6dabbfff4 100755 --- a/launcher-package/src/universal/bin/sbt.bat +++ b/launcher-package/src/universal/bin/sbt.bat @@ -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 ( diff --git a/main/src/main/scala/sbt/BuildPaths.scala b/main/src/main/scala/sbt/BuildPaths.scala index 5eb560332..0f6c014d3 100644 --- a/main/src/main/scala/sbt/BuildPaths.scala +++ b/main/src/main/scala/sbt/BuildPaths.scala @@ -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 diff --git a/main/src/main/scala/sbt/internal/SysProp.scala b/main/src/main/scala/sbt/internal/SysProp.scala index 341f028cf..e48eeae12 100644 --- a/main/src/main/scala/sbt/internal/SysProp.scala +++ b/main/src/main/scala/sbt/internal/SysProp.scala @@ -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 diff --git a/main/src/test/scala/BuildPathsTest.scala b/main/src/test/scala/BuildPathsTest.scala index b4f37906f..3166b71fd 100644 --- a/main/src/test/scala/BuildPathsTest.scala +++ b/main/src/test/scala/BuildPathsTest.scala @@ -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 diff --git a/sbt b/sbt index 6757861b9..ce3838075 100755 --- a/sbt +++ b/sbt @@ -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 to global settings/plugins directory (default: ~/.sbt) + --sbt-dir path to global settings/plugins directory (default: \$XDG_CONFIG_HOME/sbt or ~/.sbt) --sbt-boot path to shared boot directory (default: ~/.sbt/boot in 0.11 series) --sbt-cache path to global cache directory (default: operating system specific) --ivy 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=$# diff --git a/sbt-app/src/sbt-test/project/xdg-global-base/build.sbt b/sbt-app/src/sbt-test/project/xdg-global-base/build.sbt new file mode 100644 index 000000000..046ecdd87 --- /dev/null +++ b/sbt-app/src/sbt-test/project/xdg-global-base/build.sbt @@ -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") + } +) diff --git a/sbt-app/src/sbt-test/project/xdg-global-base/test b/sbt-app/src/sbt-test/project/xdg-global-base/test new file mode 100644 index 000000000..10a5e79da --- /dev/null +++ b/sbt-app/src/sbt-test/project/xdg-global-base/test @@ -0,0 +1,2 @@ +# Closes #3681: verify global base (XDG / sbt.global.base) is resolvable and absolute +> checkGlobalBase