Files
pkl/stdlib/EvaluatorSettings.pkl
T
Daniel Chao 035ef0a789 Change loading of external readers (#1394)
This introduces breaking changes for external readers are loaded:

1. In PklProject, relative paths are resolved relative to the enclosing
PklProject file (make behavior consistent with how other settings work)
2. Make CLI flags blow away any settings set on a PklProject
3. Introduce a new `workingDir` property, which defaults to the
PklProject dir

The overall goal is to make this behavior consistent with how other
settings work.
For example, relative paths for other evaluator settings are already
relative to the project directory.
Additionally, in every other case, CLI flags will overwrite any setting
set within PklProject.
2026-06-02 18:02:52 +00:00

440 lines
14 KiB
Plaintext

//===----------------------------------------------------------------------===//
// Copyright © 2024-2025 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.
//===----------------------------------------------------------------------===//
/// Common settings for Pkl's own evaluator.
@ModuleInfo { minPklVersion = "0.32.0" }
@Since { version = "0.26.0" }
module pkl.EvaluatorSettings
import "pkl:EvaluatorSettings"
import "pkl:platform"
// used by doc comments
import "pkl:Project"
/// The external properties available to Pkl, read using the `prop:` scheme.
externalProperties: Mapping<String, String>?
/// The environment variables available to Pkl, read using the `env:` scheme.
///
/// Example:
/// ```
/// env {
/// ["IS_PROD"] = "true"
/// }
/// ```
env: Mapping<String, String>?
/// The set of module URI patterns that can be imported.
///
/// Each element is a regular expression pattern that is tested against a module import.
///
/// Modules are imported either through an amends or extends clause, an import clause, or an
/// import expression.
///
/// Example:
/// ```
/// allowedModules {
/// "https:"
/// "file:"
/// "package:"
/// "projectpackage:"
/// }
/// ```
allowedModules: Listing<String(isRegex)>?
/// The set of resource URI patterns that can be imported.
///
/// Each element is a regular expression pattern that is tested against a resource read.
///
/// Example:
/// ```
/// allowedResources {
/// "https:"
/// "file:"
/// "package:"
/// "projectpackage:"
/// "env:"
/// "prop:"
/// }
/// ```
allowedResources: Listing<String(isRegex)>?
/// When to format messages with ANSI color codes.
///
/// Possible values:
///
/// - `"never"`: Never format
/// - `"auto"`: Format if the process' stdin, stdout, or stderr are connected to a console.
/// - `"always"`: Always format
@Since { version = "0.27.0" }
color: ("never" | "auto" | "always")?
/// Disables the file system cache for `package:` modules.
///
/// When caching is disabled, packages are loaded over the network and stored in memory.
noCache: Boolean?
/// A collection of jars, zips, or directories to be placed into the module path.
///
/// Module path modules and resources may be read and imported using the `modulepath:` scheme.
modulePath: Listing<String>?
/// The duration after which evaluation of a source module will be timed out.
///
/// Note that a timeout is treated the same as a program error in that any subsequent source modules will not be evaluated.
timeout: Duration?
/// The directory where `package:` modules are cached.
moduleCacheDir: String?
/// Restricts access to file-based modules and resources to those located under this directory.
rootDir: String?
/// Configuration of outgoing HTTP requests.
http: Http?
/// Configuration for external module reader processes.
@Since { version = "0.27.0" }
externalModuleReaders: Mapping<String, ExternalReader>?
/// Configuration for external resource reader processes.
@Since { version = "0.27.0" }
externalResourceReaders: Mapping<String, ExternalReader>?
/// Defines options for the formatting of calls to the trace() method.
///
/// Possible values:
///
/// - `"compact"`: All structures passed to trace() will be emitted on a single line.
/// - `"pretty"`: All structures passed to trace() will be indented and emitted across multiple lines.
@Since { version = "0.30.0" }
traceMode: ("compact" | "pretty")?
/// These evaluator settings, whose settings are resolved against [enclosingUri] using OS rules for
/// [os].
@Since { version = "0.32.0" }
function resolveForOs(enclosingUri: Uri, os: platform.OperatingSystem): EvaluatorSettings = (module) {
local forWindows = os.name == "Windows"
when (module.modulePath != null) {
modulePath = new {
for (path in module.modulePath!!) {
resolvePath(enclosingUri, path, forWindows)
}
}
}
when (module.moduleCacheDir != null) {
moduleCacheDir = resolvePath(enclosingUri, module.moduleCacheDir!!, forWindows)
}
when (module.rootDir != null) {
rootDir = resolvePath(enclosingUri, module.rootDir!!, forWindows)
}
when (module.externalResourceReaders != null) {
externalResourceReaders {
[[true]] {
executable = resolveExecutable(enclosingUri, super.executable, forWindows)
workingDir = resolvePath(enclosingUri, super.workingDir ?? "./", forWindows)
}
}
}
when (module.externalModuleReaders != null) {
externalModuleReaders {
for (readerName, reader in module.externalModuleReaders!!) {
[readerName] {
executable = resolveExecutable(enclosingUri, reader.executable, forWindows)
workingDir =
if (reader.workingDir == null)
resolvePath(enclosingUri, "./", forWindows)
else
resolvePath(enclosingUri, reader.workingDir!!, forWindows)
}
}
}
}
}
/// These evaluator settings, whose settings are resolved to URIs against [enclosingUri].
///
/// The following settings are resolved:
///
/// * [modulePath]
/// * [rootDir]
/// * [moduleCacheDir]
/// * [ExternalReader.executable]
/// * [ExternalReader.workingDir]
///
/// Returns file paths based on the host OS filesystem.
///
/// If [ExternalReader.workingDir] is `null`, it resolves to the `./` path off of
/// [enclosingUri].
///
/// On POSIX-based systems (like macOS and linux), this resolves to paths such as `/path/to/dir`.
///
/// On Windows, this resolves to drive letter paths such as `C:\path\to\dir`, or UNC paths such as
/// `\\network\share\path\to\dir`.
@Since { version = "0.32.0" }
function resolve(enclosingUri: Uri): EvaluatorSettings =
resolveForOs(
enclosingUri,
platform.current.operatingSystem,
)
local function resolveExecutable(base: String, path: String, forWindows: Boolean) =
let (hasSep = if (forWindows) path.contains(Regex(#"[/\\]"#)) else path.contains("/"))
if (hasSep) resolvePath(base, path, forWindows) else path
external local function resolvePath(
baseUri: String(startsWith("file:/")),
path: String,
forWindows: Boolean,
): String
local const hostnameRegex = Regex(#"https?://([^/?#]*)"#)
local const hasNonEmptyHostname = (it: String) ->
let (hostname = hostnameRegex.findMatchesIn(it).getOrNull(0)?.groups?.getOrNull(1)?.value)
hostname != null && hostname.length > 0
/// A key or value in [Http.rewrites].
@Since { version = "0.29.0" }
typealias HttpRewrite = String(startsWith(Regex("https?://")), endsWith("/"), hasNonEmptyHostname)
/// Settings that control how Pkl talks to HTTP(S) servers.
class Http {
/// Configuration of the HTTP proxy to use.
proxy: Proxy?
/// Replace outbound requests from one URL with another URL.
///
/// Each key describes the prefix of a request, and each value describes the replacement prefix.
///
/// This can be useful for setting up mirroring of packages, which are fetched over HTTPS.
///
/// In the case of multiple matches, the longest prefix is used.
///
/// The URL hostname is case-insensitive.
///
/// A replacement only happens once when resolving URLs.
/// Therefore, the first replacement is the final URL that is used for the outbound request.
///
/// In the following example, an original request for `https://pkg.pkl-lang.org/my/pkg@1.0.0` is
/// replaced with `https://my.internal.mirror/my/pkg@1.0.0`.
///
/// This does not affect `3XX` status code redirect following.
///
/// Example:
///
/// ```
/// rewrites {
/// ["https://pkg.pkl-lang.org/"] = "https://my.internal.mirror/"
/// }
/// ```
///
/// Rewrite targets must satisfy the following:
///
/// * It starts with either `http://`, or `https://`.
/// * It ends with `/`.
/// * It has a non-empty hostname.
///
/// An rewrite target should also not contain a query string or fragment component
/// (not schematically enforced).
@Since { version = "0.29.0" }
rewrites: Mapping<HttpRewrite, HttpRewrite>?
/// HTTP headers to add to outbound requests.
///
/// Each key is a glob pattern, and each value is a mapping of header name to header value(s).
///
/// Before an HTTP request is made, each key is matched against the request URL.
/// If any matches are found, each of their described headers are added to the request.
///
/// To add headers to all HTTP requests, use `**` as the glob pattern.
///
/// Example:
///
/// ```
/// headers {
/// // Add this user agent to all out-bound requests.
/// ["**"] {
/// ["User-Agent"] = "MyApp-Pkl"
/// }
///
/// // Add an authorization header to all requests made to the
/// // `https://my.internal.service` host.
/// ["https://my.internal.service/**"] {
/// ["Authorization"] = "Bearer abc123"
/// }
/// }
/// ```
///
/// To repeat a header multiple times, set the value to a listing:
///
/// ```
/// headers {
/// // Adds:
/// // My-Custom-Header: foo
/// // My-Custom-Header: bar
/// ["**"] {
/// ["My-Custom-Header"] { "foo"; "bar" }
/// }
/// }
/// ```
///
/// For details on glob patterns, see
/// <https://pkl-lang.org/main/current/language-reference/index.html#glob-patterns>.
@Since { version = "0.32.0" }
headers: Mapping<String(isGlobPattern), HttpHeaders>?
}
/// Settings that control how Pkl talks to HTTP proxies.
class Proxy {
/// The proxy to use for HTTP(S) connections.
///
/// Only HTTP proxies are supported.
/// The proxy address must start with `http://`, and can only contain a host and a port.
/// If a port is omitted, it defaults to port `80`.
///
/// Proxy authentication is not supported.
///
/// Example:
/// ```
/// address = "http://my.proxy.example.com:5080"
/// ```
address: Uri(startsWith("http://"))?
/// Hosts to which all connections should bypass a proxy.
///
/// Values can be either hostnames, or IP addresses.
/// IP addresses can optionally be provided using
/// [CIDR notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing#CIDR_notation).
///
/// The value `"*"` is a wildcard that disables proxying for all hosts.
///
/// A hostname matches all subdomains.
/// For example, `example.com` matches `foo.example.com`, but not `fooexample.com`.
/// A hostname that is prefixed with a dot matches the hostname itself,
/// so `.example.com` matches `example.com`.
///
/// Hostnames do not match their resolved IP addresses.
/// For example, the hostname `localhost` will not match `127.0.0.1`.
///
/// Optionally, a port can be specified.
/// If a port is omitted, all ports are matched.
///
/// Example:
///
/// ```
/// noProxy {
/// "127.0.0.1"
/// "169.254.0.0/16"
/// "example.com"
/// "localhost:5050"
/// }
/// ```
noProxy: Listing<String>(isDistinct)
}
@Since { version = "0.27.0" }
class ExternalReader {
/// The external reader executable.
///
/// Will be spawned with the same environment variables and working directory as the Pkl process.
///
/// This may be:
///
/// * An absolute or relative path.
/// * The name of the executable, to be resolved against the `PATH` environment variable.
///
/// When declared inside a [Project], relative paths are resolved against
/// [Project.projectFileUri].
executable: String
/// Additional command line arguments passed to the external reader process.
arguments: Listing<String>?
/// The working directory used to spawn [executable].
///
/// When declared inside a [Project], relative paths are resolved against
/// [Project.projectFileUri].
@Since { version = "0.32.0" }
workingDir: String?
}
local typealias ReservedHttpHeaderName =
"connection"
| "content-length"
| "expect"
| "host"
| "keep-alive"
| "te"
| "trailer"
| "transfer-encoding"
| "upgrade"
local const reservedHttpHeaderPrefix =
Set(
"proxy-",
"sec-",
)
local const isNotReservedHeaderName = (header: String) ->
!(header.toLowerCase() is ReservedHttpHeaderName)
local const doesNotStartWithReservedPrefix = (header: String) ->
!reservedHttpHeaderPrefix.any((it) -> header.toLowerCase().startsWith(it))
local const hasValidHeaderNameSyntax = (header: String) ->
header.matches(Regex(#"[a-zA-Z0-9!#$%&'*+-.^_`|~]+"#))
/// A mapping of header names to header value(s).
///
/// The mapping must have distinct header keys (case insensitive).
@Since { version = "0.32.0" }
typealias HttpHeaders =
Mapping<HttpHeaderName, *Listing<HttpHeaderValue> | HttpHeaderValue>(
keys.toList().isDistinctBy((it) -> it.toLowerCase()),
)
/// An HTTP header name.
///
/// It conforms to the following rules:
///
/// * It is not one of "connection", "keep-alive", "content-length", "expect", "host", "upgrade",
/// "te", "transfer-encoding", "trailer" (case insensitive).
/// * It does not start with "proxy-" nor "sec-".
/// * It consists of a-z, A-Z, or a character in "!#$%&'*+-.^_`|~".
/// * It is not a blank string.
@Since { version = "0.32.0" }
typealias HttpHeaderName =
String(
isNotReservedHeaderName,
doesNotStartWithReservedPrefix,
hasValidHeaderNameSyntax,
isNotBlank,
)
/// An HTTP header value.
///
/// It consists of ASCII characters between the range of 0x20 - 0x7E, and 0x80 - 0xFF (all visible
/// ASCII characters), and also the tab character.
@Since { version = "0.32.0" }
typealias HttpHeaderValue =
String(matches(Regex(#"[\t\u0020-\u007E\u0080-\u00FF]*"#)), length < 4096)