mirror of
https://github.com/apple/pkl.git
synced 2026-08-27 06:04:03 +02:00
Use XDG base directories and Known Folders on Windows (#1809)
This changes logic that previously read/wrote from `~/.pkl` to use XDG base directories (all OSes), and Known Folders locations on Windows. For example, Pkl will look for `settings.pkl` in: 1. `$XDG_CONFIG_HOME/pkl/settings.pkl` 2. `%APPDATA/pkl/settings.pkl` 3. `~/.pkl/settings.pkl` 4. Path pkl/settings/pkl within `$XDG_CONFIG_DIRS` 5. `/etc/xdg/pkl/settings.pkl` --------- Co-authored-by: Florin Ungur <florin@florinungur.com>
This commit is contained in:
co-authored by
Florin Ungur
parent
4dd37219c0
commit
6551a59f9e
@@ -58,7 +58,7 @@ public final class EvaluatorBuilder {
|
||||
|
||||
private java.time.@Nullable Duration timeout;
|
||||
|
||||
private @Nullable Path moduleCacheDir = IoUtils.getDefaultModuleCacheDir();
|
||||
private @Nullable Path moduleCacheDir = IoUtils.getSystemModuleCacheDir();
|
||||
|
||||
private @Nullable String outputFormat;
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ public final class ModuleCache {
|
||||
case "semver":
|
||||
return SemVerModule.getModule();
|
||||
case "settings":
|
||||
// always needed if ~/.pkl/settings.pkl is present
|
||||
// always needed if ~/.config/pkl/settings.pkl is present
|
||||
return SettingsModule.getModule();
|
||||
case "test":
|
||||
return TestModule.getModule();
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.pkl.core.module.ModuleKeyFactories;
|
||||
import org.pkl.core.resource.ResourceReaders;
|
||||
import org.pkl.core.runtime.VmEvalException;
|
||||
import org.pkl.core.runtime.VmExceptionBuilder;
|
||||
import org.pkl.core.util.DebugLogger;
|
||||
import org.pkl.core.util.IoUtils;
|
||||
|
||||
/**
|
||||
@@ -33,8 +34,10 @@ import org.pkl.core.util.IoUtils;
|
||||
* {@literal pkl.settings} standard library module. To load a settings file, use one of the static
|
||||
* {@code load} methods.
|
||||
*/
|
||||
// keep in sync with stdlib/settings.pkl
|
||||
public record PklSettings(Editor editor, PklEvaluatorSettings.@Nullable Http http) {
|
||||
// keep in sync with stdlib/settings.pkl
|
||||
public static final PklSettings defaultInstance = new PklSettings(Editor.SYSTEM, null);
|
||||
|
||||
private static final List<Pattern> ALLOWED_MODULES =
|
||||
List.of(Pattern.compile("pkl:"), Pattern.compile("file:"));
|
||||
|
||||
@@ -42,16 +45,43 @@ public record PklSettings(Editor editor, PklEvaluatorSettings.@Nullable Http htt
|
||||
List.of(Pattern.compile("env:"), Pattern.compile("file:"));
|
||||
|
||||
/**
|
||||
* Loads the user settings file ({@literal ~/.pkl/settings.pkl}). If this file does not exist,
|
||||
* returns default settings defined by module {@literal pkl.settings}.
|
||||
* Loads the user settings file.
|
||||
*
|
||||
* <p>Prefers XDG_CONFIG_HOME (e.g. {@code ~/.config/pkl/settings.pkl}), falling back to the
|
||||
* legacy {@code ~/.pkl/settings.pkl}.
|
||||
*
|
||||
* <p>If neither file exists, returns default settings defined by module {@code pkl.settings}.
|
||||
*/
|
||||
public static PklSettings loadFromSystem() throws VmEvalException {
|
||||
var file = IoUtils.getSystemSettingsFile();
|
||||
if (Files.exists(file)) {
|
||||
DebugLogger.log("Loading settings file from " + file.normalize().toAbsolutePath());
|
||||
return load(ModuleSource.path(file));
|
||||
}
|
||||
return defaultInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the user settings file.
|
||||
*
|
||||
* @deprecated As of 0.33.0, use {@link #loadFromSystem()}, which now prefers {@code
|
||||
* ~/.config/pkl/settings.pkl} over the legacy {@code ~/.pkl/settings.pkl}.
|
||||
*/
|
||||
@Deprecated(since = "0.33.0", forRemoval = true)
|
||||
public static PklSettings loadFromPklHomeDir() throws VmEvalException {
|
||||
return loadFromPklHomeDir(IoUtils.getPklHomeDir());
|
||||
var candidate = IoUtils.getLegacyPklHomeDir().resolve("settings.pkl");
|
||||
if (Files.exists(candidate)) {
|
||||
return load(ModuleSource.path(candidate));
|
||||
}
|
||||
return defaultInstance;
|
||||
}
|
||||
|
||||
/** For testing only. */
|
||||
static PklSettings loadFromPklHomeDir(Path pklHomeDir) throws VmEvalException {
|
||||
var path = pklHomeDir.resolve("settings.pkl");
|
||||
static PklSettings loadFromSettingsDir(Path settingsDir) throws VmEvalException {
|
||||
return loadFromSettingsFile(settingsDir.resolve("settings.pkl"));
|
||||
}
|
||||
|
||||
private static PklSettings loadFromSettingsFile(Path path) throws VmEvalException {
|
||||
return Files.exists(path)
|
||||
? load(ModuleSource.path(path))
|
||||
: new PklSettings(Editor.SYSTEM, null);
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.util;
|
||||
|
||||
final class BaseDirectories {
|
||||
public static final BaseDirectory config =
|
||||
new BaseDirectory(
|
||||
"XDG_CONFIG_HOME",
|
||||
"XDG_CONFIG_DIRS",
|
||||
"APPDATA",
|
||||
null,
|
||||
".config",
|
||||
new String[] {"/etc/xdg"});
|
||||
|
||||
public static final BaseDirectory cache =
|
||||
new BaseDirectory("XDG_CACHE_HOME", null, "LOCALAPPDATA", "Cache", ".cache", null);
|
||||
|
||||
public static final BaseDirectory state =
|
||||
new BaseDirectory("XDG_STATE_HOME", null, "LOCALAPPDATA", null, ".local/state", null);
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.util;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Predicate;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Utility library for accessing files in base directories.
|
||||
*
|
||||
* <p>On macOS and Linux, follows the XDG base directory specification.
|
||||
*
|
||||
* <p>On Windows, follows {@code $APPDATA} and {@code $LOCALAPPDATA} conventions, but can be
|
||||
* overridden by {@code XDG} style env vars.
|
||||
*/
|
||||
class BaseDirectory {
|
||||
private final String xdgHomeEnvVar;
|
||||
private final @Nullable String xdgDirsEnvVar;
|
||||
private final String windowsEnvVar;
|
||||
private final @Nullable String windowsSubpath;
|
||||
private final String homeDefault;
|
||||
private final String @Nullable [] dirsDefault;
|
||||
|
||||
BaseDirectory(
|
||||
String xdgHomeEnvVar,
|
||||
@Nullable String xdgDirsEnvVar,
|
||||
String windowsEnvVar,
|
||||
@Nullable String windowsSubpath,
|
||||
String homeDefault,
|
||||
String @Nullable [] dirsDefault) {
|
||||
this.xdgHomeEnvVar = xdgHomeEnvVar;
|
||||
this.xdgDirsEnvVar = xdgDirsEnvVar;
|
||||
this.windowsEnvVar = windowsEnvVar;
|
||||
this.windowsSubpath = windowsSubpath;
|
||||
this.homeDefault = homeDefault;
|
||||
this.dirsDefault = dirsDefault;
|
||||
}
|
||||
|
||||
@Nullable Path getHome() {
|
||||
return getHome(System.getenv(), IoUtils.isWindows());
|
||||
}
|
||||
|
||||
/** Returns the first file within the search hierarchy that exists. */
|
||||
@Nullable Path firstMatchingPath(String subpath, Predicate<Path> predicate) {
|
||||
return firstMatchingPath(subpath, System.getenv(), IoUtils.isWindows(), predicate);
|
||||
}
|
||||
|
||||
/** Returns the subpath within the {@code home} of this base directory type. */
|
||||
@Nullable Path resolveHome(String subpath) {
|
||||
var homeDir = getHome(System.getenv(), IoUtils.isWindows());
|
||||
if (homeDir != null) {
|
||||
return homeDir.resolve(subpath);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// for testing only
|
||||
@Nullable Path firstMatchingPath(
|
||||
String subpath, Map<String, String> envVars, boolean isWindows, Predicate<Path> predicate) {
|
||||
var home = getHome(envVars, isWindows);
|
||||
Path candidate;
|
||||
if (home != null) {
|
||||
candidate = home.resolve(subpath);
|
||||
if (predicate.test(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
var dirs = getDirs(envVars);
|
||||
if (dirs != null) {
|
||||
for (var dir : dirs) {
|
||||
candidate = dir.resolve(subpath);
|
||||
if (predicate.test(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// possibly null if $HOME is not set.
|
||||
private static final @Nullable Path homeDir;
|
||||
|
||||
static {
|
||||
var userHome = System.getProperty("user.home");
|
||||
if (userHome != null) {
|
||||
homeDir = Path.of(userHome);
|
||||
} else {
|
||||
homeDir = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static Path @Nullable [] getConfiguredPaths(String envVar, Map<String, String> envVars) {
|
||||
try {
|
||||
var value = envVars.get(envVar);
|
||||
if (value == null || value.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
var strs = value.split(File.pathSeparator);
|
||||
var ret = new Path[strs.length];
|
||||
for (var i = 0; i < strs.length; i++) {
|
||||
var dir = strs[i];
|
||||
if (dir.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
ret[i] = Path.of(dir).resolve("pkl");
|
||||
}
|
||||
return Arrays.stream(ret).filter(Objects::nonNull).toArray(Path[]::new);
|
||||
} catch (InvalidPathException e) {
|
||||
// can't use org.pkl.core.Logger here; logger isn't yet available
|
||||
// (can't call `VmContext.get()`).
|
||||
// do the next best thing and just write to stderr.
|
||||
System.err.println(
|
||||
"[org.pkl.core.util.BaseDirectory] '"
|
||||
+ envVar
|
||||
+ "' env var contains an invalid path: "
|
||||
+ e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static @Nullable Path getConfiguredPath(String envVar, Map<String, String> envVars) {
|
||||
try {
|
||||
var value = envVars.get(envVar);
|
||||
if (value == null || value.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
return Path.of(value);
|
||||
} catch (InvalidPathException e) {
|
||||
// can't use org.pkl.core.Logger here; logger isn't yet available
|
||||
// (can't call `VmContext.get()`).
|
||||
// do the next best thing and just write to stderr.
|
||||
System.err.println(
|
||||
"[org.pkl.core.util.BaseDirectory] '"
|
||||
+ envVar
|
||||
+ "' env var is an invalid path: "
|
||||
+ e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable Path getHome(Map<String, String> envVars, boolean isWindows) {
|
||||
var configuredHome = getConfiguredPath(xdgHomeEnvVar, envVars);
|
||||
if (configuredHome != null) {
|
||||
return configuredHome.resolve("pkl");
|
||||
}
|
||||
if (isWindows) {
|
||||
configuredHome = getConfiguredPath(windowsEnvVar, envVars);
|
||||
if (configuredHome != null) {
|
||||
configuredHome = configuredHome.resolve("pkl");
|
||||
if (windowsSubpath != null) {
|
||||
configuredHome = configuredHome.resolve(windowsSubpath);
|
||||
}
|
||||
return configuredHome;
|
||||
}
|
||||
}
|
||||
if (homeDir != null) {
|
||||
return homeDir.resolve(homeDefault).resolve("pkl");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Path @Nullable [] getDirs(Map<String, String> envVars) {
|
||||
if (xdgDirsEnvVar != null) {
|
||||
var paths = getConfiguredPaths(xdgDirsEnvVar, envVars);
|
||||
if (paths != null) {
|
||||
return paths;
|
||||
}
|
||||
}
|
||||
if (dirsDefault == null) {
|
||||
return null;
|
||||
}
|
||||
var ret = new Path[dirsDefault.length];
|
||||
for (var i = 0; i < dirsDefault.length; i++) {
|
||||
var dir = dirsDefault[i];
|
||||
if (!dir.startsWith("/")) {
|
||||
if (homeDir == null) {
|
||||
return null;
|
||||
}
|
||||
ret[i] = homeDir.resolve(dir).resolve("pkl");
|
||||
} else {
|
||||
ret[i] = Path.of(dir).resolve("pkl");
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.util;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class DebugLogger {
|
||||
private DebugLogger() {}
|
||||
|
||||
private static final boolean isEnabled;
|
||||
|
||||
static {
|
||||
isEnabled = Objects.equals(System.getenv("PKL_DEBUG"), "1");
|
||||
}
|
||||
|
||||
public static void log(String message) {
|
||||
if (!isEnabled) {
|
||||
return;
|
||||
}
|
||||
System.err.println("[pkl] " + message);
|
||||
}
|
||||
}
|
||||
@@ -49,7 +49,6 @@ import org.pkl.core.runtime.VmExceptionBuilder;
|
||||
import org.pkl.core.util.GlobResolver.InvalidGlobPatternException;
|
||||
|
||||
public final class IoUtils {
|
||||
|
||||
// Don't match paths like `C:\`, which are drive letters on Windows.
|
||||
private static final Pattern uriLike = Pattern.compile("[\\w+.-]+:[^\\\\].*");
|
||||
|
||||
@@ -228,13 +227,33 @@ public final class IoUtils {
|
||||
}
|
||||
|
||||
// not stored to avoid build-time initialization by native-image
|
||||
public static Path getPklHomeDir() {
|
||||
public static Path getLegacyPklHomeDir() {
|
||||
return Path.of(System.getProperty("user.home"), ".pkl");
|
||||
}
|
||||
|
||||
public static @Nullable Path getSystemModuleCacheDir() {
|
||||
return BaseDirectories.cache.getHome();
|
||||
}
|
||||
|
||||
public static Path getSystemSettingsFile() {
|
||||
var path = BaseDirectories.config.firstMatchingPath("settings.pkl", Files::exists);
|
||||
if (path == null) {
|
||||
return getLegacyPklHomeDir().resolve("settings.pkl");
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
public static Path getSystemCaCertsDir() {
|
||||
var path = BaseDirectories.config.firstMatchingPath("cacerts", Files::isDirectory);
|
||||
if (path == null) {
|
||||
return getLegacyPklHomeDir().resolve("cacerts");
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
// not stored to avoid build-time initialization by native-image
|
||||
public static Path getDefaultModuleCacheDir() {
|
||||
return getPklHomeDir().resolve("cache");
|
||||
public static @Nullable Path getReplHistoryFile() {
|
||||
return BaseDirectories.state.resolveHome("repl-history");
|
||||
}
|
||||
|
||||
// not stored to avoid build-time initialization by native-image
|
||||
|
||||
@@ -42,7 +42,7 @@ class PklSettingsTest {
|
||||
.trimIndent()
|
||||
)
|
||||
|
||||
val settings = PklSettings.loadFromPklHomeDir(tempDir)
|
||||
val settings = PklSettings.loadFromSettingsDir(tempDir)
|
||||
assertThat(settings).isEqualTo(PklSettings(Editor.SUBLIME, null))
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ class PklSettingsTest {
|
||||
.trimIndent()
|
||||
)
|
||||
|
||||
val settings = PklSettings.loadFromPklHomeDir(tempDir)
|
||||
val settings = PklSettings.loadFromSettingsDir(tempDir)
|
||||
val expectedHttp =
|
||||
PklEvaluatorSettings.Http(
|
||||
PklEvaluatorSettings.Proxy(
|
||||
@@ -113,7 +113,7 @@ class PklSettingsTest {
|
||||
.trimIndent()
|
||||
)
|
||||
|
||||
val settings = PklSettings.loadFromPklHomeDir(tempDir)
|
||||
val settings = PklSettings.loadFromSettingsDir(tempDir)
|
||||
val expectedHttp =
|
||||
PklEvaluatorSettings.Http(
|
||||
PklEvaluatorSettings.Proxy(URI("http://localhost:8080"), listOf()),
|
||||
@@ -169,7 +169,7 @@ class PklSettingsTest {
|
||||
@Test
|
||||
fun `invalid settings file`(@TempDir tempDir: Path) {
|
||||
val settingsFile = tempDir.resolve("settings.pkl").apply { writeString("foo = 1") }
|
||||
assertThatCode { PklSettings.loadFromPklHomeDir(tempDir) }
|
||||
assertThatCode { PklSettings.loadFromSettingsDir(tempDir) }
|
||||
.hasMessageContaining(
|
||||
"Expected `output.value` of module `${settingsFile.toUri()}` to be of type `pkl.settings`, but got type `settings`."
|
||||
)
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
/*
|
||||
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.util
|
||||
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.createDirectories
|
||||
import kotlin.io.path.createFile
|
||||
import kotlin.io.path.createParentDirectories
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
|
||||
class BaseDirectoryTest {
|
||||
private val subject = BaseDirectories.config
|
||||
|
||||
@Test
|
||||
fun `firstMatchingPath() - finds a file within the XDG-configured home`(@TempDir tempDir: Path) {
|
||||
val xdgHome = tempDir.resolve("xdg-home").createDirectories()
|
||||
xdgHome.resolve("pkl/settings.pkl").createParentDirectories().createFile()
|
||||
val envVars = mapOf("XDG_CONFIG_HOME" to xdgHome.toString())
|
||||
|
||||
assertThat(subject.firstMatchingPath("settings.pkl", envVars, false, Files::exists))
|
||||
.isEqualTo(xdgHome.resolve("pkl/settings.pkl"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `firstMatchingPath() - prefers home over dirs when both contain the file`(
|
||||
@TempDir tempDir: Path
|
||||
) {
|
||||
val xdgHome = tempDir.resolve("xdg-home").createDirectories()
|
||||
val dir1 = tempDir.resolve("dir1").createDirectories()
|
||||
xdgHome.resolve("pkl/settings.pkl").createParentDirectories().createFile()
|
||||
dir1.resolve("settings.pkl").createFile()
|
||||
val envVars =
|
||||
mapOf("XDG_CONFIG_HOME" to xdgHome.toString(), "XDG_CONFIG_DIRS" to dir1.toString())
|
||||
|
||||
assertThat(subject.firstMatchingPath("settings.pkl", envVars, false, Files::exists))
|
||||
.isEqualTo(xdgHome.resolve("pkl/settings.pkl"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `firstMatchingPath() - falls back to dirs when home does not contain the file`(
|
||||
@TempDir tempDir: Path
|
||||
) {
|
||||
val xdgHome = tempDir.resolve("xdg-home").createDirectories()
|
||||
val dir1 = tempDir.resolve("dir1").createDirectories()
|
||||
dir1.resolve("pkl/settings.pkl").let {
|
||||
it.createParentDirectories()
|
||||
it.createFile()
|
||||
}
|
||||
val envVars =
|
||||
mapOf("XDG_CONFIG_HOME" to xdgHome.toString(), "XDG_CONFIG_DIRS" to dir1.toString())
|
||||
|
||||
assertThat(subject.firstMatchingPath("settings.pkl", envVars, false, Files::exists))
|
||||
.isEqualTo(dir1.resolve("pkl/settings.pkl"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `firstMatchingPath() - searches multiple dirs in order`(@TempDir tempDir: Path) {
|
||||
val xdgHome = tempDir.resolve("xdg-home").createDirectories()
|
||||
val dir1 = tempDir.resolve("dir1").createDirectories()
|
||||
val dir2 = tempDir.resolve("dir2").createDirectories()
|
||||
val expected =
|
||||
dir2.resolve("pkl/settings.pkl").also {
|
||||
it.createParentDirectories()
|
||||
it.createFile()
|
||||
}
|
||||
val envVars =
|
||||
mapOf(
|
||||
"XDG_CONFIG_HOME" to xdgHome.toString(),
|
||||
"XDG_CONFIG_DIRS" to "$dir1${File.pathSeparator}$dir2",
|
||||
)
|
||||
|
||||
assertThat(subject.firstMatchingPath("settings.pkl", envVars, false, Files::exists))
|
||||
.isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `firstMatchingPath() - returns null when the file exists nowhere in the search hierarchy`(
|
||||
@TempDir tempDir: Path
|
||||
) {
|
||||
val xdgHome = tempDir.resolve("xdg-home").createDirectories()
|
||||
val dir1 = tempDir.resolve("dir1").createDirectories()
|
||||
val envVars =
|
||||
mapOf("XDG_CONFIG_HOME" to xdgHome.toString(), "XDG_CONFIG_DIRS" to dir1.toString())
|
||||
|
||||
assertThat(subject.firstMatchingPath("missing.pkl", envVars, false, Files::exists)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `firstMatchingPath() - XDG env var wins over the Windows env var even when isWindows is true`(
|
||||
@TempDir tempDir: Path
|
||||
) {
|
||||
val xdgHome = tempDir.resolve("xdg-home").createDirectories()
|
||||
val appData = tempDir.resolve("app-data").createDirectories()
|
||||
xdgHome.resolve("pkl/settings.pkl").createParentDirectories().createFile()
|
||||
appData.resolve("pkl/settings.pkl").createParentDirectories().createFile()
|
||||
val envVars = mapOf("XDG_CONFIG_HOME" to xdgHome.toString(), "AppData" to appData.toString())
|
||||
|
||||
assertThat(subject.firstMatchingPath("settings.pkl", envVars, true, Files::exists))
|
||||
.isEqualTo(xdgHome.resolve("pkl/settings.pkl"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `firstMatchingPath() - uses the Windows env var when the XDG env var is unset and isWindows is true`(
|
||||
@TempDir tempDir: Path
|
||||
) {
|
||||
val appData = tempDir.resolve("app-data").createDirectories()
|
||||
appData.resolve("pkl/settings.pkl").createParentDirectories().createFile()
|
||||
val envVars = mapOf("APPDATA" to appData.toString())
|
||||
|
||||
assertThat(subject.firstMatchingPath("settings.pkl", envVars, true, Files::exists))
|
||||
.isEqualTo(appData.resolve("pkl/settings.pkl"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `firstMatchingPath() - ignores the Windows env var when isWindows is false`(
|
||||
@TempDir tempDir: Path
|
||||
) {
|
||||
val appData = tempDir.resolve("app-data").createDirectories()
|
||||
appData.resolve("pkl/settings.pkl").createParentDirectories().createFile()
|
||||
val envVars = mapOf("APPDATA" to appData.toString())
|
||||
|
||||
assertThat(subject.firstMatchingPath("settings.pkl", envVars, false, Files::exists)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `firstMatchingPath() - appends the Windows subpath after 'pkl' when configured`(
|
||||
@TempDir tempDir: Path
|
||||
) {
|
||||
val localAppData = tempDir.resolve("local-app-data").createDirectories()
|
||||
localAppData.resolve("pkl/Cache/cache.db").createParentDirectories().createFile()
|
||||
val envVars = mapOf("LOCALAPPDATA" to localAppData.toString())
|
||||
|
||||
assertThat(BaseDirectories.cache.firstMatchingPath("cache.db", envVars, true, Files::exists))
|
||||
.isEqualTo(localAppData.resolve("pkl/Cache/cache.db"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `firstMatchingPath() - returns null when falling back to defaults that do not contain the file`() {
|
||||
// Doesn't touch the real filesystem: this subpath is not expected to exist under the real
|
||||
// `~/.config` or `/etc/xdg`, so the defaults are exercised without creating any real files.
|
||||
val subpath = "base-directory-test/definitely-does-not-exist.txt"
|
||||
assertThat(subject.firstMatchingPath(subpath, emptyMap(), false, Files::exists)).isNull()
|
||||
assertThat(subject.firstMatchingPath(subpath, emptyMap(), true, Files::exists)).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getHome() - appends 'pkl' to the default home directory when no env vars are set`() {
|
||||
val expected = Path.of(System.getProperty("user.home")).resolve(".config").resolve("pkl")
|
||||
|
||||
assertThat(subject.getHome(emptyMap(), false)).isEqualTo(expected)
|
||||
// Same fallback applies on Windows when the Windows env var is also unset.
|
||||
assertThat(subject.getHome(emptyMap(), true)).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getHome() - appends 'pkl' to the default cache home when no env vars are set`() {
|
||||
assertThat(BaseDirectories.cache.getHome(emptyMap(), false))
|
||||
.isEqualTo(Path.of(System.getProperty("user.home")).resolve(".cache").resolve("pkl"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getHome() - appends 'pkl' to the default state home when no env vars are set`() {
|
||||
assertThat(BaseDirectories.state.getHome(emptyMap(), false))
|
||||
.isEqualTo(Path.of(System.getProperty("user.home")).resolve(".local/state").resolve("pkl"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getHome() - treats an empty XDG env var as unset`() {
|
||||
val expected = Path.of(System.getProperty("user.home")).resolve(".config").resolve("pkl")
|
||||
|
||||
assertThat(subject.getHome(mapOf("XDG_CONFIG_HOME" to ""), false)).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getHome() - treats an empty Windows env var as unset`() {
|
||||
val expected = Path.of(System.getProperty("user.home")).resolve(".config").resolve("pkl")
|
||||
|
||||
assertThat(subject.getHome(mapOf("APPDATA" to ""), true)).isEqualTo(expected)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getHome() - an empty XDG env var falls through to a configured Windows env var`(
|
||||
@TempDir tempDir: Path
|
||||
) {
|
||||
val appData = tempDir.resolve("app-data")
|
||||
val envVars = mapOf("XDG_CONFIG_HOME" to "", "APPDATA" to appData.toString())
|
||||
|
||||
assertThat(subject.getHome(envVars, true)).isEqualTo(appData.resolve("pkl"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getDirs() - treats an empty XDG_CONFIG_DIRS as unset, falling back to defaults`() {
|
||||
assertThat(subject.getDirs(mapOf("XDG_CONFIG_DIRS" to "")))
|
||||
.containsExactly(Path.of("/etc/xdg").resolve("pkl"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getDirs() - skips empty entries within an otherwise non-empty XDG_CONFIG_DIRS list`(
|
||||
@TempDir tempDir: Path
|
||||
) {
|
||||
val dir1 = tempDir.resolve("dir1")
|
||||
val dir2 = tempDir.resolve("dir2")
|
||||
// A double separator produces a literal empty segment (`"/a::/b".split(":")` keeps the
|
||||
// middle `""`, unlike a trailing separator, which java.lang.String#split drops).
|
||||
val envVars = mapOf("XDG_CONFIG_DIRS" to "$dir1${File.pathSeparator}${File.pathSeparator}$dir2")
|
||||
|
||||
assertThat(subject.getDirs(envVars)).containsExactly(dir1.resolve("pkl"), dir2.resolve("pkl"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `firstMatchingPath() - does not crash when XDG_CONFIG_DIRS contains a leading empty segment`(
|
||||
@TempDir tempDir: Path
|
||||
) {
|
||||
val dir1 = tempDir.resolve("dir1").createDirectories()
|
||||
val envVars = mapOf("XDG_CONFIG_DIRS" to "${File.pathSeparator}$dir1")
|
||||
|
||||
assertThat(subject.firstMatchingPath("missing.pkl", envVars, false, Files::exists)).isNull()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user