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:
Daniel Chao
2024-05-28 15:56:20 -07:00
committed by GitHub
parent 5e4ccfd4e8
commit 8ec06e631f
76 changed files with 905 additions and 402 deletions
@@ -12,7 +12,7 @@ facts {
}
["versionInfo"] {
current.versionInfo.contains("macOS") || current.versionInfo.contains("Linux")
current.versionInfo.contains("macOS") || current.versionInfo.contains("Linux") || current.versionInfo.contains("Windows")
}
["commitId"] {
@@ -0,0 +1,2 @@
// In all OSes, the directory separator is forward slash.
res = import(#"..\basic\baseModule.pkl"#)
@@ -0,0 +1,12 @@
–– Pkl Error ––
Cannot find module `file:///$snippetsDir/input/errors/..%5Cbasic%5CbaseModule.pkl`.
x | res = import(#"..\basic\baseModule.pkl"#)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at invalidImportBackslashSep#res (file:///$snippetsDir/input/errors/invalidImportBackslashSep.pkl)
To resolve modules in nested directories, use `/` as the directory separator.
xxx | text = renderer.renderDocument(value)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
at pkl.base#Module.output.text (pkl:base)
@@ -1,5 +1,5 @@
–– Pkl Error ––
Cannot resolve dependency because file `/$snippetsDir/input/projects/badProjectDeps1/PklProject.deps.json` is malformed.
Cannot resolve dependency because file `file:///$snippetsDir/input/projects/badProjectDeps1/PklProject.deps.json` is malformed.
Run `pkl project resolve` to re-create this file.
x | import "@bird/Bird.pkl"
@@ -1,5 +1,5 @@
–– Pkl Error ––
Cannot resolve dependency because file `/$snippetsDir/input/projects/badProjectDeps2/PklProject.deps.json` is malformed.
Cannot resolve dependency because file `file:///$snippetsDir/input/projects/badProjectDeps2/PklProject.deps.json` is malformed.
Run `pkl project resolve` to re-create this file.
x | import "@bird/Bird.pkl"
@@ -1,5 +1,5 @@
–– Pkl Error ––
Cannot resolve dependency because file `PklProject.deps.json` is missing in project directory `/$snippetsDir/input/projects/missingProjectDeps`.
Cannot resolve dependency because file `PklProject.deps.json` is missing in project directory `file:///$snippetsDir/input/projects/missingProjectDeps/`.
x | import "@birds/Bird.pkl"
^^^^^^^^^^^^^^^^^
@@ -74,11 +74,12 @@ class EvaluatorTest {
@Test
fun `evaluate non-existing file`() {
val file = File("/non/existing")
val e = assertThrows<PklException> {
evaluator.evaluate(file(File("/non/existing")))
evaluator.evaluate(file(file))
}
assertThat(e)
.hasMessageContaining("Cannot find module `file:///non/existing`.")
.hasMessageContaining("Cannot find module `${file.toPath().toUri()}`.")
}
@Test
@@ -92,13 +93,14 @@ class EvaluatorTest {
@Test
fun `evaluate non-existing path`() {
val path = "/non/existing".toPath()
val e = assertThrows<PklException> {
evaluator.evaluate(path("/non/existing".toPath()))
evaluator.evaluate(path(path))
}
assertThat(e)
.hasMessageContaining("Cannot find module `file:///non/existing`.")
.hasMessageContaining("Cannot find module `${path.toUri()}`.")
}
@Test
fun `evaluate zip file system path`(@TempDir tempDir: Path) {
val zipFile = createModulesZip(tempDir)
@@ -13,3 +13,6 @@ class LinuxLanguageSnippetTests
@Testable
class AlpineLanguageSnippetTests
@Testable
class WindowsLanguageSnippetTests
@@ -51,7 +51,7 @@ abstract class AbstractLanguageSnippetTestsEngine : InputOutputTestEngine() {
else parent?.getProjectDir()
override fun expectedOutputFileFor(inputFile: Path): Path {
val relativePath = inputDir.relativize(inputFile).toString()
val relativePath = IoUtils.relativize(inputFile, inputDir).toString()
val stdoutPath =
if (relativePath.matches(hiddenExtensionRegex)) relativePath.dropLast(4)
else relativePath.dropLast(3) + "pcf"
@@ -62,12 +62,12 @@ abstract class AbstractLanguageSnippetTestsEngine : InputOutputTestEngine() {
// disable SHA verification for packages
IoUtils.setTestMode()
}
override fun afterAll() {
packageServer.close()
}
protected fun String.stripFilePaths() = replace(snippetsDir.toString(), "/\$snippetsDir")
protected fun String.stripFilePaths() = replace(snippetsDir.toUri().toString(), "file:///\$snippetsDir/")
protected fun String.stripLineNumbers() = replace(lineNumberRegex) { result ->
// replace line number with equivalent number of 'x' characters to keep formatting intact
@@ -82,6 +82,11 @@ abstract class AbstractLanguageSnippetTestsEngine : InputOutputTestEngine() {
protected fun String.stripStdlibLocationSha(): String =
replace("https://github.com/apple/pkl/blob/${Release.current().commitId()}/stdlib/", "https://github.com/apple/pkl/blob/\$commitId/stdlib/")
protected fun String.withUnixLineEndings(): String {
return if (System.lineSeparator() == "\r\n") replace("\r\n", "\n")
else this
}
}
class LanguageSnippetTestsEngine : AbstractLanguageSnippetTestsEngine() {
@@ -143,7 +148,7 @@ class LanguageSnippetTestsEngine : AbstractLanguageSnippetTestsEngine() {
.stripVersionCheckErrorMessage()
}
val stderr = logWriter.toString()
val stderr = logWriter.toString().withUnixLineEndings()
return (success && stderr.isBlank()) to (output + stderr).stripFilePaths().stripWebsite().stripStdlibLocationSha()
}
@@ -216,7 +221,7 @@ abstract class AbstractNativeLanguageSnippetTestsEngine : AbstractLanguageSnippe
val process = builder.start()
return try {
val (out, err) = listOf(process.inputStream, process.errorStream)
.map { it.reader().readText() }
.map { it.reader().readText().withUnixLineEndings() }
val success = process.waitFor() == 0 && err.isBlank()
success to (out + err)
.stripFilePaths()
@@ -254,3 +259,8 @@ class AlpineLanguageSnippetTestsEngine : AbstractNativeLanguageSnippetTestsEngin
override val pklExecutablePath: Path = rootProjectDir.resolve("pkl-cli/build/executable/pkl-alpine-linux-amd64")
override val testClass: KClass<*> = AlpineLanguageSnippetTests::class
}
class WindowsLanguageSnippetTestsEngine : AbstractNativeLanguageSnippetTestsEngine() {
override val pklExecutablePath: Path = rootProjectDir.resolve("pkl-cli/build/executable/pkl-windows-amd64.exe")
override val testClass: KClass<*> = WindowsLanguageSnippetTests::class
}
@@ -181,11 +181,11 @@ class SecurityManagersTest {
rootDir
)
manager.checkResolveModule(URI("file:///foo/bar/baz.pkl"))
manager.checkReadResource(URI("file:///foo/bar/baz.pkl"))
manager.checkResolveModule(Path.of("/foo/bar/baz.pkl").toUri())
manager.checkReadResource(Path.of("/foo/bar/baz.pkl").toUri())
manager.checkResolveModule(URI("file:///foo/bar/qux/../baz.pkl"))
manager.checkReadResource(URI("file:///foo/bar/qux/../baz.pkl"))
manager.checkResolveModule(Path.of("/foo/bar/qux/../baz.pkl").toUri())
manager.checkReadResource(Path.of("/foo/bar/qux/../baz.pkl").toUri())
}
@Test
@@ -233,17 +233,17 @@ class SecurityManagersTest {
)
assertThrows<SecurityManagerException> {
manager.checkResolveModule(URI("file:///foo/baz.pkl"))
manager.checkResolveModule(Path.of("/foo/baz.pkl").toUri())
}
assertThrows<SecurityManagerException> {
manager.checkReadResource(URI("file:///foo/baz.pkl"))
manager.checkReadResource(Path.of("/foo/baz.pkl").toUri())
}
assertThrows<SecurityManagerException> {
manager.checkResolveModule(URI("file:///foo/bar/../baz.pkl"))
manager.checkResolveModule(Path.of("/foo/bar/../baz.pkl").toUri())
}
assertThrows<SecurityManagerException> {
manager.checkReadResource(URI("file:///foo/bar/../baz.pkl"))
manager.checkReadResource(Path.of("/foo/bar/../baz.pkl").toUri())
}
}
}
@@ -57,7 +57,7 @@ class ModuleKeysTest {
file.writeString("age = 40")
val uri = file.toUri()
val key = ModuleKeys.file(uri, file.toAbsolutePath())
val key = ModuleKeys.file(uri)
assertThat(key.uri).isEqualTo(uri)
assertThat(key.isCached).isTrue
@@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows
import org.pkl.commons.test.FileTestUtils
import org.pkl.commons.test.PackageServer
import org.pkl.commons.toPath
import org.pkl.core.http.HttpClient
import org.pkl.core.PklException
import org.pkl.core.SecurityManagers
@@ -34,7 +35,7 @@ class ProjectDependenciesResolverTest {
@Test
fun resolveDependencies() {
val project2Path = Path.of(javaClass.getResource("project2/PklProject")!!.path)
val project2Path = javaClass.getResource("project2/PklProject")!!.toURI().toPath()
val project = Project.loadFromPath(project2Path)
val packageResolver = PackageResolver.getInstance(SecurityManagers.defaultManager, httpClient, null)
val deps = ProjectDependenciesResolver(project, packageResolver, System.out.writer()).resolve()
@@ -72,7 +73,7 @@ class ProjectDependenciesResolverTest {
@Test
fun `fails if project declares a package with an incorrect checksum`() {
val projectPath = Path.of(javaClass.getResource("badProjectChecksum/PklProject")!!.path)
val projectPath = javaClass.getResource("badProjectChecksum/PklProject")!!.toURI().toPath()
val project = Project.loadFromPath(projectPath)
val packageResolver = PackageResolver.getInstance(SecurityManagers.defaultManager, httpClient, null)
val e = assertThrows<PklException> {
@@ -137,7 +137,7 @@ class ProjectTest {
@Test
fun `evaluate project module -- invalid checksum`() {
PackageServer().use { server ->
val projectDir = Path.of(javaClass.getResource("badProjectChecksum2/")!!.path)
val projectDir = Path.of(javaClass.getResource("badProjectChecksum2/")!!.toURI())
val project = Project.loadFromPath(projectDir.resolve("PklProject"))
val httpClient = HttpClient.builder()
.addCertificates(FileTestUtils.selfSignedCertificate)
@@ -117,70 +117,69 @@ class IoUtilsTest {
@Test
fun `relativize file URLs`() {
// perhaps URI("") would be a more precise result
assertThat(
IoUtils.relativize(
URI("file://foo/bar/baz.pkl"),
URI("file://foo/bar/baz.pkl")
URI("file:///foo/bar/baz.pkl"),
URI("file:///foo/bar/baz.pkl")
)
).isEqualTo(URI("baz.pkl"))
assertThat(
IoUtils.relativize(
URI("file://foo/bar/baz.pkl"),
URI("file://foo/bar/qux.pkl")
URI("file:///foo/bar/baz.pkl"),
URI("file:///foo/bar/qux.pkl")
)
).isEqualTo(URI("baz.pkl"))
assertThat(
IoUtils.relativize(
URI("file://foo/bar/baz.pkl"),
URI("file://foo/bar/")
URI("file:///foo/bar/baz.pkl"),
URI("file:///foo/bar/")
)
).isEqualTo(URI("baz.pkl"))
assertThat(
IoUtils.relativize(
URI("file://foo/bar/baz.pkl"),
URI("file://foo/bar")
URI("file:///foo/bar/baz.pkl"),
URI("file:///foo/bar")
)
).isEqualTo(URI("bar/baz.pkl"))
// URI.relativize() returns an absolute URI here
assertThat(
IoUtils.relativize(
URI("file://foo/bar/baz.pkl"),
URI("file://foo/qux/")
URI("file:///foo/bar/baz.pkl"),
URI("file:///foo/qux/")
)
).isEqualTo(URI("../bar/baz.pkl"))
assertThat(
IoUtils.relativize(
URI("file://foo/bar/baz.pkl"),
URI("file://foo/qux/qux2/")
URI("file:///foo/bar/baz.pkl"),
URI("file:///foo/qux/qux2/")
)
).isEqualTo(URI("../../bar/baz.pkl"))
assertThat(
IoUtils.relativize(
URI("file://foo/bar/baz.pkl"),
URI("file://foo/qux/qux2")
URI("file:///foo/bar/baz.pkl"),
URI("file:///foo/qux/qux2")
)
).isEqualTo(URI("../bar/baz.pkl"))
assertThat(
IoUtils.relativize(
URI("file://foo/bar/baz.pkl"),
URI("file://qux/qux2/")
URI("file:///foo/bar/baz.pkl"),
URI("file:///qux/qux2/")
)
).isEqualTo(URI("file://foo/bar/baz.pkl"))
).isEqualTo(URI("../../foo/bar/baz.pkl"))
assertThat(
IoUtils.relativize(
URI("file://foo/bar/baz.pkl"),
URI("https://foo/bar/baz.pkl")
URI("file:///foo/bar/baz.pkl"),
URI("https:///foo/bar/baz.pkl")
)
).isEqualTo(URI("file://foo/bar/baz.pkl"))
).isEqualTo(URI("file:///foo/bar/baz.pkl"))
}
@Test
@@ -343,7 +342,7 @@ class IoUtilsTest {
val file3 = tempDir.resolve("base1/dir2/foo.pkl").createParentDirectories().createFile()
val uri = file2.toUri()
val key = ModuleKeys.file(uri, file2)
val key = ModuleKeys.file(uri)
assertThat(IoUtils.resolve(FakeSecurityManager, key, URI("..."))).isEqualTo(file1.toUri())
assertThat(IoUtils.resolve(FakeSecurityManager, key, URI(".../foo.pkl"))).isEqualTo(file1.toUri())
@@ -4,3 +4,4 @@ org.pkl.core.MacAarch64LanguageSnippetTestsEngine
org.pkl.core.LinuxAmd64LanguageSnippetTestsEngine
org.pkl.core.LinuxAarch64LanguageSnippetTestsEngine
org.pkl.core.AlpineLanguageSnippetTestsEngine
org.pkl.core.WindowsLanguageSnippetTestsEngine