-
Notifications
You must be signed in to change notification settings - Fork 10
SPICE-0027: Resolved paths in pkl:EvaluatorSettings #30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
bioball
wants to merge
2
commits into
apple:main
Choose a base branch
from
bioball:resolved-paths-in-settings
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
358 changes: 358 additions & 0 deletions
358
spices/SPICE-0027-resolved-paths-in-pkl-evaluatorsettings.adoc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,358 @@ | ||
| = SPICE-0027: Resolved paths in pkl:EvaluatorSettings | ||
|
|
||
| * Proposal: link:./SPICE-0027-resolved-paths-in-pkl-evaluatorsettings.adoc[SPICE-0027] | ||
| * Author: https://github.com/bioball[Dan Chao] | ||
| * Status: TBD | ||
| * Implemented in: Pkl <version> | ||
| * Category: Standard Library | ||
|
|
||
| == Introduction | ||
|
|
||
| Certain settings within `pkl:EvaluatorSettings` are meant to be resolved relative to an enclosing directory. | ||
|
|
||
| This SPICE proposes the following changes: | ||
|
|
||
| * Move this path resolution to within Pkl | ||
| * Change property `executable` for `ExternalReader` to be relative to this enclosing directory | ||
| * Add a property called `workingDir` | ||
|
|
||
| == Motivation | ||
|
|
||
| Certain settings within module `pkl:EvaluatorSettings` are file paths. | ||
| These are: | ||
|
|
||
| * `rootDir` | ||
| * `moduleCacheDir` | ||
| * `modulePath` | ||
| * `ExternalReader.executable` | ||
|
|
||
| Currently, Pkl has the following behavior when these settings are used within a `PklProject`: | ||
|
|
||
| * `rootDir`, `moduleCacheDir`, and `modulePath` are expected to be resolved relative to the project directory. | ||
| * This works if code is executed by via Java `Project.load` (`pkl` CLI, pkl's Java APIs) | ||
| * External clients using `Project` must take care to resolve this path correctly. | ||
| * `ExternalReader.executable` follows the OS loading rules, resolved against PWD. | ||
|
|
||
| This introduces some problems: | ||
|
|
||
| * Pkl is inconsistent about what relative paths mean. `rootDir`, `moduleCacheDir`, and `modulePath` are relative to the project dir, while `ExternalReader.executable` is relative to the PWD. | ||
| * External readers defined in a PklProject have different behavior depending on the PWD. | ||
| * The logic for resolving these paths is dependent on the caller, rather than Pkl. | ||
|
|
||
| == Proposed Solution | ||
|
|
||
| The following changes will be made: | ||
|
|
||
| * Pkl will resolve these paths in-language. | ||
| * `ExternalReader.executable` will change to be resolved relative to the project directory. | ||
| * A new property, `ExternalReader.workingDirectory` will be introduced. It will default to the project directory. | ||
|
|
||
| == Detailed design | ||
|
|
||
| === Standard library changes | ||
|
|
||
| The following new properties will be added: | ||
|
|
||
| * `EvaluatorSettings.resolved` | ||
| * `EvaluatorSettings.basePath` | ||
| * `EvaluatorSettings.ExternalReader.workingDirectory` | ||
| * `Project.resolvedEvaluatorSettings` | ||
|
|
||
| .pkl:EvaluatorSettings | ||
| [source,pkl] | ||
| ---- | ||
| module pkl.EvaluatorSettings | ||
|
|
||
| import "pkl:EvaluatorSettings" | ||
|
|
||
| /// The base path, used to resolve relative paths in [resolved]. | ||
| @Since { version = "0.31.0" } | ||
| hidden baseUri: String | ||
|
|
||
| /// These evaluator settings whose paths are resolved against [baseUri]. | ||
| @Since { version = "0.31.0" } | ||
| hidden fixed resolved: EvaluatorSettings = (module) { | ||
| // implementation | ||
| } | ||
|
|
||
| class ExternalReader { | ||
| /// The working directory used to launch the executable. | ||
| @Since { version = "0.31.0" } | ||
| workingDirectory: String | ||
| } | ||
| ---- | ||
|
|
||
| A new property called `resolvedEvaluatorSettings` will be added to `pkl:Project`. | ||
|
|
||
| .pkl:Project | ||
| [source,pkl] | ||
| ---- | ||
| module pkl.Project | ||
|
|
||
| @Since { version = "0.31.0" } | ||
| fixed resolvedEvaluatorSettings = | ||
| (evaluatorSettings) { | ||
| baseUri = projectFileUri.substring(0, projectFileUri.lastIndexOf("/")) | ||
| }.resolved | ||
| ---- | ||
|
|
||
| [[path-resolution]] | ||
| === Path resolution | ||
|
|
||
| The path resolution algorithm works like so: | ||
|
|
||
| 1. Determine the base path | ||
| 2. Join the base path and the declared path. | ||
|
|
||
| How path resolutions works is OS-dependent. | ||
| Linux and macOS follow POSIX-style path resolution. | ||
| Windows resolution behaves similarly, except that either `/` or `\` may be used as the path separator, and there is the additional handling of drive letters and UNC paths. | ||
|
|
||
| ==== Determining the base path | ||
|
|
||
| Given, `Project.projectFileUri`, get the base file URI. | ||
| For example, `\file:///project/dir/PklProject` becomes `\file:///project/dir/` | ||
|
|
||
| From base file URI, determine the base path. | ||
|
|
||
| On POSIX: | ||
| `\file:///project/dir/` -> `/project/dir/` | ||
|
|
||
| On Windows: | ||
| `\file:///C:/project/dir/` -> `C:\project\dir\` | ||
|
|
||
| On Windows with UNC paths: | ||
| `\file://server/share/dir/` -> `\\server\share\dir\` | ||
|
|
||
| Percent-encoded characters are decoded. | ||
|
|
||
| `\file:///project/dir%20with%20spaces/` -> | ||
| `/project/dir with spaces` | ||
|
|
||
| ==== Resolving the path | ||
|
|
||
| **POSIX** | ||
|
|
||
| On POSIX, a path that starts with a slash resolves to itself | ||
|
|
||
| [source,pkl] | ||
| ---- | ||
| resolve("/project/dir/", "/my/path") == "/my/path" | ||
| ---- | ||
|
|
||
| Otherwise, the path is concatenated to the base path and normalized as per POSIX rules, except: | ||
|
|
||
| [source,pkl] | ||
| ---- | ||
| resolve("/project/dir/", "my/path") == "/project/dir/my/path" | ||
|
|
||
| // multiple slashes collapse into one slash | ||
| resolve("/project/dir/", "my////path") == "/project/dir/my/path" | ||
|
|
||
| // `..` removes the previous dir | ||
| resolve("/project/dir/", "../my/path") == "/project/my/path" | ||
|
|
||
| // `.` removes the previous slash | ||
| resolve("/project/dir/", ".") == "/project/dir" | ||
| resolve("/project/dir/", "./") == "/project/dir/" | ||
|
|
||
| // `..` cannot remove the root dir | ||
| resolve("/", "../../../") == "/" | ||
|
|
||
| // Empty path resolves to the base dir | ||
| resolve("/project/dir/", "") == "/project/dir/" | ||
| ---- | ||
|
|
||
| **Windows** | ||
|
|
||
| On Windows, a path that starts with a drive letter resolves to that path. | ||
|
|
||
| [source,pkl] | ||
| ---- | ||
| resolve(#"C:\project\dir\"#, #"D:\my\path"#) == #"D:\my\path"# | ||
| // Missing slash after colon an error (this is allowed on Windows, and resolves relative to the PWD, which we don't want). | ||
| resolve(#"C:\project\dir\"#, #"D:my\path"#) // Pkl Error | ||
| ---- | ||
|
|
||
| A path that starts with the path separator resolves off the base path's drive: | ||
|
|
||
| [source,pkl] | ||
| ---- | ||
| resolve(#"C:\project\dir\"#, #"\my\path"#) == #"C:\my\path"# | ||
| ---- | ||
|
|
||
| Otherwise, the path is concatenated to the base path and normalized as per POSIX rules, except: | ||
|
|
||
| 1. Either `\` or `/` is allowed as a separators. | ||
| 2. The root includes the drive letter. | ||
|
|
||
| [source,pkl] | ||
| ---- | ||
| resolve(#"C:\project\dir\"#, #"my\path"#) == #"C:\project\dir\my\path"# | ||
|
|
||
| // forward slashes are turned into backslashes | ||
| resolve(#"C:\project\dir\"#, "my/path") == #"C:\project\dir\my\path"# | ||
|
|
||
| // `..` removes the previous directory | ||
| resolve(#"C:\project\dir\"#, #"..\my\path"#) == #"C:\project\my\path"# | ||
|
|
||
| // `.` removes the previous slash | ||
| resolve(#"C:\project\dir\"#, ".") == #"C:\project\dir"# | ||
| resolve(#"C:\project\dir\"#, #".\"#) == #"C:\project\dir\"# | ||
|
|
||
| // `..` cannot remove root dir | ||
| resolve(#"C:\"#, #"..\..\..\"#) == #"C:\"# | ||
|
|
||
| // Empty path resolves to the base dir | ||
| resolve(#"C:\project\dir\"#, "") == #"C:\project\dir\"# | ||
| ---- | ||
|
|
||
| **Windows UNC Paths** | ||
|
|
||
| UNC (Universal Naming Convention) paths on Windows have the form `\\server\share\path` and are used to access network resources. | ||
|
|
||
| A UNC path is treated as an absolute path and resolves to itself: | ||
|
|
||
| [source,pkl] | ||
| ---- | ||
| resolve(#"C:\project\dir\"#, #"\\server\share\data"#) == #"\\server\share\data"# | ||
| resolve(#"\\server1\share1\dir\"#, #"\\server2\share2\data"#) == #"\\server2\share2\data"# | ||
| ---- | ||
|
|
||
| When the base path is a UNC path, relative paths are resolved against it: | ||
|
|
||
| [source,pkl] | ||
| ---- | ||
| // Relative path appends to UNC base | ||
| resolve(#"\\server\share\dir\"#, #"my\path"#) == #"\\server\share\dir\my\path"# | ||
|
|
||
| // `..` removes the previous directory | ||
| resolve(#"\\server\share\dir\sub\"#, #"..\my\path"#) == #"\\server\share\dir\my\path"# | ||
|
|
||
| // `.` removes the previous slash | ||
| resolve(#"\\server\share\dir\"#, ".") == #"\\server\share\dir"# | ||
|
|
||
| // `..` cannot remove the UNC root (\\server\share\ is the root) | ||
| resolve(#"\\server\share\"#, #"..\..\..\"#) == #"\\server\share\"# | ||
| resolve(#"\\server\share\dir\"#, #"..\.."#) == #"\\server\share\"# | ||
|
|
||
| // Drive letter path from UNC base resolves to that drive | ||
| resolve(#"\\server\share\dir\"#, #"C:\local\path"#) == #"C:\local\path"# | ||
|
|
||
| // Path starting with `\` from UNC base is ambiguous and treated as an error | ||
| resolve(#"\\server\share\dir\"#, #"\absolute\path"#) // Pkl Error | ||
| ---- | ||
|
|
||
| NOTE: When resolving from a UNC base, a path starting with a single backslash (e.g., `\absolute\path`) is ambiguous—it could mean absolute on a drive or absolute on the same server. Pkl treats this as an error to avoid ambiguity. | ||
|
|
||
| === `ExternalReader.executable` resolution | ||
|
|
||
| When resolving `ExternalReader.executable`, most of the same rules apply, except that if the executable does not contain path separators, it is kept as-is. | ||
| The underlying external reader implementation will then resolve this off of the `PATH` environment variable. | ||
|
|
||
| [source,pkl] | ||
| ---- | ||
| externalModuleReaders { | ||
| ["foo"] { | ||
| executable = "my-foo-reader" // kept as-is. | ||
| } | ||
| } | ||
| ---- | ||
|
|
||
| Resolving this executable follows OS resolution rules: | ||
|
|
||
| 1. On Windows, it will look for a filename with an executable extension | ||
| 2. On POSIX, it will look for that filename, whose file mode marks it as executable. | ||
|
|
||
| === Language implementation changes | ||
|
|
||
| ==== External reader changes | ||
|
|
||
| The new `workingDirectory` property will be added as an optional property. | ||
| This means changes to: | ||
|
|
||
| * The message passing API | ||
| * record `org.pkl.core.evaluatorSettings.PklEvaluatorSettings.ExternalReader` | ||
|
|
||
| If not set, it defaults to the current working directory. | ||
|
|
||
| ==== Language binding changes | ||
|
|
||
| On the language binding side, the project loading will change to apply settings from `resolvedEvaluatorSettings`. | ||
|
|
||
| This logic will be changed in pkl-go, pkl-swift, and pkl-core (Java). | ||
|
|
||
| For example, the following change will be made to Java's `EvaluatorBuilder#applyFromProject`: | ||
|
|
||
| [source,diff] | ||
| ---- | ||
| EvaluatorBuilder applyFromProject(Project project) { | ||
| this.dependencies = project.getDependencies(); | ||
| - var settings = project.getEvaluatorSettings(); | ||
| + var settings = project.getResolvedEvaluatorSettings(); | ||
| ---- | ||
|
|
||
| == Compatibility | ||
|
|
||
| This is a breaking change, because the resolution rules for the external reader changes to be resolved off of the project directory, instead of the working directory. | ||
|
|
||
| There will be no deprecation period here before switching to the new behavior. | ||
|
|
||
| However, those that want Pkl to behave this way can use `env("read:PWD")` to keep resolving executables off the PWD. | ||
|
|
||
| .PklProject | ||
| [source,pkl] | ||
| ---- | ||
| amends "pkl:Project" | ||
|
|
||
| evaluatorSettings { | ||
| externalModuleReaders { | ||
| ["foo"] { | ||
| executable = "\(read("env:PWD"))/my/executable" | ||
| } | ||
| } | ||
| } | ||
| ---- | ||
|
|
||
| == Future directions | ||
|
|
||
| === URI and Path libraries | ||
|
|
||
| This SPICE requires some URI and path handling logic to be described in terms of Pkl code. | ||
| Initially, this will be implemented as `local` properties. | ||
|
|
||
| In the presence of a path and URI standard library, these local implementations should be removed. | ||
|
|
||
| == Alternatives considered | ||
|
|
||
| === Using URIs instead of file paths | ||
|
|
||
| Instead of resolving to file paths, `EvaluatorSettings.resolved` can resolve to `file:` URIs. | ||
|
|
||
| For example, instead of producing `C:\path\to\dir`, it would produce `\file:///C:/path/to/dir`. | ||
|
|
||
| This approach has the following benefits: | ||
|
|
||
| 1. It is possible to specify `rootDir` and other related settings when using a non-file `PklProject`. + | ||
| + | ||
| Currently, it is not possible to use these settings when embedding a PklProject (e.g. loading it off `modulepath:`). + | ||
| This is because the project directory itself is not a filepath, so it is not possible to eventually resolve into a file path. + | ||
| + | ||
| However, if URIs could be specified, a `file:` URI unambiguously still represents a filepath. | ||
| 2. It allows us to treat `/` as the file separator on all OSes. | ||
| 3. It allows the possibility of specifying executables in non-file locations. + | ||
| + | ||
| This might help with the portability of external readers. + | ||
| For example: `executable = "package://pkg.pkl-lang.org/pkl-readers/myreader@1.0.0#/my-executable"` | ||
|
|
||
| However, this has the following drawbacks: | ||
|
|
||
| 1. It does not align with how CLI arguments work, or in-language APIs work. + | ||
| + | ||
| Within a Windows shell, it is valid to call `pkl eval --root-dir C:\path\to\dir\`. | ||
| It is therefore inconsistent that we do not allow this value inside a PklProject. | ||
| 2. The specification for URI resolution would not correctly handle drive letters in Windows. | ||
| + | ||
| For example, according to the specification, `resolve("file:///C:/path/to/dir/", "/another/dir")` should return `"file:///another/dir"`. | ||
|
|
||
| Additionally, making this choice does not preclude allowing for URIs in the future. | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: grammatical agreement