Use XDG base directories and Known Folders on Windows (#1809)

This changes logic that previously read/wrote from `~/.pkl` to use
XDG base directories (all OSes), and Known Folders locations on Windows.

For example, Pkl will look for `settings.pkl` in:

1. `$XDG_CONFIG_HOME/pkl/settings.pkl`
2. `%APPDATA/pkl/settings.pkl`
3. `~/.pkl/settings.pkl`
4. Path pkl/settings/pkl within `$XDG_CONFIG_DIRS`
5. `/etc/xdg/pkl/settings.pkl`

---------

Co-authored-by: Florin Ungur <florin@florinungur.com>
This commit is contained in:
Daniel Chao
2026-08-20 21:50:24 +00:00
committed by GitHub
co-authored by Florin Ungur
parent 4dd37219c0
commit 6551a59f9e
24 changed files with 799 additions and 40 deletions
@@ -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()