diff --git a/pkl-cli/src/main/kotlin/org/pkl/cli/CliProjectPackager.kt b/pkl-cli/src/main/kotlin/org/pkl/cli/CliProjectPackager.kt index 8f6bfb485..348960df3 100644 --- a/pkl-cli/src/main/kotlin/org/pkl/cli/CliProjectPackager.kt +++ b/pkl-cli/src/main/kotlin/org/pkl/cli/CliProjectPackager.kt @@ -21,8 +21,17 @@ import org.pkl.commons.cli.CliBaseOptions import org.pkl.commons.cli.CliException import org.pkl.commons.cli.CliTestException import org.pkl.commons.cli.CliTestOptions +import org.pkl.core.Loggers +import org.pkl.core.SecurityManagers +import org.pkl.core.StackFrameTransformers +import org.pkl.core.module.ModuleKeyFactories import org.pkl.core.project.Project import org.pkl.core.project.ProjectPackager +import org.pkl.core.resource.ResourceReaders +import org.pkl.core.runtime.ModuleResolver +import org.pkl.core.runtime.ResourceManager +import org.pkl.core.runtime.VmContext +import org.pkl.core.runtime.VmUtils import org.pkl.core.util.ErrorMessages class CliProjectPackager( @@ -76,16 +85,38 @@ class CliProjectPackager( } } } - ProjectPackager( - projects, - cliOptions.normalizedWorkingDir, - outputPath, - stackFrameTransformer, - securityManager, - httpClient, - skipPublishCheck, - consoleWriter - ) - .createPackages() + // A VmContext is needed here because loading PklProject.deps.json uses resource readers + VmUtils.createContext { + val vmContext = VmContext.get(null) + vmContext.initialize( + VmContext.Holder( + StackFrameTransformers.defaultTransformer, + SecurityManagers.defaultManager, + httpClient, + ModuleResolver(listOf(ModuleKeyFactories.standardLibrary)), + ResourceManager(SecurityManagers.defaultManager, listOf(ResourceReaders.file())), + Loggers.noop(), + mapOf(), + mapOf(), + null, + null, + null, + null + ) + ) + + ProjectPackager( + projects, + cliOptions.normalizedWorkingDir, + outputPath, + stackFrameTransformer, + securityManager, + httpClient, + skipPublishCheck, + consoleWriter + ) + .createPackages() + } + .close() } } diff --git a/pkl-core/src/main/java/org/pkl/core/module/ModuleKeys.java b/pkl-core/src/main/java/org/pkl/core/module/ModuleKeys.java index d442650cc..0394c0122 100644 --- a/pkl-core/src/main/java/org/pkl/core/module/ModuleKeys.java +++ b/pkl-core/src/main/java/org/pkl/core/module/ModuleKeys.java @@ -326,7 +326,7 @@ public ResolvedModuleKey resolve(SecurityManager securityManager) @Override protected Map getDependencies() { var projectDepsManager = VmContext.get(null).getProjectDependenciesManager(); - if (projectDepsManager == null || !projectDepsManager.hasPath(path)) { + if (projectDepsManager == null || !projectDepsManager.hasUri(uri)) { throw new PackageLoadError("cannotResolveDependencyNoProject"); } return projectDepsManager.getDependencies(); @@ -517,11 +517,11 @@ public ResolvedModuleKey resolve(SecurityManager securityManager) } /** Base implementation; knows how to resolve dependencies prefixed with @. */ - private abstract static class DependencyAwareModuleKey implements ModuleKey { + public abstract static class DependencyAwareModuleKey implements ModuleKey { protected final URI uri; - DependencyAwareModuleKey(URI uri) { + protected DependencyAwareModuleKey(URI uri) { this.uri = uri; } @@ -672,12 +672,12 @@ private ProjectDependenciesManager getProjectDepsResolver() { return projectDepsManager; } - private @Nullable Path getLocalPath(Dependency dependency) { + private @Nullable URI getLocalUri(Dependency dependency, PackageAssetUri assetUri) { if (!(dependency instanceof LocalDependency)) { return null; } return ((LocalDependency) dependency) - .resolveAssetPath(getProjectDepsResolver().getProjectDir(), packageAssetUri); + .resolveAssetUri(getProjectDepsResolver().getProjectBaseUri(), assetUri); } @Override @@ -686,10 +686,9 @@ public ResolvedModuleKey resolve(SecurityManager securityManager) securityManager.checkResolveModule(packageAssetUri.getUri()); var dependency = getProjectDepsResolver().getResolvedDependency(packageAssetUri.getPackageUri()); - var path = getLocalPath(dependency); - if (path != null) { - securityManager.checkResolveModule(path.toUri()); - return ResolvedModuleKeys.file(this, path.toUri(), path); + var local = getLocalUri(dependency, packageAssetUri); + if (local != null) { + return VmContext.get(null).getModuleResolver().resolve(local).resolve(securityManager); } var dep = (Dependency.RemoteDependency) dependency; assert dep.getChecksums() != null; @@ -704,10 +703,12 @@ public List listElements(SecurityManager securityManager, URI baseU var packageAssetUri = PackageAssetUri.create(baseUri); var dependency = getProjectDepsResolver().getResolvedDependency(packageAssetUri.getPackageUri()); - var path = getLocalPath(dependency); - if (path != null) { - securityManager.checkResolveModule(path.toUri()); - return FileResolver.listElements(path); + var local = getLocalUri(dependency, packageAssetUri); + if (local != null) { + return VmContext.get(null) + .getModuleResolver() + .resolve(local) + .listElements(securityManager, local); } var dep = (Dependency.RemoteDependency) dependency; assert dep.getChecksums() != null; @@ -721,10 +722,12 @@ public boolean hasElement(SecurityManager securityManager, URI elementUri) var packageAssetUri = PackageAssetUri.create(elementUri); var dependency = getProjectDepsResolver().getResolvedDependency(packageAssetUri.getPackageUri()); - var path = getLocalPath(dependency); - if (path != null) { - securityManager.checkResolveModule(path.toUri()); - return FileResolver.hasElement(path); + var local = getLocalUri(dependency, packageAssetUri); + if (local != null) { + return VmContext.get(null) + .getModuleResolver() + .resolve(local) + .hasElement(securityManager, local); } var dep = (Dependency.RemoteDependency) dependency; assert dep.getChecksums() != null; diff --git a/pkl-core/src/main/java/org/pkl/core/module/ProjectDependenciesManager.java b/pkl-core/src/main/java/org/pkl/core/module/ProjectDependenciesManager.java index 85a62c7ee..a0c09e7b7 100644 --- a/pkl-core/src/main/java/org/pkl/core/module/ProjectDependenciesManager.java +++ b/pkl-core/src/main/java/org/pkl/core/module/ProjectDependenciesManager.java @@ -15,15 +15,14 @@ */ package org.pkl.core.module; -import java.io.IOException; 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.Map; +import java.util.Objects; import javax.annotation.concurrent.GuardedBy; import org.graalvm.collections.EconomicMap; +import org.pkl.core.PklBugException; import org.pkl.core.packages.Dependency; import org.pkl.core.packages.DependencyMetadata; import org.pkl.core.packages.PackageLoadError; @@ -31,7 +30,10 @@ import org.pkl.core.project.CanonicalPackageUri; import org.pkl.core.project.DeclaredDependencies; import org.pkl.core.project.ProjectDeps; +import org.pkl.core.resource.Resource; +import org.pkl.core.runtime.VmContext; import org.pkl.core.runtime.VmExceptionBuilder; +import org.pkl.core.runtime.VmTyped; import org.pkl.core.util.EconomicMaps; import org.pkl.core.util.json.Json.JsonParseException; @@ -41,7 +43,7 @@ public class ProjectDependenciesManager { public static final String PKL_PROJECT_DEPS_FILENAME = "PklProject.deps.json"; private final DeclaredDependencies declaredDependencies; - private final Path projectDir; + private final URI projectBaseUri; @GuardedBy("lock") private ProjectDeps projectDeps; @@ -60,11 +62,14 @@ public class ProjectDependenciesManager { public ProjectDependenciesManager(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 = declaredDependencies.getProjectFileUri().resolve("."); } - public boolean hasPath(Path path) { - return path.startsWith(projectDir); + public boolean hasUri(URI uri) { + return projectBaseUri.getScheme().equals(uri.getScheme()) + && Objects.equals(projectBaseUri.getAuthority(), uri.getAuthority()) + && uri.getPath().startsWith(projectBaseUri.getPath()); } private void ensureDependenciesInitialized() { @@ -195,26 +200,45 @@ public Dependency getResolvedDependency(PackageUri packageUri) { return dep; } - public Path getProjectDir() { - return projectDir; + public URI getProjectBaseUri() { + return projectBaseUri; } - public Path getProjectDepsFile() { - return projectDir.resolve(PKL_PROJECT_DEPS_FILENAME); + public URI getProjectDepsFileUri() { + return projectBaseUri.resolve(PKL_PROJECT_DEPS_FILENAME); } private ProjectDeps getProjectDeps() { synchronized (lock) { if (projectDeps == null) { - var depsPath = getProjectDepsFile(); - if (!Files.exists(depsPath)) { - throw new VmExceptionBuilder().evalError("missingProjectDepsJson", projectDir).build(); - } + var depsUri = getProjectDepsFileUri(); + try { - projectDeps = ProjectDeps.parse(depsPath); - } catch (IOException | URISyntaxException | JsonParseException e) { + var resource = VmContext.get(null).getResourceManager().read(depsUri, null); + if (resource.isEmpty()) { + throw new VmExceptionBuilder() + .evalError("missingProjectDepsJson", projectBaseUri) + .build(); + } + + var res = resource.get(); + if (res instanceof String) { + projectDeps = ProjectDeps.parse((String) res); + } else if (res instanceof VmTyped) { + var resInner = ((VmTyped) res).getExtraStorage(); + if (resInner instanceof Resource) { + projectDeps = ProjectDeps.parse(((Resource) resInner).getText()); + } else { + // ResourceManager.read() already catches this condition + throw PklBugException.unreachableCode(); + } + } else { + // ResourceManager.read() already catches this condition + throw PklBugException.unreachableCode(); + } + } catch (URISyntaxException | JsonParseException e) { throw new VmExceptionBuilder() - .evalError("invalidProjectDepsJson", depsPath, e.getMessage()) + .evalError("invalidProjectDepsJson", depsUri, e.getMessage()) .withCause(e) .build(); } diff --git a/pkl-core/src/main/java/org/pkl/core/packages/Dependency.java b/pkl-core/src/main/java/org/pkl/core/packages/Dependency.java index 2fe41448a..c98795c83 100644 --- a/pkl-core/src/main/java/org/pkl/core/packages/Dependency.java +++ b/pkl-core/src/main/java/org/pkl/core/packages/Dependency.java @@ -15,6 +15,7 @@ */ package org.pkl.core.packages; +import java.net.URI; import java.nio.file.Path; import java.util.Objects; import org.pkl.core.Version; @@ -48,10 +49,10 @@ public Path getPath() { return path; } - public Path resolveAssetPath(Path projectDir, PackageAssetUri packageAssetUri) { + public URI resolveAssetUri(URI projectBaseUri, PackageAssetUri packageAssetUri) { // drop 1 to remove leading `/` var assetPath = packageAssetUri.getAssetPath().toString().substring(1); - return projectDir.resolve(path).resolve(assetPath); + return projectBaseUri.resolve(path.resolve(assetPath).toString()); } @Override diff --git a/pkl-core/src/main/java/org/pkl/core/project/Project.java b/pkl-core/src/main/java/org/pkl/core/project/Project.java index 238c0446e..04a728e66 100644 --- a/pkl-core/src/main/java/org/pkl/core/project/Project.java +++ b/pkl-core/src/main/java/org/pkl/core/project/Project.java @@ -17,6 +17,7 @@ import java.net.URI; import java.net.URISyntaxException; +import java.nio.file.FileSystemNotFoundException; import java.nio.file.Path; import java.util.HashMap; import java.util.List; @@ -41,6 +42,7 @@ import org.pkl.core.module.ModuleKeyFactories; import org.pkl.core.packages.Checksums; import org.pkl.core.packages.Dependency.RemoteDependency; +import org.pkl.core.packages.PackageLoadError; import org.pkl.core.packages.PackageUri; import org.pkl.core.packages.PackageUtils; import org.pkl.core.resource.ResourceReaders; @@ -52,8 +54,8 @@ public final class Project { private final DeclaredDependencies dependencies; private final EvaluatorSettings evaluatorSettings; private final URI projectFileUri; - private final Path projectDir; - private final List tests; + private final URI projectBaseUri; + private final List tests; private final Map localProjectDependencies; /** @@ -143,7 +145,7 @@ public static Project parseProject(PObject module) throws URISyntaxException { var pkgObj = getNullableProperty(module, "package"); var projectFileUri = URI.create((String) module.getProperty("projectFileUri")); var dependencies = parseDependencies(module, projectFileUri, null); - var projectDir = Path.of(projectFileUri).getParent(); + var projectBaseUri = projectFileUri.resolve("."); Package pkg = null; if (pkgObj != null) { pkg = parsePackage((PObject) pkgObj); @@ -152,12 +154,12 @@ public static Project parseProject(PObject module) throws URISyntaxException { getProperty( module, "evaluatorSettings", - (settings) -> parseEvaluatorSettings(settings, projectDir)); + (settings) -> parseEvaluatorSettings(settings, projectBaseUri)); @SuppressWarnings("unchecked") var testPathStrs = (List) getProperty(module, "tests"); var tests = testPathStrs.stream() - .map((it) -> projectDir.resolve(it).normalize()) + .map((it) -> projectBaseUri.resolve(it).normalize()) .collect(Collectors.toList()); var localProjectDependencies = parseLocalProjectDependencies(module); return new Project( @@ -165,7 +167,7 @@ public static Project parseProject(PObject module) throws URISyntaxException { dependencies, evaluatorSettings, projectFileUri, - projectDir, + projectBaseUri, tests, localProjectDependencies); } @@ -185,7 +187,7 @@ private static Map parseLocalProjectDependencies(PObject module } @SuppressWarnings("unchecked") - private static EvaluatorSettings parseEvaluatorSettings(Object settings, Path projectDir) { + private static EvaluatorSettings parseEvaluatorSettings(Object settings, URI projectBaseUri) { var pSettings = (PObject) settings; var externalProperties = getNullableProperty(pSettings, "externalProperties", Project::asMap); var env = getNullableProperty(pSettings, "env", Project::asMap); @@ -194,16 +196,18 @@ private static EvaluatorSettings parseEvaluatorSettings(Object settings, Path pr getNullableProperty(pSettings, "allowedResources", Project::asPatternList); var noCache = (Boolean) getNullableProperty(pSettings, "noCache"); var modulePathStrs = (List) getNullableProperty(pSettings, "modulePath"); + var timeout = (Duration) getNullableProperty(pSettings, "timeout"); + List modulePath = null; if (modulePathStrs != null) { modulePath = modulePathStrs.stream() - .map((it) -> projectDir.resolve(it).normalize()) + .map((it) -> resolveNullablePath(it, projectBaseUri, "modulePath")) .collect(Collectors.toList()); } - var timeout = (Duration) getNullableProperty(pSettings, "timeout"); - var moduleCacheDir = getNullablePath(pSettings, "moduleCacheDir", projectDir); - var rootDir = getNullablePath(pSettings, "rootDir", projectDir); + + var moduleCacheDir = getNullablePath(pSettings, "moduleCacheDir", projectBaseUri); + var rootDir = getNullablePath(pSettings, "rootDir", projectBaseUri); return new EvaluatorSettings( externalProperties, env, @@ -261,10 +265,27 @@ private static T getProperty(PObject settings, String propertyName, Function return new URI((String) value); } + /** + * Resolve a path string against projectBaseUri Throws an exception if projectBaseUri is not a + * 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( - Composite object, String propertyName, Path projectDir) { - return getNullableProperty( - object, propertyName, (obj) -> projectDir.resolve((String) obj).normalize()); + Composite object, String propertyName, URI projectBaseUri) { + return resolveNullablePath( + (String) getNullableProperty(object, propertyName), projectBaseUri, propertyName); } @SuppressWarnings("unchecked") @@ -309,14 +330,14 @@ private Project( DeclaredDependencies dependencies, EvaluatorSettings evaluatorSettings, URI projectFileUri, - Path projectDir, - List tests, + URI projectBaseUri, + List tests, Map localProjectDependencies) { this.pkg = pkg; this.dependencies = dependencies; this.evaluatorSettings = evaluatorSettings; this.projectFileUri = projectFileUri; - this.projectDir = projectDir; + this.projectBaseUri = projectBaseUri; this.tests = tests; this.localProjectDependencies = localProjectDependencies; } @@ -334,7 +355,16 @@ public URI getProjectFileUri() { } public List getTests() { - return tests; + return tests.stream() + .map( + (it) -> { + try { + return Path.of(it); + } catch (FileSystemNotFoundException e) { + throw new PackageLoadError("invalidUsageOfProjectFromNonFileUri"); + } + }) + .collect(Collectors.toList()); } @Override @@ -366,11 +396,20 @@ public Map getLocalProjectDependencies() { return localProjectDependencies; } + public URI getProjectBaseUri() { + return projectBaseUri; + } + public Path getProjectDir() { - return projectDir; + try { + return Path.of(projectBaseUri); + } catch (FileSystemNotFoundException e) { + throw new PackageLoadError("invalidUsageOfProjectFromNonFileUri", projectBaseUri); + } } public static class EvaluatorSettings { + private final @Nullable Map externalProperties; private final @Nullable Map env; private final @Nullable List allowedModules; diff --git a/pkl-core/src/main/java/org/pkl/core/resource/ResourceReaders.java b/pkl-core/src/main/java/org/pkl/core/resource/ResourceReaders.java index ab87648e6..cab037670 100644 --- a/pkl-core/src/main/java/org/pkl/core/resource/ResourceReaders.java +++ b/pkl-core/src/main/java/org/pkl/core/resource/ResourceReaders.java @@ -22,7 +22,6 @@ import java.net.http.HttpRequest; import java.net.http.HttpResponse.BodyHandlers; import java.nio.file.Files; -import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -487,10 +486,9 @@ public Optional read(URI uri) throws IOException, URISyntaxException, SecurityManagerException { var assetUri = new PackageAssetUri(uri); var dependency = getProjectDepsResolver().getResolvedDependency(assetUri.getPackageUri()); - var path = getLocalPath(dependency, assetUri); - if (path != null) { - var bytes = Files.readAllBytes(path); - return Optional.of(new Resource(uri, bytes)); + var local = getLocalUri(dependency, assetUri); + if (local != null) { + return VmContext.get(null).getResourceManager().read(local, null); } var remoteDep = (Dependency.RemoteDependency) dependency; var bytes = getPackageResolver().getBytes(assetUri, true, remoteDep.getChecksums()); @@ -519,9 +517,9 @@ public List listElements(SecurityManager securityManager, URI baseU var packageAssetUri = PackageAssetUri.create(baseUri); var dependency = getProjectDepsResolver().getResolvedDependency(packageAssetUri.getPackageUri()); - var path = getLocalPath(dependency, packageAssetUri); - if (path != null) { - return FileResolver.listElements(path); + var local = getLocalUri(dependency, packageAssetUri); + if (local != null) { + return VmContext.get(null).getResourceManager().listElements(local); } var remoteDep = (Dependency.RemoteDependency) dependency; return getPackageResolver() @@ -535,9 +533,9 @@ public boolean hasElement(SecurityManager securityManager, URI elementUri) var packageAssetUri = PackageAssetUri.create(elementUri); var dependency = getProjectDepsResolver().getResolvedDependency(packageAssetUri.getPackageUri()); - var path = getLocalPath(dependency, packageAssetUri); - if (path != null) { - return FileResolver.hasElement(path); + var local = getLocalUri(dependency, packageAssetUri); + if (local != null) { + return VmContext.get(null).getResourceManager().hasElement(local); } var remoteDep = (Dependency.RemoteDependency) dependency; return getPackageResolver() @@ -556,12 +554,12 @@ private ProjectDependenciesManager getProjectDepsResolver() { return projectDepsManager; } - private @Nullable Path getLocalPath(Dependency dependency, PackageAssetUri packageAssetUri) { + private @Nullable URI getLocalUri(Dependency dependency, PackageAssetUri packageAssetUri) { if (!(dependency instanceof LocalDependency)) { return null; } return ((LocalDependency) dependency) - .resolveAssetPath(getProjectDepsResolver().getProjectDir(), packageAssetUri); + .resolveAssetUri(getProjectDepsResolver().getProjectBaseUri(), packageAssetUri); } } diff --git a/pkl-core/src/main/java/org/pkl/core/runtime/ResourceManager.java b/pkl-core/src/main/java/org/pkl/core/runtime/ResourceManager.java index 7a14d014b..04a2b9e43 100644 --- a/pkl-core/src/main/java/org/pkl/core/runtime/ResourceManager.java +++ b/pkl-core/src/main/java/org/pkl/core/runtime/ResourceManager.java @@ -29,6 +29,7 @@ import org.pkl.core.SecurityManagerException; import org.pkl.core.http.HttpClientInitException; import org.pkl.core.module.ModuleKey; +import org.pkl.core.module.PathElement; import org.pkl.core.packages.PackageLoadError; import org.pkl.core.resource.Resource; import org.pkl.core.resource.ResourceReader; @@ -36,6 +37,7 @@ import org.pkl.core.util.GlobResolver; import org.pkl.core.util.GlobResolver.InvalidGlobPatternException; import org.pkl.core.util.GlobResolver.ResolvedGlobElement; +import org.pkl.core.util.Nullable; public final class ResourceManager { private final Map resourceReaders = new HashMap<>(); @@ -119,20 +121,20 @@ public List resolveGlob( } @TruffleBoundary - public Optional read(URI resourceUri, Node readNode) { + public Optional read(URI resourceUri, @Nullable Node readNode) { return resources.computeIfAbsent( resourceUri.normalize(), uri -> { try { securityManager.checkReadResource(uri); } catch (SecurityManagerException e) { - throw new VmExceptionBuilder().withCause(e).withLocation(readNode).build(); + throw new VmExceptionBuilder().withCause(e).withOptionalLocation(readNode).build(); } var reader = resourceReaders.get(uri.getScheme()); if (reader == null) { throw new VmExceptionBuilder() - .withLocation(readNode) + .withOptionalLocation(readNode) .evalError("noResourceReaderRegistered", resourceUri.getScheme()) .build(); } @@ -144,16 +146,16 @@ public Optional read(URI resourceUri, Node readNode) { throw new VmExceptionBuilder() .evalError("ioErrorReadingResource", uri) .withCause(e) - .withLocation(readNode) + .withOptionalLocation(readNode) .build(); } catch (URISyntaxException e) { throw new VmExceptionBuilder() .evalError("invalidResourceUri", resourceUri) .withHint(e.getReason()) - .withLocation(readNode) + .withOptionalLocation(readNode) .build(); } 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; @@ -166,8 +168,38 @@ public Optional read(URI resourceUri, Node readNode) { throw new VmExceptionBuilder() .evalError("unsupportedResourceType", reader.getClass().getName(), res.getClass()) - .withLocation(readNode) + .withOptionalLocation(readNode) .build(); }); } + + /** + * Used by ResourceReaders.ProjectPackageResource to resolve resources from projects that may not + * be on the local filesystem + */ + public List listElements(URI baseUri) throws IOException, SecurityManagerException { + var reader = resourceReaders.get(baseUri.getScheme()); + if (reader == null) { + throw new VmExceptionBuilder() + .evalError("noResourceReaderRegistered", baseUri.getScheme()) + .build(); + } + + return reader.listElements(securityManager, baseUri); + } + + /** + * Used by ResourceReaders.ProjectPackageResource to resolve resources from projects that may not + * be on the local filesystem + */ + public boolean hasElement(URI elementUri) throws IOException, SecurityManagerException { + var reader = resourceReaders.get(elementUri.getScheme()); + if (reader == null) { + throw new VmExceptionBuilder() + .evalError("noResourceReaderRegistered", elementUri.getScheme()) + .build(); + } + + return reader.hasElement(securityManager, elementUri); + } } diff --git a/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties b/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties index 16157d212..2358aba59 100644 --- a/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties +++ b/pkl-core/src/main/resources/org/pkl/core/errorMessages.properties @@ -861,6 +861,11 @@ Cannot find a dependency named `{0}`, because it is not declared in the current \n\ 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=\ Cannot find dependency named `{0}`, because it was not declared in package `{1}`. @@ -1009,6 +1014,16 @@ No package was declared in project `{0}`.\n\ \n\ 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=\ Failed to create package `{0}`, because its API tests are failing. diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project1/PklProject b/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project1/PklProject index 693c5fa56..c2b97221e 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project1/PklProject +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project1/PklProject @@ -11,4 +11,5 @@ dependencies { uri = "package://localhost:0/badImportsWithinPackage@1.0.0" } ["project2"] = import("../project2/PklProject") + ["project6"] = import("../project6/PklProject") } diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project1/PklProject.deps.json b/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project1/PklProject.deps.json index efbcd85ec..55d204bfc 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project1/PklProject.deps.json +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project1/PklProject.deps.json @@ -20,6 +20,11 @@ "uri": "projectpackage://localhost:0/project2@1.0.0", "path": "../project2/" }, + "package://localhost:12110/project6@1": { + "type": "local", + "uri": "projectpackage://localhost:12110/project6@1.0.0", + "path": "../project6/" + }, "package://localhost:0/badImportsWithinPackage@1": { "type": "remote", "uri": "projectpackage://localhost:0/badImportsWithinPackage@1.0.0", diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project1/globbing.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project1/globbing.pkl index 570b79ef5..59c46689e 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project1/globbing.pkl +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project1/globbing.pkl @@ -34,4 +34,9 @@ examples { ["glob-read absolute package uri"] { read*("package://localhost:0/birds@0.5.0#/catalog/*.pkl") } + +// https://github.com/apple/pkl/issues/166 +// ["glob-import behind local project import"] { +// import("@project6/children.pkl") +// } } diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project3/PklProject.deps.json b/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project3/PklProject.deps.json index efbcd85ec..55d204bfc 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project3/PklProject.deps.json +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project3/PklProject.deps.json @@ -20,6 +20,11 @@ "uri": "projectpackage://localhost:0/project2@1.0.0", "path": "../project2/" }, + "package://localhost:12110/project6@1": { + "type": "local", + "uri": "projectpackage://localhost:12110/project6@1.0.0", + "path": "../project6/" + }, "package://localhost:0/badImportsWithinPackage@1": { "type": "remote", "uri": "projectpackage://localhost:0/badImportsWithinPackage@1.0.0", diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project6/PklProject b/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project6/PklProject new file mode 100644 index 000000000..a574a7a74 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project6/PklProject @@ -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" +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project6/PklProject.deps.json b/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project6/PklProject.deps.json new file mode 100644 index 000000000..836079aad --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project6/PklProject.deps.json @@ -0,0 +1,4 @@ +{ + "schemaVersion": 1, + "resolvedDependencies": {} +} \ No newline at end of file diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project6/children.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project6/children.pkl new file mode 100644 index 000000000..2e85df8c5 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project6/children.pkl @@ -0,0 +1 @@ +children = import*("children/*.pkl") diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project6/children/a.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project6/children/a.pkl new file mode 100644 index 000000000..ca5afff60 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project6/children/a.pkl @@ -0,0 +1 @@ +name = "a" diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project6/children/b.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project6/children/b.pkl new file mode 100644 index 000000000..b109b4ad2 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project6/children/b.pkl @@ -0,0 +1 @@ +name = "b" diff --git a/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project6/children/c.pkl b/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project6/children/c.pkl new file mode 100644 index 000000000..021964c9a --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/input/projects/project6/children/c.pkl @@ -0,0 +1 @@ +name = "c" diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/projects/badProjectDeps1/bug.err b/pkl-core/src/test/files/LanguageSnippetTests/output/projects/badProjectDeps1/bug.err index fe03d7024..82ef8f73c 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/projects/badProjectDeps1/bug.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/projects/badProjectDeps1/bug.err @@ -1,5 +1,5 @@ –– Pkl Error –– -Cannot resolve dependency because file `/$snippetsDir/input/projects/badProjectDeps1/PklProject.deps.json` is malformed. +Cannot resolve dependency because file `file:/$snippetsDir/input/projects/badProjectDeps1/PklProject.deps.json` is malformed. Run `pkl project resolve` to re-create this file. x | import "@bird/Bird.pkl" diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/projects/badProjectDeps2/bug.err b/pkl-core/src/test/files/LanguageSnippetTests/output/projects/badProjectDeps2/bug.err index 3d43f7418..26548cfe8 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/projects/badProjectDeps2/bug.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/projects/badProjectDeps2/bug.err @@ -1,5 +1,5 @@ –– Pkl Error –– -Cannot resolve dependency because file `/$snippetsDir/input/projects/badProjectDeps2/PklProject.deps.json` is malformed. +Cannot resolve dependency because file `file:/$snippetsDir/input/projects/badProjectDeps2/PklProject.deps.json` is malformed. Run `pkl project resolve` to re-create this file. x | import "@bird/Bird.pkl" diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/projects/badProjectDeps4/bug.err b/pkl-core/src/test/files/LanguageSnippetTests/output/projects/badProjectDeps4/bug.err index c39c1e879..0bbf5b7ab 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/projects/badProjectDeps4/bug.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/projects/badProjectDeps4/bug.err @@ -1,13 +1,13 @@ –– 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... -xxx | dependencies: Mapping - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +xxx | dependencies: Mapping + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ at pkl.Project#dependencies (pkl:Project) -* Value is not of type `LocalDependency` because: - Type constraint `this.package != null` violated. +* Value is not of type `Project(isValidLoadDependency)` because: + Type constraint `isValidLoadDependency` violated. Value: new ModuleClass { package = null; tests {}; dependencies {}; evaluatorSetti... x | dependencies { diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/projects/missingProjectDeps/bug.err b/pkl-core/src/test/files/LanguageSnippetTests/output/projects/missingProjectDeps/bug.err index f965f99b2..f02231a5b 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/projects/missingProjectDeps/bug.err +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/projects/missingProjectDeps/bug.err @@ -1,5 +1,5 @@ –– Pkl Error –– -Cannot resolve dependency because file `PklProject.deps.json` is missing in project directory `/$snippetsDir/input/projects/missingProjectDeps`. +Cannot resolve dependency because file `PklProject.deps.json` is missing in project directory `file:/$snippetsDir/input/projects/missingProjectDeps/`. x | import "@birds/Bird.pkl" ^^^^^^^^^^^^^^^^^ diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/projects/project1/globbing.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/projects/project1/globbing.pcf index 5c8dbe577..7fd20dcad 100644 --- a/pkl-core/src/test/files/LanguageSnippetTests/output/projects/project1/globbing.pcf +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/projects/project1/globbing.pcf @@ -32,7 +32,16 @@ examples { } } ["glob-import local project"] { - new {} + new { + ["@project2/penguin.pkl"] { + bird { + name = "Penguin" + favoriteFruit { + name = "Ice Fruit" + } + } + } + } } ["glob-import using dependency notation"] { Set("@birds/catalog/Ostritch.pkl", "@birds/catalog/Swallow.pkl") diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/projects/project6/children.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/projects/project6/children.pcf new file mode 100644 index 000000000..2455f8db6 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/projects/project6/children.pcf @@ -0,0 +1,11 @@ +children { + ["children/a.pkl"] { + name = "a" + } + ["children/b.pkl"] { + name = "b" + } + ["children/c.pkl"] { + name = "c" + } +} diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/projects/project6/children/a.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/projects/project6/children/a.pcf new file mode 100644 index 000000000..ca5afff60 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/projects/project6/children/a.pcf @@ -0,0 +1 @@ +name = "a" diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/projects/project6/children/b.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/projects/project6/children/b.pcf new file mode 100644 index 000000000..b109b4ad2 --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/projects/project6/children/b.pcf @@ -0,0 +1 @@ +name = "b" diff --git a/pkl-core/src/test/files/LanguageSnippetTests/output/projects/project6/children/c.pcf b/pkl-core/src/test/files/LanguageSnippetTests/output/projects/project6/children/c.pcf new file mode 100644 index 000000000..021964c9a --- /dev/null +++ b/pkl-core/src/test/files/LanguageSnippetTests/output/projects/project6/children/c.pcf @@ -0,0 +1 @@ +name = "c" diff --git a/pkl-core/src/test/kotlin/org/pkl/core/EvaluatorCustomReaders.kt b/pkl-core/src/test/kotlin/org/pkl/core/EvaluatorCustomReaders.kt new file mode 100644 index 000000000..4886ce2a3 --- /dev/null +++ b/pkl-core/src/test/kotlin/org/pkl/core/EvaluatorCustomReaders.kt @@ -0,0 +1,140 @@ +/** + * 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.core + +import org.pkl.core.module.* +import org.pkl.core.module.ModuleKeys.DependencyAwareModuleKey +import org.pkl.core.packages.Dependency +import org.pkl.core.packages.PackageLoadError +import org.pkl.core.resource.Resource +import org.pkl.core.resource.ResourceReader +import org.pkl.core.runtime.VmContext +import org.pkl.core.util.IoUtils +import java.io.FileNotFoundException +import java.net.URI +import java.nio.file.Files +import java.nio.file.Path +import java.util.* + +/** + * Provides Module- and Resource readers for use in tests + * Readers are provided a URI scheme and + */ +class EvaluatorCustomReaders { + + class CustomModuleKey(uri: URI, private val basePath: Path) : DependencyAwareModuleKey(uri) { + + override fun hasHierarchicalUris(): Boolean = true + override fun isGlobbable(): Boolean = true + + override fun hasElement(securityManager: SecurityManager, elementUri: URI): Boolean { + securityManager.checkResolveModule(elementUri); + val realPath = basePath.resolve(elementUri.path.removePrefix("/")).toRealPath() + if (!realPath.startsWith(basePath)) { + throw PklBugException("attempt to access path $realPath above base path $basePath") + } + return FileResolver.hasElement(realPath) + } + + override fun listElements(securityManager: SecurityManager, baseUri: URI): MutableList { + securityManager.checkResolveModule(baseUri) + val realPath = basePath.resolve(baseUri.path.removePrefix("/")).toRealPath() + if (!realPath.startsWith(basePath)) { + throw PklBugException("attempt to access path $realPath above base path $basePath") + } + return FileResolver.listElements(realPath) + } + + override fun resolve(securityManager: SecurityManager): ResolvedModuleKey { + securityManager.checkResolveModule(uri); + val realPath = basePath.toRealPath() + if (!realPath.startsWith(basePath) && realPath != basePath) { + throw PklBugException("attempt to access path $realPath above base path $basePath") + } + return ResolvedModuleKeys.file(this, realPath.toUri(), realPath) + } + + override fun getDependencies(): MutableMap { + val projectDepsManager = VmContext.get(null).projectDependenciesManager + if (projectDepsManager == null || !projectDepsManager.hasUri(uri)) { + throw PackageLoadError("cannotResolveDependencyNoProject") + } + return projectDepsManager.dependencies + } + + override fun cannotFindDependency(name: String): PackageLoadError { + return PackageLoadError("cannotFindDependencyInProject", name) + } + + } + + class CustomModuleKeyFactory(private val uriScheme: String, private val dir: Path) : ModuleKeyFactory { + + override fun create(uri: URI): Optional { + if (uri.scheme != uriScheme) { + return Optional.empty() + } + + val resolvedPath = dir.resolve(Path.of(uri.path.removePrefix("/"))).toRealPath() + return Optional.of(CustomModuleKey(uri, resolvedPath)) + } + + } + + class CustomResourceReader(private val customUriScheme: String, base: Path) : ResourceReader { + + private val basePath = base.toRealPath() + override fun hasHierarchicalUris(): Boolean = true + + override fun isGlobbable(): Boolean = true + + override fun getUriScheme(): String = customUriScheme + + override fun hasElement(securityManager: SecurityManager, elementUri: URI): Boolean { + securityManager.checkResolveModule(elementUri); + val realPath = basePath.resolve(elementUri.path.removePrefix("/")).toRealPath() + if (!realPath.startsWith(basePath)) { + throw PklBugException("attempt to access path $realPath above base path $basePath") + } + return FileResolver.hasElement(realPath) + } + + override fun listElements(securityManager: SecurityManager, baseUri: URI): MutableList { + securityManager.checkResolveModule(baseUri) + val realPath = basePath.resolve(baseUri.path.removePrefix("/")).toRealPath() + if (!realPath.startsWith(basePath)) { + throw PklBugException("attempt to access path $realPath above base path $basePath") + } + return FileResolver.listElements(realPath) + } + + override fun read(uri: URI): Optional { + val realPath = basePath.resolve(uri.path.removePrefix("/")).toRealPath() + if (!realPath.startsWith(basePath)) { + throw PklBugException("attempt to access path $realPath above base path $basePath") + } + + try { + val content = Files.readAllBytes(realPath) + return Optional.of(Resource(uri, content)) + } catch (e: FileNotFoundException) { + return Optional.empty() + } + } + + } + +} diff --git a/pkl-core/src/test/kotlin/org/pkl/core/EvaluatorCustomReadersTest.kt b/pkl-core/src/test/kotlin/org/pkl/core/EvaluatorCustomReadersTest.kt new file mode 100644 index 000000000..df8e97f27 --- /dev/null +++ b/pkl-core/src/test/kotlin/org/pkl/core/EvaluatorCustomReadersTest.kt @@ -0,0 +1,160 @@ +/** + * 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.core + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import org.pkl.commons.test.PackageServer +import org.pkl.core.module.ModuleKeyFactories +import org.pkl.core.project.Project +import org.pkl.core.resource.ResourceReaders +import java.nio.file.Path +import java.util.regex.Pattern +import kotlin.io.path.createDirectories +import kotlin.io.path.writeText + +class EvaluatorCustomReadersTest { + @Test + fun `evaluate with project dependencies from custom URI`(@TempDir tempDir: Path, @TempDir cacheDir: Path) { + PackageServer.populateCacheDir(cacheDir) + val libDir = tempDir.resolve("lib/").createDirectories() + libDir + .resolve("lib.pkl") + .writeText( + """ + text = "This is from lib" + """ + .trimIndent() + ) + libDir + .resolve("PklProject") + .writeText( + """ + amends "pkl:Project" + + package { + name = "lib" + baseUri = "package://localhost:12110/lib" + version = "5.0.0" + packageZipUrl = "https://localhost:12110/lib.zip" + } + """ + .trimIndent() + ) + val projectDir = tempDir.resolve("proj/").createDirectories() + val module = projectDir.resolve("mod.pkl") + module.writeText( + """ + import "@birds/Bird.pkl" + import "@lib/lib.pkl" + + res: Bird = new { + name = "Birdie" + favoriteFruit { name = "dragonfruit" } + } + + libContents = lib + """ + .trimIndent() + ) + projectDir + .resolve("PklProject") + .writeText( + """ + amends "pkl:Project" + + dependencies { + ["birds"] { + uri = "package://localhost:12110/birds@0.5.0" + } + ["lib"] = import("../lib/PklProject") + } + """ + .trimIndent() + ) + val dollar = '$' + projectDir + .resolve("PklProject.deps.json") + .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" + } + }, + "package://localhost:12110/lib@5": { + "type": "local", + "uri": "projectpackage://localhost:12110/lib@5.0.0", + "path": "../lib" + } + } + } + + """ + .trimIndent() + ) + + val evalBase = EvaluatorBuilder.unconfigured() + .setStackFrameTransformer(StackFrameTransformers.defaultTransformer) + .setAllowedModules(listOf(Pattern.compile("custom:"), Pattern.compile("pkl:"), Pattern.compile("projectpackage:"), Pattern.compile("package:"))) + .setAllowedResources(listOf(Pattern.compile("custom:"), Pattern.compile("prop:"))) + .addModuleKeyFactory(ModuleKeyFactories.standardLibrary) + .addModuleKeyFactory(ModuleKeyFactories.projectpackage) + .addModuleKeyFactory(ModuleKeyFactories.pkg) + .addModuleKeyFactory(EvaluatorCustomReaders.CustomModuleKeyFactory("custom", tempDir)) + .addResourceReader(ResourceReaders.externalProperty()) + .addResourceReader(EvaluatorCustomReaders.CustomResourceReader("custom", tempDir)) + + + val projEval = evalBase.build() + val projOutput = projEval.evaluateOutputValueAs(ModuleSource.uri("custom:///proj/PklProject"), PClassInfo.Project) + val proj = Project.parseProject(projOutput) + + val eval = evalBase.setProjectDependencies(proj.dependencies).build() + val output = eval.evaluateExpressionString(ModuleSource.uri("custom:///proj/mod.pkl"), "output.text") + assertThat(output) + .isEqualTo( + """ + res { + name = "Birdie" + favoriteFruit { + name = "dragonfruit" + } + } + libContents { + text = "This is from lib" + } + + """ + .trimIndent() + ) + + } + +} diff --git a/pkl-executor/src/test/kotlin/org/pkl/executor/EmbeddedExecutorTest.kt b/pkl-executor/src/test/kotlin/org/pkl/executor/EmbeddedExecutorTest.kt index 86c57ccf4..42187f4c8 100644 --- a/pkl-executor/src/test/kotlin/org/pkl/executor/EmbeddedExecutorTest.kt +++ b/pkl-executor/src/test/kotlin/org/pkl/executor/EmbeddedExecutorTest.kt @@ -471,7 +471,7 @@ class EmbeddedExecutorTest { ) val result = executor.evaluatePath(pklFile) { allowedModules("file:", "package:", "projectpackage:", "https:") - allowedResources("prop:", "package:", "projectpackage:", "https:") + allowedResources("file:", "prop:", "package:", "projectpackage:", "https:") moduleCacheDir(cacheDir) projectDir(projectDir) } diff --git a/pkl-server/src/main/kotlin/org/pkl/server/ClientModuleKeyFactory.kt b/pkl-server/src/main/kotlin/org/pkl/server/ClientModuleKeyFactory.kt index 4d4f954d6..76faca303 100644 --- a/pkl-server/src/main/kotlin/org/pkl/server/ClientModuleKeyFactory.kt +++ b/pkl-server/src/main/kotlin/org/pkl/server/ClientModuleKeyFactory.kt @@ -17,17 +17,16 @@ package org.pkl.server import java.io.IOException import java.net.URI -import java.util.Optional +import java.util.* import java.util.concurrent.CompletableFuture import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.Future import kotlin.random.Random import org.pkl.core.SecurityManager -import org.pkl.core.module.ModuleKey -import org.pkl.core.module.ModuleKeyFactory -import org.pkl.core.module.PathElement -import org.pkl.core.module.ResolvedModuleKey -import org.pkl.core.module.ResolvedModuleKeys +import org.pkl.core.module.* +import org.pkl.core.packages.Dependency +import org.pkl.core.packages.PackageLoadError +import org.pkl.core.runtime.VmContext internal class ClientModuleKeyFactory( private val readerSpecs: Collection, @@ -96,8 +95,10 @@ internal class ClientModuleKeyFactory( is ListModulesResponse -> { if (response.error != null) { completeExceptionally(IOException(response.error)) + } else if (response.pathElements != null) { + complete(response.pathElements) } else { - complete(response.pathElements!!) + complete(emptyList()) } } else -> completeExceptionally(ProtocolException("unexpected response")) @@ -113,7 +114,7 @@ internal class ClientModuleKeyFactory( private val uri: URI, private val spec: ModuleReaderSpec, private val resolver: ClientModuleKeyResolver, - ) : ModuleKey { + ) : ModuleKeys.DependencyAwareModuleKey(uri) { override fun isLocal(): Boolean = spec.isLocal override fun hasHierarchicalUris(): Boolean = spec.hasHierarchicalUris @@ -133,6 +134,22 @@ internal class ClientModuleKeyFactory( override fun hasElement(securityManager: SecurityManager, uri: URI): Boolean { return resolver.hasElement(securityManager, uri) } + + override fun getDependencies(): Map { + if (!hasHierarchicalUris()) { + throw PackageLoadError("cannotResolveDependencyFromReaderWithOpaqueUris", spec.scheme) + } + + val projectDepsManager = VmContext.get(null).projectDependenciesManager + if (projectDepsManager == null || !projectDepsManager.hasUri(uri)) { + throw PackageLoadError("cannotResolveDependencyNoProject") + } + return projectDepsManager.dependencies + } + + override fun cannotFindDependency(name: String): PackageLoadError { + return PackageLoadError("cannotFindDependencyInProject", name) + } } } diff --git a/pkl-server/src/main/kotlin/org/pkl/server/ClientResourceReader.kt b/pkl-server/src/main/kotlin/org/pkl/server/ClientResourceReader.kt index 998d6e2b4..6232aafa8 100644 --- a/pkl-server/src/main/kotlin/org/pkl/server/ClientResourceReader.kt +++ b/pkl-server/src/main/kotlin/org/pkl/server/ClientResourceReader.kt @@ -68,10 +68,12 @@ internal class ClientResourceReader( transport.send(request) { response -> when (response) { is ListResourcesResponse -> - if (response.pathElements != null) { + if (response.error != null) { + completeExceptionally(IOException(response.error)) + } else if (response.pathElements != null) { complete(response.pathElements) } else { - completeExceptionally(IOException(response.error)) + complete(emptyList()) } else -> completeExceptionally(ProtocolException("Unexpected response")) } diff --git a/stdlib/Project.pkl b/stdlib/Project.pkl index d7bb2684e..ed90feafe 100644 --- a/stdlib/Project.pkl +++ b/stdlib/Project.pkl @@ -91,18 +91,25 @@ package: Package? /// ``` tests: Listing(isDistinct) -/// Tells if the project is a file-based module named `PklProject`. -local isLocalPklProject = (it: Project) -> - it.projectFileUri.startsWith("file:") && it.projectFileUri.endsWith("/PklProject") +/// Tells if the project is a file-based module named `PklProject`, is not self, and has a [package] section +local isValidLoadDependency = (it: Project) -> + isUriLocal(projectFileUri, it.projectFileUri) + && it.projectFileUri.endsWith("/PklProject") + && it != module + && it.package != null + +// this regex is an approximation but is good enough to handle URIs returned by `reflect` +const local uriRegex = Regex(#"^(?:([^:/?#]+):)?(?://([^/?#]*))?[^?#]*(?:\?[^#]*)?(?:#.*)?"#) + +const local function isUriLocal(uri1: String, uri2: String): Boolean = + let (uri1Match = uriRegex.matchEntire(uri1)?.ifNonNull((it) -> Pair(it.groups[1]?.value, it.groups[2]?.value))) + let (uri2Match = uriRegex.matchEntire(uri2).ifNonNull((it) -> Pair(it.groups[1]?.value, it.groups[2]?.value))) + uri1Match == uri2Match /// A local dependency is another [Project] that is local to the file system. /// /// To declare, use `import("path/to/PklProject")` -typealias LocalDependency = Project( - isLocalPklProject, - this != module, - this.package != null -) +typealias LocalDependency = Project(isValidLoadDependency) /// The dependencies of this project. /// @@ -176,7 +183,7 @@ typealias LocalDependency = Project( /// 1. Gather all dependencies, both direct and transitive. /// 2. For each package's major version, determine the highest declared minor version. /// 3. Write each resolved dependency to sibling file `PklProject.deps.json`. -dependencies: Mapping +dependencies: Mapping /// If set, controls the base evaluator settings when running the evaluator. /// @@ -211,10 +218,10 @@ function newInstance(enclosingModule: Module): Project = new { } local hasVersion = (it: Uri) -> - let (versionSep = it.lastIndexOf("@")) - if (versionSep == -1) false - else let (version = it.drop(versionSep + 1)) - semver.parseOrNull(version) != null + let (versionSep = it.lastIndexOf("@")) + if (versionSep == -1) false + else let (version = it.drop(versionSep + 1)) + semver.parseOrNull(version) != null typealias PackageUri = Uri(startsWith("package:"), hasVersion)