Initial commit

This commit is contained in:
Peter Niederwieser
2016-01-19 14:51:19 +01:00
committed by Dan Chao
commit ecad035dca
2972 changed files with 211653 additions and 0 deletions

View File

@@ -0,0 +1,37 @@
# This is a Gradle generated file for dependency locking.
# Manual edits can break the build and are not advised.
# This file is expected to be part of source control.
com.tunnelvisionlabs:antlr4-runtime:4.9.0=testRuntimeClasspath
net.bytebuddy:byte-buddy:1.12.21=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
net.java.dev.jna:jna:5.6.0=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath
org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeOnlyDependenciesMetadata
org.assertj:assertj-core:3.24.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.graalvm.sdk:graal-sdk:22.3.1=testRuntimeClasspath
org.graalvm.truffle:truffle-api:22.3.1=testRuntimeClasspath
org.jetbrains.intellij.deps:trove4j:1.0.20200330=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-compiler-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-daemon-embeddable:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:1.7.10=kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-reflect:1.7.10=kotlinCompilerClasspath,kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-script-runtime:1.7.10=kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath
org.jetbrains.kotlin:kotlin-scripting-common:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-scripting-jvm:1.7.10=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest
org.jetbrains.kotlin:kotlin-stdlib-common:1.7.10=kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.7.10=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.7.10=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains.kotlin:kotlin-stdlib:1.7.10=kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.jetbrains:annotations:13.0=kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.junit.jupiter:junit-jupiter-api:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata
org.junit.jupiter:junit-jupiter-engine:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata
org.junit.jupiter:junit-jupiter-params:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.junit.platform:junit-platform-commons:1.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata
org.junit.platform:junit-platform-engine:1.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata
org.junit:junit-bom:5.9.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata
org.opentest4j:opentest4j:1.2.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath,testRuntimeOnlyDependenciesMetadata
org.organicdesign:Paguro:3.10.3=testRuntimeClasspath
org.slf4j:slf4j-api:1.7.36=compileClasspath,default,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.slf4j:slf4j-simple:1.7.36=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath
org.snakeyaml:snakeyaml-engine:2.5=testRuntimeClasspath
empty=annotationProcessor,apiDependenciesMetadata,archives,compile,compileOnly,compileOnlyDependenciesMetadata,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,pklDistribution,runtime,runtimeOnlyDependenciesMetadata,sourcesJar,testAnnotationProcessor,testApiDependenciesMetadata,testCompile,testCompileOnly,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions,testRuntime

View File

@@ -0,0 +1,55 @@
plugins {
pklAllProjects
pklJavaLibrary
pklPublishLibrary
pklKotlinTest
}
val pklDistribution: Configuration by configurations.creating
// Because pkl-executor doesn't depend on other Pkl modules
// (nor has overlapping dependencies that could cause a version conflict),
// clients are free to use different versions of pkl-executor and (say) pkl-config-java-all.
// (Pkl distributions used by EmbeddedExecutor are isolated via class loaders.)
dependencies {
pklDistribution(project(":pkl-config-java", "fatJar"))
implementation(libs.slf4jApi)
testImplementation(project(":pkl-commons-test"))
testImplementation(project(":pkl-core"))
testImplementation(libs.slf4jSimple)
}
// TODO why is this needed? Without this, we get error:
// `Entry org/pkl/executor/EmbeddedExecutor.java is a duplicate but no duplicate handling strategy has been set.`
// However, we do not have multiple of these Java files.
tasks.named<Jar>("sourcesJar") {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
}
publishing {
publications {
named<MavenPublication>("library") {
pom {
url.set("https://github.com/apple/pkl/tree/main/pkl-executor")
description.set("""
Library for executing Pkl code in a sandboxed environment.
""".trimIndent())
}
}
}
}
sourceSets {
main {
java {
srcDir("src/main/java")
}
}
}
tasks.test {
// used by EmbeddedExecutorTest
dependsOn(pklDistribution)
}

View File

@@ -0,0 +1,331 @@
/**
* Copyright © 2024 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.executor;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.pkl.executor.spi.v1.ExecutorSpi;
import org.pkl.executor.spi.v1.ExecutorSpiException;
import org.pkl.executor.spi.v1.ExecutorSpiOptions;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
final class EmbeddedExecutor implements Executor {
private static final Logger logger = LoggerFactory.getLogger(EmbeddedExecutor.class);
private static final Pattern MODULE_INFO_PATTERN =
Pattern.compile("@ModuleInfo\\s*\\{.*minPklVersion\\s*=\\s*\"([0-9.]*)\".*}", Pattern.DOTALL);
private final List<PklDistribution> pklDistributions = new ArrayList<>();
/**
* @throws IllegalArgumentException if a Jar file cannot be found or is not a valid PklPkl
* distribution
*/
public EmbeddedExecutor(List<Path> pklFatJars) {
for (var jarFile : pklFatJars) {
pklDistributions.add(new PklDistribution(jarFile));
}
}
public String evaluatePath(Path modulePath, ExecutorOptions options) {
logger.info("Started evaluating Pkl module. modulePath={} options={}", modulePath, options);
long startTime = System.nanoTime();
Version requestedVersion = null;
PklDistribution distribution = null;
String output = null;
RuntimeException exception = null;
try {
if (!Files.isRegularFile(modulePath)) {
throw new ExecutorException(
String.format("Cannot find Pkl module `%s`.", toDisplayPath(modulePath, options)));
}
// Note that version detection for the given module happens before security checks for its
// evaluation.
// This should be acceptable because version detection only involves the module passed
// directly to the executor
// (but not any modules imported by it) and only requires parsing (but not evaluating) the
// module.
requestedVersion = detectRequestedPklVersion(modulePath, options);
distribution = findCompatibleDistribution(modulePath, requestedVersion, options);
output = distribution.evaluatePath(modulePath, options);
} catch (RuntimeException e) {
exception = e;
}
var endTime = System.nanoTime();
// Could log exception, but this would violate "don't log and throw",
// and Pkl stack trace might contain semi-sensitive information.
logger.info(
"Finished evaluating Pkl module. modulePath={} outcome={} requestedVersion={} selectedVersion={} elapsedMillis={}",
modulePath,
exception == null ? "success" : "failure",
requestedVersion == null ? "n/a" : requestedVersion.toString(),
distribution == null ? "n/a" : distribution.getVersion().toString(),
(endTime - startTime) / 1_000_000);
if (exception != null) throw exception;
assert output != null;
return output;
}
private Version detectRequestedPklVersion(Path modulePath, ExecutorOptions options) {
String sourceText;
try {
sourceText = Files.readString(modulePath, StandardCharsets.UTF_8);
} catch (IOException e) {
throw new ExecutorException(
String.format("I/O error loading Pkl module `%s`.", toDisplayPath(modulePath, options)),
e);
}
var version = extractMinPklVersion(sourceText);
if (version != null) return version;
var availableVersions =
pklDistributions.stream()
.map(it -> it.getVersion().toString())
.collect(Collectors.joining(", "));
throw new ExecutorException(
String.format(
"Pkl module `%s` does not state which Pkl version it requires. (Available versions: %s)%n"
+ "To fix this problem, annotate the module's `amends`, `extends`, or `module` clause with `@ModuleInfo { minPklVersion = \"x.y.z\" }`.",
toDisplayPath(modulePath, options), availableVersions));
}
/* @Nullable */ static Version extractMinPklVersion(String sourceText) {
var matcher = MODULE_INFO_PATTERN.matcher(sourceText);
return matcher.find() ? Version.parse(matcher.group(1)) : null;
}
private PklDistribution findCompatibleDistribution(
Path modulePath, Version requestedVersion, ExecutorOptions options) {
var result =
pklDistributions.stream()
.filter(it -> it.getVersion().compareTo(requestedVersion) >= 0)
.min(Comparator.comparing(PklDistribution::getVersion));
if (result.isPresent()) return result.get();
var availableVersions =
pklDistributions.stream()
.map(it -> it.getVersion().toString())
.collect(Collectors.joining(", "));
throw new ExecutorException(
String.format(
"Pkl version `%s` requested by module `%s` is not supported. Available versions: %s%n"
+ "To fix this problem, edit the module's `@ModuleInfo { minPklVersion = \"%s\" }` annotation.",
requestedVersion,
toDisplayPath(modulePath, options),
availableVersions,
requestedVersion));
}
private static Path toDisplayPath(Path modulePath, ExecutorOptions options) {
var rootDir = options.getRootDir();
return rootDir == null ? modulePath : rootDir.relativize(modulePath);
}
@Override
public void close() throws Exception {
for (var pklDistribution : pklDistributions) {
pklDistribution.close();
}
}
private static final class PklDistribution implements AutoCloseable {
final URLClassLoader classLoader;
final ExecutorSpi executorSpi;
final Version version;
/**
* @throws IllegalArgumentException if the Jar file does not exist or is not a valid Pkl
* distribution
*/
PklDistribution(Path pklFatJar) {
if (!Files.isRegularFile(pklFatJar)) {
throw new IllegalArgumentException(
String.format("Invalid Pkl distribution: Cannot find Jar file `%s`.", pklFatJar));
}
classLoader = new PklDistributionClassLoader(pklFatJar);
var serviceLoader = ServiceLoader.load(ExecutorSpi.class, classLoader);
try {
executorSpi = serviceLoader.iterator().next();
} catch (NoSuchElementException e) {
throw new IllegalArgumentException(
String.format(
"Invalid Pkl distribution: Cannot find service of type `%s` in Jar file `%s`.",
ExecutorSpi.class.getTypeName(), pklFatJar));
} catch (ServiceConfigurationError e) {
throw new IllegalArgumentException(
String.format(
"Invalid Pkl distribution: Unexpected error loading service of type `%s` from Jar file `%s`.",
ExecutorSpi.class.getTypeName(), pklFatJar),
e);
}
// convert to normal to allow running with a dev version
version = Version.parse(executorSpi.getPklVersion()).toNormal();
}
Version getVersion() {
return version;
}
String evaluatePath(Path modulePath, ExecutorOptions options) {
var currentThread = Thread.currentThread();
var prevContextClassLoader = currentThread.getContextClassLoader();
// Truffle loads stuff from context class loader, so set it to our class loader
currentThread.setContextClassLoader(classLoader);
try {
return executorSpi.evaluatePath(modulePath, toEvaluatorOptions(options));
} catch (ExecutorSpiException e) {
throw new ExecutorException(e.getMessage(), e.getCause());
} finally {
currentThread.setContextClassLoader(prevContextClassLoader);
}
}
@Override
public void close() throws IOException {
classLoader.close();
}
ExecutorSpiOptions toEvaluatorOptions(ExecutorOptions options) {
return new ExecutorSpiOptions(
options.getAllowedModules(),
options.getAllowedResources(),
options.getEnvironmentVariables(),
options.getExternalProperties(),
options.getModulePath(),
options.getRootDir(),
options.getTimeout(),
options.getOutputFormat(),
options.getModuleCacheDir(),
options.getProjectDir());
}
}
private static final class PklDistributionClassLoader extends URLClassLoader {
final ClassLoader spiClassLoader = ExecutorSpi.class.getClassLoader();
PklDistributionClassLoader(Path pklFatJar) {
// pass `null` to make bootstrap class loader the effective parent
super(toUrls(pklFatJar), null);
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
var clazz = findLoadedClass(name);
if (clazz == null) {
if (name.startsWith("org.pkl.executor.spi.")) {
clazz = spiClassLoader.loadClass(name);
} else if (name.startsWith("java.")
|| name.startsWith("jdk.")
|| name.startsWith("sun.")
// Don't add all of `javax` because some packages might come from a dependency
// (e.g. jsr305)
|| name.startsWith("javax.annotation.processing")
|| name.startsWith("javax.lang.")
|| name.startsWith("javax.naming.")
|| name.startsWith("javax.net.")
|| name.startsWith("javax.crypto.")
|| name.startsWith("javax.security.")
|| name.startsWith("com.sun.")) {
clazz = getPlatformClassLoader().loadClass(name);
} else {
clazz = findClass(name);
}
}
if (resolve) {
resolveClass(clazz);
}
return clazz;
}
}
@Override
public URL getResource(String name) {
// try bootstrap class loader first
// once we move to JDK 9+, should use `getPlatformClassLoader().getResource()` instead of
// `super.getResource()`
var resource = super.getResource(name);
return resource != null ? resource : findResource(name);
}
@Override
public Enumeration<URL> getResources(String name) throws IOException {
// once we move to JDK 9+, should use `getPlatformClassLoader().getResources()` instead of
// `super.getResources()`
return ConcatenatedEnumeration.create(super.getResources(name), findResources(name));
}
static URL[] toUrls(Path pklFatJar) {
try {
return new URL[] {pklFatJar.toUri().toURL()};
} catch (MalformedURLException e) {
throw new AssertionError(e);
}
}
}
private static final class ConcatenatedEnumeration<E> implements Enumeration<E> {
final Enumeration<E> e1;
final Enumeration<E> e2;
static <E> Enumeration<E> create(Enumeration<E> e1, Enumeration<E> e2) {
return !e1.hasMoreElements()
? e2
: !e2.hasMoreElements() ? e1 : new ConcatenatedEnumeration<>(e1, e2);
}
ConcatenatedEnumeration(Enumeration<E> e1, Enumeration<E> e2) {
this.e1 = e1;
this.e2 = e2;
}
public boolean hasMoreElements() {
return e1.hasMoreElements() || e2.hasMoreElements();
}
public E nextElement() throws NoSuchElementException {
return e1.hasMoreElements() ? e1.nextElement() : e2.nextElement();
}
}
}

View File

@@ -0,0 +1,32 @@
/**
* Copyright © 2024 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.executor;
import java.nio.file.Path;
/**
* Evaluates Pkl modules in a sandbox. The modules to be evaluated must have an `amends`, `extends`,
* or `module` clause annotated with {@code @ModuleInfo { minPklVersion = "x.y.z" }}. To avoid
* resource leaks, an executor must be {@link #close() closed} after use.
*/
public interface Executor extends AutoCloseable {
/**
* Evaluates the given module with the given options, returning the module's output.
*
* <p>If evaluation fails, throws {@link ExecutorException} with a descriptive message.
*/
String evaluatePath(Path modulePath, ExecutorOptions options);
}

View File

@@ -0,0 +1,29 @@
/**
* Copyright © 2024 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.executor;
/**
* Indicates an {@link Executor} error. {@link #getMessage()} returns a user-facing error message.
*/
public final class ExecutorException extends RuntimeException {
public ExecutorException(String message) {
super(message);
}
public ExecutorException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,206 @@
/**
* Copyright © 2024 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.executor;
import java.nio.file.Path;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/** Options for {@link Executor#evaluatePath}. */
public final class ExecutorOptions {
private final List<String> allowedModules;
private final List<String> allowedResources;
private final Map<String, String> environmentVariables;
private final Map<String, String> externalProperties;
private final List<Path> modulePath;
private final /* @Nullable */ Path rootDir;
private final /* @Nullable */ Duration timeout;
private final /* @Nullable */ String outputFormat;
private final /* @Nullable */ Path moduleCacheDir;
private final /* @Nullable */ Path projectDir;
/** Returns the module cache dir that the CLI uses by default. */
public static Path defaultModuleCacheDir() {
return Path.of(System.getProperty("user.home"), ".pkl", "cache");
}
/**
* Constructs an options object.
*
* @param allowedModules API equivalent of the {@code --allowed-modules} CLI option
* @param allowedResources API equivalent of the {@code --allowed-resources} CLI option
* @param environmentVariables API equivalent of the repeatable {@code --env-var} CLI option
* @param externalProperties API equivalent of the repeatable {@code --property} CLI option
* @param modulePath API equivalent of the {@code --module-path} CLI option
* @param rootDir API equivalent of the {@code --root-dir} CLI option
* @param timeout API equivalent of the {@code --timeout} CLI option
* @param outputFormat API equivalent of the {@code --format} CLI option
* @param moduleCacheDir API equivalent of the {@code --cache-dir} CLI option. Passing {@link
* #defaultModuleCacheDir()} is equivalent to omitting {@code --cache-dir}. Passing {@code
* null} is equivalent to {@code --no-cache}.
* @param projectDir API equivalent of the {@code --project-dir} CLI option.
*/
public ExecutorOptions(
List<String> allowedModules,
List<String> allowedResources,
Map<String, String> environmentVariables,
Map<String, String> externalProperties,
List<Path> modulePath,
/* @Nullable */ Path rootDir,
/* @Nullable */ Duration timeout,
/* @Nullable */ String outputFormat,
/* @Nullable */ Path moduleCacheDir,
/* @Nullable */ Path projectDir) {
this.allowedModules = allowedModules;
this.allowedResources = allowedResources;
this.environmentVariables = environmentVariables;
this.externalProperties = externalProperties;
this.modulePath = modulePath;
this.rootDir = rootDir;
this.timeout = timeout;
this.outputFormat = outputFormat;
this.moduleCacheDir = moduleCacheDir;
this.projectDir = projectDir;
}
/** API equivalent of the {@code --allowed-modules} CLI option. */
public List<String> getAllowedModules() {
return allowedModules;
}
/** API equivalent of the {@code --allowed-resources} CLI option. */
public List<String> getAllowedResources() {
return allowedResources;
}
/** API equivalent of the repeatable {@code --env-var} CLI option. */
public Map<String, String> getEnvironmentVariables() {
return environmentVariables;
}
/** API equivalent of the repeatable {@code --property} CLI option. */
public Map<String, String> getExternalProperties() {
return externalProperties;
}
/** API equivalent of the {@code --module-path} CLI option. */
public List<Path> getModulePath() {
return modulePath;
}
/** API equivalent of the {@code --root-dir} CLI option. */
public /* @Nullable */ Path getRootDir() {
return rootDir;
}
/** API equivalent of the {@code --timeout} CLI option. */
public Duration getTimeout() {
return timeout;
}
/** API equivalent of the {@code --format} CLI option. */
public /* @Nullable */ String getOutputFormat() {
return outputFormat;
}
/**
* API equivalent of the {@code --cache-dir} CLI option. {@code null} is equivalent to {@code
* --no-cache}.
*/
public /* @Nullable */ Path getModuleCacheDir() {
return moduleCacheDir;
}
/**
* API equivalent of the {@code --project-dir} CLI option.
*
* <p>Unlike the CLI, this option only sets project dependencies. It does not set evaluator
* settings.
*/
public /* @Nullable */ Path getProjectDir() {
return projectDir;
}
@Override
public boolean equals(/* @Nullable */ Object obj) {
if (this == obj) return true;
if (!(obj instanceof ExecutorOptions)) return false;
var other = (ExecutorOptions) obj;
return allowedModules.equals(other.allowedModules)
&& allowedResources.equals(other.allowedResources)
&& environmentVariables.equals(other.environmentVariables)
&& externalProperties.equals(other.externalProperties)
&& modulePath.equals(other.modulePath)
&& Objects.equals(rootDir, other.rootDir)
&& Objects.equals(timeout, other.timeout)
&& Objects.equals(outputFormat, other.outputFormat)
&& Objects.equals(moduleCacheDir, other.moduleCacheDir)
&& Objects.equals(projectDir, other.projectDir);
}
@Override
public int hashCode() {
return Objects.hash(
allowedModules,
allowedResources,
environmentVariables,
externalProperties,
modulePath,
rootDir,
timeout,
outputFormat,
moduleCacheDir,
projectDir);
}
@Override
public String toString() {
return "ExecutorOptions{"
+ "allowedModules="
+ allowedModules
+ ", allowedResources="
+ allowedResources
+ ", environmentVariables="
+ environmentVariables
+ ", externalProperties="
+ externalProperties
+ ", modulePath="
+ modulePath
+ ", rootDir="
+ rootDir
+ ", timeout="
+ timeout
+ ", outputFormat="
+ outputFormat
+ ", cacheDir="
+ moduleCacheDir
+ ", projectDir="
+ projectDir
+ '}';
}
}

View File

@@ -0,0 +1,35 @@
/**
* Copyright © 2024 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.executor;
import java.nio.file.Path;
import java.util.List;
/** A factory for {@link Executor}s. */
public final class Executors {
private Executors() {}
/**
* Creates an executor that evaluates Pkl modules in the caller's JVM with the given fat Jar Pkl
* distributions (typically <em>pkl-config-java-all</em>).
*
* @throws IllegalArgumentException if a Jar file cannot be found or is not a valid Pkl
* distribution
*/
public static Executor embedded(List<Path> pklFatJars) {
return new EmbeddedExecutor(pklFatJars);
}
}

View File

@@ -0,0 +1,259 @@
/**
* Copyright © 2024 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.executor;
import java.util.*;
import java.util.regex.Pattern;
/**
* A semantic version (https://semver.org/spec/v2.0.0.html).
*
* <p>This class guarantees that valid semantic version numbers are handled correctly, but does
* <em>not</em> guarantee that invalid semantic version numbers are rejected.
*/
// copied from `org.pkl.core.Version` to avoid dependency on pkl-core
@SuppressWarnings("Duplicates")
final class Version implements Comparable<Version> {
// https://semver.org/#backusnaur-form-grammar-for-valid-semver-versions
private static final Pattern VERSION =
Pattern.compile("(\\d+)\\.(\\d+)\\.(\\d+)(?:-([^+]+))?(?:\\+(.+))?");
private static final Pattern NUMERIC_IDENTIFIER = Pattern.compile("(0|[1-9]\\d*)");
private static final Comparator<Version> COMPARATOR =
Comparator.comparingInt(Version::getMajor)
.thenComparingInt(Version::getMinor)
.thenComparingInt(Version::getPatch)
.thenComparing(
(v1, v2) -> {
if (v1.preRelease == null) return v2.preRelease == null ? 0 : 1;
if (v2.preRelease == null) return -1;
var ids1 = v1.getPreReleaseIdentifiers();
var ids2 = v2.getPreReleaseIdentifiers();
var minSize = Math.min(ids1.length, ids2.length);
for (var i = 0; i < minSize; i++) {
var result = ids1[i].compareTo(ids2[i]);
if (result != 0) return result;
}
return Integer.compare(ids1.length, ids2.length);
});
private final int major;
private final int minor;
private final int patch;
private final /*@Nullable*/ String preRelease;
private final /*@Nullable*/ String build;
// always access through getter
private volatile Identifier[] __preReleaseIdentifiers;
/** Constructs a semantic version. */
public Version(
int major,
int minor,
int patch,
/*@Nullable*/ String preRelease,
/*@Nullable*/ String build) {
this.major = major;
this.minor = minor;
this.patch = patch;
this.preRelease = preRelease;
this.build = build;
}
/**
* Parses the given string as a semantic version number.
*
* <p>Throws {@link IllegalArgumentException} if the given string could not be parsed as a
* semantic version number or is too large to fit into a {@link Version}.
*/
public static Version parse(String version) {
var result = parseOrNull(version);
if (result != null) return result;
if (VERSION.matcher(version).matches()) {
throw new IllegalArgumentException(
String.format("`%s` is too large to fit into a Version.", version));
}
throw new IllegalArgumentException(
String.format("`%s` could not be parsed as a semantic version number.", version));
}
/**
* Parses the given string as a semantic version number.
*
* <p>Returns {@code null} if the given string could not be parsed as a semantic version number or
* is too large to fit into a {@link Version}.
*/
public static /*@Nullable*/ Version parseOrNull(String version) {
var matcher = VERSION.matcher(version);
if (!matcher.matches()) return null;
try {
return new Version(
Integer.parseInt(matcher.group(1)),
Integer.parseInt(matcher.group(2)),
Integer.parseInt(matcher.group(3)),
matcher.group(4),
matcher.group(5));
} catch (NumberFormatException e) {
return null;
}
}
/** Returns a comparator for semantic versions. */
public static Comparator<Version> comparator() {
return COMPARATOR;
}
/** Returns the major version. */
public int getMajor() {
return major;
}
/** Returns a copy of this version with the given major version. */
public Version withMajor(int major) {
return new Version(major, minor, patch, preRelease, build);
}
/** Returns the minor version. */
public int getMinor() {
return minor;
}
/** Returns a copy of this version with the given minor version. */
public Version withMinor(int minor) {
return new Version(major, minor, patch, preRelease, build);
}
/** Returns the patch version. */
public int getPatch() {
return patch;
}
/** Returns a copy of this version with the given patch version. */
public Version withPatch(int patch) {
return new Version(major, minor, patch, preRelease, build);
}
/** Returns the pre-release version (if any). */
public /*@Nullable*/ String getPreRelease() {
return preRelease;
}
/** Returns a copy of this version with the given pre-release version. */
public Version withPreRelease(/*@Nullable*/ String preRelease) {
return new Version(major, minor, patch, preRelease, build);
}
/** Returns the build metadata (if any). */
public /*@Nullable*/ String getBuild() {
return build;
}
/** Returns a copy of this version with the given build metadata. */
public Version withBuild(/*@Nullable*/ String build) {
return new Version(major, minor, patch, preRelease, build);
}
/** Tells if this version has no pre-release version or build metadata. */
@SuppressWarnings("unused")
public boolean isNormal() {
return preRelease == null && build == null;
}
/** Tells if this version has a non-zero major version and no pre-release version. */
public boolean isStable() {
return major != 0 && preRelease == null;
}
/** Strips any pre-release version and build metadata from this version. */
public Version toNormal() {
return preRelease == null && build == null
? this
: new Version(major, minor, patch, null, null);
}
/** Compares this version to the given version according to semantic versioning rules. */
@Override
public int compareTo(@SuppressWarnings("NullableProblems") /*@Nonnull*/ Version other) {
return COMPARATOR.compare(this, other);
}
/** Tells if this version is equal to {@code obj} according to semantic versioning rules. */
@Override
public boolean equals(/* @Nullable */ Object obj) {
if (this == obj) return true;
if (!(obj instanceof Version)) return false;
var other = (Version) obj;
return major == other.major
&& minor == other.minor
&& patch == other.patch
&& Objects.equals(preRelease, other.preRelease);
}
@Override
public int hashCode() {
return Objects.hash(major, minor, patch, preRelease);
}
@Override
public String toString() {
return ""
+ major
+ "."
+ minor
+ "."
+ patch
+ (preRelease != null ? "-" + preRelease : "")
+ (build != null ? "+" + build : "");
}
private Identifier[] getPreReleaseIdentifiers() {
if (__preReleaseIdentifiers == null) {
__preReleaseIdentifiers =
preRelease == null
? new Identifier[0]
: Arrays.stream(preRelease.split("\\."))
.map(
str ->
NUMERIC_IDENTIFIER.matcher(str).matches()
? new Identifier(Long.parseLong(str), null)
: new Identifier(-1, str))
.toArray(Identifier[]::new);
}
return __preReleaseIdentifiers;
}
private static final class Identifier implements Comparable<Identifier> {
private final long numericId;
private final /*@Nullable*/ String alphanumericId;
Identifier(long numericId, /*@Nullable*/ String alphanumericId) {
this.numericId = numericId;
this.alphanumericId = alphanumericId;
}
@Override
public int compareTo(/*@Nonnull*/ @SuppressWarnings("NullableProblems") Identifier other) {
return alphanumericId != null
? other.alphanumericId != null ? alphanumericId.compareTo(other.alphanumericId) : 1
: other.alphanumericId != null ? -1 : Long.compare(numericId, other.numericId);
}
}
}

View File

@@ -0,0 +1,12 @@
/**
* <strong>Internal</strong> SPI for executing Pkl code with different Pkl distributions.
*
* <p>CAUTION: Every class X under `spi` MUST adhere to the following rules:
*
* <ol>
* <li>X MUST live in a versioned subpackage (v1, v2, etc.).
* <li>Any change made to X MUST be binary compatible.
* <li>X MUST only use classes from its own package and Java platform packages.
* </ol>
*/
package org.pkl.executor.spi;

View File

@@ -0,0 +1,29 @@
/**
* Copyright © 2024 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.executor.spi.v1;
import java.nio.file.Path;
public interface ExecutorSpi {
String getPklVersion();
/**
* Evaluation the Pkl module with the given path, returning the module's output.
*
* <p>If evaluation fails, throws {@link ExecutorSpiException} with a descriptive message.
*/
String evaluatePath(Path modulePath, ExecutorSpiOptions options);
}

View File

@@ -0,0 +1,22 @@
/**
* Copyright © 2024 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.executor.spi.v1;
public final class ExecutorSpiException extends RuntimeException {
public ExecutorSpiException(String message, Throwable cause) {
super(message, cause);
}
}

View File

@@ -0,0 +1,105 @@
/**
* Copyright © 2024 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.executor.spi.v1;
import java.nio.file.Path;
import java.time.Duration;
import java.util.List;
import java.util.Map;
public class ExecutorSpiOptions {
private final List<String> allowedModules;
private final List<String> allowedResources;
private final Map<String, String> environmentVariables;
private final Map<String, String> externalProperties;
private final List<Path> modulePath;
private final Path rootDir;
private final Duration timeout;
private final String outputFormat;
private final Path moduleCacheDir;
private final Path projectDir;
public ExecutorSpiOptions(
List<String> allowedModules,
List<String> allowedResources,
Map<String, String> environmentVariables,
Map<String, String> externalProperties,
List<Path> modulePath,
/* @Nullable */ Path rootDir,
/* @Nullable */ Duration timeout,
/* @Nullable */ String outputFormat,
/* @Nullable */ Path moduleCacheDir,
/* @Nullable */ Path projectDir) {
this.allowedModules = allowedModules;
this.allowedResources = allowedResources;
this.environmentVariables = environmentVariables;
this.externalProperties = externalProperties;
this.modulePath = modulePath;
this.rootDir = rootDir;
this.timeout = timeout;
this.outputFormat = outputFormat;
this.moduleCacheDir = moduleCacheDir;
this.projectDir = projectDir;
}
public List<String> getAllowedModules() {
return allowedModules;
}
public List<String> getAllowedResources() {
return allowedResources;
}
public Map<String, String> getEnvironmentVariables() {
return environmentVariables;
}
public Map<String, String> getExternalProperties() {
return externalProperties;
}
public List<Path> getModulePath() {
return modulePath;
}
public /* @Nullable */ Path getRootDir() {
return rootDir;
}
public /* @Nullable */ Duration getTimeout() {
return timeout;
}
public /* @Nullable */ String getOutputFormat() {
return outputFormat;
}
public /* @Nullable */ Path getModuleCacheDir() {
return moduleCacheDir;
}
public Path getProjectDir() {
return projectDir;
}
}

View File

@@ -0,0 +1,524 @@
package org.pkl.executor
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.io.TempDir
import org.pkl.commons.test.FileTestUtils
import org.pkl.commons.test.PackageServer
import org.pkl.commons.toPath
import org.pkl.commons.walk
import org.pkl.core.runtime.CertificateUtils
import java.nio.file.Files
import java.nio.file.Path
import java.time.Duration
import kotlin.io.path.createDirectories
class EmbeddedExecutorTest {
private val pklDistribution by lazy {
val libsDir = FileTestUtils.rootProjectDir.resolve("pkl-config-java/build/libs")
if (!Files.isDirectory(libsDir)) {
throw AssertionError(
"JAR `pkl-config-java-all` does not exist. Run `./gradlew :pkl-config-java:build` to create it."
)
}
libsDir.walk()
.filter { path ->
path.toString().let {
it.contains("-all") &&
it.endsWith(".jar") &&
!it.contains("-sources") &&
!it.contains("-javadoc")
}
}
.findFirst()
.orElseThrow {
AssertionError(
"JAR `pkl-config-java-all` does not exist. Run `./gradlew :pkl-config-java:build` to create it."
)
}
}
@Test
fun extractMinPklVersion() {
assertThat(
EmbeddedExecutor.extractMinPklVersion(
"""
@ModuleInfo { minPklVersion = "1.2.3" }
""".trimIndent()
)
).isEqualTo(Version.parse("1.2.3"))
assertThat(
EmbeddedExecutor.extractMinPklVersion(
"""
@ModuleInfo{minPklVersion="1.2.3"}
""".trimIndent()
)
).isEqualTo(Version.parse("1.2.3"))
assertThat(
EmbeddedExecutor.extractMinPklVersion(
"""
@ModuleInfo { minPklVersion = "1.2.3" }
""".trimIndent()
)
).isEqualTo(Version.parse("1.2.3"))
assertThat(
EmbeddedExecutor.extractMinPklVersion(
"""
@ModuleInfo {
minPklVersion = "1.2.3"
}
""".trimIndent()
)
).isEqualTo(Version.parse("1.2.3"))
assertThat(
EmbeddedExecutor.extractMinPklVersion(
"""
@ModuleInfo {
author = "foo@bar.apple.com"
minPklVersion = "1.2.3"
}
""".trimIndent()
)
).isEqualTo(Version.parse("1.2.3"))
assertThat(
EmbeddedExecutor.extractMinPklVersion(
"""
@ModuleInfo {
minPklVersion = "1.2.3"
author = "foo@bar.apple.com"
}
""".trimIndent()
)
).isEqualTo(Version.parse("1.2.3"))
}
@Test
fun `create embedded executor with non-existing Pkl distribution`() {
val e = assertThrows<IllegalArgumentException> {
Executors.embedded(listOf("/non/existing".toPath()))
}
assertThat(e.message)
.contains("Cannot find Jar file")
.contains("/non/existing")
}
@Test
fun `create embedded executor with invalid Pkl distribution that is not a Jar file`(@TempDir tempDir: Path) {
val file = Files.createFile(tempDir.resolve("pkl.jar"))
val e = assertThrows<IllegalArgumentException> {
Executors.embedded(listOf(file))
}
assertThat(e.message)
.contains("Cannot find service")
.contains("pkl.jar")
}
@Test
fun `evaluate a module that is missing a ModuleInfo annotation`(@TempDir tempDir: Path) {
val pklFile = tempDir.resolve("test.pkl")
pklFile.toFile().writeText(
"""
module test
x = 1
""".trimIndent()
)
val executor = Executors.embedded(listOf(pklDistribution))
val e = assertThrows<ExecutorException> {
executor.use {
it.evaluatePath(
pklFile,
ExecutorOptions(
listOf("file:"),
listOf("prop:"),
mapOf(),
mapOf(),
listOf(),
tempDir,
null,
null,
null,
null
)
)
}
}
assertThat(e.message)
.contains("Pkl module `test.pkl` does not state which Pkl version it requires.")
}
@Test
fun `evaluate a module that requests an incompatible Pkl version`(@TempDir tempDir: Path) {
val pklFile = tempDir.resolve("test.pkl")
pklFile.toFile().writeText(
"""
@ModuleInfo { minPklVersion = "99.99.99" }
module test
x = 1
""".trimIndent()
)
val executor = Executors.embedded(listOf(pklDistribution))
val e = assertThrows<ExecutorException> {
executor.use {
it.evaluatePath(
pklFile,
ExecutorOptions(
listOf("file:"),
listOf("prop:"),
mapOf(),
mapOf(),
listOf(),
tempDir,
null,
null,
null,
null
)
)
}
}
assertThat(e.message)
.contains("Pkl version `99.99.99` requested by module `test.pkl` is not supported.")
}
@Test
fun `evaluate a module that reads environment variables and external properties`(@TempDir tempDir: Path) {
val pklFile = tempDir.resolve("test.pkl")
pklFile.toFile().writeText(
"""
@ModuleInfo { minPklVersion = "0.11.0" }
module test
x = read("env:ENV_VAR")
y = read("prop:property")
""".trimIndent()
)
val executor = Executors.embedded(listOf(pklDistribution))
val result = executor.use {
it.evaluatePath(
pklFile,
ExecutorOptions(
listOf("file:"),
// should `prop:pkl.outputFormat` be allowed automatically?
listOf("prop:", "env:"),
mapOf("ENV_VAR" to "ENV_VAR"),
mapOf("property" to "property"),
listOf(),
null,
null,
null,
null,
null
)
)
}
assertThat(result.trim()).isEqualTo(
"""
x = "ENV_VAR"
y = "property"
""".trimIndent().trim()
)
}
@Test
fun `evaluate a module that depends on another module`(@TempDir tempDir: Path) {
val pklFile = tempDir.resolve("test.pkl")
pklFile.toFile().writeText(
"""
@ModuleInfo { minPklVersion = "0.11.0" }
amends "template.pkl"
foo {
bar = 42
}
""".trimIndent()
)
val templateFile = tempDir.resolve("template.pkl")
templateFile.toFile().writeText(
"""
foo: Foo
class Foo {
bar: Int
}
""".trimIndent()
)
val executor = Executors.embedded(listOf(pklDistribution))
val result = executor.use {
it.evaluatePath(
pklFile,
ExecutorOptions(
listOf("file:"),
listOf("prop:"),
mapOf(),
mapOf(),
listOf(),
null,
null,
null,
null,
null
)
)
}
assertThat(result.trim()).isEqualTo(
"""
foo {
bar = 42
}
""".trimIndent().trim()
)
}
@Test
fun `evaluate a module whose evaluation fails`(@TempDir tempDir: Path) {
val pklFile = tempDir.resolve("test.pkl")
pklFile.toFile().writeText(
"""
@ModuleInfo { minPklVersion = "0.11.0" }
module test
foo = throw("ouch")
""".trimIndent()
)
val executor = Executors.embedded(listOf(pklDistribution))
val e = assertThrows<ExecutorException> {
executor.use {
it.evaluatePath(
pklFile,
ExecutorOptions(
listOf("file:"),
listOf("prop:"),
mapOf(),
mapOf(),
listOf(),
tempDir,
null,
null,
null,
null
)
)
}
}
assertThat(e.message)
.contains("ouch")
// ensure module file paths are relativized
.contains("at test#foo (test.pkl)")
.doesNotContain(tempDir.toString())
}
@Test
fun `time out a module`(@TempDir tempDir: Path) {
val pklFile = tempDir.resolve("test.pkl")
pklFile.toFile().writeText(
"""
@ModuleInfo { minPklVersion = "0.11.0" }
module test
x = fib(100)
function fib(n) = if (n < 2) n else fib(n - 1) + fib(n - 2)
""".trimIndent()
)
val executor = Executors.embedded(listOf(pklDistribution))
val e = assertThrows<ExecutorException> {
executor.use {
it.evaluatePath(
pklFile,
ExecutorOptions(
listOf("file:"),
listOf("prop:"),
mapOf(),
mapOf(),
listOf(),
tempDir,
Duration.ofSeconds(1),
null,
null,
null
)
)
}
}
assertThat(e.message)
.contains("Evaluation timed out after 1 second(s).")
}
// As of 0.16, only Pkl Hub modules are cached.
// Because this test doesn't import a Pkl Hub module, it doesn't really test
// that the `moduleCacheDir` option takes effect.
@Test
fun `evaluate a module with enabled module cache`(@TempDir tempDir: Path) {
val pklFile = tempDir.resolve("test.pkl")
pklFile.toFile().writeText(
"""
@ModuleInfo { minPklVersion = "0.16.0" }
module test
x = 42
""".trimIndent()
)
val executor = Executors.embedded(listOf(pklDistribution))
val result = executor.use {
it.evaluatePath(
pklFile,
ExecutorOptions(
listOf("file:"),
listOf("prop:"),
mapOf(),
mapOf(),
listOf(),
null,
null,
null,
ExecutorOptions.defaultModuleCacheDir(),
null
)
)
}
assertThat(result.trim()).isEqualTo(
"""
x = 42
""".trimIndent().trim()
)
}
@Test
fun `evaluate a module that loads a package`(@TempDir tempDir: Path) {
val pklFile = tempDir.resolve("test.pkl")
pklFile.toFile().writeText(
"""
@ModuleInfo { minPklVersion = "0.24.0" }
module MyModule
import "package://localhost:12110/birds@0.5.0#/Bird.pkl"
chirpy = new Bird { name = "Chirpy"; favoriteFruit { name = "Orange" } }
""".trimIndent()
)
PackageServer.ensureStarted()
CertificateUtils.setupAllX509CertificatesGlobally(listOf(FileTestUtils.selfSignedCertificate))
val executor = Executors.embedded(listOf(pklDistribution))
val result = executor.use {
it.evaluatePath(pklFile,
ExecutorOptions(
listOf("file:", "package:", "https:"),
listOf("prop:", "package:", "https:"),
mapOf(),
mapOf(),
listOf(),
null,
null,
null,
ExecutorOptions.defaultModuleCacheDir(),
null)
)
}
assertThat(result.trim()).isEqualTo("""
chirpy {
name = "Chirpy"
favoriteFruit {
name = "Orange"
}
}
""".trimIndent())
}
@Test
fun `evaluate a project dependency`(@TempDir tempDir: Path) {
val cacheDir = tempDir.resolve("packages")
PackageServer.populateCacheDir(cacheDir)
val projectDir = tempDir.resolve("project/")
projectDir.createDirectories()
projectDir.resolve("PklProject").toFile().writeText("""
amends "pkl:Project"
dependencies {
["birds"] { uri = "package://localhost:12110/birds@0.5.0" }
}
""".trimIndent())
val dollar = '$'
projectDir.resolve("PklProject.deps.json").toFile().writeText("""
{
"schemaVersion": 1,
"resolvedDependencies": {
"package://localhost:12110/birds@0": {
"type": "remote",
"uri": "projectpackage://localhost:12110/birds@0.5.0",
"checksums": {
"sha256": "${dollar}skipChecksumVerification"
}
},
"package://localhost:12110/fruit@1": {
"type": "remote",
"uri": "projectpackage://localhost:12110/fruit@1.0.5",
"checksums": {
"sha256": "${dollar}skipChecksumVerification"
}
}
}
}
""".trimIndent())
val pklFile = projectDir.resolve("test.pkl")
pklFile.toFile().writeText(
"""
@ModuleInfo { minPklVersion = "0.24.0" }
module myModule
import "@birds/catalog/Swallow.pkl"
result = Swallow
""".trimIndent()
)
val executor = Executors.embedded(listOf(pklDistribution))
val result = executor.use {
it.evaluatePath(pklFile,
ExecutorOptions(
listOf("file:", "package:", "projectpackage:", "https:"),
listOf("prop:", "package:", "projectpackage:", "https:"),
mapOf(),
mapOf(),
listOf(),
null,
null,
null,
cacheDir,
projectDir)
)
}
assertThat(result).isEqualTo("""
result {
name = "Swallow"
favoriteFruit {
name = "Apple"
}
}
""".trimIndent())
}
}

View File

@@ -0,0 +1,214 @@
package org.pkl.executor
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
// copied from `org.pkl.core.VersionTest`
class VersionTest {
@Test
fun `parse release version`() {
val version = Version.parse("1.2.3")
assertThat(version.major).isEqualTo(1)
assertThat(version.minor).isEqualTo(2)
assertThat(version.patch).isEqualTo(3)
assertThat(version.preRelease).isNull()
assertThat(version.build).isNull()
}
@Test
fun `parse snapshot version`() {
val version = Version.parse("1.2.3-SNAPSHOT+build-123")
assertThat(version.major).isEqualTo(1)
assertThat(version.minor).isEqualTo(2)
assertThat(version.patch).isEqualTo(3)
assertThat(version.preRelease).isEqualTo("SNAPSHOT")
assertThat(version.build).isEqualTo("build-123")
}
@Test
fun `parse beta version`() {
val version = Version.parse("1.2.3-beta.1+build-123")
assertThat(version.major).isEqualTo(1)
assertThat(version.minor).isEqualTo(2)
assertThat(version.patch).isEqualTo(3)
assertThat(version.preRelease).isEqualTo("beta.1")
assertThat(version.build).isEqualTo("build-123")
}
@Test
fun `parse invalid version`() {
assertThat(Version.parseOrNull("not a version number"))
.isNull()
assertThrows<IllegalArgumentException> {
Version.parse("not a version number")
}
}
@Test
fun `parse too large version`() {
assertThrows<IllegalArgumentException> {
Version.parse("not a version number")
}
assertThrows<IllegalArgumentException> {
Version.parse("999999999999999.0.0")
}
}
@Test
fun toNormal() {
val stripped = Version.parse("1.2.3")
assertThat(Version.parse("1.2.3-beta-1+build-123").toNormal()).isEqualTo(stripped)
assertThat(Version.parse("1.2.3-beta-1").toNormal()).isEqualTo(stripped)
assertThat(Version.parse("1.2.3").toNormal()).isEqualTo(stripped)
}
@Test
fun withMethods() {
val version = Version.parse("0.0.0")
.withMajor(1)
.withMinor(2)
.withPatch(3)
.withPreRelease("rc.1")
.withBuild("456.789")
assertThat(version).isEqualTo(Version.parse("1.2.3-rc.1+456.789"))
val version2 = Version.parse("0.0.0")
.withBuild("456.789")
.withPreRelease("rc.1")
.withPatch(3)
.withMinor(2)
.withMajor(1)
assertThat(version2).isEqualTo(version)
}
@Test
fun `compareTo()`() {
assertThat(
Version(1, 2, 3, null, null).compareTo(
Version(1, 2, 3, null, null)
)
).isEqualTo(0)
assertThat(
Version(1, 2, 3, "SNAPSHOT", null).compareTo(
Version(1, 2, 3, "SNAPSHOT", null)
)
).isEqualTo(0)
assertThat(
Version(1, 2, 3, "alpha", null).compareTo(
Version(1, 2, 3, "alpha", null)
)
).isEqualTo(0)
assertThat(
Version(1, 2, 3, "alpha", null).compareTo(
Version(1, 2, 3, "alpha", "build123")
)
).isEqualTo(0)
assertThat(
Version(1, 2, 3, null, null).compareTo(
Version(2, 2, 3, null, null)
)
).isLessThan(0)
assertThat(
Version(1, 2, 3, null, null).compareTo(
Version(1, 3, 3, null, null)
)
).isLessThan(0)
assertThat(
Version(1, 2, 3, null, null).compareTo(
Version(1, 2, 4, null, null)
)
).isLessThan(0)
assertThat(
Version(2, 2, 3, null, null).compareTo(
Version(1, 2, 3, null, null)
)
).isGreaterThan(0)
assertThat(
Version(1, 3, 3, null, null).compareTo(
Version(1, 2, 3, null, null)
)
).isGreaterThan(0)
assertThat(
Version(1, 2, 4, null, null).compareTo(
Version(1, 2, 3, null, null)
)
).isGreaterThan(0)
assertThat(
Version(1, 2, 3, "SNAPSHOT", null).compareTo(
Version(1, 2, 3, null, null)
)
).isLessThan(0)
assertThat(
Version(1, 2, 3, "alpha", null).compareTo(
Version(1, 2, 3, "beta", null)
)
).isLessThan(0)
assertThat(
Version(1, 2, 3, "alpha", "build123").compareTo(
Version(1, 2, 3, "beta", null)
)
).isLessThan(0)
assertThat(
Version(1, 2, 3, null, null).compareTo(
Version(1, 2, 3, "SNAPSHOT", null)
)
).isGreaterThan(0)
assertThat(
Version(1, 2, 3, "beta", null).compareTo(
Version(1, 2, 3, "alpha", "build123")
)
).isGreaterThan(0)
}
@Test
fun `compare version with too large numeric pre-release identifier`() {
// error is deferred until compareTo(), but should be good enough
assertThrows<IllegalArgumentException> {
Version(1, 2, 3, "999", null).compareTo(
Version(1, 2, 3, "9999999999999999999", null)
)
}
}
@Test
fun `equals()`() {
assertThat(Version(1, 2, 3, null, null))
.isEqualTo(Version(1, 2, 3, null, null))
assertThat(Version(1, 2, 3, "SNAPSHOT", null))
.isEqualTo(Version(1, 2, 3, "SNAPSHOT", null))
assertThat(Version(1, 2, 3, "alpha", null))
.isEqualTo(Version(1, 2, 3, "alpha", null))
assertThat(Version(1, 2, 3, "beta", "build123"))
.isEqualTo(Version(1, 2, 3, "beta", "build456"))
assertThat(Version(1, 3, 3, null, null))
.isNotEqualTo(Version(1, 2, 3, null, null))
assertThat(Version(1, 2, 4, null, null))
.isNotEqualTo(Version(1, 2, 3, null, null))
assertThat(Version(1, 2, 3, "SNAPSHOT", null))
.isNotEqualTo(Version(1, 2, 3, null, null))
assertThat(Version(1, 2, 3, "beta", null))
.isNotEqualTo(Version(1, 2, 3, "alpha", null))
}
@Test
fun `hashCode()`() {
assertThat(Version(1, 2, 3, null, null).hashCode())
.isEqualTo(Version(1, 2, 3, null, null).hashCode())
assertThat(Version(1, 2, 3, "SNAPSHOT", null).hashCode())
.isEqualTo(Version(1, 2, 3, "SNAPSHOT", null).hashCode())
assertThat(Version(1, 2, 3, "alpha", null).hashCode())
.isEqualTo(Version(1, 2, 3, "alpha", null).hashCode())
assertThat(Version(1, 2, 3, "alpha", "build123").hashCode())
.isEqualTo(Version(1, 2, 3, "alpha", "build456").hashCode())
}
}