mirror of
https://github.com/apple/pkl.git
synced 2026-03-23 09:31:06 +01:00
Initial commit
This commit is contained in:
37
pkl-executor/gradle.lockfile
Normal file
37
pkl-executor/gradle.lockfile
Normal 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
|
||||
55
pkl-executor/pkl-executor.gradle.kts
Normal file
55
pkl-executor/pkl-executor.gradle.kts
Normal 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)
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
32
pkl-executor/src/main/java/org/pkl/executor/Executor.java
Normal file
32
pkl-executor/src/main/java/org/pkl/executor/Executor.java
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
206
pkl-executor/src/main/java/org/pkl/executor/ExecutorOptions.java
Normal file
206
pkl-executor/src/main/java/org/pkl/executor/ExecutorOptions.java
Normal 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
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
35
pkl-executor/src/main/java/org/pkl/executor/Executors.java
Normal file
35
pkl-executor/src/main/java/org/pkl/executor/Executors.java
Normal 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);
|
||||
}
|
||||
}
|
||||
259
pkl-executor/src/main/java/org/pkl/executor/Version.java
Normal file
259
pkl-executor/src/main/java/org/pkl/executor/Version.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
214
pkl-executor/src/test/kotlin/org/pkl/executor/VersionTest.kt
Normal file
214
pkl-executor/src/test/kotlin/org/pkl/executor/VersionTest.kt
Normal 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())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user