Skip to content

Commit 09218e7

Browse files
committed
Allow executables to be file URIs
1 parent d63619c commit 09218e7

12 files changed

Lines changed: 177 additions & 69 deletions

File tree

pkl-core/src/main/java/org/pkl/core/externalreader/ExternalReaderProcessImpl.java

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
import java.io.IOException;
1919
import java.lang.ProcessBuilder.Redirect;
20+
import java.net.URI;
21+
import java.net.URISyntaxException;
2022
import java.time.Duration;
2123
import java.util.ArrayList;
2224
import java.util.Map;
@@ -86,8 +88,26 @@ public ExternalResourceResolver getResourceResolver(long evaluatorId)
8688
return ExternalResourceResolver.of(getTransport(), evaluatorId);
8789
}
8890

89-
private @Nullable String getExecutablePath(String executable) {
90-
if (executable.contains("/") || executable.contains("\"")) {
91+
private @Nullable String getExecutablePath(String executable)
92+
throws ExternalReaderProcessException {
93+
if (IoUtils.isUriLike(executable)) {
94+
try {
95+
var uri = new URI(executable);
96+
if (!uri.getScheme().equalsIgnoreCase("file")) {
97+
throw new ExternalReaderProcessException(
98+
ErrorMessages.create("cannotSpawnNonFileExecutable", uri));
99+
}
100+
if (!uri.getPath().startsWith("/")) {
101+
throw new ExternalReaderProcessException(
102+
ErrorMessages.create("invalidOpaqueFileUri", uri));
103+
}
104+
return uri.getPath();
105+
} catch (URISyntaxException e) {
106+
throw new ExternalReaderProcessException(
107+
ErrorMessages.create("invalidReaderExecutableUri", executable));
108+
}
109+
}
110+
if (executable.contains("/")) {
91111
return executable;
92112
}
93113
var resolved = IoUtils.findExecutableOnPath(executable);
@@ -116,7 +136,7 @@ private MessageTransport getTransport() throws ExternalReaderProcessException {
116136
var executable = getExecutablePath(spec.executable());
117137
if (executable == null) {
118138
throw new ExternalReaderProcessException(
119-
ErrorMessages.create("cannotFindCommand", spec.executable()));
139+
ErrorMessages.create("cannotResolveExternalReaderCommand", spec.executable()));
120140
}
121141
command.add(executable);
122142
if (spec.arguments() != null) {

pkl-core/src/main/resources/org/pkl/core/errorMessages.properties

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1067,6 +1067,17 @@ External {0} reader does not support scheme `{1}`.
10671067
externalReaderAlreadyTerminated=\
10681068
External reader process has already terminated.
10691069

1070+
cannotSpawnNonFileExecutable=\
1071+
Invalid external reader executable `{0}`.\n\
1072+
\n\
1073+
Executables must be files.
1074+
1075+
cannotResolveExternalReaderCommand=\
1076+
Cannot resolve external reader executable `{0}`.
1077+
1078+
invalidReaderExecutableUri=\
1079+
Exernal reader executable URI `{0}` has invalid syntax.
1080+
10701081
invalidOpaqueFileUri=\
10711082
File URIs must have a path that starts with `/` (e.g. file:/path/to/my_module.pkl).\n\
10721083
To resolve relative paths, remove the scheme prefix (remove "file:").
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
amends "pkl:Project"
2+
3+
evaluatorSettings {
4+
externalModuleReaders {
5+
["foo"] {
6+
executable = "qux:/foo"
7+
}
8+
}
9+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
foo = read("foo:bar")
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
amends "pkl:Project"
2+
3+
projectFileUri = "modulepath:/foo/bar/PklProject"
4+
5+
evaluatorSettings {
6+
externalModuleReaders {
7+
["foo"] {
8+
executable = "foo/bar"
9+
}
10+
}
11+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
–– Pkl Error ––
2+
Type constraint `(this is AbsoluteUri).implies(startsWith("file:/"))` violated.
3+
Value: "qux:/foo"
4+
5+
xxx | executable: String((this is AbsoluteUri).implies(startsWith("file:/")))
6+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
7+
at pkl.EvaluatorSettings#ExternalReader.executable (pkl:EvaluatorSettings)
8+
9+
x | executable = "qux:/foo"
10+
^^^^^^^^^^
11+
at PklProject#evaluatorSettings.externalModuleReaders["foo"].executable (file:///$snippetsDir/input/projects/badPklProject6/PklProject)
12+
13+
xxx | ?.map((it) -> it.executable)
14+
^^^^^^^^^^^^^
15+
at pkl.Project#pathBasedExecutables.<function#1> (pkl:Project)
16+
17+
xxx | it
18+
^^
19+
at pkl.Project#pathBasedExecutables (pkl:Project)
20+
21+
xxx | pathBasedExecutables(externalModuleReaders).isNotEmpty.implies(isFileBasedProject),
22+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
23+
at pkl.Project#evaluatorSettings (pkl:Project)
24+
25+
x | evaluatorSettings {
26+
^^^^^^^^^^^^^^^^^^^
27+
at PklProject#evaluatorSettings (file:///$snippetsDir/input/projects/badPklProject6/PklProject)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
–– Pkl Error ––
2+
Type constraint `pathBasedExecutables(externalModuleReaders).isNotEmpty.implies(isFileBasedProject)` violated.
3+
Value: new ModuleClass { externalProperties = ?; env = ?; allowedModules = ?; allowe...
4+
5+
xxx | pathBasedExecutables(externalModuleReaders).isNotEmpty.implies(isFileBasedProject),
6+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
7+
at pkl.Project#evaluatorSettings (pkl:Project)
8+
9+
x | evaluatorSettings {
10+
^^^^^^^^^^^^^^^^^^^
11+
at PklProject#evaluatorSettings (file:///$snippetsDir/input/projects/badPklProject7/PklProject)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*
2+
* Copyright © 2026 Apple Inc. and the Pkl project authors. All rights reserved.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.pkl.core.externalreader
17+
18+
import org.assertj.core.api.Assertions.assertThatCode
19+
import org.junit.jupiter.api.Test
20+
import org.pkl.core.evaluatorSettings.PklEvaluatorSettings
21+
22+
class ExternalReaderProcessTest {
23+
@Test
24+
fun `invalid URI`() {
25+
val process =
26+
ExternalReaderProcess.of(
27+
PklEvaluatorSettings.ExternalReader("qux:/path/to/execuable", listOf())
28+
)
29+
assertThatCode { process.getModuleResolver(1) }
30+
.hasMessageContaining("Invalid external reader executable `qux:/path/to/execuable`")
31+
}
32+
33+
@Test
34+
fun `simple name is resolved off path`() {
35+
val process = ExternalReaderProcess.of(PklEvaluatorSettings.ExternalReader("foo", listOf()))
36+
assertThatCode { process.getModuleResolver(1) }
37+
.hasMessageContaining("Cannot resolve external reader executable `foo`.")
38+
}
39+
}

pkl-core/src/test/kotlin/org/pkl/core/project/ProjectTest.kt

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,31 @@ class ProjectTest {
338338
)
339339
assertThatCode { Project.load(src) }
340340
.hasMessageContaining(
341-
"Type constraint `(!relativeExecutables(externalModuleReaders).isEmpty).implies(isFileBasedProject)` violated"
341+
"Type constraint `pathBasedExecutables(externalModuleReaders).isNotEmpty.implies(isFileBasedProject)` violated."
342+
)
343+
}
344+
345+
@Test
346+
fun `cannot set non-file URI`() {
347+
val src =
348+
ModuleSource.text(
349+
// language=pkl
350+
"""
351+
amends "pkl:Project"
352+
353+
evaluatorSettings {
354+
externalModuleReaders {
355+
["foo"] {
356+
executable = "qux:///path/to/executable"
357+
}
358+
}
359+
}
360+
"""
361+
.trimIndent()
362+
)
363+
assertThatCode { Project.load(src) }
364+
.hasMessageContaining(
365+
"Type constraint `(this is AbsoluteUri).implies(startsWith(\"file:/\"))` violated."
342366
)
343367
}
344368
}

0 commit comments

Comments
 (0)