Pkl 0.26 Release Notes

Pkl 0.26 was released on June 17th, 2024.
The latest bugfix release is 0.26.2. (All Versions)

This release brings Windows support, improvements to controlling how Pkl talks over HTTP, and also fixes a scoping bug around typealiases.

The next release (0.27) is scheduled for October 10th, 2024.

Please send feedback and questions to GitHub Discussions, or submit an issue on Github.

Pkl is hosted on GitHub. To get started, follow Installation.

Highlights 💖

News you don’t want to miss.

Windows Support

Pkl is now available on Windows! (#492)

In addition to macOS and Linux, Pkl is now available for download on Windows operating systems.

For installation instructions, consult Windows Executable in the CLI reference.

Writing cross-platform Pkl programs

On Windows, the in-language filepath separator is still /. This means that most existing Pkl code should still continue working as-is.

A small portion of programs will need to be updated to handle Windows support correctly. One example is the various conversion scripts that read input files as CLI flags.

For example, here is a snippet from module k8s.contrib.convert:

local inputUri =
  if (input.startsWith(Regex(#"\w+:"#))) input      // absolute URI
  else if (input.startsWith("/")) "file://\(input)" // absolute file path
  else "file://\(read("env:PWD"))/\(input)"         // relative file path

On Windows, the env var PWD is a Windows-style path, like C:\Foo\Bar\Baz. With the above logic, inputUri can expand to a value like "file://C:\Foo\Bar\Baz/input.yaml". This would an invalid URI; the correct URI should be "file:///C:/Foo/Bar/Baz/input.yaml".

Here is a possible fix for this code:

import "pkl:platform"

local inputUri =
  if (input.startsWith(Regex(#"\w+:/"#))) input
  else if (input.startsWith("/")) "file://\(input)"
    let (pwd = read("env:PWD"))
    let (
      path =
        if ( == "Windows")
          "/\(pwd)/\(input)".replaceAll("\\", "/")
        else "\(pwd)/\(input)"

To learn more about this design, consult SPICE-0006.

HTTP proxying

Pkl now supports proxying HTTP(S) requests (#506).

For users of the CLI, Pkl will by default look for proxy settings configured within the OS when on macOS, Windows, and GNOME environments.

This can be changed by adding settings in the following locations:

  1. Settings file

  2. PklProject file

  3. --http-proxy and --http-no-proxy CLI flags

As part of this, some changes have been made to the standard library. For details, see Standard Library changes.

Users of the Java/Kotlin bindings can specify proxy settings when building an HTTP client.

HTTP proxy settings are also added as new fields in the message-passing-api. Authors of external Pkl clients are encouraged to support these new fields to provide proxy support in their libraries. For details, see Message passing API changes.

To read more about the design of HTTP proxying, consult SPICE-0004, and also the documentation.

Scheme-agnostic project dependencies

Improvements have been made to project dependencies (#486).

A project dependency is a way to manage package-based modules. It provides two uses:

  1. A package can be imported through dependency notation.

  2. Conflicting versions in the transitive dependency graph are resolved.

Currently, dependencies can only be used with file-based modules. This can be limiting in some circumstances:

  1. JVM library users typically interact with Pkl by bundling Pkl files a classpath resources, and load them using modulepath: URIs.

  2. Go library users often embed Pkl code using the go:embed directive, and load them using WithFs.

This means that these users miss out on the benefits of project dependencies.

Pkl 0.26 adds support for using dependencies in any hierarchical module scheme. This means that modulepath: users in Java can now declare projects and dependencies. This also means that other custom methods of embedding modules can also use dependency notation.

A hierarchical URI is a URI whose path part starts with /, and in which / delimits path segments.

The URI myscheme:/foo.pkl is hierarchical, whereas the URI myscheme:foo.pkl is not.

To read more about this design, consult SPICE-0005.

Typealias scoping fix and change

Improvements have been made to `typealias`es (#144, #373, #516).

Currently, a typealias that references a value on its enclosing module will break if used in another module.

typealias StartsWithFoo = String(startsWith(foo))

foo = "foo"
import "myModule.pkl"

myStr: StartsWithFoo = "fooey"

This results in error:

–– Pkl Error ––
Cannot find property `foo`.

1 | typealias StartsWithFoo = String(startsWith(foo))

This is a language bug that has been fixed.

In the process of fixing this bug, we realized that the rules around variable resolution within typealiases were problematic. Typealiases are meant to be statically defined, and shouldn’t be able to see properties that can be late-bound. Thus, a new rule is introduced: a typealias can only reference properties/methods that are const.

This is a breaking change. To read more about this breakage and remediation steps, reference Typealiases are treated as const.

Noteworthy 🎶

Ready when you need them.

Pkldoc improvements

The following improvements have been made to Pkldoc:

  1. The documentation for the standard library no longer shows dependent packages, because every package implicitly depends on the standard library (#503).

  2. If the standard library exists in the package list, it is shown first (#165).

  3. Fewer characters are percent-encoded. For example, the ( and ) characters are no longer percent-encoded (#489).

Standard library changes

To support http proxying, several changes have been made to the standard library (#506).

  • Module pkl.settings has new property http.

  • class EvaluatorSettings originally in pkl.Project has been moved to its own module. For backwards compatibility, pkl.Project now has a typealias EvaluatorSettings pointing to the new module, but it is marked @Deprecated.

Java API Changes

HTTP Java Client

A new Java API is introduced, named org.pkl.core.http.HttpClient (#217, #295, #506, #518). This interface provides the ability to manage how Pkl makes HTTP(S) calls, including how it deals with CA certificates, as well as proxies.

This client can be passed to Pkl’s evaluator using EvaluatorBuilder for users of pkl-core, and ConfigEvaluatorBuilder for users of pkl-config-java or pkl-config-kotlin.

New HTTP ModuleKeyFactory

A new module key factory for HTTP(S) modules has been added (#495), and can be built with org.pkl.core.ModuleKeyFactories#http.

The preconfigured evaluator (org.pkl.core.EvaluatorBuilder#preconfigured) includes this module key. Users that build their own evaluator from scratch should add this module key factory if HTTP(S) modules are needed.


 import org.pkl.core.EvaluatorBuilder;
 import org.pkl.core.module.ModuleKeyFactories;

 var evaluator = EvaluatorBuilder.unconfigured()
+  .addModuleKeyFactory(ModuleKeyFactories.http) (1)
1 Add ModuleKeyFactories.http to the set of module key factories used by this evaluator.

If this module key factory is not added, Pkl may still make HTTP(S) requests if ModuleKeyFactories.genericUrl is included. However, this bypasses proxy and CA certificate settings.

pkl-executor changes

A new set of parameters are now available to org.pkl.executor.Executor (#217, #518). These new parameters are exposed by org.pkl.executor.spi.v1.ExecutorSpiOptions2.

The new parameters are:

  • certificateFiles: A set of CA certificate files to trust when making HTTPS requests.

  • certificateBytes: A set of PEM-encoded CA certificate bytes to trust when making HTTPS requests.

  • testPort: An option that is used for internal testing only.

These options are ignored when using a Pkl distribution whose version is lower than 0.26.0.

Message passing API changes

A new property, http, is added to Create Evaluator Request (#506, #518).

This allows for the configuration of Pkl’s HTTP proxy, as well as CA certificates.

Ability to rename classes produced by Java/Kotlin code generators

The Java and Kotlin code generators have a new option that allows users to change the name of Java/Kotlin classes that get produced during code generation (#499).

The CLIs have a new flag, --rename, and the Gradle plugin receives a similarly named property called renames.

This option accepts a map from an old prefix to a new prefix, where longer prefixes have higher precedence than shorter prefixes.

For example, when generating module foo.Bar, specifying --rename will cause the Java/Kotlin code generators to emit package, and class Bar.

Breaking Changes 💔

Things to watch out for when upgrading.

Typealiases are treated as const

A breaking change has been made to typealiases (#516).

Typealiases are types that can stand in for another type declaration.

The aliased type can have constraints, where these constraints can reference values defined on the enclosing module.

typealias MyValue = Any(isValid) (1)

isValid = true
1 isValid is defined on the enclosing module.

One problem with this is that typealiases are meant to be statically defined. Like classes, typealiases should not be changed by amending its enclosing module.

amends "baseModule.pkl"

isValid = false (1)
1 Despite this amended value, typealias MyValue should still stand for Any(true).

To have clearer semantics, a new rule is introduced: referenced members on the enclosing module must be const. Effectively, a typealias is treated as if it is also a const member.

To fix the above typealias, the const modifier should be added to isValid.

 typealias MyValue = Any(isValid)

-isValid = true
+const isValid = true

It might not always be valid to add the const modifier. For example, this property may be overwritten in a downstream module, so adding the const modifier this would break that module. In these cases, another fix is to self-import the enclosing module. This works because import declarations introduce values that are implicitly marked const.

+import "baseModule.pkl" (1)
-typealias MyValue = Any(isValid)
+typealias MyValue = Any(baseModule.isValid)

 isValid = true
1 Self import

This change aligns with the behavior of class and annotation bodies.

To read more about the rationale behind this change, consult SPICE-0007.

Expanded dependency notation URIs

The parsing of relative path imports has changed (#486).

Currently, the declaration import "@foo/bar.pkl" is treated as the import of a dependency named foo, but only when declared within file-based and package-based modules. In an HTTP-based module, for example, the above import is treated as "bar.pkl inside directory @foo".

To improve project dependencies, such declarations are treated as dependency notation in all modules, and will fail with error "Cannot find dependency".

Any import/reads that are intentionally relative-path imports will need to be updated to be prefixed with ./.


-import "@bar/foo.pkl"
+import "./@bar/foo.pkl"

Minimum Java version bumped to Java 17

The minimum Java version for Pkl has been bumped to Java 17 (#439).

This means that when running the jpkl CLI, the installed java is expected to be Java 17 or higher. This also means that users of the JVM libraries need to be on at least Java 17 or higher.

Gradle plugin minimum version bump

The minimum Gradle version for the Gradle plugin is now 8.1 (#454).

Path encoding changes

In order to support Windows, the output of some Pkl tools have unsafe characters encoded in a special format (#489).

On Windows, the characters <, >, :, ", \, |, ?, and * are reserved and cannot exist in a filename. Additionally, the ASCII control character code points 0x0 through 0x1f are also illegal.

These characters are encoded by wrapping their hexadecimal code point value in parentheses. For example, the character : is encoded as (3a).

In some scenarios, files that get written to disk will be encoded. These are:

  • Files generated by Pkldoc.

  • *.kt files produced by the Kotlin code generator.

  • Packages written to the cache directory.

To learn more about this design, consult SPICE-0003.

The links generated by Pkldoc have changed.

For example, a module called module `foo<bar` creates file foo(3c)bar.html, instead of foo<bar.html.

As part of this change, some Pkldoc links have also changed.

Cache directory prefix changes

The cache directory has been changed from <moduleCacheDir>/package-1 to <moduleCacheDir>/package-2.

If Pkl’s dependencies are vendored within a repository, these dependencies will need to be re-vendored.

Java API Breaking Changes

The following Java APIs have breaking changes:

Class/method Breaking change


Renamed to org.pkl.cli.commands.CliPackageDownloader


Renamed to org.pkl.cli.commands.CliProjectCommand

Fields pklName and javaName are now private.


New parameter httpClient added


New parameter httpClient added


New parameters testPort, httpProxy, httpNoProxy added

Standard library breaking changes

Class pkl.Project.EvaluatorSettings has been removed. A new (deprecated) typealias is added that points to new module pkl.EvaluatorSettings. For both of these, see #506.

Type-checked settings file

The loading of the settings file has changed (#477).

The settings file is a way to control the behavior of the Pkl CLI. This module is expected to amends "pkl:settings", but this behavior was not checked.

In 0.26, it is an error if the settings module neither amends "pkl:settings", nor set its output.value to an instance of the settings module.

Empty directories excluded from packaging

Currently, the pkl project package command will bundle empty directories into the resulting ZIP file.

In version 0.26, this has been changed to exclude these directories (#330). This means that the packages can produce a different checksum if there are any empty directories.

This does not break the usage of existing packages, and does not affect compatibility of new packages used with older Pkl 0.25.

However, this can break certain workflows. The pkl project package command runs a publish check, to determine if a package has already been published but with a different checksum. When upgrading, users might encounter an error message like the following:

-- Pkl Error --
Package `package://` was already published with different contents.

To mitigate, the package’s version needs to be bumped, even if package contents have not changed.

Miscellaneous 🐸

The following changes have been made that are not new features, nor breaking changes.

  • Pkl’s user-agent header for HTTP requests has been tweaked to add a semicolon (#221). Here is an example difference:

    Before: Pkl/0.26 (macOS native)
    After: Pkl/0.26 (macOS; native)

  • Documentation improvements (#120, #121, #142, #121, #337, #341, #372, #386, #391, #397, #422, #436, #469, #484, #485, #491).

  • Optimization: const access is checked when variables are resolved, instead of every time the variable is accessed (#438).

  • Optimization: use logical AND instead of bitwise AND when comparing numbers (#102).

  • Mark Java classes final (#458).

  • Migrate code to newer JDK17 features, and clean up existing Java code (#451, #458, #512).

  • Improve rendering of string values within generated <file>.pkl-expected.pcf files when running pkl test (#416).

  • Improve performance of loading Pkl’s built-in CA certificates (#518).

Bug Fixes 🐜

The following bugs have been fixed.

  • Collection#sortWith produces un-sorted output (#394).

  • Property typedType in module pkl.reflect reflects upon Type, instead of Typed (#426).

  • const members can be assigned to when via an object spread (#428).

  • Relative globbed reads resolve to the same value in different modules (#449).

  • Performance bug: globbed imports and globbed reads expand to an unbounded number of root nodes (#449).

  • Relative globbed imports within a package match no modules (#496).

  • Constraints within typealiases resolve to the wrong values (#144).

  • Members of pkl.reflect cannot be rendered (#170, #470).

  • Throws NullPointerException if "List Resources Response" or "List Modules Response" messages contain both null pathElements and error (#480).

  • Classes of are not shaded in pkl-config-java-all and pkl-tools (#238).

  • Throws PklBugException when running publish check on an invalid URL (#441).

Contributors 🙏

We would like to thank the contributors to this release (in alphabetical order):

A special thank-you goes out to @translatenix! They submitted multiple bug fixes, improved the quality of the codebase, and provided HTTP improvements.