diff --git a/pkl-gradle/src/main/java/org/pkl/gradle/ExternalReader.java b/pkl-gradle/src/main/java/org/pkl/gradle/ExternalReader.java new file mode 100644 index 000000000..a2144d288 --- /dev/null +++ b/pkl-gradle/src/main/java/org/pkl/gradle/ExternalReader.java @@ -0,0 +1,26 @@ +/* + * Copyright © 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. + */ +package org.pkl.gradle; + +import java.io.Serializable; +import java.util.Collections; +import java.util.List; + +public record ExternalReader(String executable, List arguments) implements Serializable { + public ExternalReader(String executable) { + this(executable, Collections.emptyList()); + } +} diff --git a/pkl-gradle/src/main/java/org/pkl/gradle/PklPlugin.java b/pkl-gradle/src/main/java/org/pkl/gradle/PklPlugin.java index 619ab7837..f4b2f98c9 100644 --- a/pkl-gradle/src/main/java/org/pkl/gradle/PklPlugin.java +++ b/pkl-gradle/src/main/java/org/pkl/gradle/PklPlugin.java @@ -472,6 +472,8 @@ private void configureBaseTask(T task.getHttpProxy().set(spec.getHttpProxy()); task.getHttpNoProxy().set(spec.getHttpNoProxy()); task.getHttpRewrites().set(spec.getHttpRewrites()); + task.getExternalModuleReaders().set(spec.getExternalModuleReaders()); + task.getExternalResourceReaders().set(spec.getExternalResourceReaders()); } private List getTransitiveModules(AnalyzeImportsTask analyzeTask) { diff --git a/pkl-gradle/src/main/java/org/pkl/gradle/spec/BasePklSpec.java b/pkl-gradle/src/main/java/org/pkl/gradle/spec/BasePklSpec.java index 09a1edcae..b9f65fd1e 100644 --- a/pkl-gradle/src/main/java/org/pkl/gradle/spec/BasePklSpec.java +++ b/pkl-gradle/src/main/java/org/pkl/gradle/spec/BasePklSpec.java @@ -22,6 +22,7 @@ import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.MapProperty; import org.gradle.api.provider.Property; +import org.pkl.gradle.ExternalReader; /** Configuration options shared between plugin features. Documented in user manual. */ public interface BasePklSpec { @@ -59,4 +60,8 @@ public interface BasePklSpec { ListProperty getHttpNoProxy(); MapProperty getHttpRewrites(); + + MapProperty getExternalModuleReaders(); + + MapProperty getExternalResourceReaders(); } diff --git a/pkl-gradle/src/main/java/org/pkl/gradle/task/BasePklTask.java b/pkl-gradle/src/main/java/org/pkl/gradle/task/BasePklTask.java index 273bb83a1..f9d007a81 100644 --- a/pkl-gradle/src/main/java/org/pkl/gradle/task/BasePklTask.java +++ b/pkl-gradle/src/main/java/org/pkl/gradle/task/BasePklTask.java @@ -44,8 +44,10 @@ import org.gradle.api.tasks.TaskAction; import org.pkl.commons.cli.CliBaseOptions; import org.pkl.core.evaluatorSettings.Color; +import org.pkl.core.evaluatorSettings.PklEvaluatorSettings; import org.pkl.core.util.LateInit; import org.pkl.core.util.Nullable; +import org.pkl.gradle.ExternalReader; import org.pkl.gradle.utils.PluginUtils; public abstract class BasePklTask extends DefaultTask { @@ -146,6 +148,12 @@ public Provider getEvalRootDirPath() { @Optional public abstract MapProperty getHttpRewrites(); + @Input + public abstract MapProperty getExternalModuleReaders(); + + @Input + public abstract MapProperty getExternalResourceReaders(); + /** * There are issues with using native libraries in Gradle plugins. As a workaround for now, make * Truffle use an un-optimized runtime. @@ -200,8 +208,8 @@ protected CliBaseOptions getCliBaseOptions() { getHttpProxy().getOrNull(), getHttpNoProxy().getOrElse(List.of()), getHttpRewrites().getOrNull(), - Map.of(), - Map.of(), + parseExternalReaders(getExternalModuleReaders()), + parseExternalReaders(getExternalResourceReaders()), null); } return cachedOptions; @@ -237,4 +245,15 @@ protected List patternsFromStrings(List patterns) { @Nullable T value = provider.getOrNull(); return value == null ? null : f.apply(value); } + + protected Map parseExternalReaders( + Provider> provider) { + return provider.getOrElse(Map.of()).entrySet().stream() + .collect( + Collectors.toMap( + Map.Entry::getKey, + (it) -> + new PklEvaluatorSettings.ExternalReader( + it.getValue().executable(), it.getValue().arguments()))); + } } diff --git a/pkl-gradle/src/main/java/org/pkl/gradle/task/ModulesTask.java b/pkl-gradle/src/main/java/org/pkl/gradle/task/ModulesTask.java index 878a259ca..eaf7c12b3 100644 --- a/pkl-gradle/src/main/java/org/pkl/gradle/task/ModulesTask.java +++ b/pkl-gradle/src/main/java/org/pkl/gradle/task/ModulesTask.java @@ -164,8 +164,8 @@ protected CliBaseOptions getCliBaseOptions() { null, List.of(), getHttpRewrites().getOrNull(), - Map.of(), - Map.of(), + parseExternalReaders(getExternalModuleReaders()), + parseExternalReaders(getExternalResourceReaders()), null); } return cachedOptions; diff --git a/pkl-gradle/src/test/kotlin/org/pkl/gradle/EvaluatorsTest.kt b/pkl-gradle/src/test/kotlin/org/pkl/gradle/EvaluatorsTest.kt index a8b778a3c..248b6c3e8 100644 --- a/pkl-gradle/src/test/kotlin/org/pkl/gradle/EvaluatorsTest.kt +++ b/pkl-gradle/src/test/kotlin/org/pkl/gradle/EvaluatorsTest.kt @@ -896,6 +896,46 @@ class EvaluatorsTest : AbstractTest() { assertThat(result5.task(":evalTestGatherImports")).isNull() } + @Test + fun `external module reader`() { + writePklFile("import \"foo:test.pkl\"") + writeBuildFile( + "json", + additionalContents = + """ + allowedModules = ["foo:", "file:", "repl:text"] + externalModuleReaders = ["foo": new org.pkl.gradle.ExternalReader("echo")] + """ + .trimIndent(), + ) + + // this is not actually a valid external reader, so we expect this to fail + // this test is just asserting that Pkl is configured to use the external process + // and that it does attempt to do so + val result = runTask("evalTest", true) + assertThat(result.output).contains("IOException: Stream closed") + } + + @Test + fun `external resource reader`() { + writePklFile("result = read(\"foo:test\")") + writeBuildFile( + "json", + additionalContents = + """ + allowedResources = ["foo:", "prop:"] + externalResourceReaders = ["foo": new org.pkl.gradle.ExternalReader("echo")] + """ + .trimIndent(), + ) + + // this is not actually a valid external reader, so we expect this to fail + // this test is just asserting that Pkl is configured to use the external process + // and that it does attempt to do so + val result = runTask("evalTest", true) + assertThat(result.output).contains("IOException: Stream closed") + } + private fun writeBuildFile( // don't use `org.pkl.core.OutputFormat` // because test compile class path doesn't contain pkl-core