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
@@ -210,6 +210,11 @@ abstract class NativeImageBuild : DefaultTask() {
|
||||
add("--initialize-at-build-time=")
|
||||
// needed for messagepack-java (see https://github.com/msgpack/msgpack-java/issues/600)
|
||||
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
|
||||
add("--initialize-at-run-time=org.jline.nativ,org.jline.terminal.impl.jni")
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
Pkl complains about a _type constraint_.
|
||||
|
||||
@@ -1377,14 +1377,24 @@ it works as follows:
|
||||
The Pkl settings file allows to customize the CLI experience.
|
||||
|
||||
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 enforce default settings, use `--settings pkl:settings`.
|
||||
|
||||
The settings file is also honored by (and configurable through) the Gradle plugin and `CliEvaluator` API.
|
||||
|
||||
Here is a typical settings file:
|
||||
|
||||
.~/.pkl/settings.pkl
|
||||
.~/.config/pkl/settings.pkl
|
||||
[source%parsed,{pkl}]
|
||||
----
|
||||
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:
|
||||
|
||||
- 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. +
|
||||
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.
|
||||
|
||||
[[http-proxy]]
|
||||
|
||||
@@ -36,9 +36,15 @@ Possible values:
|
||||
.--cache-dir
|
||||
[%collapsible]
|
||||
====
|
||||
Default: `~/.pkl/cache` +
|
||||
Example: `/path/to/module/cache/` +
|
||||
|
||||
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
|
||||
@@ -97,7 +103,7 @@ Any symlinks are resolved before this check is performed.
|
||||
Default: (none) +
|
||||
Example: `mySettings.pkl` +
|
||||
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
|
||||
|
||||
@@ -64,7 +64,9 @@ Default: `null` +
|
||||
Example 1: `moduleCacheDir = layout.buildDirectory.dir("pkl-module-cache")` +
|
||||
Example 2: `moduleCacheDir.fileValue file("/absolute/path/to/cache")` +
|
||||
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>
|
||||
|
||||
@@ -69,7 +69,7 @@ Example: `settingsModule = layout.projectDirectory.file("mySettings.pkl")` +
|
||||
The Pkl settings module to use.
|
||||
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[]
|
||||
|
||||
@@ -13,7 +13,43 @@ include::partial$intro.adoc[]
|
||||
|
||||
== 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]#💔#
|
||||
|
||||
|
||||
@@ -63,7 +63,11 @@ internal class Repl(workingDir: Path, private val server: ReplServer, private va
|
||||
}
|
||||
completer(AggregateCompleter(CommandCompleter, FileCompleter(workingDir)))
|
||||
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()
|
||||
|
||||
@@ -80,7 +84,7 @@ internal class Repl(workingDir: Path, private val server: ReplServer, private va
|
||||
|
||||
fun run() {
|
||||
// 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()
|
||||
|
||||
@@ -77,8 +77,9 @@ data class CliBaseOptions(
|
||||
|
||||
/**
|
||||
* 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
|
||||
* specified in the `pkl:settings` standard library module are used.
|
||||
* standard library module. If `null`, `~/.config/pkl/settings.pkl` (falling back to the legacy
|
||||
* `~/.pkl/settings.pkl`), or the defaults specified in the `pkl:settings` standard library
|
||||
* module, are used.
|
||||
*/
|
||||
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
|
||||
* format.
|
||||
*
|
||||
* If [caCertificates] is the empty list, the certificate files in `~/.pkl/cacerts/` are used. If
|
||||
* `~/.pkl/cacerts/` does not exist or is empty, Pkl's built-in CA certificates are used.
|
||||
* If [caCertificates] is the empty list, the certificate files in `~/.config/pkl/cacerts/` (or
|
||||
* 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(),
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import java.net.URI
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.io.path.absolutePathString
|
||||
import kotlin.io.path.isRegularFile
|
||||
import org.pkl.core.*
|
||||
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.ResourceReaders
|
||||
import org.pkl.core.settings.PklSettings
|
||||
import org.pkl.core.util.DebugLogger
|
||||
import org.pkl.core.util.IoUtils
|
||||
|
||||
/** 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) {
|
||||
PklSettings.load(ModuleSource.uri(cliOptions.normalizedSettingsModule))
|
||||
} else {
|
||||
PklSettings.loadFromPklHomeDir()
|
||||
PklSettings.loadFromSystem()
|
||||
}
|
||||
} catch (e: PklException) {
|
||||
// do not use `errorRenderer` because it depends on `settings`
|
||||
@@ -146,7 +148,7 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) {
|
||||
?: evaluatorSettings?.let { settings ->
|
||||
if (settings.noCache == true) null else settings.moduleCacheDir
|
||||
}
|
||||
?: IoUtils.getDefaultModuleCacheDir()
|
||||
?: IoUtils.getSystemModuleCacheDir()
|
||||
}
|
||||
|
||||
protected val modulePath: List<Path> by lazy {
|
||||
@@ -215,7 +217,7 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) {
|
||||
}
|
||||
|
||||
private fun HttpClient.Builder.addDefaultCliCertificates() {
|
||||
val caCertsDir = IoUtils.getPklHomeDir().resolve("cacerts")
|
||||
val caCertsDir = IoUtils.getSystemCaCertsDir()
|
||||
var certsAdded = false
|
||||
if (Files.isDirectory(caCertsDir)) {
|
||||
Files.list(caCertsDir)
|
||||
@@ -225,7 +227,10 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) {
|
||||
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 =
|
||||
this@CliCommand.javaClass.classLoader.getResourceAsStream(
|
||||
"org/pkl/commons/cli/PklCARoots.pem"
|
||||
|
||||
@@ -144,7 +144,7 @@ class CliCommandTest {
|
||||
assertThat(cliTest.myRootDir).isNull()
|
||||
assertThat(builder.environmentVariables).isEqualTo(System.getenv())
|
||||
assertThat(builder.externalProperties).isEmpty()
|
||||
assertThat(builder.moduleCacheDir).isEqualTo(IoUtils.getDefaultModuleCacheDir())
|
||||
assertThat(builder.moduleCacheDir).isEqualTo(IoUtils.getSystemModuleCacheDir())
|
||||
assertThat(cliTest.myModulePath).isEmpty()
|
||||
assertThat(builder.color).isFalse
|
||||
assertThat(cliTest.myProxyAddress).isNull()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
@@ -322,7 +322,10 @@ public class PklPlugin implements Plugin<Project> {
|
||||
// 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.
|
||||
spec.getModuleCacheDir().set(IoUtils.getDefaultModuleCacheDir().toFile());
|
||||
var systemCacheDir = IoUtils.getSystemModuleCacheDir();
|
||||
if (systemCacheDir != null) {
|
||||
spec.getModuleCacheDir().set(systemCacheDir.toFile());
|
||||
}
|
||||
|
||||
spec.getNoCache().convention(false);
|
||||
|
||||
|
||||
+10
-1
@@ -17,8 +17,17 @@
|
||||
/// Configuration settings for Pkl itself.
|
||||
///
|
||||
/// Every settings file must amend this module.
|
||||
///
|
||||
/// 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" }
|
||||
module pkl.settings
|
||||
|
||||
|
||||
Reference in New Issue
Block a user