Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion pkl-core/src/main/java/org/pkl/core/module/FileResolver.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 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.
Expand All @@ -26,13 +26,29 @@
import java.util.List;
import org.pkl.core.util.IoUtils;

/** Utilities for inspecting file-system paths while resolving module glob imports. */
public final class FileResolver {
private FileResolver() {}

/**
* Returns the immediate, non-symbolic-link children of the directory identified by {@code
* baseUri}.
*
* <p>Returns an empty list if {@code baseUri} does not exist or does not identify a directory.
*
* @throws IOException if the directory cannot be read
*/
public static List<PathElement> listElements(URI baseUri) throws IOException {
return listElements(IoUtils.pathOf(baseUri));
}

/**
* Returns the immediate, non-symbolic-link children of {@code path}.
*
* <p>Returns an empty list if {@code path} does not exist or is not a directory.
*
* @throws IOException if the directory cannot be read
*/
public static List<PathElement> listElements(Path path) throws IOException {
try (var stream = Files.newDirectoryStream(path)) {
var ret = new ArrayList<PathElement>();
Expand All @@ -49,10 +65,12 @@ public static List<PathElement> listElements(Path path) throws IOException {
}
}

/** Returns whether the file-system path identified by {@code elementUri} exists. */
public static boolean hasElement(URI elementUri) {
return Files.exists(IoUtils.pathOf(elementUri));
}

/** Returns whether {@code path} exists. */
public static boolean hasElement(Path path) {
return Files.exists(path);
}
Expand Down
27 changes: 27 additions & 0 deletions pkl-core/src/main/java/org/pkl/core/module/ModulePathResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,19 @@ public final class ModulePathResolver implements AutoCloseable {

private static final ModulePathResolver EMPTY = new ModulePathResolver(Collections.emptyList());

/** Returns a resolver whose module path contains no entries. */
public static ModulePathResolver empty() {
return EMPTY;
}

/**
* Creates a resolver for the given module path entries.
*
* <p>Each entry can be a directory, JAR file, or ZIP file. Entries are searched in iteration
* order; if more than one entry contains the same path, the first entry wins.
*
* @param modulePath the module path entries to search
*/
public ModulePathResolver(Iterable<Path> modulePath) {
this.modulePath = modulePath;
}
Expand Down Expand Up @@ -103,6 +112,13 @@ private Map<String, Path> getFileCache() throws IOException {
}
}

/**
* Resolves a {@code modulepath:} URI to the path containing its source.
*
* @throws FileNotFoundException if the module path contains no matching file
* @throws IOException if a module path entry cannot be read
* @throws IllegalStateException if this resolver has been closed
*/
public Path resolve(URI uri) throws IOException {
var modulePath = getModulePath(uri);
var result = getFileCache().get(modulePath);
Expand All @@ -111,6 +127,12 @@ public Path resolve(URI uri) throws IOException {
throw new FileNotFoundException();
}

/**
* Returns whether the module path contains the element identified by {@code elementUri}.
*
* @throws UncheckedIOException if a module path entry cannot be read
* @throws IllegalStateException if this resolver has been closed
*/
public boolean hasElement(URI elementUri) {
var path = elementUri.getPath();
try {
Expand All @@ -121,6 +143,11 @@ public boolean hasElement(URI elementUri) {
}
}

/**
* Closes file systems opened for JAR and ZIP entries.
*
* <p>Calling this method more than once has no effect.
*/
@Override
public void close() {
synchronized (lock) {
Expand Down
15 changes: 15 additions & 0 deletions pkl-core/src/main/java/org/pkl/core/module/PathElement.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
import org.jspecify.annotations.Nullable;
import org.pkl.core.util.EconomicMaps;

/** Describes a child of a hierarchical module or resource path. */
public class PathElement {
/** Orders files before directories, then orders elements lexicographically by name. */
public static final Comparator<PathElement> comparator =
(o1, o2) -> {
if (o1.isDirectory && !o2.isDirectory) {
Expand All @@ -39,26 +41,31 @@ public class PathElement {

private final boolean isDirectory;

/** Creates an element whose type cannot be inspected and is therefore treated as a file. */
public static PathElement opaque(String name) {
return new PathElement(name, false);
}

/** Creates an element with the given name and directory status. */
public PathElement(String name, boolean isDirectory) {
this.name = name;
this.isDirectory = isDirectory;
}

/** Returns this element's name relative to its parent. */
public String getName() {
return name;
}

/** Returns an equivalent element with {@code name}, or this element if its name is unchanged. */
public PathElement withName(String name) {
if (name.equals(this.name)) {
return this;
}
return new PathElement(name, isDirectory);
}

/** Returns whether this element represents a directory. */
public boolean isDirectory() {
return isDirectory;
}
Expand All @@ -80,13 +87,19 @@ public String toString() {
return "PathElement{" + "name='" + name + '\'' + ", isDirectory=" + isDirectory + '}';
}

/** A path element that stores its descendants as a tree. */
public static final class TreePathElement extends PathElement {
private final EconomicMap<String, TreePathElement> children = EconomicMaps.create();

/** Creates a tree element with the given name and directory status. */
public TreePathElement(String name, boolean isDirectory) {
super(name, isDirectory);
}

/**
* Adds {@code child} unless a child named {@code name} already exists, and returns the stored
* child.
*/
public TreePathElement putIfAbsent(String name, TreePathElement child) {
children.putIfAbsent(name, child);
return children.get(name);
Expand All @@ -113,10 +126,12 @@ public TreePathElement putIfAbsent(String name, TreePathElement child) {
return getElement(Path.of(basePath));
}

/** Returns this element's children, keyed by name. */
public EconomicMap<String, TreePathElement> getChildren() {
return children;
}

/** Returns a snapshot of this element's child values. */
public List<PathElement> getChildrenValues() {
var ret = new ArrayList<PathElement>(children.size());
for (var elem : EconomicMaps.getValues(children)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,12 @@
import org.pkl.core.util.IoUtils;
import org.pkl.core.util.json.Json.JsonParseException;

/** Resolves a project's declared package dependencies against its lock file. */
public final class ProjectDependenciesManager {
/** The conventional name of a Pkl project file. */
public static final String PKL_PROJECT_FILENAME = "PklProject";

/** The conventional name of a Pkl project's dependency lock file. */
public static final String PKL_PROJECT_DEPS_FILENAME = "PklProject.deps.json";

private final DeclaredDependencies declaredDependencies;
Expand All @@ -66,6 +69,13 @@ public final class ProjectDependenciesManager {

private final Object lock = new Object();

/**
* Creates a dependency manager for a project.
*
* @param declaredDependencies the dependencies declared by the project
* @param moduleResolver the resolver used to load the project's dependency lock file
* @param securityManager the security manager used when loading the dependency lock file
*/
public ProjectDependenciesManager(
DeclaredDependencies declaredDependencies,
ModuleResolver moduleResolver,
Expand All @@ -77,6 +87,7 @@ public ProjectDependenciesManager(
this.securityManager = securityManager;
}

/** Returns whether {@code uri} is located within this project. */
public boolean hasUri(URI uri) {
return projectBaseUri.getScheme().equals(uri.getScheme())
&& Objects.equals(projectBaseUri.getAuthority(), uri.getAuthority())
Expand Down Expand Up @@ -164,6 +175,7 @@ private Map<String, Dependency> doBuildResolvedDependenciesForProject(
}

// `ensureDependenciesInitialized` makes `myDependencies` safe to access
/** Returns the project's direct dependencies, keyed by their declared names. */
@SuppressWarnings({"FieldAccessNotGuarded", "GuardedBy"})
public Map<String, Dependency> getDependencies() {
ensureDependenciesInitialized();
Expand All @@ -172,13 +184,15 @@ public Map<String, Dependency> getDependencies() {
}

// `ensureDependenciesInitialized` makes `localPackageDependencies` safe to access
/** Returns whether {@code packageUri} identifies a local dependency of this project. */
@SuppressWarnings({"FieldAccessNotGuarded", "GuardedBy"})
public boolean isLocalPackage(PackageUri packageUri) {
ensureDependenciesInitialized();
return localPackageDependencies.containsKey(packageUri);
}

// `ensureDependenciesInitialized` makes `localPackageDependencies` safe to access
/** Returns the dependencies of the local package identified by {@code packageUri}. */
@SuppressWarnings({"FieldAccessNotGuarded", "GuardedBy"})
public Map<String, Dependency> getLocalPackageDependencies(PackageUri packageUri) {
ensureDependenciesInitialized();
Expand All @@ -188,6 +202,13 @@ public Map<String, Dependency> getLocalPackageDependencies(PackageUri packageUri
return dep;
}

/**
* Resolves a package's declared dependencies against this project's lock file.
*
* @param packageUri the package whose dependencies are being resolved
* @param dependencyMetadata the package's published dependency metadata
* @return dependencies keyed by their names in the package metadata
*/
public Map<String, Dependency> getResolvedDependenciesForPackage(
PackageUri packageUri, DependencyMetadata dependencyMetadata) {
synchronized (lock) {
Expand Down Expand Up @@ -217,10 +238,12 @@ public Map<String, Dependency> getResolvedDependenciesForPackage(
}
}

/** Returns this project's declared dependencies. */
public DeclaredDependencies getDeclaredDependencies() {
return declaredDependencies;
}

/** Returns the locked dependency matching {@code packageUri}. */
public Dependency getResolvedDependency(PackageUri packageUri) {
var dep = getProjectDeps().get(CanonicalPackageUri.fromPackageUri(packageUri));
if (dep == null) {
Expand All @@ -229,14 +252,17 @@ public Dependency getResolvedDependency(PackageUri packageUri) {
return dep;
}

/** Returns the base URI used to resolve paths within this project. */
public URI getProjectBaseUri() {
return projectBaseUri;
}

/** Returns the URI of this project's dependency lock file. */
public URI getProjectDepsFileUri() {
return IoUtils.resolve(projectBaseUri, PKL_PROJECT_DEPS_FILENAME);
}

/** Returns the URI of this project's project file. */
public URI getProjectFileUri() {
return declaredDependencies.projectFileUri();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
* Copyright © 2024-2026 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.
Expand All @@ -21,6 +21,7 @@

/** SPI for identifying a resolved module and loading its source code. */
public interface ResolvedModuleKey {
/** Returns the unresolved module key from which this key was resolved. */
ModuleKey getOriginal();

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ private ResolvedModuleKeys() {}
* loaded from that file path and cached using the given URI as cache key.
*
* @param nofollow if true, the file will be opened with {@link LinkOption#NOFOLLOW_LINKS}.
* @return a resolved module key backed by {@code path}
*/
public static ResolvedModuleKey file(ModuleKey original, URI uri, Path path, boolean nofollow) {
return new FileKey(original, uri, path, nofollow);
Expand All @@ -42,6 +43,8 @@ public static ResolvedModuleKey file(ModuleKey original, URI uri, Path path, boo
/**
* Creates a resolved module key backed by the given file path. The resulting module will be
* loaded from that file path and cached using the given URI as cache key.
*
* @return a resolved module key backed by {@code path}
*/
public static ResolvedModuleKey file(ModuleKey original, URI uri, Path path) {
return new FileKey(original, uri, path, false);
Expand All @@ -50,6 +53,8 @@ public static ResolvedModuleKey file(ModuleKey original, URI uri, Path path) {
/**
* Creates a resolved module key backed by the given URL. The resulting module will be loaded from
* that URL and cached using the given URI as cache key.
*
* @return a resolved module key backed by {@code url}
*/
public static ResolvedModuleKey url(ModuleKey original, URI uri, URL url) {
return new Url(original, uri, url);
Expand All @@ -58,6 +63,8 @@ public static ResolvedModuleKey url(ModuleKey original, URI uri, URL url) {
/**
* Creates a resolved module key backed by the given source code. If {@code cached} is {@code
* true}, the resulting module will be cached using the given URI as cache key.
*
* @return a resolved module key backed by {@code sourceText}
*/
public static ResolvedModuleKey virtual(
ModuleKey original, URI uri, String sourceText, boolean cached) {
Expand All @@ -67,6 +74,8 @@ public static ResolvedModuleKey virtual(
/**
* Creates a resolved module key that behaves like {@code delegate}, except with {@code original}
* as its original module key.
*
* @return a resolved module key backed by {@code delegate}
*/
public static ResolvedModuleKey delegated(ResolvedModuleKey delegate, ModuleKey original) {
return new Delegated(delegate, original);
Expand Down
6 changes: 6 additions & 0 deletions pkl-core/src/main/java/org/pkl/core/module/package-info.java
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
/**
* APIs for resolving Pkl module URIs and loading module source code.
*
* <p>Custom module schemes can be implemented with {@link org.pkl.core.module.ModuleKeyFactory},
* {@link org.pkl.core.module.ModuleKey}, and {@link org.pkl.core.module.ResolvedModuleKey}.
*/
@NullMarked
package org.pkl.core.module;

Expand Down