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:
Daniel Chao
2026-08-20 21:50:24 +00:00
committed by GitHub
co-authored by Florin Ungur
parent 4dd37219c0
commit 6551a59f9e
24 changed files with 799 additions and 40 deletions
@@ -16,9 +16,11 @@
package org.pkl.executor;
import java.net.URI;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.time.Duration;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import org.jspecify.annotations.Nullable;
@@ -26,6 +28,8 @@ import org.pkl.executor.spi.v1.ExecutorSpiOptions;
import org.pkl.executor.spi.v1.ExecutorSpiOptions2;
import org.pkl.executor.spi.v1.ExecutorSpiOptions3;
import org.pkl.executor.spi.v1.ExecutorSpiOptions4;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Options for {@link Executor#evaluatePath}.
@@ -33,6 +37,8 @@ import org.pkl.executor.spi.v1.ExecutorSpiOptions4;
* <p>To create {@code ExecutorOptions}, use its {@linkplain #builder builder}.
*/
public final class ExecutorOptions {
private static final Logger logger = LoggerFactory.getLogger(ExecutorOptions.class);
private final List<String> allowedModules;
private final List<String> allowedResources;
@@ -67,7 +73,42 @@ public final class ExecutorOptions {
/** Returns the module cache dir that the CLI uses by default. */
public static Path defaultModuleCacheDir() {
return Path.of(System.getProperty("user.home"), ".pkl", "cache");
return defaultModuleCacheDir(
Path.of(System.getProperty("user.home")), isWindowsOs(), System.getenv());
}
// Package-private; injectable so tests can exercise the Windows code path on a Unix CI box.
static Path defaultModuleCacheDir(
Path home, boolean isWindows, Map<String, String> environmentVariables) {
// Keep in sync with org.pkl.core.util.IoUtils.getSystemModuleCacheDir (pkl-executor cannot
// depend on pkl-core).
//
// On Unix prefer the XDG-style `~/.cache/pkl`.
// On Windows prefer `%LOCALAPPDATA%/pkl/Cache`.
var xdgConfig = environmentVariables.get("XDG_CACHE_HOME");
if (xdgConfig != null && !xdgConfig.isEmpty()) {
try {
return Path.of(xdgConfig).resolve("pkl");
} catch (InvalidPathException e) {
logger.warn("'XDG_CACHE_HOME' is an invalid path: {}", e.getMessage());
}
}
if (isWindows) {
var localAppData = environmentVariables.get("LOCALAPPDATA");
if (localAppData != null && !localAppData.isEmpty()) {
try {
return Path.of(localAppData).resolve("pkl/Cache");
} catch (InvalidPathException e) {
logger.warn("'LOCALAPPDATA' is an invalid path: {}", e.getMessage());
}
}
}
return home.resolve(".cache/pkl");
}
private static boolean isWindowsOs() {
var osName = System.getProperty("os.name");
return osName != null && osName.toLowerCase(Locale.ROOT).contains("windows");
}
public static Builder builder() {
@@ -0,0 +1,69 @@
/*
* Copyright © 2024-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.executor
import java.nio.file.Path
import kotlin.io.path.createDirectories
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import org.pkl.core.util.IoUtils
class ExecutorOptionsTest {
// `ExecutorOptions.defaultModuleCacheDir()` inlines the XDG/legacy fallback because pkl-executor
// cannot depend on pkl-core. This guards against drift from `IoUtils.getDefaultModuleCacheDir()`.
@Test
fun `defaultModuleCacheDir stays in sync with pkl-core`(@TempDir home: Path) {
val original = System.getProperty("user.home")
try {
System.setProperty("user.home", home.toString())
assertThat(ExecutorOptions.defaultModuleCacheDir())
.isEqualTo(home.resolve(".cache").resolve("pkl"))
assertThat(ExecutorOptions.defaultModuleCacheDir())
.isEqualTo(IoUtils.getSystemModuleCacheDir())
} finally {
System.setProperty("user.home", original)
}
}
@Test
fun `defaultModuleCacheDir on Windows uses LOCALAPPDATA when set`(@TempDir home: Path) {
val localAppData = home.resolve("LocalAppData").createDirectories()
assertThat(
ExecutorOptions.defaultModuleCacheDir(
home,
true,
mapOf("LOCALAPPDATA" to localAppData.toString()),
)
)
.isEqualTo(localAppData.resolve("pkl").resolve("Cache"))
}
@Test
fun `defaultModuleCacheDir on Windows falls back to Unix layout when LOCALAPPDATA is unset`(
@TempDir home: Path
) {
assertThat(ExecutorOptions.defaultModuleCacheDir(home, true, mapOf()))
.isEqualTo(home.resolve(".cache").resolve("pkl"))
}
@Test
fun `defaultModuleCacheDir on Windows still falls XDG style default dir`(@TempDir home: Path) {
home.resolve(".pkl").resolve("cache").createDirectories()
assertThat(ExecutorOptions.defaultModuleCacheDir(home, true, mapOf()))
.isEqualTo(home.resolve(".cache").resolve("pkl"))
}
}