Encode filepaths to be safe on Windows

This changes the file paths to use characters that are safe for Windows.

Channges the output of the following:
* Package cache directory
* Generated pkl-doc files
* Kotlin generated code

Unsafe characters are encoded as (<hex>).
For example, the colon character `:` is encoded as `(3a)`.

Additionally, this changes the cache directory prefix (package-1 to
package-2).

Follows the design of https://github.com/apple/pkl-evolution/pull/3
This commit is contained in:
Dan Chao
2024-04-26 07:34:31 -07:00
committed by Daniel Chao
parent 110dc89e86
commit a5c13e325a
36 changed files with 185 additions and 106 deletions

View File

@@ -1164,7 +1164,7 @@ result = someLib.x
""" """
.trimIndent() .trimIndent()
) )
assertThat(tempDir.resolve("package-1")).doesNotExist() assertThat(tempDir.resolve("package-2")).doesNotExist()
} }
@Test @Test

View File

@@ -56,12 +56,12 @@ class CliPackageDownloaderTest {
noTransitive = true noTransitive = true
) )
cmd.run() cmd.run()
assertThat(tempDir.resolve("package-1/localhost:0/birds@0.5.0/birds@0.5.0.zip")).exists() assertThat(tempDir.resolve("package-2/localhost(3a)0/birds@0.5.0/birds@0.5.0.zip")).exists()
assertThat(tempDir.resolve("package-1/localhost:0/birds@0.5.0/birds@0.5.0.json")).exists() assertThat(tempDir.resolve("package-2/localhost(3a)0/birds@0.5.0/birds@0.5.0.json")).exists()
assertThat(tempDir.resolve("package-1/localhost:0/fruit@1.0.5/fruit@1.0.5.zip")).exists() assertThat(tempDir.resolve("package-2/localhost(3a)0/fruit@1.0.5/fruit@1.0.5.zip")).exists()
assertThat(tempDir.resolve("package-1/localhost:0/fruit@1.0.5/fruit@1.0.5.json")).exists() assertThat(tempDir.resolve("package-2/localhost(3a)0/fruit@1.0.5/fruit@1.0.5.json")).exists()
assertThat(tempDir.resolve("package-1/localhost:0/fruit@1.1.0/fruit@1.1.0.zip")).exists() assertThat(tempDir.resolve("package-2/localhost(3a)0/fruit@1.1.0/fruit@1.1.0.zip")).exists()
assertThat(tempDir.resolve("package-1/localhost:0/fruit@1.1.0/fruit@1.1.0.json")).exists() assertThat(tempDir.resolve("package-2/localhost(3a)0/fruit@1.1.0/fruit@1.1.0.json")).exists()
} }
@Test @Test
@@ -90,9 +90,9 @@ class CliPackageDownloaderTest {
noTransitive = true noTransitive = true
) )
cmd.run() cmd.run()
assertThat(tempDir.resolve(".my-cache/package-1/localhost:0/birds@0.5.0/birds@0.5.0.zip")) assertThat(tempDir.resolve(".my-cache/package-2/localhost(3a)0/birds@0.5.0/birds@0.5.0.zip"))
.exists() .exists()
assertThat(tempDir.resolve(".my-cache/package-1/localhost:0/birds@0.5.0/birds@0.5.0.json")) assertThat(tempDir.resolve(".my-cache/package-2/localhost(3a)0/birds@0.5.0/birds@0.5.0.json"))
.exists() .exists()
} }
@@ -113,8 +113,8 @@ class CliPackageDownloaderTest {
noTransitive = true noTransitive = true
) )
cmd.run() cmd.run()
assertThat(tempDir.resolve("package-1/localhost:0/birds@0.5.0/birds@0.5.0.zip")).exists() assertThat(tempDir.resolve("package-2/localhost(3a)0/birds@0.5.0/birds@0.5.0.zip")).exists()
assertThat(tempDir.resolve("package-1/localhost:0/birds@0.5.0/birds@0.5.0.json")).exists() assertThat(tempDir.resolve("package-2/localhost(3a)0/birds@0.5.0/birds@0.5.0.json")).exists()
} }
@Test @Test
@@ -228,9 +228,9 @@ class CliPackageDownloaderTest {
noTransitive = false noTransitive = false
) )
.run() .run()
assertThat(tempDir.resolve("package-1/localhost:0/birds@0.5.0/birds@0.5.0.zip")).exists() assertThat(tempDir.resolve("package-2/localhost(3a)0/birds@0.5.0/birds@0.5.0.zip")).exists()
assertThat(tempDir.resolve("package-1/localhost:0/birds@0.5.0/birds@0.5.0.json")).exists() assertThat(tempDir.resolve("package-2/localhost(3a)0/birds@0.5.0/birds@0.5.0.json")).exists()
assertThat(tempDir.resolve("package-1/localhost:0/fruit@1.0.5/fruit@1.0.5.zip")).exists() assertThat(tempDir.resolve("package-2/localhost(3a)0/fruit@1.0.5/fruit@1.0.5.zip")).exists()
assertThat(tempDir.resolve("package-1/localhost:0/fruit@1.0.5/fruit@1.0.5.json")).exists() assertThat(tempDir.resolve("package-2/localhost(3a)0/fruit@1.0.5/fruit@1.0.5.json")).exists()
} }
} }

View File

@@ -22,6 +22,7 @@ import java.net.URI
import java.util.* import java.util.*
import org.pkl.core.* import org.pkl.core.*
import org.pkl.core.util.CodeGeneratorUtils import org.pkl.core.util.CodeGeneratorUtils
import org.pkl.core.util.IoUtils
data class KotlinCodegenOptions( data class KotlinCodegenOptions(
/** The characters to use for indenting generated Kotlin code. */ /** The characters to use for indenting generated Kotlin code. */
@@ -89,7 +90,7 @@ class KotlinCodeGenerator(
private val propertyFileName: String private val propertyFileName: String
get() = get() =
"resources/META-INF/org/pkl/config/java/mapper/classes/${moduleSchema.moduleName}.properties" "resources/META-INF/org/pkl/config/java/mapper/classes/${IoUtils.encodePath(moduleSchema.moduleName)}.properties"
private val propertiesFile: String private val propertiesFile: String
get() { get() {
@@ -195,7 +196,7 @@ class KotlinCodeGenerator(
} }
private fun relativeOutputPathFor(moduleName: String): String { private fun relativeOutputPathFor(moduleName: String): String {
val nameParts = moduleName.split(".") val nameParts = moduleName.split(".").map(IoUtils::encodePath)
val dirPath = nameParts.dropLast(1).joinToString("/") val dirPath = nameParts.dropLast(1).joinToString("/")
val fileName = nameParts.last().replaceFirstChar { it.titlecaseChar() } val fileName = nameParts.last().replaceFirstChar { it.titlecaseChar() }
return if (dirPath.isEmpty()) { return if (dirPath.isEmpty()) {

View File

@@ -1501,6 +1501,24 @@ class KotlinCodeGeneratorTest {
confirmSerDe(bigStruct) confirmSerDe(bigStruct)
} }
@Test
fun `encoded file paths`(@TempDir path: Path) {
val kotlinCode =
generateKotlinFiles(
path,
PklModule(
"FooBar.pkl",
"""
module `Foo*Bar`
someProp: String
"""
.trimIndent()
)
)
assertThat(kotlinCode).containsKey("kotlin/Foo(2a)Bar.kt")
}
private fun generateFiles(tempDir: Path, vararg pklModules: PklModule): Map<String, String> { private fun generateFiles(tempDir: Path, vararg pklModules: PklModule): Map<String, String> {
val pklFiles = pklModules.map { it.writeToDisk(tempDir.resolve("pkl/${it.name}.pkl")) } val pklFiles = pklModules.map { it.writeToDisk(tempDir.resolve("pkl/${it.name}.pkl")) }
val evaluator = Evaluator.preconfigured() val evaluator = Evaluator.preconfigured()

View File

@@ -51,7 +51,14 @@ class PackageServer : AutoCloseable {
const val FRUIT_1_1_SHA = "8d982761d182f2185e4180c82190791d9a60c721cb3393bb2e946fab90131e8c" const val FRUIT_1_1_SHA = "8d982761d182f2185e4180c82190791d9a60c721cb3393bb2e946fab90131e8c"
fun populateCacheDir(cacheDir: Path) { fun populateCacheDir(cacheDir: Path) {
val basePath = cacheDir.resolve("package-1/localhost:$PORT") doPopulateCacheDir(cacheDir.resolve("package-2/localhost(3a)$PORT"))
}
fun populateLegacyCacheDir(cacheDir: Path) {
doPopulateCacheDir(cacheDir.resolve("package-1/localhost:$PORT"))
}
private fun doPopulateCacheDir(basePath: Path) {
basePath.deleteRecursively() basePath.deleteRecursively()
Files.walk(packagesDir).use { stream -> Files.walk(packagesDir).use { stream ->
stream.forEach { source -> stream.forEach { source ->

View File

@@ -22,6 +22,7 @@ import java.util.Properties;
import java.util.Set; import java.util.Set;
import org.pkl.config.java.InvalidMappingException; import org.pkl.config.java.InvalidMappingException;
import org.pkl.core.PClassInfo; import org.pkl.core.PClassInfo;
import org.pkl.core.util.IoUtils;
import org.pkl.core.util.Nullable; import org.pkl.core.util.Nullable;
/** /**
@@ -77,7 +78,7 @@ public class ClassRegistry {
loadedModules.add(pklModuleName); loadedModules.add(pklModuleName);
var url = var url =
ClassRegistry.class.getResourceAsStream( ClassRegistry.class.getResourceAsStream(
CLASSES_DIRECTORY + "/" + pklModuleName + ".properties"); CLASSES_DIRECTORY + "/" + IoUtils.encodePath(pklModuleName) + ".properties");
if (url == null) { if (url == null) {
return; return;
} }

View File

@@ -413,7 +413,7 @@ final class PackageResolvers {
private final Path tmpDir; private final Path tmpDir;
private static final String CACHE_DIR_PREFIX = "package-1"; private static final String CACHE_DIR_PREFIX = "package-2";
@GuardedBy("lock") @GuardedBy("lock")
private final EconomicMap<PackageUri, FileSystem> fileSystems = EconomicMaps.create(); private final EconomicMap<PackageUri, FileSystem> fileSystems = EconomicMaps.create();
@@ -438,12 +438,14 @@ final class PackageResolvers {
return path; return path;
} }
var checksumIdx = path.lastIndexOf("::"); var checksumIdx = path.lastIndexOf("::");
return path.substring(0, checksumIdx); return IoUtils.encodePath(path.substring(0, checksumIdx));
} }
private Path getRelativePath(PackageUri uri) { private Path getRelativePath(PackageUri uri) {
return Path.of( return Path.of(
CACHE_DIR_PREFIX, uri.getUri().getAuthority(), getEffectivePackageUriPath(uri)); CACHE_DIR_PREFIX,
IoUtils.encodePath(uri.getUri().getAuthority()),
getEffectivePackageUriPath(uri));
} }
private String getLastSegmentName(PackageUri packageUri) { private String getLastSegmentName(PackageUri packageUri) {

View File

@@ -597,6 +597,30 @@ public final class IoUtils {
return newUri; return newUri;
} }
/**
* Windows reserves characters {@code <>:"\|?*} in filenames.
*
* <p>For any such characters, enclose their decimal character code with parentheses. Verbatim
* {@code (} is encoded as {@code ((}.
*/
public static String encodePath(String path) {
if (path.isEmpty()) return path;
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));
}
}
return sb.toString();
}
private static int getExclamationMarkIndex(String jarUri) { private static int getExclamationMarkIndex(String jarUri) {
var index = jarUri.indexOf('!'); var index = jarUri.indexOf('!');
if (index == -1) { if (index == -1) {

View File

@@ -431,4 +431,14 @@ class IoUtilsTest {
IoUtils.readString(URI("http://example.com").toURL()) IoUtils.readString(URI("http://example.com").toURL())
} }
} }
@Test
fun `encodePath encodes characters reserved on windows`() {
assertThat(IoUtils.encodePath("foo:bar")).isEqualTo("foo(3a)bar")
assertThat(IoUtils.encodePath("<>:\"\\|?*")).isEqualTo("(3c)(3e)(3a)(22)(5c)(7c)(3f)(2a)")
assertThat(IoUtils.encodePath("foo(3a)bar")).isEqualTo("foo((3a)bar")
assertThat(IoUtils.encodePath("(")).isEqualTo("((")
assertThat(IoUtils.encodePath("3a)")).isEqualTo("3a)")
assertThat(IoUtils.encodePath("foo/bar/baz")).isEqualTo("foo/bar/baz")
}
} }

View File

@@ -121,7 +121,7 @@ class DocGenerator(
private fun createSymlinks(currentPackagesData: List<PackageData>) { private fun createSymlinks(currentPackagesData: List<PackageData>) {
for (packageData in currentPackagesData) { for (packageData in currentPackagesData) {
val basePath = outputDir.resolve(packageData.ref.pkg) val basePath = outputDir.resolve(packageData.ref.pkg.pathEncoded)
val src = basePath.resolve(packageData.ref.version) val src = basePath.resolve(packageData.ref.version)
val dest = basePath.resolve("current") val dest = basePath.resolve("current")
if (dest.exists() && dest.isSameFileAs(src)) continue if (dest.exists() && dest.isSameFileAs(src)) continue

View File

@@ -203,9 +203,9 @@ data class DocPackageInfo(
when { when {
!moduleName.startsWith(prefix) -> null !moduleName.startsWith(prefix) -> null
else -> { else -> {
val modulePath = moduleName.substring(prefix.length).replace('.', '/') val modulePath = moduleName.substring(prefix.length).replace('.', '/').pathEncoded
if (documentation == null) { if (documentation == null) {
"$name/$version/$modulePath/index.html".toUri() "${name.pathEncoded}/$version/$modulePath/index.html".toUri()
} else { } else {
documentation.resolve("$modulePath/index.html") documentation.resolve("$modulePath/index.html")
} }

View File

@@ -71,10 +71,7 @@ internal sealed class DocScope {
fun resolveModuleNameToRelativeDocUrl(name: String): URI? = fun resolveModuleNameToRelativeDocUrl(name: String): URI? =
resolveModuleNameToDocUrl(name)?.let { IoUtils.relativize(it, url) } resolveModuleNameToDocUrl(name)?.let { IoUtils.relativize(it, url) }
abstract fun resolveModuleNameToSourceUrl( abstract fun resolveModuleNameToSourceUrl(name: String, sourceLocation: SourceLocation): URI?
name: String,
sourceLocation: Member.SourceLocation
): URI?
/** Resolves the given method name relative to this scope. */ /** Resolves the given method name relative to this scope. */
abstract fun resolveMethod(name: String): MethodScope? abstract fun resolveMethod(name: String): MethodScope?
@@ -207,10 +204,7 @@ internal class SiteScope(
else -> null else -> null
} }
override fun resolveModuleNameToSourceUrl( override fun resolveModuleNameToSourceUrl(name: String, sourceLocation: SourceLocation): URI? =
name: String,
sourceLocation: Member.SourceLocation
): URI? =
when { when {
name.startsWith("pkl.") -> { name.startsWith("pkl.") -> {
val path = "/stdlib/${name.substring(4)}.pkl" val path = "/stdlib/${name.substring(4)}.pkl"
@@ -253,7 +247,9 @@ internal class PackageScope(
private val moduleScopes: Map<String, ModuleScope> by lazy { private val moduleScopes: Map<String, ModuleScope> by lazy {
modules.associate { module -> modules.associate { module ->
val docUrl = val docUrl =
url.resolve(getModulePath(module.moduleName, modulePrefix).uriEncoded + "/index.html") url.resolve(
getModulePath(module.moduleName, modulePrefix).pathEncoded.uriEncoded + "/index.html"
)
module.moduleName to ModuleScope(module, docUrl, this) module.moduleName to ModuleScope(module, docUrl, this)
} }
} }
@@ -262,9 +258,11 @@ internal class PackageScope(
ModuleScope(pklBaseModule, resolveModuleNameToDocUrl("pkl.base")!!, null) ModuleScope(pklBaseModule, resolveModuleNameToDocUrl("pkl.base")!!, null)
} }
override val url: URI by lazy { parent.url.resolve("./$name/$version/index.html") } override val url: URI by lazy { parent.url.resolve("./${name.pathEncoded}/$version/index.html") }
override val dataUrl: URI by lazy { parent.url.resolve("./data/$name/$version/index.js") } override val dataUrl: URI by lazy {
parent.url.resolve("./data/${name.pathEncoded}/$version/index.js")
}
fun getModule(name: String): ModuleScope = moduleScopes.getValue(name) fun getModule(name: String): ModuleScope = moduleScopes.getValue(name)
@@ -387,11 +385,11 @@ internal class ClassScope(
override val url: URI by lazy { override val url: URI by lazy {
// `isModuleClass` distinction is relevant when this scope is a link target // `isModuleClass` distinction is relevant when this scope is a link target
if (clazz.isModuleClass) parentUrl if (clazz.isModuleClass) parentUrl
else parentUrl.resolve("${clazz.simpleName.uriEncodedComponent}.html") else parentUrl.resolve("${clazz.simpleName.pathEncoded.uriEncodedComponent}.html")
} }
override val dataUrl: URI by lazy { override val dataUrl: URI by lazy {
parent!!.dataUrl.resolve("${clazz.simpleName.uriEncodedComponent}.js") parent!!.dataUrl.resolve("${clazz.simpleName.pathEncoded.uriEncodedComponent}.js")
} }
override fun getMethod(name: String): MethodScope? = override fun getMethod(name: String): MethodScope? =
@@ -403,10 +401,8 @@ internal class ClassScope(
override fun resolveModuleNameToDocUrl(name: String): URI? = override fun resolveModuleNameToDocUrl(name: String): URI? =
parent!!.resolveModuleNameToDocUrl(name) parent!!.resolveModuleNameToDocUrl(name)
override fun resolveModuleNameToSourceUrl( override fun resolveModuleNameToSourceUrl(name: String, sourceLocation: SourceLocation): URI? =
name: String, parent!!.resolveModuleNameToSourceUrl(name, sourceLocation)
sourceLocation: Member.SourceLocation
): URI? = parent!!.resolveModuleNameToSourceUrl(name, sourceLocation)
override fun resolveMethod(name: String): MethodScope? = override fun resolveMethod(name: String): MethodScope? =
clazz.methods[name]?.let { MethodScope(it, this) } clazz.methods[name]?.let { MethodScope(it, this) }
@@ -438,7 +434,7 @@ internal class TypeAliasScope(
// only used for page scopes // only used for page scopes
throw UnsupportedOperationException("resolveModuleNameToDocUrl") throw UnsupportedOperationException("resolveModuleNameToDocUrl")
override fun resolveModuleNameToSourceUrl(name: String, sourceLocation: Member.SourceLocation) = override fun resolveModuleNameToSourceUrl(name: String, sourceLocation: SourceLocation) =
// only used for page scopes // only used for page scopes
throw UnsupportedOperationException("resolveModuleNameToSourceUrl") throw UnsupportedOperationException("resolveModuleNameToSourceUrl")
@@ -464,7 +460,7 @@ internal class MethodScope(val method: PClass.Method, override val parent: DocSc
// only used for page scopes // only used for page scopes
throw UnsupportedOperationException("resolveModuleNameToDocUrl") throw UnsupportedOperationException("resolveModuleNameToDocUrl")
override fun resolveModuleNameToSourceUrl(name: String, sourceLocation: Member.SourceLocation) = override fun resolveModuleNameToSourceUrl(name: String, sourceLocation: SourceLocation) =
// only used for page scopes // only used for page scopes
throw UnsupportedOperationException("resolveModuleNameToSourceUrl") throw UnsupportedOperationException("resolveModuleNameToSourceUrl")
@@ -494,7 +490,7 @@ internal class PropertyScope(
// only used for page scopes // only used for page scopes
throw UnsupportedOperationException("resolveModuleNameToDocUrl") throw UnsupportedOperationException("resolveModuleNameToDocUrl")
override fun resolveModuleNameToSourceUrl(name: String, sourceLocation: Member.SourceLocation) = override fun resolveModuleNameToSourceUrl(name: String, sourceLocation: SourceLocation) =
// only used for page scopes // only used for page scopes
throw UnsupportedOperationException("resolveModuleNameToSourceUrl") throw UnsupportedOperationException("resolveModuleNameToSourceUrl")
@@ -525,7 +521,7 @@ internal class ParameterScope(val name: String, override val parent: DocScope) :
// only used for page scopes // only used for page scopes
throw UnsupportedOperationException("resolveModuleNameToDocUrl") throw UnsupportedOperationException("resolveModuleNameToDocUrl")
override fun resolveModuleNameToSourceUrl(name: String, sourceLocation: Member.SourceLocation) = override fun resolveModuleNameToSourceUrl(name: String, sourceLocation: SourceLocation) =
// only used for page scopes // only used for page scopes
throw UnsupportedOperationException("resolveModuleNameToSourceUrl") throw UnsupportedOperationException("resolveModuleNameToSourceUrl")

View File

@@ -37,9 +37,11 @@ import org.pkl.core.util.IoUtils
internal class PackageDataGenerator(private val outputDir: Path) { internal class PackageDataGenerator(private val outputDir: Path) {
fun generate(pkg: DocPackage) { fun generate(pkg: DocPackage) {
val path = val path =
outputDir.resolve(pkg.name).resolve(pkg.version).resolve("package-data.json").apply { outputDir
createParentDirectories() .resolve(pkg.name.pathEncoded)
} .resolve(pkg.version)
.resolve("package-data.json")
.apply { createParentDirectories() }
PackageData(pkg).write(path) PackageData(pkg).write(path)
} }

View File

@@ -82,60 +82,71 @@ internal class RuntimeDataGenerator(
} }
private fun writePackageFile(ref: PackageRef) { private fun writePackageFile(ref: PackageRef) {
outputDir.resolve("data/${ref.pkg}/${ref.version}/index.js").jsonWriter().use { writer -> outputDir
writer.isLenient = true .resolve("data/${ref.pkg.pathEncoded}/${ref.version.pathEncoded}/index.js")
writer.writeLinks( .jsonWriter()
HtmlConstants.KNOWN_VERSIONS, .use { writer ->
packageVersions.getOrDefault(ref.pkg, setOf()).sortedWith(descendingVersionComparator), writer.isLenient = true
{ it }, writer.writeLinks(
{ if (it == ref.version) null else ref.copy(version = it).pageUrlRelativeTo(ref) }, HtmlConstants.KNOWN_VERSIONS,
{ if (it == ref.version) CssConstants.CURRENT_VERSION else null } packageVersions.getOrDefault(ref.pkg, setOf()).sortedWith(descendingVersionComparator),
) { it },
writer.writeLinks( { if (it == ref.version) null else ref.copy(version = it).pageUrlRelativeTo(ref) },
HtmlConstants.KNOWN_USAGES, { if (it == ref.version) CssConstants.CURRENT_VERSION else null }
packageUsages.getOrDefault(ref, setOf()).packagesWithHighestVersions().sortedBy { it.pkg }, )
PackageRef::pkg, writer.writeLinks(
{ it.pageUrlRelativeTo(ref) }, HtmlConstants.KNOWN_USAGES,
{ null } packageUsages.getOrDefault(ref, setOf()).packagesWithHighestVersions().sortedBy {
) it.pkg
} },
PackageRef::pkg,
{ it.pageUrlRelativeTo(ref) },
{ null }
)
}
} }
private fun writeModuleFile(ref: ModuleRef) { private fun writeModuleFile(ref: ModuleRef) {
outputDir.resolve("data/${ref.pkg}/${ref.version}/${ref.module}/index.js").jsonWriter().use { outputDir
writer -> .resolve(
writer.isLenient = true "data/${ref.pkg.pathEncoded}/${ref.version.pathEncoded}/${ref.module.pathEncoded}/index.js"
writer.writeLinks(
HtmlConstants.KNOWN_VERSIONS,
moduleVersions.getOrDefault(ref.id, setOf()).sortedWith(descendingVersionComparator),
{ it },
{ if (it == ref.version) null else ref.copy(version = it).pageUrlRelativeTo(ref) },
{ if (it == ref.version) CssConstants.CURRENT_VERSION else null }
) )
writer.writeLinks( .jsonWriter()
HtmlConstants.KNOWN_USAGES, .use { writer ->
typeUsages.getOrDefault(ref.moduleClassRef, setOf()).typesWithHighestVersions().sortedBy { writer.isLenient = true
it.displayName writer.writeLinks(
}, HtmlConstants.KNOWN_VERSIONS,
TypeRef::displayName, moduleVersions.getOrDefault(ref.id, setOf()).sortedWith(descendingVersionComparator),
{ it.pageUrlRelativeTo(ref) }, { it },
{ null } { if (it == ref.version) null else ref.copy(version = it).pageUrlRelativeTo(ref) },
) { if (it == ref.version) CssConstants.CURRENT_VERSION else null }
writer.writeLinks( )
HtmlConstants.KNOWN_SUBTYPES, writer.writeLinks(
subtypes.getOrDefault(ref.moduleClassRef, setOf()).typesWithHighestVersions().sortedBy { HtmlConstants.KNOWN_USAGES,
it.displayName typeUsages.getOrDefault(ref.moduleClassRef, setOf()).typesWithHighestVersions().sortedBy {
}, it.displayName
TypeRef::displayName, },
{ it.pageUrlRelativeTo(ref) }, TypeRef::displayName,
{ null } { it.pageUrlRelativeTo(ref) },
) { null }
} )
writer.writeLinks(
HtmlConstants.KNOWN_SUBTYPES,
subtypes.getOrDefault(ref.moduleClassRef, setOf()).typesWithHighestVersions().sortedBy {
it.displayName
},
TypeRef::displayName,
{ it.pageUrlRelativeTo(ref) },
{ null }
)
}
} }
private fun writeClassFile(ref: TypeRef) { private fun writeClassFile(ref: TypeRef) {
outputDir outputDir
.resolve("data/${ref.pkg}/${ref.version}/${ref.module}/${ref.type}.js") .resolve(
"data/${ref.pkg.pathEncoded}/${ref.version.pathEncoded}/${ref.module.pathEncoded}/${ref.type.pathEncoded}.js"
)
.jsonWriter() .jsonWriter()
.use { writer -> .use { writer ->
writer.isLenient = true writer.isLenient = true

View File

@@ -64,7 +64,7 @@ internal class SearchIndexGenerator(private val outputDir: Path) {
fun generate(docPackage: DocPackage) { fun generate(docPackage: DocPackage) {
val path = val path =
outputDir outputDir
.resolve("${docPackage.name}/${docPackage.version}/search-index.js") .resolve("${docPackage.name.pathEncoded}/${docPackage.version}/search-index.js")
.createParentDirectories() .createParentDirectories()
JsonWriter(path.bufferedWriter()).use { writer -> JsonWriter(path.bufferedWriter()).use { writer ->
writer.apply { writer.apply {

View File

@@ -17,14 +17,13 @@ package org.pkl.doc
import java.io.InputStream import java.io.InputStream
import java.net.URI import java.net.URI
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import java.nio.file.Path import java.nio.file.Path
import kotlin.io.path.bufferedWriter import kotlin.io.path.bufferedWriter
import kotlin.io.path.outputStream import kotlin.io.path.outputStream
import org.pkl.commons.createParentDirectories import org.pkl.commons.createParentDirectories
import org.pkl.core.* import org.pkl.core.*
import org.pkl.core.parser.Lexer import org.pkl.core.parser.Lexer
import org.pkl.core.util.IoUtils
import org.pkl.core.util.json.JsonWriter import org.pkl.core.util.json.JsonWriter
// overwrites any existing file // overwrites any existing file
@@ -148,3 +147,6 @@ internal val String.asModuleName: String
internal val String.asIdentifier: String internal val String.asIdentifier: String
get() = Lexer.maybeQuoteIdentifier(this) get() = Lexer.maybeQuoteIdentifier(this)
internal val String.pathEncoded
get(): String = IoUtils.encodePath(this)

View File

@@ -114,7 +114,7 @@ age: Int
<div class="member-modifiers">package </div> <div class="member-modifiers">package </div>
</div> </div>
<div class="member-main"> <div class="member-main">
<div class="member-signature"><a class="name-decl" href="./localhost:0/birds/current/index.html">localhost:0/birds</a></div> <div class="member-signature"><a class="name-decl" href="./localhost(3a)0/birds/current/index.html">localhost:0/birds</a></div>
</div> </div>
</div> </div>
</li> </li>
@@ -125,7 +125,7 @@ age: Int
<div class="member-modifiers">package </div> <div class="member-modifiers">package </div>
</div> </div>
<div class="member-main"> <div class="member-main">
<div class="member-signature"><a class="name-decl" href="./localhost:0/fruit/current/index.html">localhost:0/fruit</a></div> <div class="member-signature"><a class="name-decl" href="./localhost(3a)0/fruit/current/index.html">localhost:0/fruit</a></div>
</div> </div>
</div> </div>
</li> </li>

View File

@@ -4,7 +4,7 @@
<title>Bird (localhost:0/birds:0.5.0) • Docsite Title</title> <title>Bird (localhost:0/birds:0.5.0) • Docsite Title</title>
<script src="../../../../scripts/pkldoc.js" defer="defer"></script> <script src="../../../../scripts/pkldoc.js" defer="defer"></script>
<script src="../../../../scripts/scroll-into-view.min.js" defer="defer"></script> <script src="../../../../scripts/scroll-into-view.min.js" defer="defer"></script>
<script src="../../../../data/localhost:0/birds/0.5.0/Bird/index.js" defer="defer"></script> <script src="../../../../data/localhost(3a)0/birds/0.5.0/Bird/index.js" defer="defer"></script>
<link href="../../../../styles/pkldoc.css" media="screen" type="text/css" rel="stylesheet"> <link href="../../../../styles/pkldoc.css" media="screen" type="text/css" rel="stylesheet">
<link rel="icon" type="image/svg+xml" href="../../../../images/favicon.svg"> <link rel="icon" type="image/svg+xml" href="../../../../images/favicon.svg">
<link rel="apple-touch-icon" sizes="180x180" href="../../../../images/apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="../../../../images/apple-touch-icon.png">

View File

@@ -4,7 +4,7 @@
<title>allFruit (localhost:0/birds:0.5.0) • Docsite Title</title> <title>allFruit (localhost:0/birds:0.5.0) • Docsite Title</title>
<script src="../../../../scripts/pkldoc.js" defer="defer"></script> <script src="../../../../scripts/pkldoc.js" defer="defer"></script>
<script src="../../../../scripts/scroll-into-view.min.js" defer="defer"></script> <script src="../../../../scripts/scroll-into-view.min.js" defer="defer"></script>
<script src="../../../../data/localhost:0/birds/0.5.0/allFruit/index.js" defer="defer"></script> <script src="../../../../data/localhost(3a)0/birds/0.5.0/allFruit/index.js" defer="defer"></script>
<link href="../../../../styles/pkldoc.css" media="screen" type="text/css" rel="stylesheet"> <link href="../../../../styles/pkldoc.css" media="screen" type="text/css" rel="stylesheet">
<link rel="icon" type="image/svg+xml" href="../../../../images/favicon.svg"> <link rel="icon" type="image/svg+xml" href="../../../../images/favicon.svg">
<link rel="apple-touch-icon" sizes="180x180" href="../../../../images/apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="../../../../images/apple-touch-icon.png">

View File

@@ -4,7 +4,7 @@
<title>catalog (localhost:0/birds:0.5.0) • Docsite Title</title> <title>catalog (localhost:0/birds:0.5.0) • Docsite Title</title>
<script src="../../../../scripts/pkldoc.js" defer="defer"></script> <script src="../../../../scripts/pkldoc.js" defer="defer"></script>
<script src="../../../../scripts/scroll-into-view.min.js" defer="defer"></script> <script src="../../../../scripts/scroll-into-view.min.js" defer="defer"></script>
<script src="../../../../data/localhost:0/birds/0.5.0/catalog/index.js" defer="defer"></script> <script src="../../../../data/localhost(3a)0/birds/0.5.0/catalog/index.js" defer="defer"></script>
<link href="../../../../styles/pkldoc.css" media="screen" type="text/css" rel="stylesheet"> <link href="../../../../styles/pkldoc.css" media="screen" type="text/css" rel="stylesheet">
<link rel="icon" type="image/svg+xml" href="../../../../images/favicon.svg"> <link rel="icon" type="image/svg+xml" href="../../../../images/favicon.svg">
<link rel="apple-touch-icon" sizes="180x180" href="../../../../images/apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="../../../../images/apple-touch-icon.png">

View File

@@ -4,7 +4,7 @@
<title>localhost:0/birds (0.5.0) • Docsite Title</title> <title>localhost:0/birds (0.5.0) • Docsite Title</title>
<script src="../../../scripts/pkldoc.js" defer="defer"></script> <script src="../../../scripts/pkldoc.js" defer="defer"></script>
<script src="../../../scripts/scroll-into-view.min.js" defer="defer"></script> <script src="../../../scripts/scroll-into-view.min.js" defer="defer"></script>
<script src="../../../data/localhost:0/birds/0.5.0/index.js" defer="defer"></script> <script src="../../../data/localhost(3a)0/birds/0.5.0/index.js" defer="defer"></script>
<link href="../../../styles/pkldoc.css" media="screen" type="text/css" rel="stylesheet"> <link href="../../../styles/pkldoc.css" media="screen" type="text/css" rel="stylesheet">
<link rel="icon" type="image/svg+xml" href="../../../images/favicon.svg"> <link rel="icon" type="image/svg+xml" href="../../../images/favicon.svg">
<link rel="apple-touch-icon" sizes="180x180" href="../../../images/apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="../../../images/apple-touch-icon.png">

View File

@@ -4,7 +4,7 @@
<title>Fruit (localhost:0/fruit:1.1.0) • Docsite Title</title> <title>Fruit (localhost:0/fruit:1.1.0) • Docsite Title</title>
<script src="../../../../scripts/pkldoc.js" defer="defer"></script> <script src="../../../../scripts/pkldoc.js" defer="defer"></script>
<script src="../../../../scripts/scroll-into-view.min.js" defer="defer"></script> <script src="../../../../scripts/scroll-into-view.min.js" defer="defer"></script>
<script src="../../../../data/localhost:0/fruit/1.1.0/Fruit/index.js" defer="defer"></script> <script src="../../../../data/localhost(3a)0/fruit/1.1.0/Fruit/index.js" defer="defer"></script>
<link href="../../../../styles/pkldoc.css" media="screen" type="text/css" rel="stylesheet"> <link href="../../../../styles/pkldoc.css" media="screen" type="text/css" rel="stylesheet">
<link rel="icon" type="image/svg+xml" href="../../../../images/favicon.svg"> <link rel="icon" type="image/svg+xml" href="../../../../images/favicon.svg">
<link rel="apple-touch-icon" sizes="180x180" href="../../../../images/apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="../../../../images/apple-touch-icon.png">

View File

@@ -4,7 +4,7 @@
<title>localhost:0/fruit (1.1.0) • Docsite Title</title> <title>localhost:0/fruit (1.1.0) • Docsite Title</title>
<script src="../../../scripts/pkldoc.js" defer="defer"></script> <script src="../../../scripts/pkldoc.js" defer="defer"></script>
<script src="../../../scripts/scroll-into-view.min.js" defer="defer"></script> <script src="../../../scripts/scroll-into-view.min.js" defer="defer"></script>
<script src="../../../data/localhost:0/fruit/1.1.0/index.js" defer="defer"></script> <script src="../../../data/localhost(3a)0/fruit/1.1.0/index.js" defer="defer"></script>
<link href="../../../styles/pkldoc.css" media="screen" type="text/css" rel="stylesheet"> <link href="../../../styles/pkldoc.css" media="screen" type="text/css" rel="stylesheet">
<link rel="icon" type="image/svg+xml" href="../../../images/favicon.svg"> <link rel="icon" type="image/svg+xml" href="../../../images/favicon.svg">
<link rel="apple-touch-icon" sizes="180x180" href="../../../images/apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="../../../images/apple-touch-icon.png">

View File

@@ -2,8 +2,11 @@ package org.pkl.executor
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.condition.DisabledOnOs
import org.junit.jupiter.api.condition.OS
import org.junit.jupiter.api.io.TempDir import org.junit.jupiter.api.io.TempDir
import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource import org.junit.jupiter.params.provider.MethodSource
@@ -424,9 +427,11 @@ class EmbeddedExecutorTest {
@ParameterizedTest @ParameterizedTest
@MethodSource("getAllTestExecutors") @MethodSource("getAllTestExecutors")
@DisabledOnOs(OS.WINDOWS, disabledReason = "Can't populate legacy cache dir on Windows")
fun `evaluate a project dependency`(executor: TestExecutor, @TempDir tempDir: Path) { fun `evaluate a project dependency`(executor: TestExecutor, @TempDir tempDir: Path) {
val cacheDir = tempDir.resolve("packages") val cacheDir = tempDir.resolve("packages")
PackageServer.populateCacheDir(cacheDir) PackageServer.populateCacheDir(cacheDir)
PackageServer.populateLegacyCacheDir(cacheDir)
val projectDir = tempDir.resolve("project/") val projectDir = tempDir.resolve("project/")
projectDir.createDirectories() projectDir.createDirectories()
projectDir.resolve("PklProject").toFile().writeText(""" projectDir.resolve("PklProject").toFile().writeText("""