mirror of
https://github.com/apple/pkl.git
synced 2026-04-24 09:18:35 +02:00
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:
@@ -1164,7 +1164,7 @@ result = someLib.x
|
|||||||
"""
|
"""
|
||||||
.trimIndent()
|
.trimIndent()
|
||||||
)
|
)
|
||||||
assertThat(tempDir.resolve("package-1")).doesNotExist()
|
assertThat(tempDir.resolve("package-2")).doesNotExist()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 ->
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
@@ -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">
|
||||||
@@ -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">
|
||||||
@@ -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">
|
||||||
@@ -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">
|
||||||
@@ -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">
|
||||||
@@ -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("""
|
||||||
|
|||||||
Reference in New Issue
Block a user