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
@@ -210,6 +210,11 @@ abstract class NativeImageBuild : DefaultTask() {
add("--initialize-at-build-time=") add("--initialize-at-build-time=")
// needed for messagepack-java (see https://github.com/msgpack/msgpack-java/issues/600) // needed for messagepack-java (see https://github.com/msgpack/msgpack-java/issues/600)
add("--initialize-at-run-time=org.msgpack.core.buffer.DirectBufferAccess") add("--initialize-at-run-time=org.msgpack.core.buffer.DirectBufferAccess")
// prevent storing `homeDir` in native image
add("--initialize-at-run-time=org.pkl.core.util.BaseDirectory")
add("--initialize-at-run-time=org.pkl.core.util.BaseDirectories")
// prevent storing `isEnabled`
add("--initialize-at-run-time=org.pkl.core.util.DebugLogger")
// needed for jline-terminal-jni // needed for jline-terminal-jni
add("--initialize-at-run-time=org.jline.nativ,org.jline.terminal.impl.jni") add("--initialize-at-run-time=org.jline.nativ,org.jline.terminal.impl.jni")
add("--no-fallback") add("--no-fallback")
@@ -360,7 +360,7 @@ at pkl.base#Module.output.text (https://github.com/apple/pkl/blob/e4d8c882d/stdl
<6> What Pkl evaluated to discover the error. <6> What Pkl evaluated to discover the error.
When Pkl prints source locations, it also prints clickable links for easy access. When Pkl prints source locations, it also prints clickable links for easy access.
For local files, it generates a link for your development environment (https://pkl-lang.org/main/current/pkl-cli/index.html#settings-file[configurable in `+~/.pkl/settings.pkl+`]). For local files, it generates a link for your development environment (https://pkl-lang.org/main/current/pkl-cli/index.html#settings-file[configurable in `+~/.config/pkl/settings.pkl+`]).
For packages imported from elsewhere, if available, Pkl produces `https://` links to their repository. For packages imported from elsewhere, if available, Pkl produces `https://` links to their repository.
Pkl complains about a _type constraint_. Pkl complains about a _type constraint_.
+23 -4
View File
@@ -1377,14 +1377,24 @@ it works as follows:
The Pkl settings file allows to customize the CLI experience. The Pkl settings file allows to customize the CLI experience.
A settings file is a Pkl module amending the `pkl.settings` standard library module. A settings file is a Pkl module amending the `pkl.settings` standard library module.
Its default location is `~/.pkl/settings.pkl`.
Unless configured explicitly, Pkl will look in the following locations:
. `$XDG_CONFIG_HOME/pkl/settings.pkl`
. `%APPDATA%/pkl/settings.pkl` (on Windows only)
. `~/.config/pkl/settings.pkl`
. Subdirectory `pkl/settings.pkl` within one of the paths described by `$XDG_CONFIG_DIRS`
. `/etc/xdg/pkl/settings.pkl`
. `~/.pkl/settings.pkl` (legacy location used by Pkl 0.32 and lower)
To use a different settings file, set the `--settings` command line option, for example `--settings mysettings.pkl`. To use a different settings file, set the `--settings` command line option, for example `--settings mysettings.pkl`.
To enforce default settings, use `--settings pkl:settings`. To enforce default settings, use `--settings pkl:settings`.
The settings file is also honored by (and configurable through) the Gradle plugin and `CliEvaluator` API. The settings file is also honored by (and configurable through) the Gradle plugin and `CliEvaluator` API.
Here is a typical settings file: Here is a typical settings file:
.~/.pkl/settings.pkl .~/.config/pkl/settings.pkl
[source%parsed,{pkl}] [source%parsed,{pkl}]
---- ----
amends "pkl:settings" // <1> amends "pkl:settings" // <1>
@@ -1406,10 +1416,19 @@ When making TLS requests, Pkl comes with its own set of {uri-certificates}[CA ce
These certificates can be overridden via either of the two options: These certificates can be overridden via either of the two options:
- Set them directly via the CLI option `--ca-certificates <path>`. - Set them directly via the CLI option `--ca-certificates <path>`.
- Add them to a directory at path `~/.pkl/cacerts/`. - Add them to a user directory.
If CA certificates are not explicitly configured, Pkl will look in the following locations:
. `$XDG_CONFIG_HOME/pkl/cacerts`
. `%APPDATA%/pkl/cacerts` (on Windows only)
. `~/.config/pkl/cacerts`
. Subdirectory `pkl/cacerts` within one of the paths described by `$XDG_CONFIG_DIRS`
. `/etc/xdg/pkl/cacerts`
. `~/.pkl/cacerts` (legacy location used by Pkl 0.32 and lower)
Both these options will *replace* the default CA certificates bundled with Pkl. + Both these options will *replace* the default CA certificates bundled with Pkl. +
The CLI option takes precedence over the certificates in `~/.pkl/cacerts/`. + The CLI option takes precedence over the certificates in the cacerts directory. +
Certificates need to be X.509 certificates in PEM format. Certificates need to be X.509 certificates in PEM format.
[[http-proxy]] [[http-proxy]]
@@ -36,9 +36,15 @@ Possible values:
.--cache-dir .--cache-dir
[%collapsible] [%collapsible]
==== ====
Default: `~/.pkl/cache` +
Example: `/path/to/module/cache/` + Example: `/path/to/module/cache/` +
The cache directory for storing packages. The cache directory for storing packages.
If unset, defaults to the following locations:
. `$XDG_CACHE_HOME/pkl`
. `$LOCALAPPDATA/pkl/Cache` (on Windows only)
. `~/.cache/pkl` (if `$XDG_CACHE_HOME` and `$LOCALAPPDATA` are both unset)
==== ====
.--no-cache .--no-cache
@@ -97,7 +103,7 @@ Any symlinks are resolved before this check is performed.
Default: (none) + Default: (none) +
Example: `mySettings.pkl` + Example: `mySettings.pkl` +
File path of the Pkl settings file to use. File path of the Pkl settings file to use.
If not set, `~/.pkl/settings.pkl` or defaults specified in the `pkl.settings` standard library module are used. If not set, `~/.config/pkl/settings.pkl` on Unix or `%APPDATA%/pkl/settings.pkl` on Windows (or the legacy `~/.pkl/settings.pkl`), or defaults specified in the `pkl.settings` standard library module are used.
==== ====
.-t, --timeout .-t, --timeout
@@ -64,7 +64,9 @@ Default: `null` +
Example 1: `moduleCacheDir = layout.buildDirectory.dir("pkl-module-cache")` + Example 1: `moduleCacheDir = layout.buildDirectory.dir("pkl-module-cache")` +
Example 2: `moduleCacheDir.fileValue file("/absolute/path/to/cache")` + Example 2: `moduleCacheDir.fileValue file("/absolute/path/to/cache")` +
The cache directory for storing packages. The cache directory for storing packages.
If `null`, defaults to `~/.pkl/cache`. If `null`, defaults to `~/.cache/pkl` on Unix or `%LOCALAPPDATA%/pkl/Cache` on Windows.
This setting can also be configured using the `$XDG_CACHE_HOME` environment variable.
==== ====
.color: Property<Boolean> .color: Property<Boolean>
@@ -69,7 +69,7 @@ Example: `settingsModule = layout.projectDirectory.file("mySettings.pkl")` +
The Pkl settings module to use. The Pkl settings module to use.
This property accepts the same input types as the `sourceModules` property. This property accepts the same input types as the `sourceModules` property.
If `null`, `~/.pkl/settings.pkl` or defaults specified in the `pkl.settings` standard library module are used. If `null`, `~/.config/pkl/settings.pkl` on Unix or `%APPDATA%/pkl/settings.pkl` on Windows (or the legacy `~/.pkl/settings.pkl`), or defaults specified in the `pkl.settings` standard library module are used.
==== ====
include::../partials/gradle-common-properties.adoc[] include::../partials/gradle-common-properties.adoc[]
+37 -1
View File
@@ -13,7 +13,43 @@ include::partial$intro.adoc[]
== Noteworthy [small]#🎶# == Noteworthy [small]#🎶#
=== XXX === CLI Changes
==== Default file locations
For new setups, the CLI no longer stores anything under `~/.pkl` (https://github.com/apple/pkl/pull/1809[#1809]).
It uses XDG-style locations on Unix and Known Folder locations on Windows:
[cols="1,2,2,2",options="header"]
|===
| Concern | Unix (Linux/macOS) | Windows | Legacy fallback
| Package cache
| `~/.cache/pkl`
| `$LOCALAPPDATA/pkl/Cache`
| none
| Settings file
| `~/.config/pkl/settings.pkl`
| `$APPDATA/pkl/settings.pkl`
| `~/.pkl/settings.pkl`
| CA certificates
| `~/.config/pkl/cacerts`
| `$APPDATA/pkl/cacerts`
| `~/.pkl/cacerts`
| REPL history
| `~/.local/state/pkl/repl-history`
| `$LOCALAPPDATA/pkl/repl-history`
| none
|===
On every OS, these locations can be overridden with XDG-style env vars.
For example, setting `XDG_CACHE_HOME` will configure the cache directory.
Note that the existing `~/.pkl/cache` directory is ignored, so Pkl will download packages to populate its cache if configured to do so.
== Breaking Changes [small]#💔# == Breaking Changes [small]#💔#
@@ -63,7 +63,11 @@ internal class Repl(workingDir: Path, private val server: ReplServer, private va
} }
completer(AggregateCompleter(CommandCompleter, FileCompleter(workingDir))) completer(AggregateCompleter(CommandCompleter, FileCompleter(workingDir)))
option(Option.DISABLE_EVENT_EXPANSION, true) option(Option.DISABLE_EVENT_EXPANSION, true)
variable(LineReader.HISTORY_FILE, (IoUtils.getPklHomeDir().resolve("repl-history"))) // Will be null if `user.home` property is not set.
// If so, don't bother writing repl history.
IoUtils.getReplHistoryFile()?.let { historyFile ->
variable(LineReader.HISTORY_FILE, historyFile)
}
} }
.build() .build()
@@ -80,7 +84,7 @@ internal class Repl(workingDir: Path, private val server: ReplServer, private va
fun run() { fun run() {
// JLine 2 history file is incompatible with JLine 3 // JLine 2 history file is incompatible with JLine 3
IoUtils.getPklHomeDir().resolve("repl-history.bin").deleteIfExists() IoUtils.getLegacyPklHomeDir().resolve("repl-history.bin").deleteIfExists()
println(ReplMessages.welcome) println(ReplMessages.welcome)
println() println()
@@ -77,8 +77,9 @@ data class CliBaseOptions(
/** /**
* The Pkl settings file to use. A settings file is a Pkl module amending the `pkl.settings` * The Pkl settings file to use. A settings file is a Pkl module amending the `pkl.settings`
* standard library module. If `null`, `~/.pkl/settings.pkl` (if present) or the defaults * standard library module. If `null`, `~/.config/pkl/settings.pkl` (falling back to the legacy
* specified in the `pkl:settings` standard library module are used. * `~/.pkl/settings.pkl`), or the defaults specified in the `pkl:settings` standard library
* module, are used.
*/ */
private val settings: URI? = null, private val settings: URI? = null,
@@ -130,8 +131,9 @@ data class CliBaseOptions(
* The given files must contain [X.509](https://en.wikipedia.org/wiki/X.509) certificates in PEM * The given files must contain [X.509](https://en.wikipedia.org/wiki/X.509) certificates in PEM
* format. * format.
* *
* If [caCertificates] is the empty list, the certificate files in `~/.pkl/cacerts/` are used. If * If [caCertificates] is the empty list, the certificate files in `~/.config/pkl/cacerts/` (or
* `~/.pkl/cacerts/` does not exist or is empty, Pkl's built-in CA certificates are used. * the legacy `~/.pkl/cacerts/`) are used. If that directory does not exist or is empty, Pkl's
* built-in CA certificates are used.
*/ */
val caCertificates: List<Path> = listOf(), val caCertificates: List<Path> = listOf(),
@@ -20,6 +20,7 @@ import java.net.URI
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlin.io.path.absolutePathString
import kotlin.io.path.isRegularFile import kotlin.io.path.isRegularFile
import org.pkl.core.* import org.pkl.core.*
import org.pkl.core.evaluatorSettings.PklEvaluatorSettings import org.pkl.core.evaluatorSettings.PklEvaluatorSettings
@@ -33,6 +34,7 @@ import org.pkl.core.project.Project
import org.pkl.core.resource.ResourceReader import org.pkl.core.resource.ResourceReader
import org.pkl.core.resource.ResourceReaders import org.pkl.core.resource.ResourceReaders
import org.pkl.core.settings.PklSettings import org.pkl.core.settings.PklSettings
import org.pkl.core.util.DebugLogger
import org.pkl.core.util.IoUtils import org.pkl.core.util.IoUtils
/** Building block for CLI commands. Configured programmatically to allow for embedding. */ /** Building block for CLI commands. Configured programmatically to allow for embedding. */
@@ -69,7 +71,7 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) {
if (cliOptions.normalizedSettingsModule != null) { if (cliOptions.normalizedSettingsModule != null) {
PklSettings.load(ModuleSource.uri(cliOptions.normalizedSettingsModule)) PklSettings.load(ModuleSource.uri(cliOptions.normalizedSettingsModule))
} else { } else {
PklSettings.loadFromPklHomeDir() PklSettings.loadFromSystem()
} }
} catch (e: PklException) { } catch (e: PklException) {
// do not use `errorRenderer` because it depends on `settings` // do not use `errorRenderer` because it depends on `settings`
@@ -146,7 +148,7 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) {
?: evaluatorSettings?.let { settings -> ?: evaluatorSettings?.let { settings ->
if (settings.noCache == true) null else settings.moduleCacheDir if (settings.noCache == true) null else settings.moduleCacheDir
} }
?: IoUtils.getDefaultModuleCacheDir() ?: IoUtils.getSystemModuleCacheDir()
} }
protected val modulePath: List<Path> by lazy { protected val modulePath: List<Path> by lazy {
@@ -215,7 +217,7 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) {
} }
private fun HttpClient.Builder.addDefaultCliCertificates() { private fun HttpClient.Builder.addDefaultCliCertificates() {
val caCertsDir = IoUtils.getPklHomeDir().resolve("cacerts") val caCertsDir = IoUtils.getSystemCaCertsDir()
var certsAdded = false var certsAdded = false
if (Files.isDirectory(caCertsDir)) { if (Files.isDirectory(caCertsDir)) {
Files.list(caCertsDir) Files.list(caCertsDir)
@@ -225,7 +227,10 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) {
addCertificates(cert) addCertificates(cert)
} }
} }
if (!certsAdded) { if (certsAdded) {
DebugLogger.log("Loading CA certificates from ${caCertsDir.normalize().absolutePathString()}")
} else {
DebugLogger.log("Using built-in CA certificates")
val defaultCerts = val defaultCerts =
this@CliCommand.javaClass.classLoader.getResourceAsStream( this@CliCommand.javaClass.classLoader.getResourceAsStream(
"org/pkl/commons/cli/PklCARoots.pem" "org/pkl/commons/cli/PklCARoots.pem"
@@ -144,7 +144,7 @@ class CliCommandTest {
assertThat(cliTest.myRootDir).isNull() assertThat(cliTest.myRootDir).isNull()
assertThat(builder.environmentVariables).isEqualTo(System.getenv()) assertThat(builder.environmentVariables).isEqualTo(System.getenv())
assertThat(builder.externalProperties).isEmpty() assertThat(builder.externalProperties).isEmpty()
assertThat(builder.moduleCacheDir).isEqualTo(IoUtils.getDefaultModuleCacheDir()) assertThat(builder.moduleCacheDir).isEqualTo(IoUtils.getSystemModuleCacheDir())
assertThat(cliTest.myModulePath).isEmpty() assertThat(cliTest.myModulePath).isEmpty()
assertThat(builder.color).isFalse assertThat(builder.color).isFalse
assertThat(cliTest.myProxyAddress).isNull() assertThat(cliTest.myProxyAddress).isNull()
@@ -58,7 +58,7 @@ public final class EvaluatorBuilder {
private java.time.@Nullable Duration timeout; private java.time.@Nullable Duration timeout;
private @Nullable Path moduleCacheDir = IoUtils.getDefaultModuleCacheDir(); private @Nullable Path moduleCacheDir = IoUtils.getSystemModuleCacheDir();
private @Nullable String outputFormat; private @Nullable String outputFormat;
@@ -113,7 +113,7 @@ public final class ModuleCache {
case "semver": case "semver":
return SemVerModule.getModule(); return SemVerModule.getModule();
case "settings": case "settings":
// always needed if ~/.pkl/settings.pkl is present // always needed if ~/.config/pkl/settings.pkl is present
return SettingsModule.getModule(); return SettingsModule.getModule();
case "test": case "test":
return TestModule.getModule(); return TestModule.getModule();
@@ -26,6 +26,7 @@ import org.pkl.core.module.ModuleKeyFactories;
import org.pkl.core.resource.ResourceReaders; import org.pkl.core.resource.ResourceReaders;
import org.pkl.core.runtime.VmEvalException; import org.pkl.core.runtime.VmEvalException;
import org.pkl.core.runtime.VmExceptionBuilder; import org.pkl.core.runtime.VmExceptionBuilder;
import org.pkl.core.util.DebugLogger;
import org.pkl.core.util.IoUtils; 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 * {@literal pkl.settings} standard library module. To load a settings file, use one of the static
* {@code load} methods. * {@code load} methods.
*/ */
// keep in sync with stdlib/settings.pkl
public record PklSettings(Editor editor, PklEvaluatorSettings.@Nullable Http http) { 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 = private static final List<Pattern> ALLOWED_MODULES =
List.of(Pattern.compile("pkl:"), Pattern.compile("file:")); 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:")); List.of(Pattern.compile("env:"), Pattern.compile("file:"));
/** /**
* Loads the user settings file ({@literal ~/.pkl/settings.pkl}). If this file does not exist, * Loads the user settings file.
* returns default settings defined by module {@literal pkl.settings}. *
* <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 { 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. */ /** For testing only. */
static PklSettings loadFromPklHomeDir(Path pklHomeDir) throws VmEvalException { static PklSettings loadFromSettingsDir(Path settingsDir) throws VmEvalException {
var path = pklHomeDir.resolve("settings.pkl"); return loadFromSettingsFile(settingsDir.resolve("settings.pkl"));
}
private static PklSettings loadFromSettingsFile(Path path) throws VmEvalException {
return Files.exists(path) return Files.exists(path)
? load(ModuleSource.path(path)) ? load(ModuleSource.path(path))
: new PklSettings(Editor.SYSTEM, null); : 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; import org.pkl.core.util.GlobResolver.InvalidGlobPatternException;
public final class IoUtils { public final class IoUtils {
// Don't match paths like `C:\`, which are drive letters on Windows. // Don't match paths like `C:\`, which are drive letters on Windows.
private static final Pattern uriLike = Pattern.compile("[\\w+.-]+:[^\\\\].*"); 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 // 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"); 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 // not stored to avoid build-time initialization by native-image
public static Path getDefaultModuleCacheDir() { public static @Nullable Path getReplHistoryFile() {
return getPklHomeDir().resolve("cache"); return BaseDirectories.state.resolveHome("repl-history");
} }
// not stored to avoid build-time initialization by native-image // not stored to avoid build-time initialization by native-image
@@ -42,7 +42,7 @@ class PklSettingsTest {
.trimIndent() .trimIndent()
) )
val settings = PklSettings.loadFromPklHomeDir(tempDir) val settings = PklSettings.loadFromSettingsDir(tempDir)
assertThat(settings).isEqualTo(PklSettings(Editor.SUBLIME, null)) assertThat(settings).isEqualTo(PklSettings(Editor.SUBLIME, null))
} }
@@ -80,7 +80,7 @@ class PklSettingsTest {
.trimIndent() .trimIndent()
) )
val settings = PklSettings.loadFromPklHomeDir(tempDir) val settings = PklSettings.loadFromSettingsDir(tempDir)
val expectedHttp = val expectedHttp =
PklEvaluatorSettings.Http( PklEvaluatorSettings.Http(
PklEvaluatorSettings.Proxy( PklEvaluatorSettings.Proxy(
@@ -113,7 +113,7 @@ class PklSettingsTest {
.trimIndent() .trimIndent()
) )
val settings = PklSettings.loadFromPklHomeDir(tempDir) val settings = PklSettings.loadFromSettingsDir(tempDir)
val expectedHttp = val expectedHttp =
PklEvaluatorSettings.Http( PklEvaluatorSettings.Http(
PklEvaluatorSettings.Proxy(URI("http://localhost:8080"), listOf()), PklEvaluatorSettings.Proxy(URI("http://localhost:8080"), listOf()),
@@ -169,7 +169,7 @@ class PklSettingsTest {
@Test @Test
fun `invalid settings file`(@TempDir tempDir: Path) { fun `invalid settings file`(@TempDir tempDir: Path) {
val settingsFile = tempDir.resolve("settings.pkl").apply { writeString("foo = 1") } val settingsFile = tempDir.resolve("settings.pkl").apply { writeString("foo = 1") }
assertThatCode { PklSettings.loadFromPklHomeDir(tempDir) } assertThatCode { PklSettings.loadFromSettingsDir(tempDir) }
.hasMessageContaining( .hasMessageContaining(
"Expected `output.value` of module `${settingsFile.toUri()}` to be of type `pkl.settings`, but got type `settings`." "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()
}
}
@@ -16,9 +16,11 @@
package org.pkl.executor; package org.pkl.executor;
import java.net.URI; import java.net.URI;
import java.nio.file.InvalidPathException;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.Duration; import java.time.Duration;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import org.jspecify.annotations.Nullable; 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.ExecutorSpiOptions2;
import org.pkl.executor.spi.v1.ExecutorSpiOptions3; import org.pkl.executor.spi.v1.ExecutorSpiOptions3;
import org.pkl.executor.spi.v1.ExecutorSpiOptions4; import org.pkl.executor.spi.v1.ExecutorSpiOptions4;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** /**
* Options for {@link Executor#evaluatePath}. * 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}. * <p>To create {@code ExecutorOptions}, use its {@linkplain #builder builder}.
*/ */
public final class ExecutorOptions { public final class ExecutorOptions {
private static final Logger logger = LoggerFactory.getLogger(ExecutorOptions.class);
private final List<String> allowedModules; private final List<String> allowedModules;
private final List<String> allowedResources; private final List<String> allowedResources;
@@ -67,7 +73,42 @@ public final class ExecutorOptions {
/** Returns the module cache dir that the CLI uses by default. */ /** Returns the module cache dir that the CLI uses by default. */
public static Path defaultModuleCacheDir() { 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() { 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"))
}
}
@@ -322,7 +322,10 @@ public class PklPlugin implements Plugin<Project> {
// Therefore, we don't set any initial value for the environmentVariables property. // Therefore, we don't set any initial value for the environmentVariables property.
// Not using `convention()` to allow the user to unset this property, disabling the cache. // Not using `convention()` to allow the user to unset this property, disabling the cache.
spec.getModuleCacheDir().set(IoUtils.getDefaultModuleCacheDir().toFile()); var systemCacheDir = IoUtils.getSystemModuleCacheDir();
if (systemCacheDir != null) {
spec.getModuleCacheDir().set(systemCacheDir.toFile());
}
spec.getNoCache().convention(false); spec.getNoCache().convention(false);
+10 -1
View File
@@ -17,8 +17,17 @@
/// Configuration settings for Pkl itself. /// Configuration settings for Pkl itself.
/// ///
/// Every settings file must amend this module. /// Every settings file must amend this module.
///
/// Unless CLI commands and build tool plugins are explicitly configured with a settings file, /// Unless CLI commands and build tool plugins are explicitly configured with a settings file,
/// they will use `~/.pkl/settings.pkl` or the defaults specified in this module. /// they look in the following locations in order of precedence:
///
/// 1. `$XDG_CONFIG_HOME/pkl/settings.pkl`
/// 2. `$APPDATA/pkl/settings.pkl` (on Windows only)
/// 3. `~/.config/pkl/settings.pkl`
/// 4. Path `pkl/settings.pkl` within the `$XDG_CONFIG_DIRS` search path
/// (dirs separated by `:` on Unix, `;` on Windows).
/// 4. `/etc/xdg/pkl/settings.pkl`
/// 5. `~/.pkl/settings.pkl` (legacy location used by Pkl 0.32 and lower)
@ModuleInfo { minPklVersion = "0.33.0" } @ModuleInfo { minPklVersion = "0.33.0" }
module pkl.settings module pkl.settings