mirror of
https://github.com/apple/pkl.git
synced 2026-06-29 08:46:22 +02:00
Add support for Windows (#492)
This adds support for Windows. The in-language path separator is still `/`, to ensure Pkl programs are cross-platform. Log lines are written using CRLF endings on Windows. Modules that are combined with `--module-output-separator` uses LF endings to ensure consistent rendering across platforms. `jpkl` does not work on Windows as a direct executable. However, it can work with `java -jar jpkl`. Additional details: * Adjust git settings for Windows * Add native executable for pkl cli * Add jdk17 windows Gradle check in CI * Adjust CI test reports to be staged within Gradle rather than by shell script. * Fix: encode more characters that are not safe Windows paths * Skip running tests involving symbolic links on Windows (these require administrator privileges to run). * Introduce custom implementation of `IoUtils.relativize` * Allow Gradle to initialize ExecutableJar `Property` values * Add Gradle flag to enable remote JVM debugging Co-authored-by: Philip K.F. Hölzenspies <holzensp@gmail.com>
This commit is contained in:
@@ -31,6 +31,7 @@ public final class Platform {
|
||||
var pklVersion = Release.current().version().toString();
|
||||
var osName = System.getProperty("os.name");
|
||||
if (osName.equals("Mac OS X")) osName = "macOS";
|
||||
if (osName.contains("Windows")) osName = "Windows";
|
||||
var osVersion = System.getProperty("os.version");
|
||||
var architecture = System.getProperty("os.arch");
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ public final class Release {
|
||||
var commitId = properties.getProperty("commitId");
|
||||
var osName = System.getProperty("os.name");
|
||||
if (osName.equals("Mac OS X")) osName = "macOS";
|
||||
if (osName.contains("Windows")) osName = "Windows";
|
||||
var osVersion = System.getProperty("os.version");
|
||||
var os = osName + " " + osVersion;
|
||||
var flavor = TruffleOptions.AOT ? "native" : "Java " + System.getProperty("java.version");
|
||||
|
||||
@@ -1794,10 +1794,17 @@ public final class AstBuilder extends AbstractAstBuilder<Object> {
|
||||
try {
|
||||
resolvedUri = IoUtils.resolve(context.getSecurityManager(), moduleKey, parsedUri);
|
||||
} catch (FileNotFoundException e) {
|
||||
throw exceptionBuilder()
|
||||
.evalError("cannotFindModule", importUri)
|
||||
.withSourceSection(createSourceSection(importUriCtx))
|
||||
.build();
|
||||
|
||||
var exceptionBuilder =
|
||||
exceptionBuilder()
|
||||
.evalError("cannotFindModule", importUri)
|
||||
.withSourceSection(createSourceSection(importUriCtx));
|
||||
var path = parsedUri.getPath();
|
||||
if (path != null && path.contains("\\")) {
|
||||
exceptionBuilder.withHint(
|
||||
"To resolve modules in nested directories, use `/` as the directory separator.");
|
||||
}
|
||||
throw exceptionBuilder.build();
|
||||
} catch (URISyntaxException e) {
|
||||
throw exceptionBuilder()
|
||||
.evalError("invalidModuleUri", importUri)
|
||||
|
||||
@@ -18,8 +18,8 @@ package org.pkl.core.module;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.FileSystemNotFoundException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.spi.FileSystemProvider;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
@@ -141,15 +141,20 @@ public final class ModuleKeyFactories {
|
||||
private static class File implements ModuleKeyFactory {
|
||||
@Override
|
||||
public Optional<ModuleKey> create(URI uri) {
|
||||
Path path;
|
||||
try {
|
||||
path = Path.of(uri);
|
||||
} catch (FileSystemNotFoundException | IllegalArgumentException e) {
|
||||
// none of the installed file system providers can handle this URI
|
||||
// skip loading providers if the scheme is `file`.
|
||||
if (uri.getScheme().equalsIgnoreCase("file")) {
|
||||
return Optional.of(ModuleKeys.file(uri));
|
||||
}
|
||||
// don't handle jar-file URIs (these are handled by GenericUrl).
|
||||
if (uri.getScheme().equalsIgnoreCase("jar")) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
return Optional.of(ModuleKeys.file(uri, path));
|
||||
for (FileSystemProvider provider : FileSystemProvider.installedProviders()) {
|
||||
if (provider.getScheme().equalsIgnoreCase(uri.getScheme())) {
|
||||
return Optional.of(ModuleKeys.file(uri));
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,9 +16,11 @@
|
||||
package org.pkl.core.module;
|
||||
|
||||
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.JarURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.http.HttpRequest;
|
||||
@@ -88,8 +90,8 @@ public final class ModuleKeys {
|
||||
}
|
||||
|
||||
/** Creates a module key for a {@code file:} module. */
|
||||
public static ModuleKey file(URI uri, Path path) {
|
||||
return new File(uri, path);
|
||||
public static ModuleKey file(URI uri) {
|
||||
return new File(uri);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -290,12 +292,10 @@ public final class ModuleKeys {
|
||||
|
||||
private static class File extends DependencyAwareModuleKey {
|
||||
final URI uri;
|
||||
final Path path;
|
||||
|
||||
File(URI uri, Path path) {
|
||||
File(URI uri) {
|
||||
super(uri);
|
||||
this.uri = uri;
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -316,7 +316,13 @@ public final class ModuleKeys {
|
||||
public ResolvedModuleKey resolve(SecurityManager securityManager)
|
||||
throws IOException, SecurityManagerException {
|
||||
securityManager.checkResolveModule(uri);
|
||||
var realPath = path.toRealPath();
|
||||
// Disallow paths that contain `\\` characters if on Windows
|
||||
// (require `/` as the path separator on all OSes)
|
||||
var uriPath = uri.getPath();
|
||||
if (java.io.File.separatorChar == '\\' && uriPath != null && uriPath.contains("\\")) {
|
||||
throw new FileNotFoundException();
|
||||
}
|
||||
var realPath = Path.of(uri).toRealPath();
|
||||
var resolvedUri = realPath.toUri();
|
||||
securityManager.checkResolveModule(resolvedUri);
|
||||
return ResolvedModuleKeys.file(this, resolvedUri, realPath);
|
||||
@@ -325,7 +331,7 @@ public final class ModuleKeys {
|
||||
@Override
|
||||
protected Map<String, ? extends Dependency> getDependencies() {
|
||||
var projectDepsManager = VmContext.get(null).getProjectDependenciesManager();
|
||||
if (projectDepsManager == null || !projectDepsManager.hasPath(path)) {
|
||||
if (projectDepsManager == null || !projectDepsManager.hasPath(Path.of(uri))) {
|
||||
throw new PackageLoadError("cannotResolveDependencyNoProject");
|
||||
}
|
||||
return projectDepsManager.getDependencies();
|
||||
@@ -519,6 +525,12 @@ public final class ModuleKeys {
|
||||
var url = IoUtils.toUrl(uri);
|
||||
var conn = url.openConnection();
|
||||
conn.connect();
|
||||
if (conn instanceof JarURLConnection && IoUtils.isWindows()) {
|
||||
// On Windows, opening a JarURLConnection prevents the jar file from being deleted, unless
|
||||
// cacheing is disabled.
|
||||
// See https://bugs.openjdk.org/browse/JDK-8239054
|
||||
conn.setUseCaches(false);
|
||||
}
|
||||
try (InputStream stream = conn.getInputStream()) {
|
||||
URI redirected;
|
||||
try {
|
||||
|
||||
@@ -30,6 +30,7 @@ import java.util.Map;
|
||||
import javax.annotation.concurrent.GuardedBy;
|
||||
import org.pkl.core.module.PathElement.TreePathElement;
|
||||
import org.pkl.core.runtime.FileSystemManager;
|
||||
import org.pkl.core.util.IoUtils;
|
||||
import org.pkl.core.util.LateInit;
|
||||
|
||||
/**
|
||||
@@ -152,8 +153,8 @@ public final class ModulePathResolver implements AutoCloseable {
|
||||
// in case of duplicate path, first entry wins (cf. class loader)
|
||||
stream.forEach(
|
||||
(path) -> {
|
||||
var relativized = basePath.relativize(path);
|
||||
fileCache.putIfAbsent(relativized.toString(), path);
|
||||
var relativized = IoUtils.relativize(path, basePath);
|
||||
fileCache.putIfAbsent(IoUtils.toNormalizedPathString(relativized), path);
|
||||
var element = cachedPathElementRoot;
|
||||
for (var i = 0; i < relativized.getNameCount(); i++) {
|
||||
var name = relativized.getName(i).toString();
|
||||
|
||||
@@ -207,13 +207,15 @@ public final class ProjectDependenciesManager {
|
||||
if (projectDeps == null) {
|
||||
var depsPath = getProjectDepsFile();
|
||||
if (!Files.exists(depsPath)) {
|
||||
throw new VmExceptionBuilder().evalError("missingProjectDepsJson", projectDir).build();
|
||||
throw new VmExceptionBuilder()
|
||||
.evalError("missingProjectDepsJson", projectDir.toUri())
|
||||
.build();
|
||||
}
|
||||
try {
|
||||
projectDeps = ProjectDeps.parse(depsPath);
|
||||
} catch (IOException | URISyntaxException | JsonParseException e) {
|
||||
throw new VmExceptionBuilder()
|
||||
.evalError("invalidProjectDepsJson", depsPath, e.getMessage())
|
||||
.evalError("invalidProjectDepsJson", depsPath.toUri(), e.getMessage())
|
||||
.withCause(e)
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -15,10 +15,12 @@
|
||||
*/
|
||||
package org.pkl.core.module;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import org.pkl.core.util.IoUtils;
|
||||
@@ -75,7 +77,16 @@ public final class ResolvedModuleKeys {
|
||||
|
||||
@Override
|
||||
public String loadSource() throws IOException {
|
||||
return Files.readString(path, StandardCharsets.UTF_8);
|
||||
try {
|
||||
return Files.readString(path, StandardCharsets.UTF_8);
|
||||
} catch (AccessDeniedException e) {
|
||||
// Windows throws `AccessDeniedException` when reading directories.
|
||||
// Sync error between different OSes.
|
||||
if (Files.isDirectory(path)) {
|
||||
throw new IOException("Is a directory");
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ public abstract class Dependency {
|
||||
|
||||
public Path resolveAssetPath(Path projectDir, PackageAssetUri packageAssetUri) {
|
||||
// drop 1 to remove leading `/`
|
||||
var assetPath = packageAssetUri.getAssetPath().toString().substring(1);
|
||||
var assetPath = packageAssetUri.getAssetPath().substring(1);
|
||||
return projectDir.resolve(path).resolve(assetPath);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import java.net.URISyntaxException;
|
||||
import java.nio.file.Path;
|
||||
import org.pkl.core.Version;
|
||||
import org.pkl.core.util.ErrorMessages;
|
||||
import org.pkl.core.util.IoUtils;
|
||||
|
||||
/**
|
||||
* The canonical URI of an asset within a package, i.e., a package URI with a fragment path. For
|
||||
@@ -28,7 +29,7 @@ import org.pkl.core.util.ErrorMessages;
|
||||
public final class PackageAssetUri {
|
||||
private final URI uri;
|
||||
private final PackageUri packageUri;
|
||||
private final Path assetPath;
|
||||
private final String assetPath;
|
||||
|
||||
public static PackageAssetUri create(URI uri) {
|
||||
try {
|
||||
@@ -41,7 +42,7 @@ public final class PackageAssetUri {
|
||||
public PackageAssetUri(PackageUri packageUri, String assetPath) {
|
||||
this.uri = packageUri.getUri().resolve("#" + assetPath);
|
||||
this.packageUri = packageUri;
|
||||
this.assetPath = Path.of(assetPath);
|
||||
this.assetPath = assetPath;
|
||||
}
|
||||
|
||||
public PackageAssetUri(String uri) throws URISyntaxException {
|
||||
@@ -60,7 +61,7 @@ public final class PackageAssetUri {
|
||||
throw new URISyntaxException(
|
||||
uri.toString(), ErrorMessages.create("cannotHaveRelativeFragment", fragment, uri));
|
||||
}
|
||||
this.assetPath = Path.of(fragment);
|
||||
this.assetPath = fragment;
|
||||
}
|
||||
|
||||
public URI getUri() {
|
||||
@@ -71,7 +72,7 @@ public final class PackageAssetUri {
|
||||
return packageUri;
|
||||
}
|
||||
|
||||
public Path getAssetPath() {
|
||||
public String getAssetPath() {
|
||||
return assetPath;
|
||||
}
|
||||
|
||||
@@ -102,6 +103,7 @@ public final class PackageAssetUri {
|
||||
}
|
||||
|
||||
public PackageAssetUri resolve(String path) {
|
||||
return new PackageAssetUri(packageUri, assetPath.resolve(path).toString());
|
||||
var resolvedPath = IoUtils.toNormalizedPathString(Path.of(assetPath).resolve(path));
|
||||
return new PackageAssetUri(packageUri, resolvedPath);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,8 +335,7 @@ final class PackageResolvers {
|
||||
var entries = cachedEntries.get(packageUri);
|
||||
// need to normalize here but not in `doListElments` nor `doHasElement` because
|
||||
// `TreePathElement.getElement` does normalization already.
|
||||
var path = uri.getAssetPath().normalize().toString();
|
||||
assert path.startsWith("/");
|
||||
var path = IoUtils.toNormalizedPathString(Path.of(uri.getAssetPath()).normalize());
|
||||
return entries.get(path).array();
|
||||
}
|
||||
|
||||
@@ -496,7 +495,9 @@ final class PackageResolvers {
|
||||
downloadMetadata(packageUri, requestUri, tmpPath, checksums);
|
||||
Files.createDirectories(cachePath.getParent());
|
||||
Files.move(tmpPath, cachePath, StandardCopyOption.ATOMIC_MOVE);
|
||||
Files.setPosixFilePermissions(cachePath, FILE_PERMISSIONS);
|
||||
if (!IoUtils.isWindows()) {
|
||||
Files.setPosixFilePermissions(cachePath, FILE_PERMISSIONS);
|
||||
}
|
||||
return cachePath;
|
||||
} finally {
|
||||
Files.deleteIfExists(tmpPath);
|
||||
@@ -545,7 +546,9 @@ final class PackageResolvers {
|
||||
verifyPackageZipBytes(packageUri, dependencyMetadata, checksumBytes);
|
||||
Files.createDirectories(cachePath.getParent());
|
||||
Files.move(tmpPath, cachePath, StandardCopyOption.ATOMIC_MOVE);
|
||||
Files.setPosixFilePermissions(cachePath, FILE_PERMISSIONS);
|
||||
if (!IoUtils.isWindows()) {
|
||||
Files.setPosixFilePermissions(cachePath, FILE_PERMISSIONS);
|
||||
}
|
||||
return cachePath;
|
||||
} finally {
|
||||
Files.deleteIfExists(tmpPath);
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.pkl.core.packages.PackageUri;
|
||||
import org.pkl.core.util.EconomicMaps;
|
||||
import org.pkl.core.util.EconomicSets;
|
||||
import org.pkl.core.util.ErrorMessages;
|
||||
import org.pkl.core.util.IoUtils;
|
||||
import org.pkl.core.util.Nullable;
|
||||
|
||||
/**
|
||||
@@ -78,7 +79,7 @@ public final class ProjectDependenciesResolver {
|
||||
|
||||
private void log(String message) {
|
||||
try {
|
||||
logWriter.write(message + "\n");
|
||||
logWriter.write(message + IoUtils.getLineSeparator());
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
@@ -130,7 +131,7 @@ public final class ProjectDependenciesResolver {
|
||||
var packageUri = declaredDependencies.getMyPackageUri();
|
||||
assert packageUri != null;
|
||||
var projectDir = Path.of(declaredDependencies.getProjectFileUri()).getParent();
|
||||
var relativePath = this.project.getProjectDir().relativize(projectDir);
|
||||
var relativePath = IoUtils.relativize(projectDir, this.project.getProjectDir());
|
||||
var localDependency = new LocalDependency(packageUri.toProjectPackageUri(), relativePath);
|
||||
updateDependency(localDependency);
|
||||
buildResolvedDependencies(declaredDependencies);
|
||||
|
||||
@@ -35,6 +35,7 @@ import org.pkl.core.packages.PackageLoadError;
|
||||
import org.pkl.core.packages.PackageUtils;
|
||||
import org.pkl.core.runtime.VmExceptionBuilder;
|
||||
import org.pkl.core.util.EconomicMaps;
|
||||
import org.pkl.core.util.IoUtils;
|
||||
import org.pkl.core.util.Nullable;
|
||||
import org.pkl.core.util.json.Json;
|
||||
import org.pkl.core.util.json.Json.FormatException;
|
||||
@@ -196,7 +197,7 @@ public final class ProjectDeps {
|
||||
jsonWriter.beginObject();
|
||||
jsonWriter.name("type").value("local");
|
||||
jsonWriter.name("uri").value(localDependency.getPackageUri().toString());
|
||||
jsonWriter.name("path").value(localDependency.getPath().toString());
|
||||
jsonWriter.name("path").value(IoUtils.toNormalizedPathString(localDependency.getPath()));
|
||||
jsonWriter.endObject();
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.io.Writer;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.DigestOutputStream;
|
||||
@@ -119,13 +121,18 @@ public final class ProjectPackager {
|
||||
this.outputWriter = outputWriter;
|
||||
}
|
||||
|
||||
private void writeLine(String line) throws IOException {
|
||||
outputWriter.write(line);
|
||||
outputWriter.write(IoUtils.getLineSeparator());
|
||||
}
|
||||
|
||||
public void createPackages() throws IOException {
|
||||
for (var project : projects) {
|
||||
var packageResult = doPackage(project);
|
||||
outputWriter.write(workingDir.relativize(packageResult.getMetadataFile()) + "\n");
|
||||
outputWriter.write(workingDir.relativize(packageResult.getMetadataChecksumFile()) + "\n");
|
||||
outputWriter.write(workingDir.relativize(packageResult.getZipFile()) + "\n");
|
||||
outputWriter.write(workingDir.relativize(packageResult.getZipChecksumFile()) + "\n");
|
||||
writeLine(IoUtils.relativize(packageResult.getMetadataFile(), workingDir).toString());
|
||||
writeLine(IoUtils.relativize(packageResult.getMetadataChecksumFile(), workingDir).toString());
|
||||
writeLine(IoUtils.relativize(packageResult.getZipFile(), workingDir).toString());
|
||||
writeLine(IoUtils.relativize(packageResult.getZipChecksumFile(), workingDir).toString());
|
||||
outputWriter.flush();
|
||||
}
|
||||
}
|
||||
@@ -302,8 +309,8 @@ public final class ProjectPackager {
|
||||
}
|
||||
try (var zos = new ZipOutputStream(digestOutputStream)) {
|
||||
for (var file : files) {
|
||||
var relativePath = project.getProjectDir().relativize(file);
|
||||
var zipEntry = new ZipEntry(relativePath.toString());
|
||||
var relativePath = IoUtils.relativize(file, project.getProjectDir());
|
||||
var zipEntry = new ZipEntry(IoUtils.toNormalizedPathString(relativePath));
|
||||
zipEntry.setTimeLocal(ZIP_ENTRY_MTIME);
|
||||
zos.putNextEntry(zipEntry);
|
||||
Files.copy(file, zos);
|
||||
@@ -342,8 +349,8 @@ public final class ProjectPackager {
|
||||
.filter(Files::isRegularFile)
|
||||
.filter(
|
||||
(it) -> {
|
||||
var fileNameRelativeToProjectRoot =
|
||||
project.getProjectDir().relativize(it).toString();
|
||||
var relativePath = IoUtils.relativize(it, project.getProjectDir());
|
||||
var fileNameRelativeToProjectRoot = IoUtils.toNormalizedPathString(relativePath);
|
||||
for (var pattern : excludePatterns) {
|
||||
if (pattern.matcher(it.getFileName().toString()).matches()) {
|
||||
return false;
|
||||
@@ -363,7 +370,7 @@ public final class ProjectPackager {
|
||||
}
|
||||
|
||||
private boolean isAbsoluteImport(String importStr) {
|
||||
return importStr.matches("\\w:.*") || importStr.startsWith("@");
|
||||
return importStr.matches("\\w+:.*") || importStr.startsWith("@");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -386,8 +393,17 @@ public final class ProjectPackager {
|
||||
if (isAbsoluteImport(importStr)) {
|
||||
continue;
|
||||
}
|
||||
var importPath = Path.of(importStr);
|
||||
if (importPath.isAbsolute() && !project.getProjectDir().toString().equals("/")) {
|
||||
URI importUri;
|
||||
try {
|
||||
importUri = IoUtils.toUri(importStr);
|
||||
} catch (URISyntaxException e) {
|
||||
throw new VmExceptionBuilder()
|
||||
.evalError("invalidModuleUri", importStr)
|
||||
.withSourceSection(sourceSection)
|
||||
.build()
|
||||
.toPklException(stackFrameTransformer);
|
||||
}
|
||||
if (importStr.startsWith("/") && !project.getProjectDir().toString().equals("/")) {
|
||||
throw new VmExceptionBuilder()
|
||||
.evalError("invalidRelativeProjectImport", importStr)
|
||||
.withSourceSection(sourceSection)
|
||||
@@ -395,6 +411,7 @@ public final class ProjectPackager {
|
||||
.toPklException(stackFrameTransformer);
|
||||
}
|
||||
var currentPath = pklModulePath.getParent();
|
||||
var importPath = Path.of(importUri.getPath());
|
||||
// It's not good enough to just check the normalized path to see whether it exists within the
|
||||
// root dir.
|
||||
// It's possible that the import path resolves to a path outside the project dir,
|
||||
@@ -416,7 +433,7 @@ public final class ProjectPackager {
|
||||
|
||||
private @Nullable List<Pair<String, SourceSection>> getImportsAndReads(Path pklModulePath) {
|
||||
try {
|
||||
var moduleKey = ModuleKeys.file(pklModulePath.toUri(), pklModulePath);
|
||||
var moduleKey = ModuleKeys.file(pklModulePath.toUri());
|
||||
var resolvedModuleKey = ResolvedModuleKeys.file(moduleKey, moduleKey.getUri(), pklModulePath);
|
||||
return ImportsAndReadsParser.parse(moduleKey, resolvedModuleKey);
|
||||
} catch (IOException e) {
|
||||
|
||||
@@ -195,10 +195,16 @@ public final class ModuleCache {
|
||||
} catch (SecurityManagerException | PackageLoadError e) {
|
||||
throw new VmExceptionBuilder().withOptionalLocation(importNode).withCause(e).build();
|
||||
} catch (FileNotFoundException | NoSuchFileException e) {
|
||||
throw new VmExceptionBuilder()
|
||||
.withOptionalLocation(importNode)
|
||||
.evalError("cannotFindModule", module.getUri())
|
||||
.build();
|
||||
var exceptionBuilder =
|
||||
new VmExceptionBuilder()
|
||||
.withOptionalLocation(importNode)
|
||||
.evalError("cannotFindModule", module.getUri());
|
||||
var path = module.getUri().getPath();
|
||||
if (path != null && path.contains("\\")) {
|
||||
exceptionBuilder.withHint(
|
||||
"To resolve modules in nested directories, use `/` as the directory separator.");
|
||||
}
|
||||
throw exceptionBuilder.build();
|
||||
} catch (IOException e) {
|
||||
throw new VmExceptionBuilder()
|
||||
.withOptionalLocation(importNode)
|
||||
|
||||
@@ -35,6 +35,7 @@ import java.util.regex.Pattern;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
import org.pkl.core.PklBugException;
|
||||
import org.pkl.core.Platform;
|
||||
import org.pkl.core.SecurityManager;
|
||||
import org.pkl.core.SecurityManagerException;
|
||||
import org.pkl.core.module.ModuleKey;
|
||||
@@ -43,7 +44,10 @@ import org.pkl.core.runtime.VmExceptionBuilder;
|
||||
|
||||
public final class IoUtils {
|
||||
|
||||
private static final Pattern uriLike = Pattern.compile("\\w+:.*");
|
||||
// Don't match paths like `C:\`, which are drive letters on Windows.
|
||||
private static final Pattern uriLike = Pattern.compile("\\w+:[^\\\\].*");
|
||||
|
||||
private static final Pattern windowsPathLike = Pattern.compile("\\w:\\\\.*");
|
||||
|
||||
private IoUtils() {}
|
||||
|
||||
@@ -66,12 +70,20 @@ public final class IoUtils {
|
||||
return uriLike.matcher(str).matches();
|
||||
}
|
||||
|
||||
public static boolean isWindowsAbsolutePath(String str) {
|
||||
if (!isWindows()) return false;
|
||||
return windowsPathLike.matcher(str).matches();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the given string to a {@link URI}. This method MUST be used for constructing module
|
||||
* and resource URIs. Unlike {@code new URI(str)}, it correctly escapes paths of relative URIs.
|
||||
*/
|
||||
public static URI toUri(String str) throws URISyntaxException {
|
||||
return isUriLike(str) ? new URI(str) : new URI(null, null, str, null);
|
||||
if (isUriLike(str)) {
|
||||
return new URI(str);
|
||||
}
|
||||
return new URI(null, null, str, null);
|
||||
}
|
||||
|
||||
/** Like {@link #toUri(String)}, except without checked exceptions. */
|
||||
@@ -150,7 +162,8 @@ public final class IoUtils {
|
||||
new SimpleFileVisitor<>() {
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
|
||||
throws IOException {
|
||||
zipStream.putNextEntry(new ZipEntry(sourceDir.relativize(file).toString()));
|
||||
var relativePath = relativize(file, sourceDir);
|
||||
zipStream.putNextEntry(new ZipEntry(toNormalizedPathString(relativePath)));
|
||||
Files.copy(file, zipStream);
|
||||
zipStream.closeEntry();
|
||||
return FileVisitResult.CONTINUE;
|
||||
@@ -180,6 +193,10 @@ public final class IoUtils {
|
||||
return System.getProperty("line.separator");
|
||||
}
|
||||
|
||||
public static Boolean isWindows() {
|
||||
return Platform.current().operatingSystem().name().equals("Windows");
|
||||
}
|
||||
|
||||
public static String getName(String path) {
|
||||
var lastSep = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
|
||||
return path.substring(lastSep + 1);
|
||||
@@ -362,7 +379,9 @@ public final class IoUtils {
|
||||
}
|
||||
}
|
||||
|
||||
// URI.relativize() won't construct relative paths containing ".."
|
||||
// URI.relativize won't construct relative paths containing `..`.
|
||||
// Can't use Path.relativize because certain URI characters will throw InvalidPathException
|
||||
// on Windows.
|
||||
public static URI relativize(URI uri, URI base) {
|
||||
if (uri.isOpaque()
|
||||
|| base.isOpaque()
|
||||
@@ -370,19 +389,60 @@ public final class IoUtils {
|
||||
|| !Objects.equals(uri.getAuthority(), base.getAuthority())) {
|
||||
return uri;
|
||||
}
|
||||
|
||||
var basePath = Path.of(base.getPath());
|
||||
if (!base.getRawPath().endsWith("/")) basePath = basePath.getParent();
|
||||
var resultPath = basePath.relativize(Path.of(uri.getPath()));
|
||||
|
||||
var uriPath = uri.normalize().getPath();
|
||||
var basePath = base.normalize().getPath();
|
||||
try {
|
||||
return new URI(
|
||||
null, null, null, -1, resultPath.toString(), uri.getQuery(), uri.getFragment());
|
||||
if (basePath.isEmpty()) {
|
||||
return uri;
|
||||
}
|
||||
var uriParts = Arrays.asList(uriPath.split("/"));
|
||||
var baseParts = Arrays.asList(basePath.split("/"));
|
||||
if (!basePath.endsWith("/")) {
|
||||
// strip the last path segment of the base uri, unless it ends in a slash. `/foo/bar.pkl` ->
|
||||
// `/foo`
|
||||
baseParts = baseParts.subList(0, baseParts.size() - 1);
|
||||
}
|
||||
if (uriParts.equals(baseParts)) {
|
||||
return new URI(null, null, null, -1, "", uri.getQuery(), uri.getFragment());
|
||||
}
|
||||
var start = 0;
|
||||
while (start < Math.min(uriParts.size(), baseParts.size())) {
|
||||
if (!uriParts.get(start).equals(baseParts.get(start))) {
|
||||
break;
|
||||
}
|
||||
start++;
|
||||
}
|
||||
var uriPartsRemaining = uriParts.subList(start, uriParts.size());
|
||||
var basePartsRemainig = baseParts.subList(start, baseParts.size());
|
||||
if (basePartsRemainig.isEmpty()) {
|
||||
return new URI(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
-1,
|
||||
String.join("/", uriPartsRemaining),
|
||||
uri.getQuery(),
|
||||
uri.getFragment());
|
||||
}
|
||||
var resultingPath =
|
||||
"../".repeat(basePartsRemainig.size()) + String.join("/", uriPartsRemaining);
|
||||
return new URI(null, null, null, -1, resultingPath, uri.getQuery(), uri.getFragment());
|
||||
} catch (URISyntaxException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
// Impossible; started from a valid URI to begin with.
|
||||
throw PklBugException.unreachableCode();
|
||||
}
|
||||
}
|
||||
|
||||
// On Windows, `Path.relativize` will fail if the two paths have different roots.
|
||||
public static Path relativize(Path path, Path base) {
|
||||
if (isWindows()) {
|
||||
if (path.isAbsolute() && base.isAbsolute() && !path.getRoot().equals(base.getRoot())) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
return base.relativize(path);
|
||||
}
|
||||
|
||||
public static boolean isWhitespace(String str) {
|
||||
return str.codePoints().allMatch(Character::isWhitespace);
|
||||
}
|
||||
@@ -597,6 +657,63 @@ public final class IoUtils {
|
||||
return newUri;
|
||||
}
|
||||
|
||||
public static boolean isReservedFilenameChar(char character) {
|
||||
if (isWindows()) {
|
||||
return isReservedWindowsFilenameChar(character);
|
||||
}
|
||||
// posix; only NULL and `/` are reserved.
|
||||
return character == 0 || character == '/';
|
||||
}
|
||||
|
||||
/** Tells if this character cannot be used for filenames on Windows. */
|
||||
public static boolean isReservedWindowsFilenameChar(char character) {
|
||||
return switch (character) {
|
||||
case 0,
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
25,
|
||||
26,
|
||||
27,
|
||||
28,
|
||||
29,
|
||||
30,
|
||||
31,
|
||||
'<',
|
||||
'>',
|
||||
':',
|
||||
'"',
|
||||
'\\',
|
||||
'/',
|
||||
'|',
|
||||
'?',
|
||||
'*' ->
|
||||
true;
|
||||
default -> false;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Windows reserves characters {@code <>:"\|?*} in filenames.
|
||||
*
|
||||
@@ -608,19 +725,27 @@ public final class IoUtils {
|
||||
var sb = new StringBuilder();
|
||||
for (var i = 0; i < path.length(); i++) {
|
||||
var character = path.charAt(i);
|
||||
switch (character) {
|
||||
case '<', '>', ':', '"', '\\', '|', '?', '*' -> {
|
||||
sb.append('(');
|
||||
sb.append(ByteArrayUtils.toHex(new byte[] {(byte) character}));
|
||||
sb.append(")");
|
||||
}
|
||||
case '(' -> sb.append("((");
|
||||
default -> sb.append(path.charAt(i));
|
||||
if (isReservedWindowsFilenameChar(character) && character != '/') {
|
||||
sb.append('(');
|
||||
sb.append(ByteArrayUtils.toHex(new byte[] {(byte) character}));
|
||||
sb.append(")");
|
||||
} else if (character == '(') {
|
||||
sb.append("((");
|
||||
} else {
|
||||
sb.append(character);
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/** Returns a path string that uses unix-like path separators. */
|
||||
public static String toNormalizedPathString(Path path) {
|
||||
if (isWindows()) {
|
||||
return path.toString().replace("\\", "/");
|
||||
}
|
||||
return path.toString();
|
||||
}
|
||||
|
||||
private static int getExclamationMarkIndex(String jarUri) {
|
||||
var index = jarUri.indexOf('!');
|
||||
if (index == -1) {
|
||||
|
||||
Reference in New Issue
Block a user