mirror of
https://github.com/apple/pkl.git
synced 2026-08-26 21:54: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=")
|
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_.
|
||||||
|
|||||||
@@ -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[]
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user