Support scheme-agnostic projects (#486)

This adds changes to support loading project dependencies in non-file based projects.

The design for this feature can be found in SPICE-0005: https://github.com/apple/pkl-evolution/pull/6

Changes:
* Consider all imports prefixed with `@` as dependency notation.
* Bugfix: fix resolution of glob expressions in a local dependency.
* Adjust pkl.Project:
  - Allow local dependencies from a scheme-local paths.
  - Disallow certain evaluator settings if not loaded as a file-based module.
* Breaking API change: `ProjectDependenciesManager` constructor now requires `ModuleResolver` and `SecurityManager`.
This commit is contained in:
Daniel Chao
2024-06-04 16:52:20 -07:00
committed by GitHub
parent c0a7080287
commit d5ba8fa736
49 changed files with 764 additions and 235 deletions

View File

@@ -105,7 +105,8 @@ public class EvaluatorImpl implements Evaluator {
packageResolver, packageResolver,
projectDependencies == null projectDependencies == null
? null ? null
: new ProjectDependenciesManager(projectDependencies))); : new ProjectDependenciesManager(
projectDependencies, moduleResolver, securityManager)));
}); });
this.timeout = timeout; this.timeout = timeout;
// NOTE: would probably make sense to share executor between evaluators // NOTE: would probably make sense to share executor between evaluators

View File

@@ -1825,6 +1825,8 @@ public final class AstBuilder extends AbstractAstBuilder<Object> {
} catch (VmException e) { } catch (VmException e) {
throw exceptionBuilder() throw exceptionBuilder()
.evalError(e.getMessage(), e.getMessageArguments()) .evalError(e.getMessage(), e.getMessageArguments())
.withCause(e.getCause())
.withHint(e.getHint())
.withSourceSection(createSourceSection(importUriCtx)) .withSourceSection(createSourceSection(importUriCtx))
.build(); .build();
} }

View File

@@ -22,6 +22,7 @@ import com.oracle.truffle.api.frame.FrameDescriptor;
import com.oracle.truffle.api.nodes.NodeInfo; import com.oracle.truffle.api.nodes.NodeInfo;
import com.oracle.truffle.api.source.SourceSection; import com.oracle.truffle.api.source.SourceSection;
import java.io.IOException; import java.io.IOException;
import java.net.URISyntaxException;
import org.graalvm.collections.EconomicMap; import org.graalvm.collections.EconomicMap;
import org.pkl.core.SecurityManagerException; import org.pkl.core.SecurityManagerException;
import org.pkl.core.ast.member.SharedMemberNode; import org.pkl.core.ast.member.SharedMemberNode;
@@ -33,6 +34,7 @@ import org.pkl.core.runtime.VmMapping;
import org.pkl.core.runtime.VmObjectBuilder; import org.pkl.core.runtime.VmObjectBuilder;
import org.pkl.core.util.GlobResolver; import org.pkl.core.util.GlobResolver;
import org.pkl.core.util.GlobResolver.InvalidGlobPatternException; import org.pkl.core.util.GlobResolver.InvalidGlobPatternException;
import org.pkl.core.util.IoUtils;
import org.pkl.core.util.LateInit; import org.pkl.core.util.LateInit;
@NodeInfo(shortName = "read*") @NodeInfo(shortName = "read*")
@@ -73,7 +75,7 @@ public abstract class ReadGlobNode extends AbstractReadNode {
var globUri = parseUri(globPattern); var globUri = parseUri(globPattern);
var context = VmContext.get(this); var context = VmContext.get(this);
try { try {
var resolvedUri = currentModule.resolveUri(globUri); var resolvedUri = IoUtils.resolve(context.getSecurityManager(), currentModule, globUri);
var reader = context.getResourceManager().getReader(resolvedUri, this); var reader = context.getResourceManager().getReader(resolvedUri, this);
if (!reader.isGlobbable()) { if (!reader.isGlobbable()) {
throw exceptionBuilder().evalError("cannotGlobUri", globUri, globUri.getScheme()).build(); throw exceptionBuilder().evalError("cannotGlobUri", globUri, globUri.getScheme()).build();
@@ -94,7 +96,7 @@ public abstract class ReadGlobNode extends AbstractReadNode {
return cachedResult; return cachedResult;
} catch (IOException e) { } catch (IOException e) {
throw exceptionBuilder().evalError("ioErrorResolvingGlob", globPattern).withCause(e).build(); throw exceptionBuilder().evalError("ioErrorResolvingGlob", globPattern).withCause(e).build();
} catch (SecurityManagerException | HttpClientInitException e) { } catch (SecurityManagerException | HttpClientInitException | URISyntaxException e) {
throw exceptionBuilder().withCause(e).build(); throw exceptionBuilder().withCause(e).build();
} catch (InvalidGlobPatternException e) { } catch (InvalidGlobPatternException e) {
throw exceptionBuilder() throw exceptionBuilder()

View File

@@ -41,7 +41,6 @@ import org.pkl.core.util.ErrorMessages;
import org.pkl.core.util.HttpUtils; import org.pkl.core.util.HttpUtils;
import org.pkl.core.util.IoUtils; import org.pkl.core.util.IoUtils;
import org.pkl.core.util.Nullable; import org.pkl.core.util.Nullable;
import org.pkl.core.util.Pair;
/** Utilities for creating and using {@link ModuleKey}s. */ /** Utilities for creating and using {@link ModuleKey}s. */
public final class ModuleKeys { public final class ModuleKeys {
@@ -290,14 +289,18 @@ public final class ModuleKeys {
} }
} }
private static class File extends DependencyAwareModuleKey { private static class File implements ModuleKey {
final URI uri; final URI uri;
File(URI uri) { File(URI uri) {
super(uri);
this.uri = uri; this.uri = uri;
} }
@Override
public URI getUri() {
return uri;
}
@Override @Override
public boolean hasElement(SecurityManager securityManager, URI uri) public boolean hasElement(SecurityManager securityManager, URI uri)
throws SecurityManagerException { throws SecurityManagerException {
@@ -329,17 +332,18 @@ public final class ModuleKeys {
} }
@Override @Override
protected Map<String, ? extends Dependency> getDependencies() { public boolean isGlobbable() {
var projectDepsManager = VmContext.get(null).getProjectDependenciesManager(); return true;
if (projectDepsManager == null || !projectDepsManager.hasPath(Path.of(uri))) {
throw new PackageLoadError("cannotResolveDependencyNoProject");
}
return projectDepsManager.getDependencies();
} }
@Override @Override
protected PackageLoadError cannotFindDependency(String name) { public boolean isLocal() {
return new PackageLoadError("cannotFindDependencyInProject", name); return true;
}
@Override
public boolean hasHierarchicalUris() {
return true;
} }
} }
@@ -547,40 +551,29 @@ public final class ModuleKeys {
} }
} }
/** Base implementation; knows how to resolve dependencies prefixed with <code>@</code>. */ private abstract static class AbstractPackage implements ModuleKey {
private abstract static class DependencyAwareModuleKey implements ModuleKey {
protected final URI uri; protected final PackageAssetUri packageAssetUri;
DependencyAwareModuleKey(URI uri) { AbstractPackage(PackageAssetUri packageAssetUri) {
this.uri = uri; this.packageAssetUri = packageAssetUri;
}
@Override
public URI getUri() {
return uri;
}
protected Pair<String, String> parseDependencyNotation(String importPath) {
var idx = importPath.indexOf('/');
if (idx == -1) {
// treat named dependency without a subpath as the root path.
// i.e. resolve to `@foo` to `package://example.com/foo@1.0.0#/`
return Pair.of(importPath.substring(1), "/");
}
return Pair.of(importPath.substring(1, idx), importPath.substring(idx));
} }
protected abstract Map<String, ? extends Dependency> getDependencies() protected abstract Map<String, ? extends Dependency> getDependencies()
throws IOException, SecurityManagerException; throws IOException, SecurityManagerException;
@Override @Override
public boolean isLocal() { public boolean hasHierarchicalUris() {
return true; return true;
} }
@Override @Override
public boolean hasHierarchicalUris() { public boolean hasFragmentPaths() {
return true;
}
@Override
public boolean isLocal() {
return true; return true;
} }
@@ -589,37 +582,36 @@ public final class ModuleKeys {
return true; return true;
} }
private URI resolveDependencyNotation(String notation) @Override
throws IOException, SecurityManagerException { public URI getUri() {
var parsed = parseDependencyNotation(notation); return packageAssetUri.getUri();
var name = parsed.getFirst();
var path = parsed.getSecond();
var dependency = getDependencies().get(name);
if (dependency == null) {
throw cannotFindDependency(name);
}
return dependency.getPackageUri().toPackageAssetUri(path).getUri();
} }
@Override @Override
public URI resolveUri(URI baseUri, URI importUri) throws IOException, SecurityManagerException { public URI resolveUri(URI baseUri, URI importUri) throws IOException, SecurityManagerException {
if (importUri.isAbsolute() || !importUri.getPath().startsWith("@")) { var ssp = importUri.getSchemeSpecificPart();
if (importUri.isAbsolute() || !ssp.startsWith("@")) {
return ModuleKey.super.resolveUri(baseUri, importUri); return ModuleKey.super.resolveUri(baseUri, importUri);
} }
return resolveDependencyNotation(importUri.getPath()); var parsed = IoUtils.parseDependencyNotation(ssp);
var name = parsed.getFirst();
var path = parsed.getSecond();
var dependency = getDependencies().get(name);
if (dependency == null) {
throw new PackageLoadError(
"cannotFindDependencyInPackage",
name,
packageAssetUri.getPackageUri().getDisplayName());
}
return dependency.getPackageUri().toPackageAssetUri(path).getUri();
} }
protected abstract PackageLoadError cannotFindDependency(String name);
} }
/** Represents a module imported via the {@code package} scheme. */ /** Represents a module imported via the {@code package} scheme. */
private static class Package extends DependencyAwareModuleKey { private static class Package extends AbstractPackage {
private final PackageAssetUri packageAssetUri;
Package(PackageAssetUri packageAssetUri) { Package(PackageAssetUri packageAssetUri) {
super(packageAssetUri.getUri()); super(packageAssetUri);
this.packageAssetUri = packageAssetUri;
} }
private PackageResolver getPackageResolver() { private PackageResolver getPackageResolver() {
@@ -631,6 +623,7 @@ public final class ModuleKeys {
@Override @Override
public ResolvedModuleKey resolve(SecurityManager securityManager) public ResolvedModuleKey resolve(SecurityManager securityManager)
throws IOException, SecurityManagerException { throws IOException, SecurityManagerException {
var uri = packageAssetUri.getUri();
securityManager.checkResolveModule(uri); securityManager.checkResolveModule(uri);
var bytes = var bytes =
getPackageResolver() getPackageResolver()
@@ -654,11 +647,6 @@ public final class ModuleKeys {
return getPackageResolver().hasElement(assetUri, assetUri.getPackageUri().getChecksums()); return getPackageResolver().hasElement(assetUri, assetUri.getPackageUri().getChecksums());
} }
@Override
public boolean hasFragmentPaths() {
return true;
}
@Override @Override
protected Map<String, ? extends Dependency> getDependencies() protected Map<String, ? extends Dependency> getDependencies()
throws IOException, SecurityManagerException { throws IOException, SecurityManagerException {
@@ -667,12 +655,6 @@ public final class ModuleKeys {
packageAssetUri.getPackageUri(), packageAssetUri.getPackageUri().getChecksums()) packageAssetUri.getPackageUri(), packageAssetUri.getPackageUri().getChecksums())
.getDependencies(); .getDependencies();
} }
@Override
protected PackageLoadError cannotFindDependency(String name) {
return new PackageLoadError(
"cannotFindDependencyInPackage", name, packageAssetUri.getPackageUri().getDisplayName());
}
} }
/** /**
@@ -680,15 +662,11 @@ public final class ModuleKeys {
* *
* <p>The {@code projectpackage} scheme is what project-local dependencies resolve to when * <p>The {@code projectpackage} scheme is what project-local dependencies resolve to when
* imported using dependency notation (for example, {@code import "@foo/bar.pkl"}). This scheme is * imported using dependency notation (for example, {@code import "@foo/bar.pkl"}). This scheme is
* an internal implementation detail, and we do not expect a project to declare this. * an internal implementation detail, and we do not expect a module to declare this.
*/ */
private static class ProjectPackage extends DependencyAwareModuleKey { public static class ProjectPackage extends AbstractPackage {
private final PackageAssetUri packageAssetUri;
ProjectPackage(PackageAssetUri packageAssetUri) { ProjectPackage(PackageAssetUri packageAssetUri) {
super(packageAssetUri.getUri()); super(packageAssetUri);
this.packageAssetUri = packageAssetUri;
} }
private PackageResolver getPackageResolver() { private PackageResolver getPackageResolver() {
@@ -697,37 +675,36 @@ public final class ModuleKeys {
return packageResolver; return packageResolver;
} }
private ProjectDependenciesManager getProjectDepsResolver() { private ProjectDependenciesManager getProjectDependenciesManager() {
var projectDepsManager = VmContext.get(null).getProjectDependenciesManager(); var projectDepsManager = VmContext.get(null).getProjectDependenciesManager();
assert projectDepsManager != null; assert projectDepsManager != null;
return projectDepsManager; return projectDepsManager;
} }
private @Nullable Path getLocalPath(Dependency dependency, PackageAssetUri packageAssetUri) { private @Nullable URI getLocalUri(Dependency dependency) {
return getLocalUri(dependency, packageAssetUri);
}
private @Nullable URI getLocalUri(Dependency dependency, PackageAssetUri assetUri) {
if (!(dependency instanceof LocalDependency localDependency)) { if (!(dependency instanceof LocalDependency localDependency)) {
return null; return null;
} }
return localDependency.resolveAssetPath( return localDependency.resolveAssetUri(
getProjectDepsResolver().getProjectDir(), packageAssetUri); getProjectDependenciesManager().getProjectBaseUri(), assetUri);
}
private @Nullable Path getLocalPath(Dependency dependency) {
if (!(dependency instanceof LocalDependency)) {
return null;
}
return getLocalPath(dependency, packageAssetUri);
} }
@Override @Override
public ResolvedModuleKey resolve(SecurityManager securityManager) public ResolvedModuleKey resolve(SecurityManager securityManager)
throws IOException, SecurityManagerException { throws IOException, SecurityManagerException {
securityManager.checkResolveModule(packageAssetUri.getUri()); var uri = packageAssetUri.getUri();
securityManager.checkResolveModule(uri);
var dependency = var dependency =
getProjectDepsResolver().getResolvedDependency(packageAssetUri.getPackageUri()); getProjectDependenciesManager().getResolvedDependency(packageAssetUri.getPackageUri());
var path = getLocalPath(dependency); var local = getLocalUri(dependency);
if (path != null) { if (local != null) {
securityManager.checkResolveModule(path.toUri()); var resolved =
return ResolvedModuleKeys.file(this, path.toUri(), path); VmContext.get(null).getModuleResolver().resolve(local).resolve(securityManager);
return ResolvedModuleKeys.delegated(resolved, this);
} }
var dep = (Dependency.RemoteDependency) dependency; var dep = (Dependency.RemoteDependency) dependency;
assert dep.getChecksums() != null; assert dep.getChecksums() != null;
@@ -741,11 +718,15 @@ public final class ModuleKeys {
securityManager.checkResolveModule(baseUri); securityManager.checkResolveModule(baseUri);
var packageAssetUri = PackageAssetUri.create(baseUri); var packageAssetUri = PackageAssetUri.create(baseUri);
var dependency = var dependency =
getProjectDepsResolver().getResolvedDependency(packageAssetUri.getPackageUri()); getProjectDependenciesManager().getResolvedDependency(packageAssetUri.getPackageUri());
var path = getLocalPath(dependency, packageAssetUri); var local = getLocalUri(dependency, packageAssetUri);
if (path != null) { if (local != null) {
securityManager.checkResolveModule(path.toUri()); var moduleKey = VmContext.get(null).getModuleResolver().resolve(local);
return FileResolver.listElements(path); if (!moduleKey.isGlobbable()) {
throw new PackageLoadError(
"cannotResolveInLocalDependencyNotGlobbable", local.getScheme());
}
return moduleKey.listElements(securityManager, local);
} }
var dep = (Dependency.RemoteDependency) dependency; var dep = (Dependency.RemoteDependency) dependency;
assert dep.getChecksums() != null; assert dep.getChecksums() != null;
@@ -758,42 +739,36 @@ public final class ModuleKeys {
securityManager.checkResolveModule(elementUri); securityManager.checkResolveModule(elementUri);
var packageAssetUri = PackageAssetUri.create(elementUri); var packageAssetUri = PackageAssetUri.create(elementUri);
var dependency = var dependency =
getProjectDepsResolver().getResolvedDependency(packageAssetUri.getPackageUri()); getProjectDependenciesManager().getResolvedDependency(packageAssetUri.getPackageUri());
var path = getLocalPath(dependency, packageAssetUri); var local = getLocalUri(dependency, packageAssetUri);
if (path != null) { if (local != null) {
securityManager.checkResolveModule(path.toUri()); var moduleKey = VmContext.get(null).getModuleResolver().resolve(local);
return FileResolver.hasElement(path); if (!moduleKey.isGlobbable() && !moduleKey.isLocal()) {
throw new PackageLoadError(
"cannotResolveInLocalDependencyNotGlobbableNorLocal", local.getScheme());
}
return moduleKey.hasElement(securityManager, local);
} }
var dep = (Dependency.RemoteDependency) dependency; var dep = (Dependency.RemoteDependency) dependency;
assert dep.getChecksums() != null; assert dep.getChecksums() != null;
return getPackageResolver().hasElement(packageAssetUri, dep.getChecksums()); return getPackageResolver().hasElement(packageAssetUri, dep.getChecksums());
} }
@Override
public boolean hasFragmentPaths() {
return true;
}
@Override @Override
protected Map<String, ? extends Dependency> getDependencies() protected Map<String, ? extends Dependency> getDependencies()
throws IOException, SecurityManagerException { throws IOException, SecurityManagerException {
var packageUri = packageAssetUri.getPackageUri(); var packageUri = packageAssetUri.getPackageUri();
var projectResolver = getProjectDepsResolver(); var projectResolver = getProjectDependenciesManager();
if (projectResolver.isLocalPackage(packageUri)) { if (projectResolver.isLocalPackage(packageUri)) {
return projectResolver.getLocalPackageDependencies(packageUri); return projectResolver.getLocalPackageDependencies(packageUri);
} }
var dep = var dep =
(Dependency.RemoteDependency) getProjectDepsResolver().getResolvedDependency(packageUri); (Dependency.RemoteDependency)
getProjectDependenciesManager().getResolvedDependency(packageUri);
assert dep.getChecksums() != null; assert dep.getChecksums() != null;
var dependencyMetadata = var dependencyMetadata =
getPackageResolver().getDependencyMetadata(packageUri, dep.getChecksums()); getPackageResolver().getDependencyMetadata(packageUri, dep.getChecksums());
return projectResolver.getResolvedDependenciesForPackage(packageUri, dependencyMetadata); return projectResolver.getResolvedDependenciesForPackage(packageUri, dependencyMetadata);
} }
@Override
protected PackageLoadError cannotFindDependency(String name) {
return new PackageLoadError(
"cannotFindDependencyInPackage", name, packageAssetUri.getPackageUri().getDisplayName());
}
} }
} }

View File

@@ -17,13 +17,14 @@ package org.pkl.core.module;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.GuardedBy;
import org.graalvm.collections.EconomicMap; import org.graalvm.collections.EconomicMap;
import org.pkl.core.PklBugException;
import org.pkl.core.SecurityManager;
import org.pkl.core.SecurityManagerException;
import org.pkl.core.packages.Dependency; import org.pkl.core.packages.Dependency;
import org.pkl.core.packages.DependencyMetadata; import org.pkl.core.packages.DependencyMetadata;
import org.pkl.core.packages.PackageLoadError; import org.pkl.core.packages.PackageLoadError;
@@ -31,8 +32,10 @@ import org.pkl.core.packages.PackageUri;
import org.pkl.core.project.CanonicalPackageUri; import org.pkl.core.project.CanonicalPackageUri;
import org.pkl.core.project.DeclaredDependencies; import org.pkl.core.project.DeclaredDependencies;
import org.pkl.core.project.ProjectDeps; import org.pkl.core.project.ProjectDeps;
import org.pkl.core.runtime.ModuleResolver;
import org.pkl.core.runtime.VmExceptionBuilder; import org.pkl.core.runtime.VmExceptionBuilder;
import org.pkl.core.util.EconomicMaps; import org.pkl.core.util.EconomicMaps;
import org.pkl.core.util.IoUtils;
import org.pkl.core.util.json.Json.JsonParseException; import org.pkl.core.util.json.Json.JsonParseException;
public final class ProjectDependenciesManager { public final class ProjectDependenciesManager {
@@ -41,7 +44,9 @@ public final class ProjectDependenciesManager {
public static final String PKL_PROJECT_DEPS_FILENAME = "PklProject.deps.json"; public static final String PKL_PROJECT_DEPS_FILENAME = "PklProject.deps.json";
private final DeclaredDependencies declaredDependencies; private final DeclaredDependencies declaredDependencies;
private final Path projectDir; private final URI projectBaseUri;
private final ModuleResolver moduleResolver;
private final SecurityManager securityManager;
@GuardedBy("lock") @GuardedBy("lock")
private ProjectDeps projectDeps; private ProjectDeps projectDeps;
@@ -59,13 +64,21 @@ public final class ProjectDependenciesManager {
private final Object lock = new Object(); private final Object lock = new Object();
public ProjectDependenciesManager(DeclaredDependencies declaredDependencies) { public ProjectDependenciesManager(
DeclaredDependencies declaredDependencies,
ModuleResolver moduleResolver,
SecurityManager securityManager) {
this.declaredDependencies = declaredDependencies; this.declaredDependencies = declaredDependencies;
this.projectDir = Path.of(declaredDependencies.getProjectFileUri()).getParent(); // new URI("scheme://host/a/b/c.txt").resolve(".") == new URI("scheme://host/a/b/")
this.projectBaseUri = IoUtils.resolve(declaredDependencies.getProjectFileUri(), ".");
this.moduleResolver = moduleResolver;
this.securityManager = securityManager;
} }
public boolean hasPath(Path path) { public boolean hasUri(URI uri) {
return path.startsWith(projectDir); return projectBaseUri.getScheme().equals(uri.getScheme())
&& Objects.equals(projectBaseUri.getAuthority(), uri.getAuthority())
&& uri.getPath().startsWith(projectBaseUri.getPath());
} }
private void ensureDependenciesInitialized() { private void ensureDependenciesInitialized() {
@@ -194,30 +207,39 @@ public final class ProjectDependenciesManager {
return dep; return dep;
} }
public Path getProjectDir() { public URI getProjectBaseUri() {
return projectDir; return projectBaseUri;
} }
public Path getProjectDepsFile() { public URI getProjectDepsFileUri() {
return projectDir.resolve(PKL_PROJECT_DEPS_FILENAME); return IoUtils.resolve(projectBaseUri, PKL_PROJECT_DEPS_FILENAME);
}
public URI getProjectFileUri() {
return declaredDependencies.getProjectFileUri();
} }
private ProjectDeps getProjectDeps() { private ProjectDeps getProjectDeps() {
synchronized (lock) { synchronized (lock) {
if (projectDeps == null) { if (projectDeps == null) {
var depsPath = getProjectDepsFile(); var depsUri = getProjectDepsFileUri();
if (!Files.exists(depsPath)) { var moduleKey = moduleResolver.resolve(depsUri);
throw new VmExceptionBuilder()
.evalError("missingProjectDepsJson", projectDir.toUri())
.build();
}
try { try {
projectDeps = ProjectDeps.parse(depsPath); // treat PklProject.deps.json as a module read, rather than introduce a new API.
} catch (IOException | URISyntaxException | JsonParseException e) { var depsJson = moduleKey.resolve(securityManager).loadSource();
projectDeps = ProjectDeps.parse(depsJson);
} catch (IOException e) {
throw new VmExceptionBuilder() throw new VmExceptionBuilder()
.evalError("invalidProjectDepsJson", depsPath.toUri(), e.getMessage()) .evalError("cannotLoadProjectDepsJson", depsUri)
.withCause(e) .withCause(e)
.withHint(e.getMessage() != null ? e.getMessage() : ("Encountered error: " + e))
.build(); .build();
} catch (JsonParseException e) {
throw new VmExceptionBuilder()
.evalError("invalidProjectDepsJson", depsUri, e.getMessage())
.build();
} catch (SecurityManagerException e) {
throw PklBugException.unreachableCode();
} }
} }
return projectDeps; return projectDeps;

View File

@@ -54,6 +54,14 @@ public final class ResolvedModuleKeys {
return new Virtual(original, uri, sourceText, cached); return new Virtual(original, uri, sourceText, cached);
} }
/**
* Creates a resolved module key that behaves like {@code delegate}, except with {@code original}
* as its original module key.
*/
public static ResolvedModuleKey delegated(ResolvedModuleKey delegate, ModuleKey original) {
return new Delegated(delegate, original);
}
private static class File implements ResolvedModuleKey { private static class File implements ResolvedModuleKey {
final ModuleKey original; final ModuleKey original;
final URI uri; final URI uri;
@@ -145,4 +153,30 @@ public final class ResolvedModuleKeys {
return sourceText; return sourceText;
} }
} }
private static class Delegated implements ResolvedModuleKey {
private final ResolvedModuleKey delegate;
private final ModuleKey original;
Delegated(ResolvedModuleKey delegate, ModuleKey original) {
this.delegate = delegate;
this.original = original;
}
@Override
public ModuleKey getOriginal() {
return original;
}
@Override
public URI getUri() {
return delegate.getUri();
}
@Override
public String loadSource() throws IOException {
return delegate.loadSource();
}
}
} }

View File

@@ -15,9 +15,11 @@
*/ */
package org.pkl.core.packages; package org.pkl.core.packages;
import java.net.URI;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.Objects; import java.util.Objects;
import org.pkl.core.Version; import org.pkl.core.Version;
import org.pkl.core.util.IoUtils;
import org.pkl.core.util.Nullable; import org.pkl.core.util.Nullable;
public abstract class Dependency { public abstract class Dependency {
@@ -48,10 +50,10 @@ public abstract class Dependency {
return path; return path;
} }
public Path resolveAssetPath(Path projectDir, PackageAssetUri packageAssetUri) { public URI resolveAssetUri(URI projectBaseUri, PackageAssetUri packageAssetUri) {
// drop 1 to remove leading `/` // drop 1 to remove leading `/`
var assetPath = packageAssetUri.getAssetPath().substring(1); var assetPath = packageAssetUri.getAssetPath().substring(1);
return projectDir.resolve(path).resolve(assetPath); return projectBaseUri.resolve(IoUtils.toNormalizedPathString(path.resolve(assetPath)));
} }
@Override @Override

View File

@@ -17,6 +17,7 @@ package org.pkl.core.project;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.nio.file.FileSystemNotFoundException;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@@ -27,6 +28,7 @@ import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.pkl.core.Composite; import org.pkl.core.Composite;
import org.pkl.core.Duration; import org.pkl.core.Duration;
import org.pkl.core.Evaluator;
import org.pkl.core.EvaluatorBuilder; import org.pkl.core.EvaluatorBuilder;
import org.pkl.core.ModuleSource; import org.pkl.core.ModuleSource;
import org.pkl.core.PClassInfo; import org.pkl.core.PClassInfo;
@@ -41,9 +43,11 @@ import org.pkl.core.Version;
import org.pkl.core.module.ModuleKeyFactories; import org.pkl.core.module.ModuleKeyFactories;
import org.pkl.core.packages.Checksums; import org.pkl.core.packages.Checksums;
import org.pkl.core.packages.Dependency.RemoteDependency; import org.pkl.core.packages.Dependency.RemoteDependency;
import org.pkl.core.packages.PackageLoadError;
import org.pkl.core.packages.PackageUri; import org.pkl.core.packages.PackageUri;
import org.pkl.core.packages.PackageUtils; import org.pkl.core.packages.PackageUtils;
import org.pkl.core.resource.ResourceReaders; import org.pkl.core.resource.ResourceReaders;
import org.pkl.core.util.IoUtils;
import org.pkl.core.util.Nullable; import org.pkl.core.util.Nullable;
/** Java representation of module {@code pkl.Project}. */ /** Java representation of module {@code pkl.Project}. */
@@ -52,8 +56,8 @@ public final class Project {
private final DeclaredDependencies dependencies; private final DeclaredDependencies dependencies;
private final EvaluatorSettings evaluatorSettings; private final EvaluatorSettings evaluatorSettings;
private final URI projectFileUri; private final URI projectFileUri;
private final Path projectDir; private final URI projectBaseUri;
private final List<Path> tests; private final List<URI> tests;
private final Map<String, Project> localProjectDependencies; private final Map<String, Project> localProjectDependencies;
/** /**
@@ -81,10 +85,7 @@ public final class Project {
.addEnvironmentVariables(envVars) .addEnvironmentVariables(envVars)
.setTimeout(timeout) .setTimeout(timeout)
.build()) { .build()) {
var output = evaluator.evaluateOutputValueAs(ModuleSource.path(path), PClassInfo.Project); return load(evaluator, ModuleSource.path(path));
return Project.parseProject(output);
} catch (URISyntaxException e) {
throw new PklException(e.getMessage(), e);
} }
} }
@@ -103,6 +104,31 @@ public final class Project {
return loadFromPath(path, SecurityManagers.defaultManager, null); return loadFromPath(path, SecurityManagers.defaultManager, null);
} }
/** Loads a project from the given source. */
public static Project load(ModuleSource moduleSource) {
try (var evaluator =
EvaluatorBuilder.unconfigured()
.setSecurityManager(SecurityManagers.defaultManager)
.setStackFrameTransformer(StackFrameTransformers.defaultTransformer)
.addModuleKeyFactory(ModuleKeyFactories.standardLibrary)
.addModuleKeyFactory(ModuleKeyFactories.file)
.addModuleKeyFactory(ModuleKeyFactories.classPath(Project.class.getClassLoader()))
.addResourceReader(ResourceReaders.environmentVariable())
.addResourceReader(ResourceReaders.file())
.build()) {
return load(evaluator, moduleSource);
}
}
public static Project load(Evaluator evaluator, ModuleSource moduleSource) {
try {
var output = evaluator.evaluateOutputValueAs(moduleSource, PClassInfo.Project);
return Project.parseProject(output);
} catch (URISyntaxException e) {
throw new PklException(e.getMessage(), e);
}
}
private static DeclaredDependencies parseDependencies( private static DeclaredDependencies parseDependencies(
PObject module, URI projectFileUri, @Nullable PackageUri packageUri) PObject module, URI projectFileUri, @Nullable PackageUri packageUri)
throws URISyntaxException { throws URISyntaxException {
@@ -143,7 +169,7 @@ public final class Project {
var pkgObj = getNullableProperty(module, "package"); var pkgObj = getNullableProperty(module, "package");
var projectFileUri = URI.create((String) module.getProperty("projectFileUri")); var projectFileUri = URI.create((String) module.getProperty("projectFileUri"));
var dependencies = parseDependencies(module, projectFileUri, null); var dependencies = parseDependencies(module, projectFileUri, null);
var projectDir = Path.of(projectFileUri).getParent(); var projectBaseUri = IoUtils.resolve(projectFileUri, ".");
Package pkg = null; Package pkg = null;
if (pkgObj != null) { if (pkgObj != null) {
pkg = parsePackage((PObject) pkgObj); pkg = parsePackage((PObject) pkgObj);
@@ -152,12 +178,12 @@ public final class Project {
getProperty( getProperty(
module, module,
"evaluatorSettings", "evaluatorSettings",
(settings) -> parseEvaluatorSettings(settings, projectDir)); (settings) -> parseEvaluatorSettings(settings, projectBaseUri));
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
var testPathStrs = (List<String>) getProperty(module, "tests"); var testPathStrs = (List<String>) getProperty(module, "tests");
var tests = var tests =
testPathStrs.stream() testPathStrs.stream()
.map((it) -> projectDir.resolve(it).normalize()) .map((it) -> projectBaseUri.resolve(it).normalize())
.collect(Collectors.toList()); .collect(Collectors.toList());
var localProjectDependencies = parseLocalProjectDependencies(module); var localProjectDependencies = parseLocalProjectDependencies(module);
return new Project( return new Project(
@@ -165,7 +191,7 @@ public final class Project {
dependencies, dependencies,
evaluatorSettings, evaluatorSettings,
projectFileUri, projectFileUri,
projectDir, projectBaseUri,
tests, tests,
localProjectDependencies); localProjectDependencies);
} }
@@ -185,7 +211,7 @@ public final class Project {
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private static EvaluatorSettings parseEvaluatorSettings(Object settings, Path projectDir) { private static EvaluatorSettings parseEvaluatorSettings(Object settings, URI projectBaseUri) {
var pSettings = (PObject) settings; var pSettings = (PObject) settings;
var externalProperties = getNullableProperty(pSettings, "externalProperties", Project::asMap); var externalProperties = getNullableProperty(pSettings, "externalProperties", Project::asMap);
var env = getNullableProperty(pSettings, "env", Project::asMap); var env = getNullableProperty(pSettings, "env", Project::asMap);
@@ -194,16 +220,18 @@ public final class Project {
getNullableProperty(pSettings, "allowedResources", Project::asPatternList); getNullableProperty(pSettings, "allowedResources", Project::asPatternList);
var noCache = (Boolean) getNullableProperty(pSettings, "noCache"); var noCache = (Boolean) getNullableProperty(pSettings, "noCache");
var modulePathStrs = (List<String>) getNullableProperty(pSettings, "modulePath"); var modulePathStrs = (List<String>) getNullableProperty(pSettings, "modulePath");
var timeout = (Duration) getNullableProperty(pSettings, "timeout");
List<Path> modulePath = null; List<Path> modulePath = null;
if (modulePathStrs != null) { if (modulePathStrs != null) {
modulePath = modulePath =
modulePathStrs.stream() modulePathStrs.stream()
.map((it) -> projectDir.resolve(it).normalize()) .map((it) -> resolveNullablePath(it, projectBaseUri, "modulePath"))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
var timeout = (Duration) getNullableProperty(pSettings, "timeout");
var moduleCacheDir = getNullablePath(pSettings, "moduleCacheDir", projectDir); var moduleCacheDir = getNullablePath(pSettings, "moduleCacheDir", projectBaseUri);
var rootDir = getNullablePath(pSettings, "rootDir", projectDir); var rootDir = getNullablePath(pSettings, "rootDir", projectBaseUri);
return new EvaluatorSettings( return new EvaluatorSettings(
externalProperties, externalProperties,
env, env,
@@ -261,10 +289,28 @@ public final class Project {
return new URI((String) value); return new URI((String) value);
} }
/**
* Resolve a path string against projectBaseUri.
*
* @throws PackageLoadError if projectBaseUri is not a {@code file:} URI.
*/
private static @Nullable Path resolveNullablePath(
@Nullable String path, URI projectBaseUri, String propertyName) {
if (path == null) {
return null;
}
try {
return Path.of(projectBaseUri).resolve(path).normalize();
} catch (FileSystemNotFoundException e) {
throw new PackageLoadError(
"relativePathPropertyDefinedByProjectFromNonFileUri", projectBaseUri, propertyName);
}
}
private static @Nullable Path getNullablePath( private static @Nullable Path getNullablePath(
Composite object, String propertyName, Path projectDir) { Composite object, String propertyName, URI projectBaseUri) {
return getNullableProperty( return resolveNullablePath(
object, propertyName, (obj) -> projectDir.resolve((String) obj).normalize()); (String) getNullableProperty(object, propertyName), projectBaseUri, propertyName);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@@ -309,14 +355,14 @@ public final class Project {
DeclaredDependencies dependencies, DeclaredDependencies dependencies,
EvaluatorSettings evaluatorSettings, EvaluatorSettings evaluatorSettings,
URI projectFileUri, URI projectFileUri,
Path projectDir, URI projectBaseUri,
List<Path> tests, List<URI> tests,
Map<String, Project> localProjectDependencies) { Map<String, Project> localProjectDependencies) {
this.pkg = pkg; this.pkg = pkg;
this.dependencies = dependencies; this.dependencies = dependencies;
this.evaluatorSettings = evaluatorSettings; this.evaluatorSettings = evaluatorSettings;
this.projectFileUri = projectFileUri; this.projectFileUri = projectFileUri;
this.projectDir = projectDir; this.projectBaseUri = projectBaseUri;
this.tests = tests; this.tests = tests;
this.localProjectDependencies = localProjectDependencies; this.localProjectDependencies = localProjectDependencies;
} }
@@ -334,7 +380,16 @@ public final class Project {
} }
public List<Path> getTests() { public List<Path> getTests() {
return tests; return tests.stream()
.map(
(it) -> {
try {
return Path.of(it);
} catch (FileSystemNotFoundException e) {
throw new PackageLoadError("invalidUsageOfProjectFromNonFileUri");
}
})
.collect(Collectors.toList());
} }
@Override @Override
@@ -366,11 +421,17 @@ public final class Project {
return localProjectDependencies; return localProjectDependencies;
} }
public URI getProjectBaseUri() {
return projectBaseUri;
}
public Path getProjectDir() { public Path getProjectDir() {
return projectDir; assert projectBaseUri.getScheme().equalsIgnoreCase("file");
return Path.of(projectBaseUri);
} }
public static class EvaluatorSettings { public static class EvaluatorSettings {
private final @Nullable Map<String, String> externalProperties; private final @Nullable Map<String, String> externalProperties;
private final @Nullable Map<String, String> env; private final @Nullable Map<String, String> env;
private final @Nullable List<Pattern> allowedModules; private final @Nullable List<Pattern> allowedModules;

View File

@@ -45,6 +45,7 @@ import org.pkl.core.SecurityManagerException;
import org.pkl.core.StackFrameTransformer; import org.pkl.core.StackFrameTransformer;
import org.pkl.core.ast.builder.ImportsAndReadsParser; import org.pkl.core.ast.builder.ImportsAndReadsParser;
import org.pkl.core.http.HttpClient; import org.pkl.core.http.HttpClient;
import org.pkl.core.module.ModuleKeyFactories;
import org.pkl.core.module.ModuleKeys; import org.pkl.core.module.ModuleKeys;
import org.pkl.core.module.ProjectDependenciesManager; import org.pkl.core.module.ProjectDependenciesManager;
import org.pkl.core.module.ResolvedModuleKeys; import org.pkl.core.module.ResolvedModuleKeys;
@@ -55,6 +56,7 @@ import org.pkl.core.packages.DependencyMetadata;
import org.pkl.core.packages.PackageLoadError; import org.pkl.core.packages.PackageLoadError;
import org.pkl.core.packages.PackageResolver; import org.pkl.core.packages.PackageResolver;
import org.pkl.core.packages.PackageUri; import org.pkl.core.packages.PackageUri;
import org.pkl.core.runtime.ModuleResolver;
import org.pkl.core.runtime.VmExceptionBuilder; import org.pkl.core.runtime.VmExceptionBuilder;
import org.pkl.core.util.ByteArrayUtils; import org.pkl.core.util.ByteArrayUtils;
import org.pkl.core.util.ErrorMessages; import org.pkl.core.util.ErrorMessages;
@@ -98,6 +100,7 @@ public final class ProjectPackager {
private final Path workingDir; private final Path workingDir;
private final String outputPathPattern; private final String outputPathPattern;
private final StackFrameTransformer stackFrameTransformer; private final StackFrameTransformer stackFrameTransformer;
private final SecurityManager securityManager;
private final PackageResolver packageResolver; private final PackageResolver packageResolver;
private final boolean skipPublishCheck; private final boolean skipPublishCheck;
private final Writer outputWriter; private final Writer outputWriter;
@@ -115,6 +118,7 @@ public final class ProjectPackager {
this.workingDir = workingDir; this.workingDir = workingDir;
this.outputPathPattern = outputPathPattern; this.outputPathPattern = outputPathPattern;
this.stackFrameTransformer = stackFrameTransformer; this.stackFrameTransformer = stackFrameTransformer;
this.securityManager = securityManager;
// intentionally use InMemoryPackageResolver // intentionally use InMemoryPackageResolver
this.packageResolver = PackageResolver.getInstance(securityManager, httpClient, null); this.packageResolver = PackageResolver.getInstance(securityManager, httpClient, null);
this.skipPublishCheck = skipPublishCheck; this.skipPublishCheck = skipPublishCheck;
@@ -226,7 +230,12 @@ public final class ProjectPackager {
new HashMap<String, RemoteDependency>( new HashMap<String, RemoteDependency>(
project.getDependencies().getLocalDependencies().size() project.getDependencies().getLocalDependencies().size()
+ project.getDependencies().getRemoteDependencies().size()); + project.getDependencies().getRemoteDependencies().size());
var projectDependenciesManager = new ProjectDependenciesManager(project.getDependencies()); // module resolver is only used for reading PklProject.deps.json, so provide one that reads
// files.
var moduleResolver = new ModuleResolver(List.of(ModuleKeyFactories.file));
var projectDependenciesManager =
new ProjectDependenciesManager(
project.getDependencies(), moduleResolver, this.securityManager);
for (var entry : project.getDependencies().getRemoteDependencies().entrySet()) { for (var entry : project.getDependencies().getRemoteDependencies().entrySet()) {
var resolved = var resolved =
(RemoteDependency) (RemoteDependency)

View File

@@ -89,7 +89,9 @@ public class ReplServer implements AutoCloseable {
var languageRef = new MutableReference<VmLanguage>(null); var languageRef = new MutableReference<VmLanguage>(null);
packageResolver = PackageResolver.getInstance(securityManager, httpClient, moduleCacheDir); packageResolver = PackageResolver.getInstance(securityManager, httpClient, moduleCacheDir);
projectDependenciesManager = projectDependenciesManager =
projectDependencies == null ? null : new ProjectDependenciesManager(projectDependencies); projectDependencies == null
? null
: new ProjectDependenciesManager(projectDependencies, moduleResolver, securityManager);
polyglotContext = polyglotContext =
VmUtils.createContext( VmUtils.createContext(
() -> { () -> {

View File

@@ -22,7 +22,6 @@ import java.net.URISyntaxException;
import java.net.http.HttpRequest; import java.net.http.HttpRequest;
import java.net.http.HttpResponse.BodyHandlers; import java.net.http.HttpResponse.BodyHandlers;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@@ -39,6 +38,7 @@ import org.pkl.core.packages.Dependency.LocalDependency;
import org.pkl.core.packages.PackageAssetUri; import org.pkl.core.packages.PackageAssetUri;
import org.pkl.core.packages.PackageResolver; import org.pkl.core.packages.PackageResolver;
import org.pkl.core.runtime.VmContext; import org.pkl.core.runtime.VmContext;
import org.pkl.core.runtime.VmExceptionBuilder;
import org.pkl.core.util.ErrorMessages; import org.pkl.core.util.ErrorMessages;
import org.pkl.core.util.HttpUtils; import org.pkl.core.util.HttpUtils;
import org.pkl.core.util.IoUtils; import org.pkl.core.util.IoUtils;
@@ -486,10 +486,9 @@ public final class ResourceReaders {
throws IOException, URISyntaxException, SecurityManagerException { throws IOException, URISyntaxException, SecurityManagerException {
var assetUri = new PackageAssetUri(uri); var assetUri = new PackageAssetUri(uri);
var dependency = getProjectDepsResolver().getResolvedDependency(assetUri.getPackageUri()); var dependency = getProjectDepsResolver().getResolvedDependency(assetUri.getPackageUri());
var path = getLocalPath(dependency, assetUri); var local = getLocalUri(dependency, assetUri);
if (path != null) { if (local != null) {
var bytes = Files.readAllBytes(path); return VmContext.get(null).getResourceManager().read(local, null);
return Optional.of(new Resource(uri, bytes));
} }
var remoteDep = (Dependency.RemoteDependency) dependency; var remoteDep = (Dependency.RemoteDependency) dependency;
var bytes = getPackageResolver().getBytes(assetUri, true, remoteDep.getChecksums()); var bytes = getPackageResolver().getBytes(assetUri, true, remoteDep.getChecksums());
@@ -518,9 +517,15 @@ public final class ResourceReaders {
var packageAssetUri = PackageAssetUri.create(baseUri); var packageAssetUri = PackageAssetUri.create(baseUri);
var dependency = var dependency =
getProjectDepsResolver().getResolvedDependency(packageAssetUri.getPackageUri()); getProjectDepsResolver().getResolvedDependency(packageAssetUri.getPackageUri());
var path = getLocalPath(dependency, packageAssetUri); var local = getLocalUri(dependency, packageAssetUri);
if (path != null) { if (local != null) {
return FileResolver.listElements(path); var reader = VmContext.get(null).getResourceManager().getResourceReader(local);
if (reader == null) {
throw new VmExceptionBuilder()
.evalError("noResourceReaderRegistered", local.getScheme())
.build();
}
return reader.listElements(securityManager, local);
} }
var remoteDep = (Dependency.RemoteDependency) dependency; var remoteDep = (Dependency.RemoteDependency) dependency;
return getPackageResolver() return getPackageResolver()
@@ -534,9 +539,15 @@ public final class ResourceReaders {
var packageAssetUri = PackageAssetUri.create(elementUri); var packageAssetUri = PackageAssetUri.create(elementUri);
var dependency = var dependency =
getProjectDepsResolver().getResolvedDependency(packageAssetUri.getPackageUri()); getProjectDepsResolver().getResolvedDependency(packageAssetUri.getPackageUri());
var path = getLocalPath(dependency, packageAssetUri); var local = getLocalUri(dependency, packageAssetUri);
if (path != null) { if (local != null) {
return FileResolver.hasElement(path); var reader = VmContext.get(null).getResourceManager().getResourceReader(local);
if (reader == null) {
throw new VmExceptionBuilder()
.evalError("noResourceReaderRegistered", local.getScheme())
.build();
}
return reader.hasElement(securityManager, local);
} }
var remoteDep = (Dependency.RemoteDependency) dependency; var remoteDep = (Dependency.RemoteDependency) dependency;
return getPackageResolver() return getPackageResolver()
@@ -555,12 +566,12 @@ public final class ResourceReaders {
return projectDepsManager; return projectDepsManager;
} }
private @Nullable Path getLocalPath(Dependency dependency, PackageAssetUri packageAssetUri) { private @Nullable URI getLocalUri(Dependency dependency, PackageAssetUri packageAssetUri) {
if (!(dependency instanceof LocalDependency localDependency)) { if (!(dependency instanceof LocalDependency localDependency)) {
return null; return null;
} }
return localDependency.resolveAssetPath( return localDependency.resolveAssetUri(
getProjectDepsResolver().getProjectDir(), packageAssetUri); getProjectDepsResolver().getProjectBaseUri(), packageAssetUri);
} }
} }

View File

@@ -31,6 +31,7 @@ import org.pkl.core.packages.PackageLoadError;
import org.pkl.core.resource.Resource; import org.pkl.core.resource.Resource;
import org.pkl.core.resource.ResourceReader; import org.pkl.core.resource.ResourceReader;
import org.pkl.core.stdlib.VmObjectFactory; import org.pkl.core.stdlib.VmObjectFactory;
import org.pkl.core.util.Nullable;
public final class ResourceManager { public final class ResourceManager {
private final Map<String, ResourceReader> resourceReaders = new HashMap<>(); private final Map<String, ResourceReader> resourceReaders = new HashMap<>();
@@ -67,17 +68,23 @@ public final class ResourceManager {
} }
@TruffleBoundary @TruffleBoundary
public Optional<Object> read(URI resourceUri, Node readNode) { public Optional<Object> read(URI resourceUri, @Nullable Node readNode) {
return resources.computeIfAbsent( return resources.computeIfAbsent(
resourceUri.normalize(), resourceUri.normalize(),
uri -> { uri -> {
try { try {
securityManager.checkReadResource(uri); securityManager.checkReadResource(uri);
} catch (SecurityManagerException e) { } catch (SecurityManagerException e) {
throw new VmExceptionBuilder().withCause(e).withLocation(readNode).build(); throw new VmExceptionBuilder().withCause(e).withOptionalLocation(readNode).build();
} }
var reader = getReader(resourceUri, readNode); var reader = resourceReaders.get(uri.getScheme());
if (reader == null) {
throw new VmExceptionBuilder()
.withOptionalLocation(readNode)
.evalError("noResourceReaderRegistered", resourceUri.getScheme())
.build();
}
Optional<Object> resource; Optional<Object> resource;
try { try {
@@ -86,16 +93,16 @@ public final class ResourceManager {
throw new VmExceptionBuilder() throw new VmExceptionBuilder()
.evalError("ioErrorReadingResource", uri) .evalError("ioErrorReadingResource", uri)
.withCause(e) .withCause(e)
.withLocation(readNode) .withOptionalLocation(readNode)
.build(); .build();
} catch (URISyntaxException e) { } catch (URISyntaxException e) {
throw new VmExceptionBuilder() throw new VmExceptionBuilder()
.evalError("invalidResourceUri", resourceUri) .evalError("invalidResourceUri", resourceUri)
.withHint(e.getReason()) .withHint(e.getReason())
.withLocation(readNode) .withOptionalLocation(readNode)
.build(); .build();
} catch (SecurityManagerException | PackageLoadError | HttpClientInitException e) { } catch (SecurityManagerException | PackageLoadError | HttpClientInitException e) {
throw new VmExceptionBuilder().withCause(e).withLocation(readNode).build(); throw new VmExceptionBuilder().withCause(e).withOptionalLocation(readNode).build();
} }
if (resource.isEmpty()) return resource; if (resource.isEmpty()) return resource;
@@ -108,8 +115,16 @@ public final class ResourceManager {
throw new VmExceptionBuilder() throw new VmExceptionBuilder()
.evalError("unsupportedResourceType", reader.getClass().getName(), res.getClass()) .evalError("unsupportedResourceType", reader.getClass().getName(), res.getClass())
.withLocation(readNode) .withOptionalLocation(readNode)
.build(); .build();
}); });
} }
/**
* Returns a {@link ResourceReader} registered to read the resource at {@code baseUri}, or {@code
* null} if there is none.
*/
public @Nullable ResourceReader getResourceReader(URI baseUri) {
return resourceReaders.get(baseUri.getScheme());
}
} }

View File

@@ -324,7 +324,7 @@ public final class VmExceptionBuilder {
return this; return this;
} }
public VmExceptionBuilder withHint(String hint) { public VmExceptionBuilder withHint(@Nullable String hint) {
this.hint = hint; this.hint = hint;
return this; return this;
} }

View File

@@ -28,6 +28,7 @@ import java.util.Map;
import java.util.WeakHashMap; import java.util.WeakHashMap;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.pkl.core.PklBugException;
import org.pkl.core.SecurityManager; import org.pkl.core.SecurityManager;
import org.pkl.core.SecurityManagerException; import org.pkl.core.SecurityManagerException;
import org.pkl.core.module.ModuleKey; import org.pkl.core.module.ModuleKey;
@@ -490,7 +491,14 @@ public final class GlobResolver {
} }
return result; return result;
} }
var baseUri = enclosingModuleKey.resolveUri(enclosingUri, URI.create(basePath)); URI baseUri;
try {
baseUri = IoUtils.resolve(securityManager, enclosingModuleKey, URI.create(basePath));
} catch (URISyntaxException e) {
// assertion: this is only thrown if the pattern starts with a triple-dot import.
// the language will throw an error if glob imports is combined with triple-dots.
throw new PklBugException(e);
}
resolveHierarchicalGlob( resolveHierarchicalGlob(
securityManager, securityManager,

View File

@@ -39,7 +39,9 @@ import org.pkl.core.Platform;
import org.pkl.core.SecurityManager; import org.pkl.core.SecurityManager;
import org.pkl.core.SecurityManagerException; import org.pkl.core.SecurityManagerException;
import org.pkl.core.module.ModuleKey; import org.pkl.core.module.ModuleKey;
import org.pkl.core.packages.PackageLoadError;
import org.pkl.core.runtime.ReaderBase; import org.pkl.core.runtime.ReaderBase;
import org.pkl.core.runtime.VmContext;
import org.pkl.core.runtime.VmExceptionBuilder; import org.pkl.core.runtime.VmExceptionBuilder;
public final class IoUtils { public final class IoUtils {
@@ -299,29 +301,15 @@ public final class IoUtils {
return resolve(baseUri, importUri); return resolve(baseUri, importUri);
} }
/** private static URI resolveTripleDotImport(
* Resolves {@code importUri} against the module key. SecurityManager securityManager, ModuleKey moduleKey, String tripleDotPath)
* throws IOException, SecurityManagerException {
* <p>When {@code importUri} contains a triple-dot, it is resolved if the module key returns true
* for both {@link ModuleKey#isLocal()} and {@link ModuleKey#hasHierarchicalUris()}. Otherwise, an
* error is thrown.
*/
public static URI resolve(SecurityManager securityManager, ModuleKey moduleKey, URI importUri)
throws URISyntaxException, IOException, SecurityManagerException {
if (importUri.isAbsolute()) {
return moduleKey.resolveUri(importUri);
}
var moduleKeyUri = moduleKey.getUri(); var moduleKeyUri = moduleKey.getUri();
var tripleDotPath = parseTripleDotPath(importUri);
if (tripleDotPath == null) {
return moduleKey.resolveUri(importUri);
}
if (!moduleKey.isLocal() || !moduleKey.hasHierarchicalUris()) { if (!moduleKey.isLocal() || !moduleKey.hasHierarchicalUris()) {
throw new VmExceptionBuilder() throw new VmExceptionBuilder()
.evalError("cannotResolveTripleDotImports", moduleKeyUri) .evalError("cannotResolveTripleDotImports", moduleKeyUri)
.build(); .build();
} }
var currentPath = var currentPath =
moduleKey.hasFragmentPaths() ? moduleKeyUri.getFragment() : moduleKeyUri.getPath(); moduleKey.hasFragmentPaths() ? moduleKeyUri.getFragment() : moduleKeyUri.getPath();
var effectiveImportPath = var effectiveImportPath =
@@ -351,6 +339,69 @@ public final class IoUtils {
throw new FileNotFoundException(); throw new FileNotFoundException();
} }
public static Pair<String, String> parseDependencyNotation(String importPath) {
var idx = importPath.indexOf('/');
if (idx == -1) {
// treat named dependency without a subpath as the root path.
// i.e. resolve to `@foo` to `package://example.com/foo@1.0.0#/`
return Pair.of(importPath.substring(1), "/");
}
return Pair.of(importPath.substring(1, idx), importPath.substring(idx));
}
private static URI resolveProjectDependency(ModuleKey moduleKey, String notation) {
var parsed = parseDependencyNotation(notation);
var name = parsed.getFirst();
var path = parsed.getSecond();
var projectDependenciesManager = VmContext.get(null).getProjectDependenciesManager();
if (!moduleKey.hasHierarchicalUris() && projectDependenciesManager != null) {
throw new PackageLoadError(
"cannotResolveDependencyWithoutHierarchicalUris",
projectDependenciesManager.getProjectFileUri());
}
if (projectDependenciesManager == null
|| !projectDependenciesManager.hasUri(moduleKey.getUri())) {
throw new PackageLoadError("cannotResolveDependencyNoProject");
}
var dependency = projectDependenciesManager.getDependencies().get(name);
if (dependency != null) {
return dependency.getPackageUri().toPackageAssetUri(path).getUri();
}
throw new PackageLoadError("cannotFindDependencyInProject", name);
}
/**
* Resolves {@code importUri} against the module key.
*
* <p>When {@code importUri} contains a triple-dot, it is resolved if the module key returns true
* for both {@link ModuleKey#isLocal()} and {@link ModuleKey#hasHierarchicalUris()}. Otherwise, an
* error is thrown.
*
* <p>When {@code importUri} starts with a {@code @}, it is resolved if the module key supports
* dependency notation ()
*/
public static URI resolve(SecurityManager securityManager, ModuleKey moduleKey, URI importUri)
throws URISyntaxException, IOException, SecurityManagerException {
if (importUri.isAbsolute()) {
return moduleKey.resolveUri(importUri);
}
var tripleDotPath = parseTripleDotPath(importUri);
if (tripleDotPath != null) {
return resolveTripleDotImport(securityManager, moduleKey, tripleDotPath);
}
var moduleScheme = moduleKey.getUri().getScheme();
var isPackage =
moduleScheme.equalsIgnoreCase("package") || moduleScheme.equalsIgnoreCase("projectpackage");
var relativePart = importUri.getSchemeSpecificPart();
// Special-case handling of project dependencies.
// We'll allow the Package and ProjectPackage module keys to resolve dependency notation on
// their own.
if (relativePart.startsWith("@") && !isPackage) {
return resolveProjectDependency(moduleKey, relativePart);
}
return moduleKey.resolveUri(importUri);
}
public static URI resolve(URI baseUri, URI newUri) { public static URI resolve(URI baseUri, URI newUri) {
if (newUri.isAbsolute()) return newUri; if (newUri.isAbsolute()) return newUri;

View File

@@ -542,6 +542,12 @@ Cannot combine glob imports with triple-dot module URIs.
cannotGlobUri=\ cannotGlobUri=\
Cannot expand glob pattern `{0}` because scheme `{1}` is not globbable. Cannot expand glob pattern `{0}` because scheme `{1}` is not globbable.
cannotResolveInLocalDependencyNotGlobbable=\
Cannot resolve import in local dependency because scheme `{0}` is not globbable.
cannotResolveInLocalDependencyNotGlobbableNorLocal=\
Cannot resolve import in local dependency because scheme `{0}` is not globbable and is not local.
expectedAnnotationClass=\ expectedAnnotationClass=\
Expected an annotation class. Expected an annotation class.
@@ -680,6 +686,11 @@ Cannot resolve a triple-dot import from module URI `{0}`.\n\
\n\ \n\
Triple-dot imports may only be resolved by module schemes that are considered local, and have hierarchical URIs. Triple-dot imports may only be resolved by module schemes that are considered local, and have hierarchical URIs.
moduleDoesNotSupportDependencies=\
Module `{0}` does not support importing dependencies.\n\
\n\
Dependencies can only be imported in modules that belong to a project, or within a package.
cannotHaveRelativeImport=\ cannotHaveRelativeImport=\
Module `{0}` cannot have a relative import URI. Module `{0}` cannot have a relative import URI.
@@ -819,6 +830,9 @@ Only type unions can have a default marker (*).
invalidModuleOutputValue=\ invalidModuleOutputValue=\
Expected `output.value` of module `{2}` to be of type `{0}`, but got type `{1}`. Expected `output.value` of module `{2}` to be of type `{0}`, but got type `{1}`.
cannotResolveDependencyWithoutHierarchicalUris=\
Cannot import dependency because project URI `{0}` does not have a hierarchical path.
cannotResolveDependencyNoProject=\ cannotResolveDependencyNoProject=\
Cannot import dependency because there is no project found.\n\ Cannot import dependency because there is no project found.\n\
\n\ \n\
@@ -830,6 +844,11 @@ Cannot find a dependency named `{0}`, because it is not declared in the current
\n\ \n\
To fix this, add it to the `dependencies` section of your `PklProject` file, and resolve your dependencies. To fix this, add it to the `dependencies` section of your `PklProject` file, and resolve your dependencies.
cannotResolveDependencyFromReaderWithOpaqueUris=\
Cannot resolve dependencies from module reader with opaque URIs.\n\
\n\
Module reader for scheme `{0}` does not support hierarchical URIs.
cannotFindDependencyInPackage=\ cannotFindDependencyInPackage=\
Cannot find dependency named `{0}`, because it was not declared in package `{1}`. Cannot find dependency named `{0}`, because it was not declared in package `{1}`.
@@ -856,10 +875,10 @@ For example, `example.com` in URI `project://example.com/my/package@1.0.0`.
unexpectedChecksumInPackageUri=\ unexpectedChecksumInPackageUri=\
Did not expect to find a checksum component in this package URI. Did not expect to find a checksum component in this package URI.
missingProjectDepsJson=\ cannotLoadProjectDepsJson=\
Cannot resolve dependency because file `PklProject.deps.json` is missing in project directory `{0}`.\n\ Encountered an error when attempting to load `PklProject.deps.json` at `{0}`.\n\
\n\ \n\
Run `pkl project resolve` to create a new set of dependencies. Try running `pkl project resolve` within the project directory to create a new set of dependencies.
invalidProjectDepsJson=\ invalidProjectDepsJson=\
Cannot resolve dependency because file `{0}` is malformed.\n\ Cannot resolve dependency because file `{0}` is malformed.\n\
@@ -978,6 +997,16 @@ No package was declared in project `{0}`.\n\
\n\ \n\
Add a `package` section to the PklProject file. Add a `package` section to the PklProject file.
relativePathPropertyDefinedByProjectFromNonFileUri=\
Invalid property specified in project `{0}`\n\
\n\
Property `{1}` is only permitted in PklProject files loaded from `file:` URIs.
invalidUsageOfProjectFromNonFileUri=\
Invalid usage of project `{0}`\n\
\n\
This action can only be performed with PklProject files loaded from `file:` URIs.
packageTestsFailed=\ packageTestsFailed=\
Failed to create package `{0}`, because its API tests are failing. Failed to create package `{0}`, because its API tests are failing.

View File

@@ -11,4 +11,5 @@ dependencies {
uri = "package://localhost:0/badImportsWithinPackage@1.0.0" uri = "package://localhost:0/badImportsWithinPackage@1.0.0"
} }
["project2"] = import("../project2/PklProject") ["project2"] = import("../project2/PklProject")
["project6"] = import("../project6/PklProject")
} }

View File

@@ -20,6 +20,11 @@
"uri": "projectpackage://localhost:0/project2@1.0.0", "uri": "projectpackage://localhost:0/project2@1.0.0",
"path": "../project2/" "path": "../project2/"
}, },
"package://localhost:12110/project6@1": {
"type": "local",
"uri": "projectpackage://localhost:12110/project6@1.0.0",
"path": "../project6/"
},
"package://localhost:0/badImportsWithinPackage@1": { "package://localhost:0/badImportsWithinPackage@1": {
"type": "remote", "type": "remote",
"uri": "projectpackage://localhost:0/badImportsWithinPackage@1.0.0", "uri": "projectpackage://localhost:0/badImportsWithinPackage@1.0.0",

View File

@@ -34,4 +34,8 @@ examples {
["glob-read absolute package uri"] { ["glob-read absolute package uri"] {
read*("package://localhost:0/birds@0.5.0#/catalog/*.pkl") read*("package://localhost:0/birds@0.5.0#/catalog/*.pkl")
} }
["glob-import behind local project import"] {
import("@project6/children.pkl")
}
} }

View File

@@ -20,6 +20,11 @@
"uri": "projectpackage://localhost:0/project2@1.0.0", "uri": "projectpackage://localhost:0/project2@1.0.0",
"path": "../project2/" "path": "../project2/"
}, },
"package://localhost:12110/project6@1": {
"type": "local",
"uri": "projectpackage://localhost:12110/project6@1.0.0",
"path": "../project6/"
},
"package://localhost:0/badImportsWithinPackage@1": { "package://localhost:0/badImportsWithinPackage@1": {
"type": "remote", "type": "remote",
"uri": "projectpackage://localhost:0/badImportsWithinPackage@1.0.0", "uri": "projectpackage://localhost:0/badImportsWithinPackage@1.0.0",

View File

@@ -0,0 +1,8 @@
amends "pkl:Project"
package {
name = "project6"
baseUri = "package://localhost:12110/project6"
version = "1.0.0"
packageZipUrl = "https://localhost:12110/project6/project6-\(version).zip"
}

View File

@@ -0,0 +1,4 @@
{
"schemaVersion": 1,
"resolvedDependencies": {}
}

View File

@@ -0,0 +1 @@
children = import*("children/*.pkl")

View File

@@ -0,0 +1 @@
name = "a"

View File

@@ -0,0 +1 @@
name = "b"

View File

@@ -0,0 +1 @@
name = "c"

View File

@@ -1,13 +1,13 @@
Pkl Error Pkl Error
Expected value of type `*RemoteDependency|LocalDependency`, but got a different `pkl.Project`. Expected value of type `*RemoteDependency|Project(isValidLoadDependency)`, but got a different `pkl.Project`.
Value: new ModuleClass { package = null; tests {}; dependencies {}; evaluatorSetting... Value: new ModuleClass { package = null; tests {}; dependencies {}; evaluatorSetting...
xxx | dependencies: Mapping<String(!contains("/")), *RemoteDependency|LocalDependency> xxx | dependencies: Mapping<String(!contains("/")), *RemoteDependency|Project(isValidLoadDependency)>
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at pkl.Project#dependencies (pkl:Project) at pkl.Project#dependencies (pkl:Project)
* Value is not of type `LocalDependency` because: * Value is not of type `Project(isValidLoadDependency)` because:
Type constraint `this.package != null` violated. Type constraint `isValidLoadDependency` violated.
Value: new ModuleClass { package = null; tests {}; dependencies {}; evaluatorSetti... Value: new ModuleClass { package = null; tests {}; dependencies {}; evaluatorSetti...
x | dependencies { x | dependencies {

View File

@@ -1,8 +1,9 @@
Pkl Error Pkl Error
Cannot resolve dependency because file `PklProject.deps.json` is missing in project directory `file:///$snippetsDir/input/projects/missingProjectDeps/`. Encountered an error when attempting to load `PklProject.deps.json` at `file:///$snippetsDir/input/projects/missingProjectDeps/PklProject.deps.json`.
NoSuchFileException: /$snippetsDir/input/projects/missingProjectDeps/PklProject.deps.json
x | import "@birds/Bird.pkl" x | import "@birds/Bird.pkl"
^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^
at bug (file:///$snippetsDir/input/projects/missingProjectDeps/bug.pkl) at bug (file:///$snippetsDir/input/projects/missingProjectDeps/bug.pkl)
Run `pkl project resolve` to create a new set of dependencies. Try running `pkl project resolve` within the project directory to create a new set of dependencies.

View File

@@ -262,4 +262,19 @@ examples {
} }
} }
} }
["glob-import behind local project import"] {
new {
children {
["children/a.pkl"] {
name = "a"
}
["children/b.pkl"] {
name = "b"
}
["children/c.pkl"] {
name = "c"
}
}
}
}
} }

View File

@@ -0,0 +1,11 @@
children {
["children/a.pkl"] {
name = "a"
}
["children/b.pkl"] {
name = "b"
}
["children/c.pkl"] {
name = "c"
}
}

View File

@@ -0,0 +1 @@
name = "a"

View File

@@ -0,0 +1 @@
name = "b"

View File

@@ -0,0 +1 @@
name = "c"

View File

@@ -6,6 +6,7 @@ import java.net.URI
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatCode
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.io.TempDir import org.junit.jupiter.api.io.TempDir
@@ -15,7 +16,16 @@ import org.pkl.commons.writeString
import org.pkl.core.ModuleSource.* import org.pkl.core.ModuleSource.*
import org.pkl.core.util.IoUtils import org.pkl.core.util.IoUtils
import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.AfterAll
import org.pkl.commons.test.PackageServer
import org.pkl.core.module.ModuleKey
import org.pkl.core.module.ModuleKeyFactories
import org.pkl.core.module.ModuleKeyFactory
import org.pkl.core.module.ResolvedModuleKey
import org.pkl.core.project.Project
import java.nio.charset.StandardCharsets
import java.nio.file.FileSystems import java.nio.file.FileSystems
import java.util.*
import java.util.regex.Pattern
import kotlin.io.path.writeText import kotlin.io.path.writeText
class EvaluatorTest { class EvaluatorTest {
@@ -24,6 +34,28 @@ class EvaluatorTest {
private const val sourceText = "name = \"pigeon\"; age = 10 + 20" private const val sourceText = "name = \"pigeon\"; age = 10 + 20"
private object CustomModuleKeyFactory : ModuleKeyFactory {
override fun create(uri: URI): Optional<ModuleKey> {
return if (uri.scheme == "custom") Optional.of(CustomModuleKey(uri))
else Optional.empty<ModuleKey>()
}
}
private class CustomModuleKey(private val uri: URI) : ModuleKey, ResolvedModuleKey {
override fun hasHierarchicalUris(): Boolean = true
override fun isGlobbable(): Boolean = false
override fun getOriginal(): ModuleKey = this
override fun getUri(): URI = uri
override fun loadSource(): String = javaClass.classLoader.getResourceAsStream(uri.path.drop(1))!!.use { it.readAllBytes().toString(
StandardCharsets.UTF_8) }
override fun resolve(securityManager: SecurityManager): ResolvedModuleKey = this
}
@AfterAll @AfterAll
@JvmStatic @JvmStatic
fun afterAll() { fun afterAll() {
@@ -291,6 +323,132 @@ class EvaluatorTest {
assertThat(output["bar/../bark.yml"]?.text).isEqualTo("bark: bark bark") assertThat(output["bar/../bark.yml"]?.text).isEqualTo("bark: bark bark")
} }
@Test
fun `project set from modulepath`(@TempDir cacheDir: Path) {
PackageServer.populateCacheDir(cacheDir)
val evaluatorBuilder = EvaluatorBuilder.preconfigured().setModuleCacheDir(cacheDir)
val project = Project.load(modulePath("/org/pkl/core/project/project5/PklProject"))
val result = evaluatorBuilder.setProjectDependencies(project.dependencies).build().use { evaluator ->
evaluator.evaluateOutputText(modulePath("/org/pkl/core/project/project5/main.pkl"))
}
assertThat(result).isEqualTo("""
prop1 {
name = "Apple"
}
prop2 {
res = 1
}
""".trimIndent())
}
@Test
fun `project set from custom ModuleKeyFactory`(@TempDir cacheDir: Path) {
PackageServer.populateCacheDir(cacheDir)
val evaluatorBuilder = with(EvaluatorBuilder.preconfigured()) {
setAllowedModules(SecurityManagers.defaultAllowedModules + Pattern.compile("custom:"))
setAllowedResources(SecurityManagers.defaultAllowedResources + Pattern.compile("custom:"))
setModuleCacheDir(cacheDir)
setModuleKeyFactories(
listOf(
CustomModuleKeyFactory,
ModuleKeyFactories.standardLibrary,
ModuleKeyFactories.pkg,
ModuleKeyFactories.projectpackage,
ModuleKeyFactories.file
)
)
}
val project = evaluatorBuilder.build().use { Project.load(it, uri("custom:/org/pkl/core/project/project5/PklProject")) }
val evaluator = evaluatorBuilder.setProjectDependencies(project.dependencies).build()
val output = evaluator.use { it.evaluateOutputText(uri("custom:/org/pkl/core/project/project5/main.pkl")) }
assertThat(output)
.isEqualTo(
"""
prop1 {
name = "Apple"
}
prop2 {
res = 1
}
"""
.trimIndent()
)
}
@Test
fun `project base path set to non-hierarchical scheme`() {
class FooBarModuleKey(val moduleUri: URI) : ModuleKey, ResolvedModuleKey {
override fun hasHierarchicalUris(): Boolean = false
override fun isGlobbable(): Boolean = false
override fun getOriginal(): ModuleKey = this
override fun getUri(): URI = moduleUri
override fun loadSource(): String =
if (uri.schemeSpecificPart.endsWith("PklProject")) {
"""
amends "pkl:Project"
""".trimIndent()
} else """
birds = import("@birds/catalog/Ostritch.pkl")
""".trimIndent()
override fun resolve(securityManager: SecurityManager): ResolvedModuleKey {
return this
}
}
val fooBayModuleKeyFactory = ModuleKeyFactory { uri ->
if (uri.scheme == "foobar") Optional.of(FooBarModuleKey(uri))
else Optional.empty()
}
val evaluatorBuilder = with(EvaluatorBuilder.preconfigured()) {
setAllowedModules(SecurityManagers.defaultAllowedModules + Pattern.compile("foobar:"))
setAllowedResources(SecurityManagers.defaultAllowedResources + Pattern.compile("foobar:"))
setModuleKeyFactories(
listOf(
fooBayModuleKeyFactory,
ModuleKeyFactories.standardLibrary,
ModuleKeyFactories.pkg,
ModuleKeyFactories.projectpackage,
ModuleKeyFactories.file
)
)
}
val project = evaluatorBuilder.build().use { Project.load(it, uri("foobar:foo/PklProject")) }
val evaluator = evaluatorBuilder.setProjectDependencies(project.dependencies).build()
assertThatCode { evaluator.use { it.evaluateOutputText(uri("foobar:baz")) } }
.hasMessageContaining("Cannot import dependency because project URI `foobar:foo/PklProject` does not have a hierarchical path.")
}
@Test
fun `cannot glob import in local dependency from modulepath`(@TempDir cacheDir: Path) {
PackageServer.populateCacheDir(cacheDir)
val evaluatorBuilder = EvaluatorBuilder.preconfigured().setModuleCacheDir(cacheDir)
val project = Project.load(modulePath("/org/pkl/core/project/project6/PklProject"))
evaluatorBuilder.setProjectDependencies(project.dependencies).build().use { evaluator ->
assertThatCode {
evaluator.evaluateOutputText(modulePath("/org/pkl/core/project/project6/globWithinDependency.pkl"))
}.hasMessageContaining("""
Cannot resolve import in local dependency because scheme `modulepath` is not globbable.
1 | res = import*("*.pkl")
^^^^^^^^^^^^^^^^
""".trimIndent())
assertThatCode {
evaluator.evaluateOutputText(modulePath("/org/pkl/core/project/project6/globIntoDependency.pkl"))
}.hasMessageContaining("""
Pkl Error
Cannot resolve import in local dependency because scheme `modulepath` is not globbable.
1 | import* "@project7/*.pkl" as proj7Files
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
""".trimIndent())
}
}
private fun checkModule(module: PModule) { private fun checkModule(module: PModule) {
assertThat(module.properties.size).isEqualTo(2) assertThat(module.properties.size).isEqualTo(2)
assertThat(module.getProperty("name")).isEqualTo("pigeon") assertThat(module.getProperty("name")).isEqualTo("pigeon")

View File

@@ -38,10 +38,15 @@ abstract class AbstractLanguageSnippetTestsEngine : InputOutputTestEngine() {
internal val selection: String = "" internal val selection: String = ""
protected val packageServer: PackageServer = PackageServer() protected val packageServer: PackageServer = PackageServer()
override val includedTests: List<Regex> = listOf(Regex(".*$selection\\.pkl")) override val includedTests: List<Regex> = listOf(Regex(".*$selection\\.pkl"))
override val excludedTests: List<Regex> = listOf(Regex(".*/native/.*")) override val excludedTests: List<Regex> = buildList {
add(Regex(".*/native/.*"))
if (IoUtils.isWindows()) {
addAll(windowsExcludedTests)
}
}
override val inputDir: Path = snippetsDir.resolve("input") override val inputDir: Path = snippetsDir.resolve("input")
@@ -68,7 +73,12 @@ abstract class AbstractLanguageSnippetTestsEngine : InputOutputTestEngine() {
packageServer.close() packageServer.close()
} }
protected fun String.stripFilePaths() = replace(snippetsDir.toUri().toString(), "file:///\$snippetsDir/") private val replacement by lazy {
if (snippetsDir.root.toString() != "/") "\$snippetsDir" else "/\$snippetsDir"
}
protected fun String.stripFilePaths(): String =
replace(IoUtils.toNormalizedPathString(snippetsDir), replacement)
protected fun String.stripLineNumbers() = replace(lineNumberRegex) { result -> protected fun String.stripLineNumbers() = replace(lineNumberRegex) { result ->
// replace line number with equivalent number of 'x' characters to keep formatting intact // replace line number with equivalent number of 'x' characters to keep formatting intact
@@ -80,7 +90,7 @@ abstract class AbstractLanguageSnippetTestsEngine : InputOutputTestEngine() {
// can't think of a better solution right now // can't think of a better solution right now
protected fun String.stripVersionCheckErrorMessage() = protected fun String.stripVersionCheckErrorMessage() =
replace("Pkl version is ${Release.current().version()}", "Pkl version is xxx") replace("Pkl version is ${Release.current().version()}", "Pkl version is xxx")
protected fun String.stripStdlibLocationSha(): String = protected fun String.stripStdlibLocationSha(): String =
replace("https://github.com/apple/pkl/blob/${Release.current().commitId()}/stdlib/", "https://github.com/apple/pkl/blob/\$commitId/stdlib/") replace("https://github.com/apple/pkl/blob/${Release.current().commitId()}/stdlib/", "https://github.com/apple/pkl/blob/\$commitId/stdlib/")
@@ -261,7 +271,12 @@ class AlpineLanguageSnippetTestsEngine : AbstractNativeLanguageSnippetTestsEngin
override val testClass: KClass<*> = AlpineLanguageSnippetTests::class override val testClass: KClass<*> = AlpineLanguageSnippetTests::class
} }
// error message contains different file path on Windows
private val windowsExcludedTests get() = listOf(Regex(".*missingProjectDeps/bug\\.pkl"))
class WindowsLanguageSnippetTestsEngine : AbstractNativeLanguageSnippetTestsEngine() { class WindowsLanguageSnippetTestsEngine : AbstractNativeLanguageSnippetTestsEngine() {
override val pklExecutablePath: Path = PklExecutablePaths.windowsAmd64 override val pklExecutablePath: Path = PklExecutablePaths.windowsAmd64
override val testClass: KClass<*> = WindowsLanguageSnippetTests::class override val testClass: KClass<*> = WindowsLanguageSnippetTests::class
override val excludedTests: List<Regex>
get() = super.excludedTests + windowsExcludedTests
} }

View File

@@ -1,16 +1,16 @@
package org.pkl.core.project package org.pkl.core.project
import org.pkl.commons.test.PackageServer
import org.pkl.commons.writeString
import org.pkl.core.*
import org.pkl.core.packages.PackageUri
import org.pkl.core.project.Project.EvaluatorSettings
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.Assertions.assertThatCode import org.assertj.core.api.Assertions.assertThatCode
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir import org.junit.jupiter.api.io.TempDir
import org.pkl.commons.test.FileTestUtils import org.pkl.commons.test.FileTestUtils
import org.pkl.commons.test.PackageServer
import org.pkl.commons.writeString
import org.pkl.core.*
import org.pkl.core.http.HttpClient import org.pkl.core.http.HttpClient
import org.pkl.core.packages.PackageUri
import org.pkl.core.project.Project.EvaluatorSettings
import java.net.URI import java.net.URI
import java.nio.file.Path import java.nio.file.Path
import java.util.regex.Pattern import java.util.regex.Pattern

View File

@@ -0,0 +1,8 @@
amends "pkl:Project"
dependencies {
["fruit"] {
uri = "package://localhost:0/fruit@1.0.5"
}
["project4"] = import("../project4/PklProject")
}

View File

@@ -0,0 +1,17 @@
{
"schemaVersion": 1,
"resolvedDependencies": {
"package://localhost:0/fruit@1": {
"type": "remote",
"uri": "projectpackage://localhost:0/fruit@1.0.5",
"checksums": {
"sha256": "$skipChecksumVerification"
}
},
"package://localhost:0/project4@1": {
"type": "local",
"uri": "projectpackage://localhost:0/project4@1.0.0",
"path": "../project4"
}
}
}

View File

@@ -0,0 +1,5 @@
import "@fruit/catalog/apple.pkl"
import "@project4/module1.pkl"
prop1 = apple
prop2 = module1

View File

@@ -0,0 +1,5 @@
amends "pkl:Project"
dependencies {
["project7"] = import("../project7/PklProject")
}

View File

@@ -0,0 +1,10 @@
{
"schemaVersion": 1,
"resolvedDependencies": {
"package://localhost:0/project7@1": {
"type": "local",
"uri": "projectpackage://localhost:0/project7@1.0.0",
"path": "../project7"
}
}
}

View File

@@ -0,0 +1,3 @@
import* "@project7/*.pkl" as proj7Files
res = proj7Files

View File

@@ -0,0 +1,3 @@
import "@project7/main.pkl"
res = main.res

View File

@@ -0,0 +1,8 @@
amends "pkl:Project"
package {
name = "project7"
version = "1.0.0"
packageZipUrl = "https://bogus.value"
baseUri = "package://localhost:0/project7"
}

View File

@@ -0,0 +1 @@
res = import*("*.pkl")

View File

@@ -478,7 +478,7 @@ class EmbeddedExecutorTest {
) )
val result = executor.evaluatePath(pklFile) { val result = executor.evaluatePath(pklFile) {
allowedModules("file:", "package:", "projectpackage:", "https:") allowedModules("file:", "package:", "projectpackage:", "https:")
allowedResources("prop:", "package:", "projectpackage:", "https:") allowedResources("file:", "prop:", "package:", "projectpackage:", "https:")
moduleCacheDir(cacheDir) moduleCacheDir(cacheDir)
projectDir(projectDir) projectDir(projectDir)
} }

View File

@@ -91,18 +91,17 @@ package: Package?
/// ``` /// ```
tests: Listing<String>(isDistinct) tests: Listing<String>(isDistinct)
/// Tells if the project is a file-based module named `PklProject`. /// Tells if the project is a local module named `PklProject`, is not self, and has a [package] section
local isLocalPklProject = (it: Project) -> local isValidLoadDependency = (it: Project) ->
it.projectFileUri.startsWith("file:") && it.projectFileUri.endsWith("/PklProject") isUriLocal(projectFileUri, it.projectFileUri)
&& it.projectFileUri.endsWith("/PklProject")
&& it != module
&& it.package != null
/// A local dependency is another [Project] that is local to the file system. const local function isUriLocal(uri1: Uri, uri2: Uri): Boolean =
/// // This is an imperfect check; should also check that the URIs have the same authority.
/// To declare, use `import("path/to/PklProject")` // We should improve this if/when there is a URI library in the stdlib.
typealias LocalDependency = Project( uri1.substring(0, uri1.indexOf(":")) == uri2.substring(0, uri2.indexOf(":"))
isLocalPklProject,
this != module,
this.package != null
)
/// The dependencies of this project. /// The dependencies of this project.
/// ///
@@ -176,19 +175,31 @@ typealias LocalDependency = Project(
/// 1. Gather all dependencies, both direct and transitive. /// 1. Gather all dependencies, both direct and transitive.
/// 2. For each package's major version, determine the highest declared minor version. /// 2. For each package's major version, determine the highest declared minor version.
/// 3. Write each resolved dependency to sibling file `PklProject.deps.json`. /// 3. Write each resolved dependency to sibling file `PklProject.deps.json`.
dependencies: Mapping<String(!contains("/")), *RemoteDependency|LocalDependency> dependencies: Mapping<String(!contains("/")), *RemoteDependency|Project(isValidLoadDependency)>
local isFileBasedProject = projectFileUri.startsWith("file:")
/// If set, controls the base evaluator settings when running the evaluator. /// If set, controls the base evaluator settings when running the evaluator.
/// ///
/// These settings influence the behavior of the evaluator when running the `pkl eval`, `pkl test`, /// These settings influence the behavior of the evaluator when running the `pkl eval`, `pkl test`,
/// and `pkl repl` CLI commands. /// and `pkl repl` CLI commands.
/// Note that command line flags passed to the CLI will override any settings defined here. /// Command line flags passed to the CLI will override any settings defined here.
/// ///
/// Other integrations can possibly ignore these evaluator settings. /// Other integrations can possibly ignore these evaluator settings.
/// ///
/// Evaluator settings do not get published as part of a package. /// Evaluator settings do not get published as part of a package.
/// It is not possible for a package dependency to influence the evaluator settings of a project. /// It is not possible for a package dependency to influence the evaluator settings of a project.
evaluatorSettings: EvaluatorSettings ///
/// The following values can only be set if this is a file-based project.
///
/// - [modulePath][EvaluatorSettings.modulePath]
/// - [rootDir][EvaluatorSettings.rootDir]
/// - [moduleCacheDir][EvaluatorSettings.moduleCacheDir]
evaluatorSettings: EvaluatorSettings(
(modulePath != null).implies(isFileBasedProject),
(rootDir != null).implies(isFileBasedProject),
(moduleCacheDir != null).implies(isFileBasedProject)
)
/// The URI of the PklProject file. /// The URI of the PklProject file.
/// ///