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
+11 -1
View File
@@ -14,7 +14,7 @@
// limitations under the License. // limitations under the License.
//===----------------------------------------------------------------------===// //===----------------------------------------------------------------------===//
// File gets rendered to .circleci/config.yml via git hook. // File gets rendered to .circleci/config.yml via git hook.
amends "package://pkg.pkl-lang.org/pkl-project-commons/pkl.impl.circleci@1.1.0#/PklCI.pkl" amends "package://pkg.pkl-lang.org/pkl-project-commons/pkl.impl.circleci@1.1.1#/PklCI.pkl"
import "jobs/BuildNativeJob.pkl" import "jobs/BuildNativeJob.pkl"
import "jobs/GradleCheckJob.pkl" import "jobs/GradleCheckJob.pkl"
@@ -96,6 +96,11 @@ local buildNativeJobs: Mapping<String, BuildNativeJob> = new {
musl = true musl = true
isRelease = _dist == "release" isRelease = _dist == "release"
} }
["pkl-cli-windows-amd64-\(_dist)"] {
arch = "amd64"
os = "windows"
isRelease = _dist == "release"
}
} }
} }
@@ -108,6 +113,11 @@ local gradleCheckJobs: Mapping<String, GradleCheckJob> = new {
javaVersion = "21.0" javaVersion = "21.0"
isRelease = false isRelease = false
} }
["gradle-check-jdk17-windows"] {
javaVersion = "17.0"
isRelease = false
os = "windows"
}
} }
jobs { jobs {
+92 -112
View File
@@ -12,18 +12,12 @@ jobs:
- run: - run:
command: |- command: |-
export PATH=~/staticdeps/bin:$PATH export PATH=~/staticdeps/bin:$PATH
./gradlew --info --stacktrace -DreleaseBuild=true pkl-cli:macExecutableAmd64 pkl-core:testMacExecutableAmd64 ./gradlew --info --stacktrace -DtestReportsDir=${HOME}/test-results -DreleaseBuild=true pkl-cli:macExecutableAmd64 pkl-core:testMacExecutableAmd64
name: gradle buildNative name: gradle buildNative
- persist_to_workspace: - persist_to_workspace:
root: '.' root: '.'
paths: paths:
- pkl-cli/build/executable/ - pkl-cli/build/executable/
- run:
command: |-
mkdir ~/test-results/
find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \;
name: Gather test results
when: always
- store_test_results: - store_test_results:
path: ~/test-results path: ~/test-results
environment: environment:
@@ -94,18 +88,12 @@ jobs:
- run: - run:
command: |- command: |-
export PATH=~/staticdeps/bin:$PATH export PATH=~/staticdeps/bin:$PATH
./gradlew --info --stacktrace -DreleaseBuild=true pkl-cli:linuxExecutableAmd64 pkl-core:testLinuxExecutableAmd64 ./gradlew --info --stacktrace -DtestReportsDir=${HOME}/test-results -DreleaseBuild=true pkl-cli:linuxExecutableAmd64 pkl-core:testLinuxExecutableAmd64
name: gradle buildNative name: gradle buildNative
- persist_to_workspace: - persist_to_workspace:
root: '.' root: '.'
paths: paths:
- pkl-cli/build/executable/ - pkl-cli/build/executable/
- run:
command: |-
mkdir ~/test-results/
find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \;
name: Gather test results
when: always
- store_test_results: - store_test_results:
path: ~/test-results path: ~/test-results
environment: environment:
@@ -120,18 +108,12 @@ jobs:
- run: - run:
command: |- command: |-
export PATH=~/staticdeps/bin:$PATH export PATH=~/staticdeps/bin:$PATH
./gradlew --info --stacktrace -DreleaseBuild=true pkl-cli:macExecutableAarch64 pkl-core:testMacExecutableAarch64 ./gradlew --info --stacktrace -DtestReportsDir=${HOME}/test-results -DreleaseBuild=true pkl-cli:macExecutableAarch64 pkl-core:testMacExecutableAarch64
name: gradle buildNative name: gradle buildNative
- persist_to_workspace: - persist_to_workspace:
root: '.' root: '.'
paths: paths:
- pkl-cli/build/executable/ - pkl-cli/build/executable/
- run:
command: |-
mkdir ~/test-results/
find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \;
name: Gather test results
when: always
- store_test_results: - store_test_results:
path: ~/test-results path: ~/test-results
environment: environment:
@@ -186,18 +168,12 @@ jobs:
- run: - run:
command: |- command: |-
export PATH=~/staticdeps/bin:$PATH export PATH=~/staticdeps/bin:$PATH
./gradlew --info --stacktrace -DreleaseBuild=true pkl-cli:linuxExecutableAarch64 pkl-core:testLinuxExecutableAarch64 ./gradlew --info --stacktrace -DtestReportsDir=${HOME}/test-results -DreleaseBuild=true pkl-cli:linuxExecutableAarch64 pkl-core:testLinuxExecutableAarch64
name: gradle buildNative name: gradle buildNative
- persist_to_workspace: - persist_to_workspace:
root: '.' root: '.'
paths: paths:
- pkl-cli/build/executable/ - pkl-cli/build/executable/
- run:
command: |-
mkdir ~/test-results/
find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \;
name: Gather test results
when: always
- store_test_results: - store_test_results:
path: ~/test-results path: ~/test-results
environment: environment:
@@ -269,18 +245,12 @@ jobs:
- run: - run:
command: |- command: |-
export PATH=~/staticdeps/bin:$PATH export PATH=~/staticdeps/bin:$PATH
./gradlew --info --stacktrace -DreleaseBuild=true pkl-cli:alpineExecutableAmd64 pkl-core:testAlpineExecutableAmd64 ./gradlew --info --stacktrace -DtestReportsDir=${HOME}/test-results -DreleaseBuild=true pkl-cli:alpineExecutableAmd64 pkl-core:testAlpineExecutableAmd64
name: gradle buildNative name: gradle buildNative
- persist_to_workspace: - persist_to_workspace:
root: '.' root: '.'
paths: paths:
- pkl-cli/build/executable/ - pkl-cli/build/executable/
- run:
command: |-
mkdir ~/test-results/
find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \;
name: Gather test results
when: always
- store_test_results: - store_test_results:
path: ~/test-results path: ~/test-results
environment: environment:
@@ -289,6 +259,26 @@ jobs:
resource_class: xlarge resource_class: xlarge
docker: docker:
- image: oraclelinux:8-slim - image: oraclelinux:8-slim
pkl-cli-windows-amd64-release:
steps:
- checkout
- run:
command: |-
export PATH=~/staticdeps/bin:$PATH
./gradlew --info --stacktrace -DtestReportsDir=${HOME}/test-results -DreleaseBuild=true pkl-cli:windowsExecutableAmd64 pkl-core:testWindowsExecutableAmd64
name: gradle buildNative
shell: bash.exe
- persist_to_workspace:
root: '.'
paths:
- pkl-cli/build/executable/
- store_test_results:
path: ~/test-results
environment:
LANG: en_US.UTF-8
resource_class: windows.large
machine:
image: windows-server-2022-gui:current
pkl-cli-macOS-amd64-snapshot: pkl-cli-macOS-amd64-snapshot:
steps: steps:
- checkout - checkout
@@ -298,18 +288,12 @@ jobs:
- run: - run:
command: |- command: |-
export PATH=~/staticdeps/bin:$PATH export PATH=~/staticdeps/bin:$PATH
./gradlew --info --stacktrace pkl-cli:macExecutableAmd64 pkl-core:testMacExecutableAmd64 ./gradlew --info --stacktrace -DtestReportsDir=${HOME}/test-results pkl-cli:macExecutableAmd64 pkl-core:testMacExecutableAmd64
name: gradle buildNative name: gradle buildNative
- persist_to_workspace: - persist_to_workspace:
root: '.' root: '.'
paths: paths:
- pkl-cli/build/executable/ - pkl-cli/build/executable/
- run:
command: |-
mkdir ~/test-results/
find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \;
name: Gather test results
when: always
- store_test_results: - store_test_results:
path: ~/test-results path: ~/test-results
environment: environment:
@@ -380,18 +364,12 @@ jobs:
- run: - run:
command: |- command: |-
export PATH=~/staticdeps/bin:$PATH export PATH=~/staticdeps/bin:$PATH
./gradlew --info --stacktrace pkl-cli:linuxExecutableAmd64 pkl-core:testLinuxExecutableAmd64 ./gradlew --info --stacktrace -DtestReportsDir=${HOME}/test-results pkl-cli:linuxExecutableAmd64 pkl-core:testLinuxExecutableAmd64
name: gradle buildNative name: gradle buildNative
- persist_to_workspace: - persist_to_workspace:
root: '.' root: '.'
paths: paths:
- pkl-cli/build/executable/ - pkl-cli/build/executable/
- run:
command: |-
mkdir ~/test-results/
find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \;
name: Gather test results
when: always
- store_test_results: - store_test_results:
path: ~/test-results path: ~/test-results
environment: environment:
@@ -406,18 +384,12 @@ jobs:
- run: - run:
command: |- command: |-
export PATH=~/staticdeps/bin:$PATH export PATH=~/staticdeps/bin:$PATH
./gradlew --info --stacktrace pkl-cli:macExecutableAarch64 pkl-core:testMacExecutableAarch64 ./gradlew --info --stacktrace -DtestReportsDir=${HOME}/test-results pkl-cli:macExecutableAarch64 pkl-core:testMacExecutableAarch64
name: gradle buildNative name: gradle buildNative
- persist_to_workspace: - persist_to_workspace:
root: '.' root: '.'
paths: paths:
- pkl-cli/build/executable/ - pkl-cli/build/executable/
- run:
command: |-
mkdir ~/test-results/
find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \;
name: Gather test results
when: always
- store_test_results: - store_test_results:
path: ~/test-results path: ~/test-results
environment: environment:
@@ -472,18 +444,12 @@ jobs:
- run: - run:
command: |- command: |-
export PATH=~/staticdeps/bin:$PATH export PATH=~/staticdeps/bin:$PATH
./gradlew --info --stacktrace pkl-cli:linuxExecutableAarch64 pkl-core:testLinuxExecutableAarch64 ./gradlew --info --stacktrace -DtestReportsDir=${HOME}/test-results pkl-cli:linuxExecutableAarch64 pkl-core:testLinuxExecutableAarch64
name: gradle buildNative name: gradle buildNative
- persist_to_workspace: - persist_to_workspace:
root: '.' root: '.'
paths: paths:
- pkl-cli/build/executable/ - pkl-cli/build/executable/
- run:
command: |-
mkdir ~/test-results/
find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \;
name: Gather test results
when: always
- store_test_results: - store_test_results:
path: ~/test-results path: ~/test-results
environment: environment:
@@ -555,18 +521,12 @@ jobs:
- run: - run:
command: |- command: |-
export PATH=~/staticdeps/bin:$PATH export PATH=~/staticdeps/bin:$PATH
./gradlew --info --stacktrace pkl-cli:alpineExecutableAmd64 pkl-core:testAlpineExecutableAmd64 ./gradlew --info --stacktrace -DtestReportsDir=${HOME}/test-results pkl-cli:alpineExecutableAmd64 pkl-core:testAlpineExecutableAmd64
name: gradle buildNative name: gradle buildNative
- persist_to_workspace: - persist_to_workspace:
root: '.' root: '.'
paths: paths:
- pkl-cli/build/executable/ - pkl-cli/build/executable/
- run:
command: |-
mkdir ~/test-results/
find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \;
name: Gather test results
when: always
- store_test_results: - store_test_results:
path: ~/test-results path: ~/test-results
environment: environment:
@@ -575,18 +535,32 @@ jobs:
resource_class: xlarge resource_class: xlarge
docker: docker:
- image: oraclelinux:8-slim - image: oraclelinux:8-slim
pkl-cli-windows-amd64-snapshot:
steps:
- checkout
- run:
command: |-
export PATH=~/staticdeps/bin:$PATH
./gradlew --info --stacktrace -DtestReportsDir=${HOME}/test-results pkl-cli:windowsExecutableAmd64 pkl-core:testWindowsExecutableAmd64
name: gradle buildNative
shell: bash.exe
- persist_to_workspace:
root: '.'
paths:
- pkl-cli/build/executable/
- store_test_results:
path: ~/test-results
environment:
LANG: en_US.UTF-8
resource_class: windows.large
machine:
image: windows-server-2022-gui:current
gradle-check-jdk17: gradle-check-jdk17:
steps: steps:
- checkout - checkout
- run: - run:
command: ./gradlew --info --stacktrace check command: ./gradlew --info --stacktrace -DtestReportsDir=${HOME}/test-results check
name: gradle check name: gradle check
- run:
command: |-
mkdir ~/test-results/
find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \;
name: Gather test results
when: always
- store_test_results: - store_test_results:
path: ~/test-results path: ~/test-results
environment: environment:
@@ -597,32 +571,33 @@ jobs:
steps: steps:
- checkout - checkout
- run: - run:
command: ./gradlew --info --stacktrace check command: ./gradlew --info --stacktrace -DtestReportsDir=${HOME}/test-results check
name: gradle check name: gradle check
- run:
command: |-
mkdir ~/test-results/
find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \;
name: Gather test results
when: always
- store_test_results: - store_test_results:
path: ~/test-results path: ~/test-results
environment: environment:
LANG: en_US.UTF-8 LANG: en_US.UTF-8
docker: docker:
- image: cimg/openjdk:21.0 - image: cimg/openjdk:21.0
gradle-check-jdk17-windows:
steps:
- checkout
- run:
command: ./gradlew --info --stacktrace -DtestReportsDir=${HOME}/test-results check
name: gradle check
- store_test_results:
path: ~/test-results
environment:
LANG: en_US.UTF-8
resource_class: windows.large
machine:
image: windows-server-2022-gui:current
bench: bench:
steps: steps:
- checkout - checkout
- run: - run:
command: ./gradlew --info --stacktrace bench:jmh command: ./gradlew --info --stacktrace -DtestReportsDir=${HOME}/test-results bench:jmh
name: bench:jmh name: bench:jmh
- run:
command: |-
mkdir ~/test-results/
find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \;
name: Gather test results
when: always
- store_test_results: - store_test_results:
path: ~/test-results path: ~/test-results
environment: environment:
@@ -634,16 +609,10 @@ jobs:
- checkout - checkout
- run: - run:
command: |- command: |-
./gradlew --info --stacktrace :pkl-gradle:build \ ./gradlew --info --stacktrace -DtestReportsDir=${HOME}/test-results :pkl-gradle:build \
:pkl-gradle:compatibilityTestReleases \ :pkl-gradle:compatibilityTestReleases \
:pkl-gradle:compatibilityTestCandidate :pkl-gradle:compatibilityTestCandidate
name: gradle compatibility name: gradle compatibility
- run:
command: |-
mkdir ~/test-results/
find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \;
name: Gather test results
when: always
- store_test_results: - store_test_results:
path: ~/test-results path: ~/test-results
environment: environment:
@@ -656,17 +625,11 @@ jobs:
- attach_workspace: - attach_workspace:
at: '.' at: '.'
- run: - run:
command: ./gradlew --info --stacktrace publishToSonatype command: ./gradlew --info --stacktrace -DtestReportsDir=${HOME}/test-results publishToSonatype
- persist_to_workspace: - persist_to_workspace:
root: '.' root: '.'
paths: paths:
- pkl-cli/build/executable/ - pkl-cli/build/executable/
- run:
command: |-
mkdir ~/test-results/
find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \;
name: Gather test results
when: always
- store_test_results: - store_test_results:
path: ~/test-results path: ~/test-results
environment: environment:
@@ -679,17 +642,11 @@ jobs:
- attach_workspace: - attach_workspace:
at: '.' at: '.'
- run: - run:
command: ./gradlew --info --stacktrace -DreleaseBuild=true publishToSonatype closeAndReleaseSonatypeStagingRepository command: ./gradlew --info --stacktrace -DtestReportsDir=${HOME}/test-results -DreleaseBuild=true publishToSonatype closeAndReleaseSonatypeStagingRepository
- persist_to_workspace: - persist_to_workspace:
root: '.' root: '.'
paths: paths:
- pkl-cli/build/executable/ - pkl-cli/build/executable/
- run:
command: |-
mkdir ~/test-results/
find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \;
name: Gather test results
when: always
- store_test_results: - store_test_results:
path: ~/test-results path: ~/test-results
environment: environment:
@@ -753,6 +710,9 @@ workflows:
- gradle-check-jdk21: - gradle-check-jdk21:
requires: requires:
- hold - hold
- gradle-check-jdk17-windows:
requires:
- hold
when: when:
matches: matches:
value: << pipeline.git.branch >> value: << pipeline.git.branch >>
@@ -761,6 +721,7 @@ workflows:
jobs: jobs:
- gradle-check-jdk17 - gradle-check-jdk17
- gradle-check-jdk21 - gradle-check-jdk21
- gradle-check-jdk17-windows
- bench - bench
- gradle-compatibility - gradle-compatibility
- pkl-cli-macOS-amd64-snapshot - pkl-cli-macOS-amd64-snapshot
@@ -768,10 +729,12 @@ workflows:
- pkl-cli-macOS-aarch64-snapshot - pkl-cli-macOS-aarch64-snapshot
- pkl-cli-linux-aarch64-snapshot - pkl-cli-linux-aarch64-snapshot
- pkl-cli-linux-alpine-amd64-snapshot - pkl-cli-linux-alpine-amd64-snapshot
- pkl-cli-windows-amd64-snapshot
- deploy-snapshot: - deploy-snapshot:
requires: requires:
- gradle-check-jdk17 - gradle-check-jdk17
- gradle-check-jdk21 - gradle-check-jdk21
- gradle-check-jdk17-windows
- bench - bench
- gradle-compatibility - gradle-compatibility
- pkl-cli-macOS-amd64-snapshot - pkl-cli-macOS-amd64-snapshot
@@ -779,6 +742,7 @@ workflows:
- pkl-cli-macOS-aarch64-snapshot - pkl-cli-macOS-aarch64-snapshot
- pkl-cli-linux-aarch64-snapshot - pkl-cli-linux-aarch64-snapshot
- pkl-cli-linux-alpine-amd64-snapshot - pkl-cli-linux-alpine-amd64-snapshot
- pkl-cli-windows-amd64-snapshot
context: pkl-maven-release context: pkl-maven-release
- trigger-docsite-build: - trigger-docsite-build:
requires: requires:
@@ -803,6 +767,12 @@ workflows:
ignore: /.*/ ignore: /.*/
tags: tags:
only: /^v?\d+\.\d+\.\d+$/ only: /^v?\d+\.\d+\.\d+$/
- gradle-check-jdk17-windows:
filters:
branches:
ignore: /.*/
tags:
only: /^v?\d+\.\d+\.\d+$/
- bench: - bench:
filters: filters:
branches: branches:
@@ -845,10 +815,17 @@ workflows:
ignore: /.*/ ignore: /.*/
tags: tags:
only: /^v?\d+\.\d+\.\d+$/ only: /^v?\d+\.\d+\.\d+$/
- pkl-cli-windows-amd64-release:
filters:
branches:
ignore: /.*/
tags:
only: /^v?\d+\.\d+\.\d+$/
- github-release: - github-release:
requires: requires:
- gradle-check-jdk17 - gradle-check-jdk17
- gradle-check-jdk21 - gradle-check-jdk21
- gradle-check-jdk17-windows
- bench - bench
- gradle-compatibility - gradle-compatibility
- pkl-cli-macOS-amd64-release - pkl-cli-macOS-amd64-release
@@ -856,6 +833,7 @@ workflows:
- pkl-cli-macOS-aarch64-release - pkl-cli-macOS-aarch64-release
- pkl-cli-linux-aarch64-release - pkl-cli-linux-aarch64-release
- pkl-cli-linux-alpine-amd64-release - pkl-cli-linux-alpine-amd64-release
- pkl-cli-windows-amd64-release
context: pkl-github-release context: pkl-github-release
filters: filters:
branches: branches:
@@ -885,6 +863,7 @@ workflows:
jobs: jobs:
- gradle-check-jdk17 - gradle-check-jdk17
- gradle-check-jdk21 - gradle-check-jdk21
- gradle-check-jdk17-windows
- bench - bench
- gradle-compatibility - gradle-compatibility
- pkl-cli-macOS-amd64-release - pkl-cli-macOS-amd64-release
@@ -892,6 +871,7 @@ workflows:
- pkl-cli-macOS-aarch64-release - pkl-cli-macOS-aarch64-release
- pkl-cli-linux-aarch64-release - pkl-cli-linux-aarch64-release
- pkl-cli-linux-alpine-amd64-release - pkl-cli-linux-alpine-amd64-release
- pkl-cli-windows-amd64-release
when: when:
matches: matches:
value: << pipeline.git.branch >> value: << pipeline.git.branch >>
+14 -6
View File
@@ -16,12 +16,9 @@
/// Builds the native `pkl` CLI /// Builds the native `pkl` CLI
extends "GradleJob.pkl" extends "GradleJob.pkl"
import "package://pkg.pkl-lang.org/pkl-pantry/com.circleci.v2@1.1.0#/Config.pkl" import "package://pkg.pkl-lang.org/pkl-pantry/com.circleci.v2@1.1.2#/Config.pkl"
import "package://pkg.pkl-lang.org/pkl-pantry/pkl.experimental.uri@1.0.0#/URI.pkl" import "package://pkg.pkl-lang.org/pkl-pantry/pkl.experimental.uri@1.0.0#/URI.pkl"
/// The OS to run on
os: "macOS"|"linux"
/// The architecture to use /// The architecture to use
arch: "amd64"|"aarch64" arch: "amd64"|"aarch64"
@@ -119,10 +116,14 @@ steps {
new Config.RunStep { new Config.RunStep {
name = "gradle buildNative" name = "gradle buildNative"
local _os = local _os =
if (os == "macOS") "mac" if (module.os == "macOS") "mac"
else if (musl) "alpine" else if (musl) "alpine"
else if (module.os == "windows") "windows"
else "linux" else "linux"
local jobName = "\(_os)Executable\(arch.capitalize())" local jobName = "\(_os)Executable\(arch.capitalize())"
when (module.os == "windows") {
shell = "bash.exe"
}
command = #""" command = #"""
export PATH=~/staticdeps/bin:$PATH export PATH=~/staticdeps/bin:$PATH
./gradlew \#(module.gradleArgs) pkl-cli:\#(jobName) pkl-core:test\#(jobName.capitalize()) ./gradlew \#(module.gradleArgs) pkl-cli:\#(jobName) pkl-core:test\#(jobName.capitalize())
@@ -142,7 +143,8 @@ job {
xcode = "15.3.0" xcode = "15.3.0"
} }
resource_class = "macos.m1.large.gen1" resource_class = "macos.m1.large.gen1"
} else { }
when (os == "linux") {
docker { docker {
new { new {
image = if (arch == "aarch64") "arm64v8/oraclelinux:8-slim" else "oraclelinux:8-slim" image = if (arch == "aarch64") "arm64v8/oraclelinux:8-slim" else "oraclelinux:8-slim"
@@ -153,4 +155,10 @@ job {
} }
resource_class = if (arch == "aarch64") "arm.xlarge" else "xlarge" resource_class = if (arch == "aarch64") "arm.xlarge" else "xlarge"
} }
when (os == "windows") {
machine {
image = "windows-server-2022-gui:current"
}
resource_class = "windows.large"
}
} }
+3 -1
View File
@@ -15,7 +15,7 @@
//===----------------------------------------------------------------------===// //===----------------------------------------------------------------------===//
extends "GradleJob.pkl" extends "GradleJob.pkl"
import "package://pkg.pkl-lang.org/pkl-pantry/com.circleci.v2@1.1.0#/Config.pkl" import "package://pkg.pkl-lang.org/pkl-pantry/com.circleci.v2@1.1.2#/Config.pkl"
local self = this local self = this
@@ -27,6 +27,8 @@ job {
} }
} }
os = "linux"
steps { steps {
new Config.AttachWorkspaceStep { at = "." } new Config.AttachWorkspaceStep { at = "." }
new Config.RunStep { new Config.RunStep {
+15 -5
View File
@@ -15,9 +15,11 @@
//===----------------------------------------------------------------------===// //===----------------------------------------------------------------------===//
extends "GradleJob.pkl" extends "GradleJob.pkl"
import "package://pkg.pkl-lang.org/pkl-pantry/com.circleci.v2@1.1.0#/Config.pkl" import "package://pkg.pkl-lang.org/pkl-pantry/com.circleci.v2@1.1.2#/Config.pkl"
javaVersion: "11.0"|"17.0"|"21.0" javaVersion: "17.0"|"21.0"
os = "linux"
steps { steps {
new Config.RunStep { new Config.RunStep {
@@ -27,9 +29,17 @@ steps {
} }
job { job {
docker { when (os == "linux") {
new { docker {
image = "cimg/openjdk:\(javaVersion)" new {
image = "cimg/openjdk:\(javaVersion)"
}
} }
} }
when (os == "windows") {
machine {
image = "windows-server-2022-gui:current"
}
resource_class = "windows.large"
}
} }
+5 -10
View File
@@ -15,14 +15,18 @@
//===----------------------------------------------------------------------===// //===----------------------------------------------------------------------===//
abstract module GradleJob abstract module GradleJob
import "package://pkg.pkl-lang.org/pkl-pantry/com.circleci.v2@1.1.0#/Config.pkl" import "package://pkg.pkl-lang.org/pkl-pantry/com.circleci.v2@1.1.2#/Config.pkl"
/// Whether this is a release build or not. /// Whether this is a release build or not.
isRelease: Boolean = false isRelease: Boolean = false
/// The OS to run on
os: "macOS"|"linux"|"windows"
fixed gradleArgs = new Listing { fixed gradleArgs = new Listing {
"--info" "--info"
"--stacktrace" "--stacktrace"
"-DtestReportsDir=${HOME}/test-results"
when (isRelease) { when (isRelease) {
"-DreleaseBuild=true" "-DreleaseBuild=true"
} }
@@ -37,15 +41,6 @@ job: Config.Job = new {
steps { steps {
"checkout" "checkout"
...module.steps ...module.steps
new Config.RunStep {
// find all test results and write them to the home dir
name = "Gather test results"
command = """
mkdir ~/test-results/
find . -type f -regex ".*/build/test-results/.*xml" -exec cp {} ~/test-results/ \\;
"""
`when` = "always"
}
new Config.StoreTestResults { new Config.StoreTestResults {
path = "~/test-results" path = "~/test-results"
} }
+3 -1
View File
@@ -15,12 +15,14 @@
//===----------------------------------------------------------------------===// //===----------------------------------------------------------------------===//
extends "GradleJob.pkl" extends "GradleJob.pkl"
import "package://pkg.pkl-lang.org/pkl-pantry/com.circleci.v2@1.1.0#/Config.pkl" import "package://pkg.pkl-lang.org/pkl-pantry/com.circleci.v2@1.1.2#/Config.pkl"
name: String = command name: String = command
command: String command: String
os = "linux"
steps { steps {
new Config.RunStep { new Config.RunStep {
name = module.name name = module.name
+1
View File
@@ -4,3 +4,4 @@
/docs/** linguist-documentation /docs/** linguist-documentation
*.pkl linguist-language=Groovy *.pkl linguist-language=Groovy
* text eol=lf
+8
View File
@@ -40,6 +40,7 @@ pkl-cli/build/executable/jpkl # run Java executable
pkl-cli/build/executable/pkl-macos-amd64 # run Mac executable pkl-cli/build/executable/pkl-macos-amd64 # run Mac executable
pkl-cli/build/executable/pkl-linux-amd64 # run Linux executable pkl-cli/build/executable/pkl-linux-amd64 # run Linux executable
pkl-cli/build/executable/pkl-alpine-linux-amd64 # run Alpine Linux executable pkl-cli/build/executable/pkl-alpine-linux-amd64 # run Alpine Linux executable
pkl-cli/build/executable/pkl-windows-amd64.exe # run Windows executable
---- ----
== Update Gradle == Update Gradle
@@ -68,6 +69,13 @@ based on version information from https://search.maven.org, https://plugins.grad
* ANTLR code generation is performed by task `:pkl-core:generateGrammarSource` * ANTLR code generation is performed by task `:pkl-core:generateGrammarSource`
** Output dir is `generated/antlr/` ** Output dir is `generated/antlr/`
== Remote JVM Debugging
To enable remote JVM debugging when running Gradle tasks (e.g. test), add the flag `-Djvmdebug=true`.
This will listen on port 5005.
Example: `./gradlew test -Djvmdebug=true`
== Resources == Resources
For automated build setup examples see our https://github.com/apple/pkl/blob/main/.circleci/[CircleCI] jobs like our https://github.com/apple/pkl/blob/main/.circleci/jobs/BuildNativeJob.pkl[BuildNativeJob.pkl], where we build Pkl automatically. For automated build setup examples see our https://github.com/apple/pkl/blob/main/.circleci/[CircleCI] jobs like our https://github.com/apple/pkl/blob/main/.circleci/jobs/BuildNativeJob.pkl[BuildNativeJob.pkl], where we build Pkl automatically.
+11 -4
View File
@@ -26,6 +26,7 @@ open class BuildInfo(project: Project) {
when { when {
os.isMacOsX -> "macos" os.isMacOsX -> "macos"
os.isLinux -> "linux" os.isLinux -> "linux"
os.isWindows -> "windows"
else -> throw RuntimeException("${os.familyName} is not supported.") else -> throw RuntimeException("${os.familyName} is not supported.")
} }
} }
@@ -36,7 +37,8 @@ open class BuildInfo(project: Project) {
val downloadUrl: String by lazy { val downloadUrl: String by lazy {
val jdkMajor = graalVmJdkVersion.takeWhile { it != '.' } val jdkMajor = graalVmJdkVersion.takeWhile { it != '.' }
"https://download.oracle.com/graalvm/$jdkMajor/archive/$baseName.tar.gz" val extension = if (os.isWindows) "zip" else "tar.gz"
"https://download.oracle.com/graalvm/$jdkMajor/archive/$baseName.$extension"
} }
val installDir: File by lazy { val installDir: File by lazy {
@@ -85,9 +87,14 @@ open class BuildInfo(project: Project) {
val commitId: String by lazy { val commitId: String by lazy {
// only run command once per build invocation // only run command once per build invocation
if (project === project.rootProject) { if (project === project.rootProject) {
Runtime.getRuntime() val process = ProcessBuilder()
.exec(arrayOf("git", "rev-parse", "--short", "HEAD"), arrayOf(), project.rootDir) .command("git", "rev-parse", "--short", "HEAD")
.inputStream.reader().readText().trim() .directory(project.rootDir)
.start()
process.waitFor().also { exitCode ->
if (exitCode == -1) throw RuntimeException(process.errorStream.reader().readText())
}
process.inputStream.reader().readText().trim()
} else { } else {
project.rootProject.extensions.getByType(BuildInfo::class.java).commitId project.rootProject.extensions.getByType(BuildInfo::class.java).commitId
} }
+6 -8
View File
@@ -1,11 +1,11 @@
import org.gradle.api.DefaultTask import org.gradle.api.DefaultTask
import org.gradle.api.file.RegularFileProperty import org.gradle.api.file.RegularFileProperty
import org.gradle.api.provider.ListProperty import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input import org.gradle.api.tasks.Input
import org.gradle.api.tasks.InputFile import org.gradle.api.tasks.InputFile
import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.TaskAction
import org.gradle.kotlin.dsl.listProperty
/** /**
* Builds a self-contained Pkl CLI Jar that is directly executable on *nix * Builds a self-contained Pkl CLI Jar that is directly executable on *nix
@@ -15,27 +15,25 @@ import org.gradle.kotlin.dsl.listProperty
* *
* https://skife.org/java/unix/2011/06/20/really_executable_jars.html * https://skife.org/java/unix/2011/06/20/really_executable_jars.html
*/ */
open class ExecutableJar : DefaultTask() { abstract class ExecutableJar : DefaultTask() {
@get:InputFile @get:InputFile
val inJar: RegularFileProperty = project.objects.fileProperty() abstract val inJar: RegularFileProperty
@get:OutputFile @get:OutputFile
val outJar: RegularFileProperty = project.objects.fileProperty() abstract val outJar: RegularFileProperty
@get:Input @get:Input
val jvmArgs: ListProperty<String> = project.objects.listProperty() abstract val jvmArgs: ListProperty<String>
@TaskAction @TaskAction
fun buildJar() { fun buildJar() {
val inFile = inJar.get().asFile val inFile = inJar.get().asFile
val outFile = outJar.get().asFile val outFile = outJar.get().asFile
val escapedJvmArgs = jvmArgs.get().joinToString(separator = " ") { "\"$it\"" } val escapedJvmArgs = jvmArgs.get().joinToString(separator = " ") { "\"$it\"" }
val startScript = """ val startScript = """
#!/bin/sh #!/bin/sh
exec java $escapedJvmArgs -jar $0 "$@" exec java $escapedJvmArgs -jar $0 "$@"
""".trim().trimMargin() + "\n\n\n" """.trimIndent() + "\n\n\n"
outFile.outputStream().use { outStream -> outFile.outputStream().use { outStream ->
startScript.byteInputStream().use { it.copyTo(outStream) } startScript.byteInputStream().use { it.copyTo(outStream) }
inFile.inputStream().use { it.copyTo(outStream) } inFile.inputStream().use { it.copyTo(outStream) }
@@ -93,3 +93,28 @@ val updateDependencyLocks by tasks.registering {
} }
val allDependencies by tasks.registering(DependencyReportTask::class) val allDependencies by tasks.registering(DependencyReportTask::class)
tasks.withType(Test::class).configureEach {
System.getProperty("testReportsDir")?.let { reportsDir ->
reports.junitXml.outputLocation.set(file(reportsDir).resolve(project.name).resolve(name))
}
debugOptions {
enabled = System.getProperty("jvmdebug")?.toBoolean() ?: false
@Suppress("UnstableApiUsage")
host = "*"
port = 5005
suspend = true
server = true
}
}
tasks.withType(JavaExec::class).configureEach {
debugOptions {
enabled = System.getProperty("jvmdebug")?.toBoolean() ?: false
@Suppress("UnstableApiUsage")
host = "*"
port = 5005
suspend = true
server = true
}
}
+12 -8
View File
@@ -2,6 +2,7 @@ import java.nio.file.*
import java.util.UUID import java.util.UUID
import de.undercouch.gradle.tasks.download.Download import de.undercouch.gradle.tasks.download.Download
import de.undercouch.gradle.tasks.download.Verify import de.undercouch.gradle.tasks.download.Verify
import kotlin.io.path.createDirectories
plugins { plugins {
id("de.undercouch.download") id("de.undercouch.download")
@@ -9,7 +10,10 @@ plugins {
val buildInfo = project.extensions.getByType<BuildInfo>() val buildInfo = project.extensions.getByType<BuildInfo>()
val BuildInfo.GraalVm.downloadFile get() = file(homeDir).resolve("${baseName}.tar.gz") val BuildInfo.GraalVm.downloadFile get(): File {
val extension = if (buildInfo.os.isWindows) "zip" else "tar.gz"
return file(homeDir).resolve("${baseName}.$extension")
}
// tries to minimize chance of corruption by download-to-temp-file-and-move // tries to minimize chance of corruption by download-to-temp-file-and-move
val downloadGraalVmAarch64 by tasks.registering(Download::class) { val downloadGraalVmAarch64 by tasks.registering(Download::class) {
@@ -72,11 +76,10 @@ fun Task.configureInstallGraalVm(graalVm: BuildInfo.GraalVm) {
} }
doLast { doLast {
val distroDir = "${graalVm.homeDir}/${UUID.randomUUID()}" val distroDir = Paths.get(graalVm.homeDir, UUID.randomUUID().toString())
try { try {
mkdir(distroDir) distroDir.createDirectories()
println("Extracting ${graalVm.downloadFile} into $distroDir") println("Extracting ${graalVm.downloadFile} into $distroDir")
// faster and more reliable than Gradle's `copy { from tarTree() }` // faster and more reliable than Gradle's `copy { from tarTree() }`
exec { exec {
@@ -85,17 +88,18 @@ fun Task.configureInstallGraalVm(graalVm: BuildInfo.GraalVm) {
args("--strip-components=1", "-xzf", graalVm.downloadFile) args("--strip-components=1", "-xzf", graalVm.downloadFile)
} }
val distroBinDir = if (buildInfo.os.isMacOsX) "$distroDir/Contents/Home/bin" else "$distroDir/bin" val distroBinDir = if (buildInfo.os.isMacOsX) distroDir.resolve("Contents/Home/bin") else distroDir.resolve("bin")
println("Installing native-image into $distroDir") println("Installing native-image into $distroDir")
exec { exec {
executable = "$distroBinDir/gu" val executableName = if (buildInfo.os.isWindows) "gu.cmd" else "gu"
executable = distroBinDir.resolve(executableName).toString()
args("install", "--no-progress", "native-image") args("install", "--no-progress", "native-image")
} }
println("Creating symlink ${graalVm.installDir} for $distroDir") println("Creating symlink ${graalVm.installDir} for $distroDir")
val tempLink = Paths.get("${graalVm.homeDir}/${UUID.randomUUID()}") val tempLink = Paths.get(graalVm.homeDir, UUID.randomUUID().toString())
Files.createSymbolicLink(tempLink, Paths.get(distroDir)) Files.createSymbolicLink(tempLink, distroDir)
try { try {
Files.move(tempLink, graalVm.installDir.toPath(), StandardCopyOption.ATOMIC_MOVE) Files.move(tempLink, graalVm.installDir.toPath(), StandardCopyOption.ATOMIC_MOVE)
} catch (e: Exception) { } catch (e: Exception) {
@@ -2140,9 +2140,18 @@ For example, a module with URI `modulepath:/animals/birds/pigeon.pkl`
can import `modulepath:/animals/birds/parrot.pkl` can import `modulepath:/animals/birds/parrot.pkl`
with `import "parrot.pkl"` or `import "/animals/birds/parrot.pkl"`. with `import "parrot.pkl"` or `import "/animals/birds/parrot.pkl"`.
NOTE: When importing a relative folder or file that starts with `@`, the import string must be prefixed with `./`. [NOTE]
Otherwise, this syntax will be interpreted as dependency notation. .Paths on Windows
====
Relative paths use the `/` character as the directory separator on all platforms, including Windows.
Paths that contain drive letters (e.g. `C:`) must be declared as an absolute file URI, for example: `import "file:///C:/path/to/my/module.pkl"`. Otherwise, they are interpreted as a URI scheme.
====
NOTE: When importing a relative directory or file that starts with `@`, the import string must be prefixed with `./`.
Otherwise, this syntax will be interpreted as xref:dependency-notation[dependency notation].
[#dependency-notation]
==== Dependency notation URIs ==== Dependency notation URIs
Example: `+@birds/bird.pkl+` Example: `+@birds/bird.pkl+`
+28 -27
View File
@@ -8,6 +8,7 @@ include::ROOT:partial$component-attributes.adoc[]
:uri-pkl-linux-amd64-download: {uri-sonatype-snapshot-download}&a=pkl-cli-linux-amd64&e=bin :uri-pkl-linux-amd64-download: {uri-sonatype-snapshot-download}&a=pkl-cli-linux-amd64&e=bin
:uri-pkl-linux-aarch64-download: {uri-sonatype-snapshot-download}&a=pkl-cli-linux-aarch64&e=bin :uri-pkl-linux-aarch64-download: {uri-sonatype-snapshot-download}&a=pkl-cli-linux-aarch64&e=bin
:uri-pkl-alpine-download: {uri-sonatype-snapshot-download}&a=pkl-cli-alpine-linux-amd64&e=bin :uri-pkl-alpine-download: {uri-sonatype-snapshot-download}&a=pkl-cli-alpine-linux-amd64&e=bin
:uri-pkl-windows-download: {uri-sonatype-snapshot-download}&a=pkl-cli-windows-amd64&e=exe
:uri-pkl-java-download: {uri-sonatype-snapshot-download}&a=pkl-cli-java&e=jar :uri-pkl-java-download: {uri-sonatype-snapshot-download}&a=pkl-cli-java&e=jar
ifdef::is-release-version[] ifdef::is-release-version[]
@@ -16,6 +17,7 @@ ifdef::is-release-version[]
:uri-pkl-linux-amd64-download: {github-releases}/pkl-linux-amd64 :uri-pkl-linux-amd64-download: {github-releases}/pkl-linux-amd64
:uri-pkl-linux-aarch64-download: {github-releases}/pkl-linux-aarch64 :uri-pkl-linux-aarch64-download: {github-releases}/pkl-linux-aarch64
:uri-pkl-alpine-download: {github-releases}/pkl-alpine-linux-amd64 :uri-pkl-alpine-download: {github-releases}/pkl-alpine-linux-amd64
:uri-pkl-windows-download: {github-releases}/pkl-windows-amd64.exe
:uri-pkl-java-download: {uri-maven-repo}/org/pkl-lang/pkl-cli-java/{pkl-artifact-version}/pkl-cli-java-{pkl-artifact-version}.jar :uri-pkl-java-download: {uri-maven-repo}/org/pkl-lang/pkl-cli-java/{pkl-artifact-version}/pkl-cli-java-{pkl-artifact-version}.jar
endif::[] endif::[]
@@ -37,9 +39,10 @@ The CLI comes in multiple flavors:
* Native Linux executable for amd64 * Native Linux executable for amd64
* Native Linux executable for aarch64 * Native Linux executable for aarch64
* Native Alpine Linux executable for amd64 (cross-compiled and tested on Oracle Linux 8) * Native Alpine Linux executable for amd64 (cross-compiled and tested on Oracle Linux 8)
* Java executable (tested with Java 17/21 on macOS and Oracle Linux, may work on other platforms) * Native Windows executable for amd64 (tested on Windows Server 2022)
* Java executable (tested with Java 17/21 on macOS and Oracle Linux)
On macOS and Linux, we recommend using the native executables. On macOS, Linux, and Windows, we recommend using the native executables.
They are self-contained, start up instantly, and run complex Pkl code much faster than the Java executable. They are self-contained, start up instantly, and run complex Pkl code much faster than the Java executable.
.What is the Difference Between the Linux and Alpine Linux Executables? .What is the Difference Between the Linux and Alpine Linux Executables?
@@ -59,7 +62,7 @@ Except where noted otherwise, the rest of this page discusses the native executa
[[homebrew]] [[homebrew]]
=== Homebrew === Homebrew
Release versions can be installed with {uri-homebrew}[Homebrew]. On macOS and Linux, release versions can be installed with {uri-homebrew}[Homebrew].
ifdef::is-release-version[] ifdef::is-release-version[]
To install Pkl, run: To install Pkl, run:
@@ -111,7 +114,7 @@ chmod +x pkl
This should print something similar to: This should print something similar to:
[source,shell] [source]
[subs="+attributes"] [subs="+attributes"]
---- ----
Pkl {pkl-version} (macOS, native) Pkl {pkl-version} (macOS, native)
@@ -145,7 +148,7 @@ chmod +x pkl
This should print something similar to: This should print something similar to:
[source,shell] [source]
[subs="+attributes"] [subs="+attributes"]
---- ----
Pkl {pkl-version} (Linux, native) Pkl {pkl-version} (Linux, native)
@@ -167,7 +170,7 @@ chmod +x pkl
This should print something similar to: This should print something similar to:
[source,shell] [source]
[subs="+attributes"] [subs="+attributes"]
---- ----
Pkl {pkl-version} (Linux, native) Pkl {pkl-version} (Linux, native)
@@ -175,6 +178,23 @@ Pkl {pkl-version} (Linux, native)
NOTE: We currently do not support the aarch64 architecture for Alpine Linux. NOTE: We currently do not support the aarch64 architecture for Alpine Linux.
=== Windows Executable
[source,PowerShell]
[subs="+attributes"]
----
Invoke-WebRequest '{uri-pkl-windows-download}' -OutFile pkl.exe
.\pkl --version
----
[source]
[subs="+attributes"]
----
Pkl {pkl-version} (Windows 10.0, native)
----
NOTE: We currently do not support the aarch64 architecture for Windows.
=== Java Executable === Java Executable
[source,shell] [source,shell]
@@ -193,27 +213,8 @@ This should print something similar to:
Pkl {pkl-version} (macOS 14.2, Java 17.0.10) Pkl {pkl-version} (macOS 14.2, Java 17.0.10)
---- ----
=== Windows support NOTE: The Java executable does not work as an executable file on Windows.
Pkl does not currently support running natively on Windows. Pkl has been reported to work on the https://learn.microsoft.com/en-us/windows/wsl/install[Windows Subsystem for Linux] and on a https://www.oracle.com/java/technologies/downloads/#jdk21-windows[Java Runtime] using https://search.maven.org/remote_content?g=org.pkl-lang&a=pkl-cli-java&v=LATEST[`jpkl` (the Java executable version of Pkl)]. However, it will work as a jar, for example, with `java -jar jpkl`.
The following is from successful uses of `jpkl` on Windows:
[source,shell]
[subs="+attributes"]
----
> java -jar pkl-cli-java.jar --version
Pkl 0.26.0-dev+21e0e14 (Windows 10 10.0, Java 21.0.2)
----
Note: You must use forward slashes in all paths for Windows with absolute paths prefixed with file:///. The following examples are valid:
[source,shell]
[subs="+attributes"]
----
> java -jar pkl-cli-java.jar eval file:///C:/Code/pkl/test.pkl
> java -jar pkl-cli-java.jar eval ../Code/pkl/test.pkl
> java -jar pkl-cli-java.jar eval pkl/test.pkl
----
https://github.com/apple/pkl/issues/20[GitHub Issue #20] is used to track progress on support for the Windows platform.
[[usage]] [[usage]]
== Usage == Usage
+1
View File
@@ -15,6 +15,7 @@ graalVmSha256-macos-x64 = "14f4bd6417809905f86e786c779d0fc2feb840d7dac35ae3503eb
graalVmSha256-macos-aarch64 = "e944c5ce5da56e683fc8f1a57191b46d9cb702930b1688bda064fcf467d876b8" graalVmSha256-macos-aarch64 = "e944c5ce5da56e683fc8f1a57191b46d9cb702930b1688bda064fcf467d876b8"
graalVmSha256-linux-x64 = "112dc9b92d81a946f1b5b334646151b790785c813e76fcf13527a319003d7e2c" graalVmSha256-linux-x64 = "112dc9b92d81a946f1b5b334646151b790785c813e76fcf13527a319003d7e2c"
graalVmSha256-linux-aarch64 = "c95ac550d070f06666cf8c1023a098380dd565be00866473caf6ff1b7cdf680c" graalVmSha256-linux-aarch64 = "c95ac550d070f06666cf8c1023a098380dd565be00866473caf6ff1b7cdf680c"
graalVmSha256-windows-x64 = "1ab2291e71f54d73e3e57b7fccbf184cabcba37e16ca9d1cf42d08474a7c02f0"
ideaExtPlugin = "1.1" ideaExtPlugin = "1.1"
javaPoet = "1.+" javaPoet = "1.+"
javaxInject = "1" javaxInject = "1"
+39 -4
View File
@@ -33,6 +33,7 @@ val stagedMacAarch64Executable: Configuration by configurations.creating
val stagedLinuxAmd64Executable: Configuration by configurations.creating val stagedLinuxAmd64Executable: Configuration by configurations.creating
val stagedLinuxAarch64Executable: Configuration by configurations.creating val stagedLinuxAarch64Executable: Configuration by configurations.creating
val stagedAlpineLinuxAmd64Executable: Configuration by configurations.creating val stagedAlpineLinuxAmd64Executable: Configuration by configurations.creating
val stagedWindowsAmd64Executable: Configuration by configurations.creating
dependencies { dependencies {
compileOnly(libs.svm) compileOnly(libs.svm)
@@ -63,6 +64,7 @@ dependencies {
stagedLinuxAmd64Executable(executableDir("pkl-linux-amd64")) stagedLinuxAmd64Executable(executableDir("pkl-linux-amd64"))
stagedLinuxAarch64Executable(executableDir("pkl-linux-aarch64")) stagedLinuxAarch64Executable(executableDir("pkl-linux-aarch64"))
stagedAlpineLinuxAmd64Executable(executableDir("pkl-alpine-linux-amd64")) stagedAlpineLinuxAmd64Executable(executableDir("pkl-alpine-linux-amd64"))
stagedWindowsAmd64Executable(executableDir("pkl-windows-amd64.exe"))
} }
tasks.jar { tasks.jar {
@@ -122,8 +124,13 @@ val testStartJavaExecutable by tasks.registering(Exec::class) {
val outputFile = layout.buildDirectory.file("testStartJavaExecutable") // dummy output to satisfy up-to-date check val outputFile = layout.buildDirectory.file("testStartJavaExecutable") // dummy output to satisfy up-to-date check
outputs.file(outputFile) outputs.file(outputFile)
executable = javaExecutable.get().outputs.files.singleFile.toString() if (buildInfo.os.isWindows) {
args("--version") executable = "java"
args("-jar", javaExecutable.get().outputs.files.singleFile.toString(), "--version")
} else {
executable = javaExecutable.get().outputs.files.singleFile.toString()
args("--version")
}
doFirst { outputFile.get().asFile.delete() } doFirst { outputFile.get().asFile.delete() }
@@ -141,12 +148,13 @@ fun Exec.configureExecutable(
) { ) {
inputs.files(sourceSets.main.map { it.output }).withPropertyName("mainSourceSets").withPathSensitivity(PathSensitivity.RELATIVE) inputs.files(sourceSets.main.map { it.output }).withPropertyName("mainSourceSets").withPathSensitivity(PathSensitivity.RELATIVE)
inputs.files(configurations.runtimeClasspath).withPropertyName("runtimeClasspath").withNormalizer(ClasspathNormalizer::class) inputs.files(configurations.runtimeClasspath).withPropertyName("runtimeClasspath").withNormalizer(ClasspathNormalizer::class)
inputs.files(file(graalVm.baseDir).resolve("bin/native-image")).withPropertyName("graalVmNativeImage").withPathSensitivity(PathSensitivity.ABSOLUTE) val nativeImageCommandName = if (buildInfo.os.isWindows) "native-image.cmd" else "native-image"
inputs.files(file(graalVm.baseDir).resolve("bin/$nativeImageCommandName")).withPropertyName("graalVmNativeImage").withPathSensitivity(PathSensitivity.ABSOLUTE)
outputs.file(outputFile) outputs.file(outputFile)
outputs.cacheIf { true } outputs.cacheIf { true }
workingDir(outputFile.map { it.asFile.parentFile }) workingDir(outputFile.map { it.asFile.parentFile })
executable = "${graalVm.baseDir}/bin/native-image" executable = "${graalVm.baseDir}/bin/$nativeImageCommandName"
// JARs to exclude from the class path for the native-image build. // JARs to exclude from the class path for the native-image build.
val exclusions = listOf(libs.truffleApi, libs.graalSdk).map { it.get().module.name } val exclusions = listOf(libs.truffleApi, libs.graalSdk).map { it.get().module.name }
@@ -276,6 +284,15 @@ val alpineExecutableAmd64: TaskProvider<Exec> by tasks.registering(Exec::class)
) )
} }
val windowsExecutableAmd64: TaskProvider<Exec> by tasks.registering(Exec::class) {
dependsOn(":installGraalVmAmd64")
configureExecutable(
buildInfo.graalVmAmd64,
layout.buildDirectory.file("executable/pkl-windows-amd64"),
listOf("-Dfile.encoding=UTF-8")
)
}
tasks.assembleNative { tasks.assembleNative {
when { when {
buildInfo.os.isMacOsX -> { buildInfo.os.isMacOsX -> {
@@ -284,6 +301,9 @@ tasks.assembleNative {
dependsOn(macExecutableAarch64) dependsOn(macExecutableAarch64)
} }
} }
buildInfo.os.isWindows -> {
dependsOn(windowsExecutableAmd64)
}
buildInfo.os.isLinux && buildInfo.arch == "aarch64" -> { buildInfo.os.isLinux && buildInfo.arch == "aarch64" -> {
dependsOn(linuxExecutableAarch64) dependsOn(linuxExecutableAarch64)
} }
@@ -393,6 +413,20 @@ publishing {
description.set("Native Pkl CLI executable for linux/amd64 and statically linked to musl.") description.set("Native Pkl CLI executable for linux/amd64 and statically linked to musl.")
} }
} }
create<MavenPublication>("windowsExecutableAmd64") {
artifactId = "pkl-cli-windows-amd64"
artifact(stagedWindowsAmd64Executable.singleFile) {
classifier = null
extension = "exe"
builtBy(stagedWindowsAmd64Executable)
}
pom {
name.set("pkl-cli-windows-amd64")
url.set("https://github.com/apple/pkl/tree/main/pkl-cli")
description.set("Native Pkl CLI executable for windows/amd64.")
}
}
} }
} }
@@ -403,5 +437,6 @@ signing {
sign(publishing.publications["macExecutableAarch64"]) sign(publishing.publications["macExecutableAarch64"])
sign(publishing.publications["macExecutableAmd64"]) sign(publishing.publications["macExecutableAmd64"])
sign(publishing.publications["alpineLinuxExecutableAmd64"]) sign(publishing.publications["alpineLinuxExecutableAmd64"])
sign(publishing.publications["windowsExecutableAmd64"])
} }
//endregion //endregion
@@ -111,7 +111,9 @@ constructor(
return moduleUris.associateWith { uri -> return moduleUris.associateWith { uri ->
val moduleDir: String? = val moduleDir: String? =
IoUtils.toPath(uri)?.let { workingDir.relativize(it.parent).toString().ifEmpty { "." } } IoUtils.toPath(uri)?.let {
IoUtils.relativize(it.parent, workingDir).toString().ifEmpty { "." }
}
val moduleKey = val moduleKey =
try { try {
moduleResolver.resolve(uri) moduleResolver.resolve(uri)
@@ -158,7 +160,7 @@ constructor(
} else { } else {
if (output.isNotEmpty()) { if (output.isNotEmpty()) {
outputFile.writeString( outputFile.writeString(
options.moduleOutputSeparator + IoUtils.getLineSeparator(), options.moduleOutputSeparator + '\n',
Charsets.UTF_8, Charsets.UTF_8,
StandardOpenOption.WRITE, StandardOpenOption.WRITE,
StandardOpenOption.APPEND StandardOpenOption.APPEND
@@ -192,6 +194,14 @@ constructor(
if (uri == VmUtils.REPL_TEXT_URI) ModuleSource.create(uri, reader.readText()) if (uri == VmUtils.REPL_TEXT_URI) ModuleSource.create(uri, reader.readText())
else ModuleSource.uri(uri) else ModuleSource.uri(uri)
private fun checkPathSpec(pathSpec: String) {
val illegal = pathSpec.indexOfFirst { IoUtils.isReservedFilenameChar(it) && it != '/' }
if (illegal == -1) {
return
}
throw CliException("Path spec `$pathSpec` contains illegal character `${pathSpec[illegal]}`.")
}
/** /**
* Renders each module's `output.files`, writing each entry as a file into the specified output * Renders each module's `output.files`, writing each entry as a file into the specified output
* directory. * directory.
@@ -207,6 +217,7 @@ constructor(
val moduleSource = toModuleSource(moduleUri, consoleReader) val moduleSource = toModuleSource(moduleUri, consoleReader)
val output = evaluator.evaluateOutputFiles(moduleSource) val output = evaluator.evaluateOutputFiles(moduleSource)
for ((pathSpec, fileOutput) in output) { for ((pathSpec, fileOutput) in output) {
checkPathSpec(pathSpec)
val resolvedPath = outputDir.resolve(pathSpec).normalize() val resolvedPath = outputDir.resolve(pathSpec).normalize()
val realPath = if (resolvedPath.exists()) resolvedPath.toRealPath() else resolvedPath val realPath = if (resolvedPath.exists()) resolvedPath.toRealPath() else resolvedPath
if (!realPath.startsWith(outputDir)) { if (!realPath.startsWith(outputDir)) {
@@ -228,7 +239,10 @@ constructor(
writtenFiles[realPath] = OutputFile(pathSpec, moduleUri) writtenFiles[realPath] = OutputFile(pathSpec, moduleUri)
realPath.createParentDirectories() realPath.createParentDirectories()
realPath.writeString(fileOutput.text) realPath.writeString(fileOutput.text)
consoleWriter.write(currentWorkingDir.relativize(resolvedPath).toString() + "\n") consoleWriter.write(
IoUtils.relativize(resolvedPath, currentWorkingDir).toString() +
IoUtils.getLineSeparator()
)
consoleWriter.flush() consoleWriter.flush()
} }
} }
@@ -42,7 +42,11 @@ class CliPackageDownloader(
} }
when (errors.size) { when (errors.size) {
0 -> return 0 -> return
1 -> throw CliException(errors.values.single().message!!) 1 ->
throw CliException(
errors.values.single().message
?: ("An unexpected error occurred: " + errors.values.single())
)
else -> else ->
throw CliException( throw CliException(
buildString { buildString {
@@ -22,6 +22,7 @@ import com.github.ajalt.clikt.parameters.groups.provideDelegate
import java.net.URI import java.net.URI
import org.pkl.cli.CliTestRunner import org.pkl.cli.CliTestRunner
import org.pkl.commons.cli.commands.BaseCommand import org.pkl.commons.cli.commands.BaseCommand
import org.pkl.commons.cli.commands.BaseOptions
import org.pkl.commons.cli.commands.ProjectOptions import org.pkl.commons.cli.commands.ProjectOptions
import org.pkl.commons.cli.commands.TestOptions import org.pkl.commons.cli.commands.TestOptions
@@ -29,7 +30,7 @@ class TestCommand(helpLink: String) :
BaseCommand(name = "test", help = "Run tests within the given module(s)", helpLink = helpLink) { BaseCommand(name = "test", help = "Run tests within the given module(s)", helpLink = helpLink) {
val modules: List<URI> by val modules: List<URI> by
argument(name = "<modules>", help = "Module paths or URIs to evaluate.") argument(name = "<modules>", help = "Module paths or URIs to evaluate.")
.convert { parseModuleName(it) } .convert { BaseOptions.parseModuleName(it) }
.multiple() .multiple()
private val projectOptions by ProjectOptions() private val projectOptions by ProjectOptions()
@@ -28,6 +28,9 @@ import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.AfterEach
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.EnabledOnOs
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.EnumSource import org.junit.jupiter.params.provider.EnumSource
@@ -424,7 +427,10 @@ result = someLib.x
checkOutputFile(outputFiles[0], "result.pcf", contents) checkOutputFile(outputFiles[0], "result.pcf", contents)
} }
// Can't reliably create symlinks on Windows.
// Might get errors like "A required privilege is not held by the client".
@Test @Test
@DisabledOnOs(OS.WINDOWS)
fun `moduleDir is relative to workingDir even through symlinks`() { fun `moduleDir is relative to workingDir even through symlinks`() {
val contents = "foo = 42" val contents = "foo = 42"
val realWorkingDir = tempDir.resolve("workingDir").createDirectories() val realWorkingDir = tempDir.resolve("workingDir").createDirectories()
@@ -978,6 +984,56 @@ result = someLib.x
.hasMessageContaining("resolve to the same file path") .hasMessageContaining("resolve to the same file path")
} }
@Test
@EnabledOnOs(OS.WINDOWS)
fun `multiple-file output throws when using invalid Windows characters`() {
val moduleUri =
writePklFile(
"test.pkl",
"""
output {
files {
["foo:bar"] { text = "bar" }
}
}
"""
.trimIndent()
)
val options =
CliEvaluatorOptions(
CliBaseOptions(sourceModules = listOf(moduleUri), workingDir = tempDir),
multipleFileOutputPath = ".output"
)
assertThatCode { evalToConsole(options) }
.hasMessageContaining("Path spec `foo:bar` contains illegal character `:`.")
}
@Test
@EnabledOnOs(OS.WINDOWS)
fun `multiple-file output - cannot use backslash as dir separator on Windows`() {
val moduleUri =
writePklFile(
"test.pkl",
"""
output {
files {
["foo\\bar"] { text = "bar" }
}
}
"""
.trimIndent()
)
val options =
CliEvaluatorOptions(
CliBaseOptions(sourceModules = listOf(moduleUri), workingDir = tempDir),
multipleFileOutputPath = ".output"
)
assertThatCode { evalToConsole(options) }
.hasMessageContaining("Path spec `foo\\bar` contains illegal character `\\`.")
}
@Test @Test
fun `evaluate output expression`() { fun `evaluate output expression`() {
val moduleUri = val moduleUri =
@@ -24,6 +24,8 @@ import org.assertj.core.api.Assertions.assertThat
import org.assertj.core.api.AssertionsForClassTypes.assertThatCode import org.assertj.core.api.AssertionsForClassTypes.assertThatCode
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.pkl.cli.commands.EvalCommand import org.pkl.cli.commands.EvalCommand
import org.pkl.cli.commands.RootCommand import org.pkl.cli.commands.RootCommand
@@ -54,7 +56,10 @@ class CliMainTest {
assertThatCode { cmd.parse(arrayOf("eval")) }.hasMessage("""Missing argument "<modules>"""") assertThatCode { cmd.parse(arrayOf("eval")) }.hasMessage("""Missing argument "<modules>"""")
} }
// Can't reliably create symlinks on Windows.
// Might get errors like "A required privilege is not held by the client".
@Test @Test
@DisabledOnOs(OS.WINDOWS)
fun `output to symlinked directory works`(@TempDir tempDir: Path) { fun `output to symlinked directory works`(@TempDir tempDir: Path) {
val code = val code =
""" """
@@ -15,6 +15,7 @@
*/ */
package org.pkl.cli package org.pkl.cli
import java.io.File
import java.io.StringWriter import java.io.StringWriter
import java.net.URI import java.net.URI
import java.nio.file.FileSystems import java.nio.file.FileSystems
@@ -27,6 +28,8 @@ import org.assertj.core.api.Assertions.assertThatCode
import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.AfterAll
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.pkl.commons.cli.CliBaseOptions import org.pkl.commons.cli.CliBaseOptions
import org.pkl.commons.cli.CliException import org.pkl.commons.cli.CliException
@@ -35,6 +38,7 @@ import org.pkl.commons.readString
import org.pkl.commons.test.FileTestUtils import org.pkl.commons.test.FileTestUtils
import org.pkl.commons.test.PackageServer import org.pkl.commons.test.PackageServer
import org.pkl.commons.writeString import org.pkl.commons.writeString
import org.pkl.core.util.IoUtils
class CliProjectPackagerTest { class CliProjectPackagerTest {
companion object { companion object {
@@ -690,7 +694,11 @@ class CliProjectPackagerTest {
) )
} }
// Absolute path imports on Windows must use an absolute URI (e.g. file:///C:/Foo/Bar), because
// they must contain drive letters, which conflict with schemes.
// We skip validation for absolute URIs, so effectively we skip this check on Windows.
@Test @Test
@DisabledOnOs(OS.WINDOWS)
fun `import path verification -- absolute import from root dir`(@TempDir tempDir: Path) { fun `import path verification -- absolute import from root dir`(@TempDir tempDir: Path) {
tempDir.writeFile( tempDir.writeFile(
"main.pkl", "main.pkl",
@@ -738,6 +746,7 @@ class CliProjectPackagerTest {
} }
@Test @Test
@DisabledOnOs(OS.WINDOWS)
fun `import path verification -- absolute read from root dir`(@TempDir tempDir: Path) { fun `import path verification -- absolute read from root dir`(@TempDir tempDir: Path) {
tempDir.writeFile( tempDir.writeFile(
"main.pkl", "main.pkl",
@@ -858,17 +867,18 @@ class CliProjectPackagerTest {
consoleWriter = out consoleWriter = out
) )
.run() .run()
val sep = File.separatorChar
assertThat(out.toString()) assertThat(out.toString())
.isEqualTo( .isEqualToNormalizingNewlines(
""" """
.out/project1@1.0.0/project1@1.0.0.zip .out${sep}project1@1.0.0${sep}project1@1.0.0.zip
.out/project1@1.0.0/project1@1.0.0.zip.sha256 .out${sep}project1@1.0.0${sep}project1@1.0.0.zip.sha256
.out/project1@1.0.0/project1@1.0.0 .out${sep}project1@1.0.0${sep}project1@1.0.0
.out/project1@1.0.0/project1@1.0.0.sha256 .out${sep}project1@1.0.0${sep}project1@1.0.0.sha256
.out/project2@2.0.0/project2@2.0.0.zip .out${sep}project2@2.0.0${sep}project2@2.0.0.zip
.out/project2@2.0.0/project2@2.0.0.zip.sha256 .out${sep}project2@2.0.0${sep}project2@2.0.0.zip.sha256
.out/project2@2.0.0/project2@2.0.0 .out${sep}project2@2.0.0${sep}project2@2.0.0
.out/project2@2.0.0/project2@2.0.0.sha256 .out${sep}project2@2.0.0${sep}project2@2.0.0.sha256
""" """
.trimIndent() .trimIndent()
@@ -956,13 +966,14 @@ class CliProjectPackagerTest {
consoleWriter = out consoleWriter = out
) )
.run() .run()
val sep = File.separatorChar
assertThat(out.toString()) assertThat(out.toString())
.isEqualTo( .isEqualToNormalizingNewlines(
""" """
.out/mangos@1.0.0/mangos@1.0.0.zip .out${sep}mangos@1.0.0${sep}mangos@1.0.0.zip
.out/mangos@1.0.0/mangos@1.0.0.zip.sha256 .out${sep}mangos@1.0.0${sep}mangos@1.0.0.zip.sha256
.out/mangos@1.0.0/mangos@1.0.0 .out${sep}mangos@1.0.0${sep}mangos@1.0.0
.out/mangos@1.0.0/mangos@1.0.0.sha256 .out${sep}mangos@1.0.0${sep}mangos@1.0.0.sha256
""" """
.trimIndent() .trimIndent()
@@ -971,7 +982,7 @@ class CliProjectPackagerTest {
private fun Path.zipFilePaths(): List<String> { private fun Path.zipFilePaths(): List<String> {
return FileSystems.newFileSystem(URI("jar:${toUri()}"), emptyMap<String, String>()).use { fs -> return FileSystems.newFileSystem(URI("jar:${toUri()}"), emptyMap<String, String>()).use { fs ->
Files.walk(fs.getPath("/")).map { it.toString() }.collect(Collectors.toList()) Files.walk(fs.getPath("/")).map(IoUtils::toNormalizedPathString).collect(Collectors.toList())
} }
} }
} }
@@ -15,6 +15,7 @@
*/ */
package org.pkl.cli package org.pkl.cli
import java.io.File
import java.io.StringWriter import java.io.StringWriter
import java.nio.file.Path import java.nio.file.Path
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
@@ -26,6 +27,7 @@ import org.pkl.commons.cli.CliBaseOptions
import org.pkl.commons.cli.CliException import org.pkl.commons.cli.CliException
import org.pkl.commons.test.FileTestUtils import org.pkl.commons.test.FileTestUtils
import org.pkl.commons.test.PackageServer import org.pkl.commons.test.PackageServer
import org.pkl.core.util.IoUtils
class CliProjectResolverTest { class CliProjectResolverTest {
companion object { companion object {
@@ -354,7 +356,7 @@ class CliProjectResolverTest {
) )
assertThat(errOut.toString()) assertThat(errOut.toString())
.isEqualTo( .isEqualTo(
"WARN: local dependency `package://localhost:0/fruit@1.0.0` was overridden to remote dependency `package://localhost:0/fruit@1.0.5`.\n" "WARN: local dependency `package://localhost:0/fruit@1.0.0` was overridden to remote dependency `package://localhost:0/fruit@1.0.5`.${IoUtils.getLineSeparator()}"
) )
} }
@@ -401,11 +403,12 @@ class CliProjectResolverTest {
errWriter = errOut errWriter = errOut
) )
.run() .run()
val sep = File.separatorChar
assertThat(consoleOut.toString()) assertThat(consoleOut.toString())
.isEqualTo( .isEqualToNormalizingNewlines(
""" """
$tempDir/project1/PklProject.deps.json $tempDir${sep}project1${sep}PklProject.deps.json
$tempDir/project2/PklProject.deps.json $tempDir${sep}project2${sep}PklProject.deps.json
""" """
.trimIndent() .trimIndent()
@@ -17,11 +17,6 @@ package org.pkl.commons.cli.commands
import com.github.ajalt.clikt.core.CliktCommand import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.groups.provideDelegate import com.github.ajalt.clikt.parameters.groups.provideDelegate
import java.net.URI
import java.net.URISyntaxException
import org.pkl.commons.cli.CliException
import org.pkl.core.runtime.VmUtils
import org.pkl.core.util.IoUtils
abstract class BaseCommand(name: String, helpLink: String, help: String = "") : abstract class BaseCommand(name: String, helpLink: String, help: String = "") :
CliktCommand( CliktCommand(
@@ -30,30 +25,4 @@ abstract class BaseCommand(name: String, helpLink: String, help: String = "") :
epilog = "For more information, visit $helpLink", epilog = "For more information, visit $helpLink",
) { ) {
val baseOptions by BaseOptions() val baseOptions by BaseOptions()
/**
* Parses [moduleName] into a URI. If scheme is not present, we expect that this is a file path
* and encode any possibly invalid characters. If a scheme is present, we expect that this is a
* valid URI.
*/
protected fun parseModuleName(moduleName: String): URI =
when (moduleName) {
"-" -> VmUtils.REPL_TEXT_URI
else ->
try {
IoUtils.toUri(moduleName)
} catch (e: URISyntaxException) {
val message = buildString {
append("Module URI `$moduleName` has invalid syntax (${e.reason}).")
if (e.index > -1) {
append("\n\n")
append(moduleName)
append("\n")
append(" ".repeat(e.index))
append("^")
}
}
throw CliException(message)
}
}
} }
@@ -22,14 +22,50 @@ import com.github.ajalt.clikt.parameters.types.long
import com.github.ajalt.clikt.parameters.types.path import com.github.ajalt.clikt.parameters.types.path
import java.io.File import java.io.File
import java.net.URI import java.net.URI
import java.net.URISyntaxException
import java.nio.file.Path import java.nio.file.Path
import java.time.Duration import java.time.Duration
import java.util.regex.Pattern import java.util.regex.Pattern
import org.pkl.commons.cli.CliBaseOptions import org.pkl.commons.cli.CliBaseOptions
import org.pkl.commons.cli.CliException
import org.pkl.core.runtime.VmUtils
import org.pkl.core.util.IoUtils import org.pkl.core.util.IoUtils
@Suppress("MemberVisibilityCanBePrivate") @Suppress("MemberVisibilityCanBePrivate")
class BaseOptions : OptionGroup() { class BaseOptions : OptionGroup() {
companion object {
/**
* Parses [moduleName] into a URI. If scheme is not present, we expect that this is a file path
* and encode any possibly invalid characters, and also normalize directory separators. If a
* scheme is present, we expect that this is a valid URI.
*/
fun parseModuleName(moduleName: String): URI =
when (moduleName) {
"-" -> VmUtils.REPL_TEXT_URI
else ->
// Don't use `IoUtils.toUri` here becaus we need to normalize `\` paths to `/` on Windows.
try {
if (IoUtils.isUriLike(moduleName)) URI(moduleName)
// Can't just use URI constructor, because URI(null, null, "C:/foo/bar", null) turns
// into `URI("C", null, "/foo/bar", null)`.
else if (IoUtils.isWindowsAbsolutePath(moduleName)) Path.of(moduleName).toUri()
else URI(null, null, IoUtils.toNormalizedPathString(Path.of(moduleName)), null)
} catch (e: URISyntaxException) {
val message = buildString {
append("Module URI `$moduleName` has invalid syntax (${e.reason}).")
if (e.index > -1) {
append("\n\n")
append(moduleName)
append("\n")
append(" ".repeat(e.index))
append("^")
}
}
throw CliException(message)
}
}
}
private val defaults = CliBaseOptions() private val defaults = CliBaseOptions()
private val output = private val output =
@@ -114,7 +150,7 @@ class BaseOptions : OptionGroup() {
val settings: URI? by val settings: URI? by
option(names = arrayOf("--settings"), help = "Pkl settings module to use.").single().convert { option(names = arrayOf("--settings"), help = "Pkl settings module to use.").single().convert {
IoUtils.toUri(it) parseModuleName(it)
} }
val timeout: Duration? by val timeout: Duration? by
@@ -29,7 +29,7 @@ abstract class ModulesCommand(name: String, helpLink: String, help: String = "")
) { ) {
open val modules: List<URI> by open val modules: List<URI> by
argument(name = "<modules>", help = "Module paths or URIs to evaluate.") argument(name = "<modules>", help = "Module paths or URIs to evaluate.")
.convert { parseModuleName(it) } .convert { BaseOptions.parseModuleName(it) }
.multiple(required = true) .multiple(required = true)
protected val projectOptions by ProjectOptions() protected val projectOptions by ProjectOptions()
@@ -58,6 +58,11 @@ for (packageDir in file("src/main/files/packages").listFiles()!!) {
} }
doLast { doLast {
val outputFile = destinationDir.get().asFile.resolve("${packageDir.name}.json") val outputFile = destinationDir.get().asFile.resolve("${packageDir.name}.json")
if (buildInfo.os.isWindows) {
val contents = outputFile.readText()
// workaround for https://github.com/gradle/gradle/issues/1151
outputFile.writeText(contents.replace("\r\n", "\n"))
}
shasumFile.get().asFile.writeText(outputFile.computeChecksum()) shasumFile.get().asFile.writeText(outputFile.computeChecksum())
} }
} }
@@ -31,6 +31,7 @@ import org.junit.platform.engine.support.hierarchical.EngineExecutionContext
import org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine import org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine
import org.junit.platform.engine.support.hierarchical.Node import org.junit.platform.engine.support.hierarchical.Node
import org.junit.platform.engine.support.hierarchical.Node.DynamicTestExecutor import org.junit.platform.engine.support.hierarchical.Node.DynamicTestExecutor
import org.pkl.commons.toNormalizedPathString
abstract class InputOutputTestEngine : abstract class InputOutputTestEngine :
HierarchicalTestEngine<InputOutputTestEngine.ExecutionContext>() { HierarchicalTestEngine<InputOutputTestEngine.ExecutionContext>() {
@@ -106,7 +107,7 @@ abstract class InputOutputTestEngine :
): TestDescriptor { ): TestDescriptor {
dirNode.inputDir.useDirectoryEntries { children -> dirNode.inputDir.useDirectoryEntries { children ->
for (child in children) { for (child in children) {
val testPath = child.toString() val testPath = child.toNormalizedPathString()
val testName = child.fileName.toString() val testName = child.fileName.toString()
if (child.isRegularFile()) { if (child.isRegularFile()) {
if ( if (
@@ -71,3 +71,13 @@ fun Path.deleteRecursively() {
walk().use { paths -> paths.sorted(Comparator.reverseOrder()).forEach { it.deleteIfExists() } } walk().use { paths -> paths.sorted(Comparator.reverseOrder()).forEach { it.deleteIfExists() } }
} }
} }
private val isWindows by lazy { System.getProperty("os.name").contains("Windows") }
/** Copy implementation from IoUtils.toNormalizedPathString */
fun Path.toNormalizedPathString(): String {
if (isWindows) {
return toString().replace("\\", "/")
}
return toString()
}
@@ -15,15 +15,24 @@
*/ */
package org.pkl.commons package org.pkl.commons
import java.io.File
import java.net.URI import java.net.URI
import java.nio.file.Path import java.nio.file.Path
import java.util.regex.Pattern
fun String.toPath(): Path = Path.of(this) fun String.toPath(): Path = Path.of(this)
private val uriLike = Pattern.compile("\\w+:[^\\\\].*")
private val windowsPathLike = Pattern.compile("\\w:\\\\.*")
/** Copy of org.pkl.core.util.IoUtils.toUri */ /** Copy of org.pkl.core.util.IoUtils.toUri */
fun String.toUri(): URI = fun String.toUri(): URI {
if (contains(":")) { if (uriLike.matcher(this).matches()) {
URI(this) return URI(this)
} else {
URI(null, null, this, null)
} }
if (windowsPathLike.matcher(this).matches()) {
return File(this).toURI()
}
return URI(null, null, this, null)
}
+18
View File
@@ -267,6 +267,21 @@ val testAlpineExecutableAmd64 by tasks.registering(Test::class) {
} }
} }
val testWindowsExecutableAmd64 by tasks.registering(Test::class) {
dependsOn(":pkl-cli:windowsExecutableAmd64")
inputs.dir("src/test/files/LanguageSnippetTests/input")
inputs.dir("src/test/files/LanguageSnippetTests/input-helper")
inputs.dir("src/test/files/LanguageSnippetTests/output")
testClassesDirs = files(tasks.test.get().testClassesDirs)
classpath = tasks.test.get().classpath
useJUnitPlatform {
includeEngines("WindowsLanguageSnippetTestsEngine")
}
}
tasks.testNative { tasks.testNative {
when { when {
buildInfo.os.isMacOsX -> { buildInfo.os.isMacOsX -> {
@@ -284,6 +299,9 @@ tasks.testNative {
dependsOn(testAlpineExecutableAmd64) dependsOn(testAlpineExecutableAmd64)
} }
} }
buildInfo.os.isWindows -> {
dependsOn(testWindowsExecutableAmd64)
}
} }
} }
@@ -31,6 +31,7 @@ public final class Platform {
var pklVersion = Release.current().version().toString(); var pklVersion = Release.current().version().toString();
var osName = System.getProperty("os.name"); var osName = System.getProperty("os.name");
if (osName.equals("Mac OS X")) osName = "macOS"; if (osName.equals("Mac OS X")) osName = "macOS";
if (osName.contains("Windows")) osName = "Windows";
var osVersion = System.getProperty("os.version"); var osVersion = System.getProperty("os.version");
var architecture = System.getProperty("os.arch"); var architecture = System.getProperty("os.arch");
@@ -49,6 +49,7 @@ public final class Release {
var commitId = properties.getProperty("commitId"); var commitId = properties.getProperty("commitId");
var osName = System.getProperty("os.name"); var osName = System.getProperty("os.name");
if (osName.equals("Mac OS X")) osName = "macOS"; if (osName.equals("Mac OS X")) osName = "macOS";
if (osName.contains("Windows")) osName = "Windows";
var osVersion = System.getProperty("os.version"); var osVersion = System.getProperty("os.version");
var os = osName + " " + osVersion; var os = osName + " " + osVersion;
var flavor = TruffleOptions.AOT ? "native" : "Java " + System.getProperty("java.version"); var flavor = TruffleOptions.AOT ? "native" : "Java " + System.getProperty("java.version");
@@ -1794,10 +1794,17 @@ public final class AstBuilder extends AbstractAstBuilder<Object> {
try { try {
resolvedUri = IoUtils.resolve(context.getSecurityManager(), moduleKey, parsedUri); resolvedUri = IoUtils.resolve(context.getSecurityManager(), moduleKey, parsedUri);
} catch (FileNotFoundException e) { } catch (FileNotFoundException e) {
throw exceptionBuilder()
.evalError("cannotFindModule", importUri) var exceptionBuilder =
.withSourceSection(createSourceSection(importUriCtx)) exceptionBuilder()
.build(); .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) { } catch (URISyntaxException e) {
throw exceptionBuilder() throw exceptionBuilder()
.evalError("invalidModuleUri", importUri) .evalError("invalidModuleUri", importUri)
@@ -18,8 +18,8 @@ package org.pkl.core.module;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.nio.file.FileSystemNotFoundException; import java.nio.file.FileSystemNotFoundException;
import java.nio.file.Path;
import java.nio.file.Paths; import java.nio.file.Paths;
import java.nio.file.spi.FileSystemProvider;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@@ -141,15 +141,20 @@ public final class ModuleKeyFactories {
private static class File implements ModuleKeyFactory { private static class File implements ModuleKeyFactory {
@Override @Override
public Optional<ModuleKey> create(URI uri) { public Optional<ModuleKey> create(URI uri) {
Path path; // skip loading providers if the scheme is `file`.
try { if (uri.getScheme().equalsIgnoreCase("file")) {
path = Path.of(uri); return Optional.of(ModuleKeys.file(uri));
} catch (FileSystemNotFoundException | IllegalArgumentException e) { }
// none of the installed file system providers can handle this URI // don't handle jar-file URIs (these are handled by GenericUrl).
if (uri.getScheme().equalsIgnoreCase("jar")) {
return Optional.empty(); return Optional.empty();
} }
for (FileSystemProvider provider : FileSystemProvider.installedProviders()) {
return Optional.of(ModuleKeys.file(uri, path)); if (provider.getScheme().equalsIgnoreCase(uri.getScheme())) {
return Optional.of(ModuleKeys.file(uri));
}
}
return Optional.empty();
} }
} }
@@ -16,9 +16,11 @@
package org.pkl.core.module; package org.pkl.core.module;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import java.io.File;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.net.JarURLConnection;
import java.net.URI; import java.net.URI;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.net.http.HttpRequest; import java.net.http.HttpRequest;
@@ -88,8 +90,8 @@ public final class ModuleKeys {
} }
/** Creates a module key for a {@code file:} module. */ /** Creates a module key for a {@code file:} module. */
public static ModuleKey file(URI uri, Path path) { public static ModuleKey file(URI uri) {
return new File(uri, path); return new File(uri);
} }
/** /**
@@ -290,12 +292,10 @@ public final class ModuleKeys {
private static class File extends DependencyAwareModuleKey { private static class File extends DependencyAwareModuleKey {
final URI uri; final URI uri;
final Path path;
File(URI uri, Path path) { File(URI uri) {
super(uri); super(uri);
this.uri = uri; this.uri = uri;
this.path = path;
} }
@Override @Override
@@ -316,7 +316,13 @@ public final class ModuleKeys {
public ResolvedModuleKey resolve(SecurityManager securityManager) public ResolvedModuleKey resolve(SecurityManager securityManager)
throws IOException, SecurityManagerException { throws IOException, SecurityManagerException {
securityManager.checkResolveModule(uri); 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(); var resolvedUri = realPath.toUri();
securityManager.checkResolveModule(resolvedUri); securityManager.checkResolveModule(resolvedUri);
return ResolvedModuleKeys.file(this, resolvedUri, realPath); return ResolvedModuleKeys.file(this, resolvedUri, realPath);
@@ -325,7 +331,7 @@ public final class ModuleKeys {
@Override @Override
protected Map<String, ? extends Dependency> getDependencies() { protected Map<String, ? extends Dependency> getDependencies() {
var projectDepsManager = VmContext.get(null).getProjectDependenciesManager(); var projectDepsManager = VmContext.get(null).getProjectDependenciesManager();
if (projectDepsManager == null || !projectDepsManager.hasPath(path)) { if (projectDepsManager == null || !projectDepsManager.hasPath(Path.of(uri))) {
throw new PackageLoadError("cannotResolveDependencyNoProject"); throw new PackageLoadError("cannotResolveDependencyNoProject");
} }
return projectDepsManager.getDependencies(); return projectDepsManager.getDependencies();
@@ -519,6 +525,12 @@ public final class ModuleKeys {
var url = IoUtils.toUrl(uri); var url = IoUtils.toUrl(uri);
var conn = url.openConnection(); var conn = url.openConnection();
conn.connect(); 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()) { try (InputStream stream = conn.getInputStream()) {
URI redirected; URI redirected;
try { try {
@@ -30,6 +30,7 @@ import java.util.Map;
import javax.annotation.concurrent.GuardedBy; import javax.annotation.concurrent.GuardedBy;
import org.pkl.core.module.PathElement.TreePathElement; import org.pkl.core.module.PathElement.TreePathElement;
import org.pkl.core.runtime.FileSystemManager; import org.pkl.core.runtime.FileSystemManager;
import org.pkl.core.util.IoUtils;
import org.pkl.core.util.LateInit; 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) // in case of duplicate path, first entry wins (cf. class loader)
stream.forEach( stream.forEach(
(path) -> { (path) -> {
var relativized = basePath.relativize(path); var relativized = IoUtils.relativize(path, basePath);
fileCache.putIfAbsent(relativized.toString(), path); fileCache.putIfAbsent(IoUtils.toNormalizedPathString(relativized), path);
var element = cachedPathElementRoot; var element = cachedPathElementRoot;
for (var i = 0; i < relativized.getNameCount(); i++) { for (var i = 0; i < relativized.getNameCount(); i++) {
var name = relativized.getName(i).toString(); var name = relativized.getName(i).toString();
@@ -207,13 +207,15 @@ public final class ProjectDependenciesManager {
if (projectDeps == null) { if (projectDeps == null) {
var depsPath = getProjectDepsFile(); var depsPath = getProjectDepsFile();
if (!Files.exists(depsPath)) { if (!Files.exists(depsPath)) {
throw new VmExceptionBuilder().evalError("missingProjectDepsJson", projectDir).build(); throw new VmExceptionBuilder()
.evalError("missingProjectDepsJson", projectDir.toUri())
.build();
} }
try { try {
projectDeps = ProjectDeps.parse(depsPath); projectDeps = ProjectDeps.parse(depsPath);
} catch (IOException | URISyntaxException | JsonParseException e) { } catch (IOException | URISyntaxException | JsonParseException e) {
throw new VmExceptionBuilder() throw new VmExceptionBuilder()
.evalError("invalidProjectDepsJson", depsPath, e.getMessage()) .evalError("invalidProjectDepsJson", depsPath.toUri(), e.getMessage())
.withCause(e) .withCause(e)
.build(); .build();
} }
@@ -15,10 +15,12 @@
*/ */
package org.pkl.core.module; package org.pkl.core.module;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.net.URL; import java.net.URL;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.AccessDeniedException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import org.pkl.core.util.IoUtils; import org.pkl.core.util.IoUtils;
@@ -75,7 +77,16 @@ public final class ResolvedModuleKeys {
@Override @Override
public String loadSource() throws IOException { 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) { public Path resolveAssetPath(Path projectDir, PackageAssetUri packageAssetUri) {
// drop 1 to remove leading `/` // drop 1 to remove leading `/`
var assetPath = packageAssetUri.getAssetPath().toString().substring(1); var assetPath = packageAssetUri.getAssetPath().substring(1);
return projectDir.resolve(path).resolve(assetPath); return projectDir.resolve(path).resolve(assetPath);
} }
@@ -20,6 +20,7 @@ import java.net.URISyntaxException;
import java.nio.file.Path; import java.nio.file.Path;
import org.pkl.core.Version; import org.pkl.core.Version;
import org.pkl.core.util.ErrorMessages; 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 * 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 { public final class PackageAssetUri {
private final URI uri; private final URI uri;
private final PackageUri packageUri; private final PackageUri packageUri;
private final Path assetPath; private final String assetPath;
public static PackageAssetUri create(URI uri) { public static PackageAssetUri create(URI uri) {
try { try {
@@ -41,7 +42,7 @@ public final class PackageAssetUri {
public PackageAssetUri(PackageUri packageUri, String assetPath) { public PackageAssetUri(PackageUri packageUri, String assetPath) {
this.uri = packageUri.getUri().resolve("#" + assetPath); this.uri = packageUri.getUri().resolve("#" + assetPath);
this.packageUri = packageUri; this.packageUri = packageUri;
this.assetPath = Path.of(assetPath); this.assetPath = assetPath;
} }
public PackageAssetUri(String uri) throws URISyntaxException { public PackageAssetUri(String uri) throws URISyntaxException {
@@ -60,7 +61,7 @@ public final class PackageAssetUri {
throw new URISyntaxException( throw new URISyntaxException(
uri.toString(), ErrorMessages.create("cannotHaveRelativeFragment", fragment, uri)); uri.toString(), ErrorMessages.create("cannotHaveRelativeFragment", fragment, uri));
} }
this.assetPath = Path.of(fragment); this.assetPath = fragment;
} }
public URI getUri() { public URI getUri() {
@@ -71,7 +72,7 @@ public final class PackageAssetUri {
return packageUri; return packageUri;
} }
public Path getAssetPath() { public String getAssetPath() {
return assetPath; return assetPath;
} }
@@ -102,6 +103,7 @@ public final class PackageAssetUri {
} }
public PackageAssetUri resolve(String path) { 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); var entries = cachedEntries.get(packageUri);
// need to normalize here but not in `doListElments` nor `doHasElement` because // need to normalize here but not in `doListElments` nor `doHasElement` because
// `TreePathElement.getElement` does normalization already. // `TreePathElement.getElement` does normalization already.
var path = uri.getAssetPath().normalize().toString(); var path = IoUtils.toNormalizedPathString(Path.of(uri.getAssetPath()).normalize());
assert path.startsWith("/");
return entries.get(path).array(); return entries.get(path).array();
} }
@@ -496,7 +495,9 @@ final class PackageResolvers {
downloadMetadata(packageUri, requestUri, tmpPath, checksums); downloadMetadata(packageUri, requestUri, tmpPath, checksums);
Files.createDirectories(cachePath.getParent()); Files.createDirectories(cachePath.getParent());
Files.move(tmpPath, cachePath, StandardCopyOption.ATOMIC_MOVE); Files.move(tmpPath, cachePath, StandardCopyOption.ATOMIC_MOVE);
Files.setPosixFilePermissions(cachePath, FILE_PERMISSIONS); if (!IoUtils.isWindows()) {
Files.setPosixFilePermissions(cachePath, FILE_PERMISSIONS);
}
return cachePath; return cachePath;
} finally { } finally {
Files.deleteIfExists(tmpPath); Files.deleteIfExists(tmpPath);
@@ -545,7 +546,9 @@ final class PackageResolvers {
verifyPackageZipBytes(packageUri, dependencyMetadata, checksumBytes); verifyPackageZipBytes(packageUri, dependencyMetadata, checksumBytes);
Files.createDirectories(cachePath.getParent()); Files.createDirectories(cachePath.getParent());
Files.move(tmpPath, cachePath, StandardCopyOption.ATOMIC_MOVE); Files.move(tmpPath, cachePath, StandardCopyOption.ATOMIC_MOVE);
Files.setPosixFilePermissions(cachePath, FILE_PERMISSIONS); if (!IoUtils.isWindows()) {
Files.setPosixFilePermissions(cachePath, FILE_PERMISSIONS);
}
return cachePath; return cachePath;
} finally { } finally {
Files.deleteIfExists(tmpPath); Files.deleteIfExists(tmpPath);
@@ -33,6 +33,7 @@ import org.pkl.core.packages.PackageUri;
import org.pkl.core.util.EconomicMaps; import org.pkl.core.util.EconomicMaps;
import org.pkl.core.util.EconomicSets; import org.pkl.core.util.EconomicSets;
import org.pkl.core.util.ErrorMessages; import org.pkl.core.util.ErrorMessages;
import org.pkl.core.util.IoUtils;
import org.pkl.core.util.Nullable; import org.pkl.core.util.Nullable;
/** /**
@@ -78,7 +79,7 @@ public final class ProjectDependenciesResolver {
private void log(String message) { private void log(String message) {
try { try {
logWriter.write(message + "\n"); logWriter.write(message + IoUtils.getLineSeparator());
} catch (IOException e) { } catch (IOException e) {
throw new UncheckedIOException(e); throw new UncheckedIOException(e);
} }
@@ -130,7 +131,7 @@ public final class ProjectDependenciesResolver {
var packageUri = declaredDependencies.getMyPackageUri(); var packageUri = declaredDependencies.getMyPackageUri();
assert packageUri != null; assert packageUri != null;
var projectDir = Path.of(declaredDependencies.getProjectFileUri()).getParent(); 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); var localDependency = new LocalDependency(packageUri.toProjectPackageUri(), relativePath);
updateDependency(localDependency); updateDependency(localDependency);
buildResolvedDependencies(declaredDependencies); buildResolvedDependencies(declaredDependencies);
@@ -35,6 +35,7 @@ import org.pkl.core.packages.PackageLoadError;
import org.pkl.core.packages.PackageUtils; import org.pkl.core.packages.PackageUtils;
import org.pkl.core.runtime.VmExceptionBuilder; import org.pkl.core.runtime.VmExceptionBuilder;
import org.pkl.core.util.EconomicMaps; import org.pkl.core.util.EconomicMaps;
import org.pkl.core.util.IoUtils;
import org.pkl.core.util.Nullable; import org.pkl.core.util.Nullable;
import org.pkl.core.util.json.Json; import org.pkl.core.util.json.Json;
import org.pkl.core.util.json.Json.FormatException; import org.pkl.core.util.json.Json.FormatException;
@@ -196,7 +197,7 @@ public final class ProjectDeps {
jsonWriter.beginObject(); jsonWriter.beginObject();
jsonWriter.name("type").value("local"); jsonWriter.name("type").value("local");
jsonWriter.name("uri").value(localDependency.getPackageUri().toString()); jsonWriter.name("uri").value(localDependency.getPackageUri().toString());
jsonWriter.name("path").value(localDependency.getPath().toString()); jsonWriter.name("path").value(IoUtils.toNormalizedPathString(localDependency.getPath()));
jsonWriter.endObject(); jsonWriter.endObject();
} }
@@ -20,6 +20,8 @@ import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.io.UncheckedIOException; import java.io.UncheckedIOException;
import java.io.Writer; import java.io.Writer;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.security.DigestOutputStream; import java.security.DigestOutputStream;
@@ -119,13 +121,18 @@ public final class ProjectPackager {
this.outputWriter = outputWriter; this.outputWriter = outputWriter;
} }
private void writeLine(String line) throws IOException {
outputWriter.write(line);
outputWriter.write(IoUtils.getLineSeparator());
}
public void createPackages() throws IOException { public void createPackages() throws IOException {
for (var project : projects) { for (var project : projects) {
var packageResult = doPackage(project); var packageResult = doPackage(project);
outputWriter.write(workingDir.relativize(packageResult.getMetadataFile()) + "\n"); writeLine(IoUtils.relativize(packageResult.getMetadataFile(), workingDir).toString());
outputWriter.write(workingDir.relativize(packageResult.getMetadataChecksumFile()) + "\n"); writeLine(IoUtils.relativize(packageResult.getMetadataChecksumFile(), workingDir).toString());
outputWriter.write(workingDir.relativize(packageResult.getZipFile()) + "\n"); writeLine(IoUtils.relativize(packageResult.getZipFile(), workingDir).toString());
outputWriter.write(workingDir.relativize(packageResult.getZipChecksumFile()) + "\n"); writeLine(IoUtils.relativize(packageResult.getZipChecksumFile(), workingDir).toString());
outputWriter.flush(); outputWriter.flush();
} }
} }
@@ -302,8 +309,8 @@ public final class ProjectPackager {
} }
try (var zos = new ZipOutputStream(digestOutputStream)) { try (var zos = new ZipOutputStream(digestOutputStream)) {
for (var file : files) { for (var file : files) {
var relativePath = project.getProjectDir().relativize(file); var relativePath = IoUtils.relativize(file, project.getProjectDir());
var zipEntry = new ZipEntry(relativePath.toString()); var zipEntry = new ZipEntry(IoUtils.toNormalizedPathString(relativePath));
zipEntry.setTimeLocal(ZIP_ENTRY_MTIME); zipEntry.setTimeLocal(ZIP_ENTRY_MTIME);
zos.putNextEntry(zipEntry); zos.putNextEntry(zipEntry);
Files.copy(file, zos); Files.copy(file, zos);
@@ -342,8 +349,8 @@ public final class ProjectPackager {
.filter(Files::isRegularFile) .filter(Files::isRegularFile)
.filter( .filter(
(it) -> { (it) -> {
var fileNameRelativeToProjectRoot = var relativePath = IoUtils.relativize(it, project.getProjectDir());
project.getProjectDir().relativize(it).toString(); var fileNameRelativeToProjectRoot = IoUtils.toNormalizedPathString(relativePath);
for (var pattern : excludePatterns) { for (var pattern : excludePatterns) {
if (pattern.matcher(it.getFileName().toString()).matches()) { if (pattern.matcher(it.getFileName().toString()).matches()) {
return false; return false;
@@ -363,7 +370,7 @@ public final class ProjectPackager {
} }
private boolean isAbsoluteImport(String importStr) { 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)) { if (isAbsoluteImport(importStr)) {
continue; continue;
} }
var importPath = Path.of(importStr); URI importUri;
if (importPath.isAbsolute() && !project.getProjectDir().toString().equals("/")) { 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() throw new VmExceptionBuilder()
.evalError("invalidRelativeProjectImport", importStr) .evalError("invalidRelativeProjectImport", importStr)
.withSourceSection(sourceSection) .withSourceSection(sourceSection)
@@ -395,6 +411,7 @@ public final class ProjectPackager {
.toPklException(stackFrameTransformer); .toPklException(stackFrameTransformer);
} }
var currentPath = pklModulePath.getParent(); 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 // It's not good enough to just check the normalized path to see whether it exists within the
// root dir. // root dir.
// It's possible that the import path resolves to a path outside the project 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) { private @Nullable List<Pair<String, SourceSection>> getImportsAndReads(Path pklModulePath) {
try { try {
var moduleKey = ModuleKeys.file(pklModulePath.toUri(), pklModulePath); var moduleKey = ModuleKeys.file(pklModulePath.toUri());
var resolvedModuleKey = ResolvedModuleKeys.file(moduleKey, moduleKey.getUri(), pklModulePath); var resolvedModuleKey = ResolvedModuleKeys.file(moduleKey, moduleKey.getUri(), pklModulePath);
return ImportsAndReadsParser.parse(moduleKey, resolvedModuleKey); return ImportsAndReadsParser.parse(moduleKey, resolvedModuleKey);
} catch (IOException e) { } catch (IOException e) {
@@ -195,10 +195,16 @@ public final class ModuleCache {
} catch (SecurityManagerException | PackageLoadError e) { } catch (SecurityManagerException | PackageLoadError e) {
throw new VmExceptionBuilder().withOptionalLocation(importNode).withCause(e).build(); throw new VmExceptionBuilder().withOptionalLocation(importNode).withCause(e).build();
} catch (FileNotFoundException | NoSuchFileException e) { } catch (FileNotFoundException | NoSuchFileException e) {
throw new VmExceptionBuilder() var exceptionBuilder =
.withOptionalLocation(importNode) new VmExceptionBuilder()
.evalError("cannotFindModule", module.getUri()) .withOptionalLocation(importNode)
.build(); .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) { } catch (IOException e) {
throw new VmExceptionBuilder() throw new VmExceptionBuilder()
.withOptionalLocation(importNode) .withOptionalLocation(importNode)
@@ -35,6 +35,7 @@ import java.util.regex.Pattern;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream; import java.util.zip.ZipOutputStream;
import org.pkl.core.PklBugException; import org.pkl.core.PklBugException;
import org.pkl.core.Platform;
import org.pkl.core.SecurityManager; import org.pkl.core.SecurityManager;
import org.pkl.core.SecurityManagerException; import org.pkl.core.SecurityManagerException;
import org.pkl.core.module.ModuleKey; import org.pkl.core.module.ModuleKey;
@@ -43,7 +44,10 @@ import org.pkl.core.runtime.VmExceptionBuilder;
public final class IoUtils { 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() {} private IoUtils() {}
@@ -66,12 +70,20 @@ public final class IoUtils {
return uriLike.matcher(str).matches(); 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 * 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. * and resource URIs. Unlike {@code new URI(str)}, it correctly escapes paths of relative URIs.
*/ */
public static URI toUri(String str) throws URISyntaxException { 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. */ /** Like {@link #toUri(String)}, except without checked exceptions. */
@@ -150,7 +162,8 @@ public final class IoUtils {
new SimpleFileVisitor<>() { new SimpleFileVisitor<>() {
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException { 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); Files.copy(file, zipStream);
zipStream.closeEntry(); zipStream.closeEntry();
return FileVisitResult.CONTINUE; return FileVisitResult.CONTINUE;
@@ -180,6 +193,10 @@ public final class IoUtils {
return System.getProperty("line.separator"); return System.getProperty("line.separator");
} }
public static Boolean isWindows() {
return Platform.current().operatingSystem().name().equals("Windows");
}
public static String getName(String path) { public static String getName(String path) {
var lastSep = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')); var lastSep = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
return path.substring(lastSep + 1); 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) { public static URI relativize(URI uri, URI base) {
if (uri.isOpaque() if (uri.isOpaque()
|| base.isOpaque() || base.isOpaque()
@@ -370,19 +389,60 @@ public final class IoUtils {
|| !Objects.equals(uri.getAuthority(), base.getAuthority())) { || !Objects.equals(uri.getAuthority(), base.getAuthority())) {
return uri; return uri;
} }
var uriPath = uri.normalize().getPath();
var basePath = Path.of(base.getPath()); var basePath = base.normalize().getPath();
if (!base.getRawPath().endsWith("/")) basePath = basePath.getParent();
var resultPath = basePath.relativize(Path.of(uri.getPath()));
try { try {
return new URI( if (basePath.isEmpty()) {
null, null, null, -1, resultPath.toString(), uri.getQuery(), uri.getFragment()); 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) { } 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) { public static boolean isWhitespace(String str) {
return str.codePoints().allMatch(Character::isWhitespace); return str.codePoints().allMatch(Character::isWhitespace);
} }
@@ -597,6 +657,63 @@ public final class IoUtils {
return newUri; 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. * Windows reserves characters {@code <>:"\|?*} in filenames.
* *
@@ -608,19 +725,27 @@ public final class IoUtils {
var sb = new StringBuilder(); var sb = new StringBuilder();
for (var i = 0; i < path.length(); i++) { for (var i = 0; i < path.length(); i++) {
var character = path.charAt(i); var character = path.charAt(i);
switch (character) { if (isReservedWindowsFilenameChar(character) && character != '/') {
case '<', '>', ':', '"', '\\', '|', '?', '*' -> { sb.append('(');
sb.append('('); sb.append(ByteArrayUtils.toHex(new byte[] {(byte) character}));
sb.append(ByteArrayUtils.toHex(new byte[] {(byte) character})); sb.append(")");
sb.append(")"); } else if (character == '(') {
} sb.append("((");
case '(' -> sb.append("(("); } else {
default -> sb.append(path.charAt(i)); sb.append(character);
} }
} }
return sb.toString(); 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) { private static int getExclamationMarkIndex(String jarUri) {
var index = jarUri.indexOf('!'); var index = jarUri.indexOf('!');
if (index == -1) { if (index == -1) {
@@ -12,7 +12,7 @@ facts {
} }
["versionInfo"] { ["versionInfo"] {
current.versionInfo.contains("macOS") || current.versionInfo.contains("Linux") current.versionInfo.contains("macOS") || current.versionInfo.contains("Linux") || current.versionInfo.contains("Windows")
} }
["commitId"] { ["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 –– –– 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. Run `pkl project resolve` to re-create this file.
x | import "@bird/Bird.pkl" x | import "@bird/Bird.pkl"
@@ -1,5 +1,5 @@
–– Pkl Error –– –– 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. Run `pkl project resolve` to re-create this file.
x | import "@bird/Bird.pkl" x | import "@bird/Bird.pkl"
@@ -1,5 +1,5 @@
–– Pkl Error –– –– 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" x | import "@birds/Bird.pkl"
^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^
@@ -74,11 +74,12 @@ class EvaluatorTest {
@Test @Test
fun `evaluate non-existing file`() { fun `evaluate non-existing file`() {
val file = File("/non/existing")
val e = assertThrows<PklException> { val e = assertThrows<PklException> {
evaluator.evaluate(file(File("/non/existing"))) evaluator.evaluate(file(file))
} }
assertThat(e) assertThat(e)
.hasMessageContaining("Cannot find module `file:///non/existing`.") .hasMessageContaining("Cannot find module `${file.toPath().toUri()}`.")
} }
@Test @Test
@@ -92,11 +93,12 @@ class EvaluatorTest {
@Test @Test
fun `evaluate non-existing path`() { fun `evaluate non-existing path`() {
val path = "/non/existing".toPath()
val e = assertThrows<PklException> { val e = assertThrows<PklException> {
evaluator.evaluate(path("/non/existing".toPath())) evaluator.evaluate(path(path))
} }
assertThat(e) assertThat(e)
.hasMessageContaining("Cannot find module `file:///non/existing`.") .hasMessageContaining("Cannot find module `${path.toUri()}`.")
} }
@Test @Test
@@ -13,3 +13,6 @@ class LinuxLanguageSnippetTests
@Testable @Testable
class AlpineLanguageSnippetTests class AlpineLanguageSnippetTests
@Testable
class WindowsLanguageSnippetTests
@@ -51,7 +51,7 @@ abstract class AbstractLanguageSnippetTestsEngine : InputOutputTestEngine() {
else parent?.getProjectDir() else parent?.getProjectDir()
override fun expectedOutputFileFor(inputFile: Path): Path { override fun expectedOutputFileFor(inputFile: Path): Path {
val relativePath = inputDir.relativize(inputFile).toString() val relativePath = IoUtils.relativize(inputFile, inputDir).toString()
val stdoutPath = val stdoutPath =
if (relativePath.matches(hiddenExtensionRegex)) relativePath.dropLast(4) if (relativePath.matches(hiddenExtensionRegex)) relativePath.dropLast(4)
else relativePath.dropLast(3) + "pcf" else relativePath.dropLast(3) + "pcf"
@@ -67,7 +67,7 @@ abstract class AbstractLanguageSnippetTestsEngine : InputOutputTestEngine() {
packageServer.close() 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 -> protected fun String.stripLineNumbers() = replace(lineNumberRegex) { result ->
// replace line number with equivalent number of 'x' characters to keep formatting intact // 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 = protected fun String.stripStdlibLocationSha(): String =
replace("https://github.com/apple/pkl/blob/${Release.current().commitId()}/stdlib/", "https://github.com/apple/pkl/blob/\$commitId/stdlib/") 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() { class LanguageSnippetTestsEngine : AbstractLanguageSnippetTestsEngine() {
@@ -143,7 +148,7 @@ class LanguageSnippetTestsEngine : AbstractLanguageSnippetTestsEngine() {
.stripVersionCheckErrorMessage() .stripVersionCheckErrorMessage()
} }
val stderr = logWriter.toString() val stderr = logWriter.toString().withUnixLineEndings()
return (success && stderr.isBlank()) to (output + stderr).stripFilePaths().stripWebsite().stripStdlibLocationSha() return (success && stderr.isBlank()) to (output + stderr).stripFilePaths().stripWebsite().stripStdlibLocationSha()
} }
@@ -216,7 +221,7 @@ abstract class AbstractNativeLanguageSnippetTestsEngine : AbstractLanguageSnippe
val process = builder.start() val process = builder.start()
return try { return try {
val (out, err) = listOf(process.inputStream, process.errorStream) val (out, err) = listOf(process.inputStream, process.errorStream)
.map { it.reader().readText() } .map { it.reader().readText().withUnixLineEndings() }
val success = process.waitFor() == 0 && err.isBlank() val success = process.waitFor() == 0 && err.isBlank()
success to (out + err) success to (out + err)
.stripFilePaths() .stripFilePaths()
@@ -254,3 +259,8 @@ class AlpineLanguageSnippetTestsEngine : AbstractNativeLanguageSnippetTestsEngin
override val pklExecutablePath: Path = rootProjectDir.resolve("pkl-cli/build/executable/pkl-alpine-linux-amd64") override val pklExecutablePath: Path = rootProjectDir.resolve("pkl-cli/build/executable/pkl-alpine-linux-amd64")
override val testClass: KClass<*> = AlpineLanguageSnippetTests::class 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 rootDir
) )
manager.checkResolveModule(URI("file:///foo/bar/baz.pkl")) manager.checkResolveModule(Path.of("/foo/bar/baz.pkl").toUri())
manager.checkReadResource(URI("file:///foo/bar/baz.pkl")) manager.checkReadResource(Path.of("/foo/bar/baz.pkl").toUri())
manager.checkResolveModule(URI("file:///foo/bar/qux/../baz.pkl")) manager.checkResolveModule(Path.of("/foo/bar/qux/../baz.pkl").toUri())
manager.checkReadResource(URI("file:///foo/bar/qux/../baz.pkl")) manager.checkReadResource(Path.of("/foo/bar/qux/../baz.pkl").toUri())
} }
@Test @Test
@@ -233,17 +233,17 @@ class SecurityManagersTest {
) )
assertThrows<SecurityManagerException> { assertThrows<SecurityManagerException> {
manager.checkResolveModule(URI("file:///foo/baz.pkl")) manager.checkResolveModule(Path.of("/foo/baz.pkl").toUri())
} }
assertThrows<SecurityManagerException> { assertThrows<SecurityManagerException> {
manager.checkReadResource(URI("file:///foo/baz.pkl")) manager.checkReadResource(Path.of("/foo/baz.pkl").toUri())
} }
assertThrows<SecurityManagerException> { assertThrows<SecurityManagerException> {
manager.checkResolveModule(URI("file:///foo/bar/../baz.pkl")) manager.checkResolveModule(Path.of("/foo/bar/../baz.pkl").toUri())
} }
assertThrows<SecurityManagerException> { 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") file.writeString("age = 40")
val uri = file.toUri() val uri = file.toUri()
val key = ModuleKeys.file(uri, file.toAbsolutePath()) val key = ModuleKeys.file(uri)
assertThat(key.uri).isEqualTo(uri) assertThat(key.uri).isEqualTo(uri)
assertThat(key.isCached).isTrue assertThat(key.isCached).isTrue
@@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows import org.junit.jupiter.api.assertThrows
import org.pkl.commons.test.FileTestUtils import org.pkl.commons.test.FileTestUtils
import org.pkl.commons.test.PackageServer import org.pkl.commons.test.PackageServer
import org.pkl.commons.toPath
import org.pkl.core.http.HttpClient import org.pkl.core.http.HttpClient
import org.pkl.core.PklException import org.pkl.core.PklException
import org.pkl.core.SecurityManagers import org.pkl.core.SecurityManagers
@@ -34,7 +35,7 @@ class ProjectDependenciesResolverTest {
@Test @Test
fun resolveDependencies() { 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 project = Project.loadFromPath(project2Path)
val packageResolver = PackageResolver.getInstance(SecurityManagers.defaultManager, httpClient, null) val packageResolver = PackageResolver.getInstance(SecurityManagers.defaultManager, httpClient, null)
val deps = ProjectDependenciesResolver(project, packageResolver, System.out.writer()).resolve() val deps = ProjectDependenciesResolver(project, packageResolver, System.out.writer()).resolve()
@@ -72,7 +73,7 @@ class ProjectDependenciesResolverTest {
@Test @Test
fun `fails if project declares a package with an incorrect checksum`() { 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 project = Project.loadFromPath(projectPath)
val packageResolver = PackageResolver.getInstance(SecurityManagers.defaultManager, httpClient, null) val packageResolver = PackageResolver.getInstance(SecurityManagers.defaultManager, httpClient, null)
val e = assertThrows<PklException> { val e = assertThrows<PklException> {
@@ -137,7 +137,7 @@ class ProjectTest {
@Test @Test
fun `evaluate project module -- invalid checksum`() { fun `evaluate project module -- invalid checksum`() {
PackageServer().use { server -> 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 project = Project.loadFromPath(projectDir.resolve("PklProject"))
val httpClient = HttpClient.builder() val httpClient = HttpClient.builder()
.addCertificates(FileTestUtils.selfSignedCertificate) .addCertificates(FileTestUtils.selfSignedCertificate)
@@ -117,70 +117,69 @@ class IoUtilsTest {
@Test @Test
fun `relativize file URLs`() { fun `relativize file URLs`() {
// perhaps URI("") would be a more precise result
assertThat( assertThat(
IoUtils.relativize( 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")) ).isEqualTo(URI("baz.pkl"))
assertThat( assertThat(
IoUtils.relativize( IoUtils.relativize(
URI("file://foo/bar/baz.pkl"), URI("file:///foo/bar/baz.pkl"),
URI("file://foo/bar/qux.pkl") URI("file:///foo/bar/qux.pkl")
) )
).isEqualTo(URI("baz.pkl")) ).isEqualTo(URI("baz.pkl"))
assertThat( assertThat(
IoUtils.relativize( IoUtils.relativize(
URI("file://foo/bar/baz.pkl"), URI("file:///foo/bar/baz.pkl"),
URI("file://foo/bar/") URI("file:///foo/bar/")
) )
).isEqualTo(URI("baz.pkl")) ).isEqualTo(URI("baz.pkl"))
assertThat( assertThat(
IoUtils.relativize( IoUtils.relativize(
URI("file://foo/bar/baz.pkl"), URI("file:///foo/bar/baz.pkl"),
URI("file://foo/bar") URI("file:///foo/bar")
) )
).isEqualTo(URI("bar/baz.pkl")) ).isEqualTo(URI("bar/baz.pkl"))
// URI.relativize() returns an absolute URI here // URI.relativize() returns an absolute URI here
assertThat( assertThat(
IoUtils.relativize( IoUtils.relativize(
URI("file://foo/bar/baz.pkl"), URI("file:///foo/bar/baz.pkl"),
URI("file://foo/qux/") URI("file:///foo/qux/")
) )
).isEqualTo(URI("../bar/baz.pkl")) ).isEqualTo(URI("../bar/baz.pkl"))
assertThat( assertThat(
IoUtils.relativize( IoUtils.relativize(
URI("file://foo/bar/baz.pkl"), URI("file:///foo/bar/baz.pkl"),
URI("file://foo/qux/qux2/") URI("file:///foo/qux/qux2/")
) )
).isEqualTo(URI("../../bar/baz.pkl")) ).isEqualTo(URI("../../bar/baz.pkl"))
assertThat( assertThat(
IoUtils.relativize( IoUtils.relativize(
URI("file://foo/bar/baz.pkl"), URI("file:///foo/bar/baz.pkl"),
URI("file://foo/qux/qux2") URI("file:///foo/qux/qux2")
) )
).isEqualTo(URI("../bar/baz.pkl")) ).isEqualTo(URI("../bar/baz.pkl"))
assertThat( assertThat(
IoUtils.relativize( IoUtils.relativize(
URI("file://foo/bar/baz.pkl"), URI("file:///foo/bar/baz.pkl"),
URI("file://qux/qux2/") URI("file:///qux/qux2/")
) )
).isEqualTo(URI("file://foo/bar/baz.pkl")) ).isEqualTo(URI("../../foo/bar/baz.pkl"))
assertThat( assertThat(
IoUtils.relativize( IoUtils.relativize(
URI("file://foo/bar/baz.pkl"), URI("file:///foo/bar/baz.pkl"),
URI("https://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 @Test
@@ -343,7 +342,7 @@ class IoUtilsTest {
val file3 = tempDir.resolve("base1/dir2/foo.pkl").createParentDirectories().createFile() val file3 = tempDir.resolve("base1/dir2/foo.pkl").createParentDirectories().createFile()
val uri = file2.toUri() 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("..."))).isEqualTo(file1.toUri())
assertThat(IoUtils.resolve(FakeSecurityManager, key, URI(".../foo.pkl"))).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.LinuxAmd64LanguageSnippetTestsEngine
org.pkl.core.LinuxAarch64LanguageSnippetTestsEngine org.pkl.core.LinuxAarch64LanguageSnippetTestsEngine
org.pkl.core.AlpineLanguageSnippetTestsEngine org.pkl.core.AlpineLanguageSnippetTestsEngine
org.pkl.core.WindowsLanguageSnippetTestsEngine
@@ -22,6 +22,7 @@ import kotlin.Pair
import org.pkl.commons.cli.CliBaseOptions.Companion.getProjectFile import org.pkl.commons.cli.CliBaseOptions.Companion.getProjectFile
import org.pkl.commons.cli.CliCommand import org.pkl.commons.cli.CliCommand
import org.pkl.commons.cli.CliException import org.pkl.commons.cli.CliException
import org.pkl.commons.toPath
import org.pkl.core.* import org.pkl.core.*
import org.pkl.core.module.ModuleKeyFactories import org.pkl.core.module.ModuleKeyFactories
import org.pkl.core.packages.* import org.pkl.core.packages.*
@@ -136,7 +137,7 @@ class CliDocGenerator(private val options: CliDocGeneratorOptions) : CliCommand(
val packageUris = mutableListOf<PackageUri>() val packageUris = mutableListOf<PackageUri>()
for (moduleUri in options.base.normalizedSourceModules) { for (moduleUri in options.base.normalizedSourceModules) {
if (moduleUri.scheme == "file") { if (moduleUri.scheme == "file") {
val dir = Path.of(moduleUri).parent val dir = moduleUri.toPath().parent
val projectFile = dir.getProjectFile(options.base.normalizedRootDir) val projectFile = dir.getProjectFile(options.base.normalizedRootDir)
if (projectFile != null) { if (projectFile != null) {
pklProjectPaths.add(projectFile) pklProjectPaths.add(projectFile)
@@ -229,13 +230,12 @@ class CliDocGenerator(private val options: CliDocGeneratorOptions) : CliCommand(
DocPackageInfo.fromPkl(module).apply { DocPackageInfo.fromPkl(module).apply {
evaluator.collectImportedModules(overviewImports) evaluator.collectImportedModules(overviewImports)
} }
schemasByDocPackageInfoAndPath[docPackageInfo to Path.of(uri.path).parent] = schemasByDocPackageInfoAndPath[docPackageInfo to uri.toPath().parent] = mutableSetOf()
mutableSetOf()
} }
for (uri in regularModuleUris) { for (uri in regularModuleUris) {
val entry = val entry =
schemasByDocPackageInfoAndPath.keys.find { uri.path.startsWith(it.second.toString()) } schemasByDocPackageInfoAndPath.keys.find { uri.toPath().startsWith(it.second) }
?: throw CliException("Could not find a doc-package-info.pkl for module $uri") ?: throw CliException("Could not find a doc-package-info.pkl for module $uri")
val schema = val schema =
evaluator.evaluateSchema(ModuleSource.uri(uri)).apply { evaluator.evaluateSchema(ModuleSource.uri(uri)).apply {
@@ -26,6 +26,7 @@ import org.pkl.commons.deleteRecursively
import org.pkl.core.ModuleSchema import org.pkl.core.ModuleSchema
import org.pkl.core.PClassInfo import org.pkl.core.PClassInfo
import org.pkl.core.Version import org.pkl.core.Version
import org.pkl.core.util.IoUtils
/** /**
* Entry point for the low-level Pkldoc API. * Entry point for the low-level Pkldoc API.
@@ -126,7 +127,7 @@ class DocGenerator(
val dest = basePath.resolve("current") val dest = basePath.resolve("current")
if (dest.exists() && dest.isSameFileAs(src)) continue if (dest.exists() && dest.isSameFileAs(src)) continue
dest.deleteIfExists() dest.deleteIfExists()
dest.createSymbolicLinkPointingTo(basePath.relativize(src)) dest.createSymbolicLinkPointingTo(IoUtils.relativize(src, basePath))
} }
} }
} }
@@ -28,6 +28,7 @@ import java.net.URI
import java.nio.file.Path import java.nio.file.Path
import org.pkl.commons.cli.cliMain import org.pkl.commons.cli.cliMain
import org.pkl.commons.cli.commands.BaseCommand import org.pkl.commons.cli.commands.BaseCommand
import org.pkl.commons.cli.commands.BaseOptions.Companion.parseModuleName
import org.pkl.commons.cli.commands.ProjectOptions import org.pkl.commons.cli.commands.ProjectOptions
import org.pkl.core.Release import org.pkl.core.Release
@@ -37,6 +37,7 @@ import org.pkl.commons.test.PackageServer
import org.pkl.commons.test.listFilesRecursively import org.pkl.commons.test.listFilesRecursively
import org.pkl.commons.toPath import org.pkl.commons.toPath
import org.pkl.core.Version import org.pkl.core.Version
import org.pkl.core.util.IoUtils
import org.pkl.doc.DocGenerator.Companion.current import org.pkl.doc.DocGenerator.Companion.current
class CliDocGeneratorTest { class CliDocGeneratorTest {
@@ -92,11 +93,17 @@ class CliDocGeneratorTest {
private val actualOutputFiles: List<Path> by lazy { actualOutputDir.listFilesRecursively() } private val actualOutputFiles: List<Path> by lazy { actualOutputDir.listFilesRecursively() }
private val expectedRelativeOutputFiles: List<String> by lazy { private val expectedRelativeOutputFiles: List<String> by lazy {
expectedOutputFiles.map { expectedOutputDir.relativize(it).toString() } expectedOutputFiles.map { path ->
IoUtils.toNormalizedPathString(expectedOutputDir.relativize(path)).let { str ->
// Git will by default clone symlinks as shortcuts on Windows, and shortcuts have a
// `.lnk` extension.
if (IoUtils.isWindows() && str.endsWith(".lnk")) str.dropLast(4) else str
}
}
} }
private val actualRelativeOutputFiles: List<String> by lazy { private val actualRelativeOutputFiles: List<String> by lazy {
actualOutputFiles.map { actualOutputDir.relativize(it).toString() } actualOutputFiles.map { IoUtils.toNormalizedPathString(actualOutputDir.relativize(it)) }
} }
private val binaryFileExtensions = private val binaryFileExtensions =
@@ -219,6 +226,11 @@ class CliDocGeneratorTest {
.withFailMessage("Test bug: $actualFile should exist but does not.") .withFailMessage("Test bug: $actualFile should exist but does not.")
.exists() .exists()
// symlinks on Git and Windows is rather finnicky; they create shortcuts by default unless
// a core Git option is set. Also, by default, symlinks require administrator privileges to run.
// We'll just test that the symlink got created but skip verifying that it points to the right
// place.
if (actualFile.isSymbolicLink() && IoUtils.isWindows()) return
val expectedFile = expectedOutputDir.resolve(relativeFilePath) val expectedFile = expectedOutputDir.resolve(relativeFilePath)
if (expectedFile.exists()) { if (expectedFile.exists()) {
when { when {
@@ -16,6 +16,7 @@
package org.pkl.doc package org.pkl.doc
import java.net.URI import java.net.URI
import java.nio.file.Path
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.Test import org.junit.jupiter.api.Test
@@ -221,6 +222,6 @@ class DocScopeTest {
val scope = SiteScope(listOf(), mapOf(), { evaluator.evaluateSchema(uri(it)) }, outputDir) val scope = SiteScope(listOf(), mapOf(), { evaluator.evaluateSchema(uri(it)) }, outputDir)
// used to return `/non/index.html` // used to return `/non/index.html`
assertThat(scope.url.path).isEqualTo("/non/existing/index.html") assertThat(scope.url.toPath()).isEqualTo(Path.of("/non/existing/index.html").toAbsolutePath())
} }
} }
+17 -5
View File
@@ -1,4 +1,5 @@
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.LinkOption
plugins { plugins {
pklAllProjects pklAllProjects
@@ -62,12 +63,23 @@ val prepareHistoricalDistributions by tasks.registering {
val distributionDir = outputDir.get().asFile.toPath() val distributionDir = outputDir.get().asFile.toPath()
.also(Files::createDirectories) .also(Files::createDirectories)
for (file in pklHistoricalDistributions.files) { for (file in pklHistoricalDistributions.files) {
val link = distributionDir.resolve(file.name) val target = distributionDir.resolve(file.name)
if (!Files.isSymbolicLink(link)) { // Create normal files on Windows, symlink on macOS/linux (need admin priveleges to create
if (Files.exists(link)) { // symlinks on Windows)
Files.delete(link) if (buildInfo.os.isWindows) {
if (!Files.isRegularFile(target, LinkOption.NOFOLLOW_LINKS)) {
if (Files.exists(target)) {
Files.delete(target)
}
Files.copy(file.toPath(), target)
}
} else {
if (!Files.isSymbolicLink(target)) {
if (Files.exists(target)) {
Files.delete(target)
}
Files.createSymbolicLink(target, file.toPath())
} }
Files.createSymbolicLink(link, file.toPath())
} }
} }
} }
@@ -157,7 +157,15 @@ final class EmbeddedExecutor implements Executor {
private static Path toDisplayPath(Path modulePath, ExecutorOptions options) { private static Path toDisplayPath(Path modulePath, ExecutorOptions options) {
var rootDir = options.getRootDir(); var rootDir = options.getRootDir();
return rootDir == null ? modulePath : rootDir.relativize(modulePath); return rootDir == null ? modulePath : relativize(modulePath, rootDir);
}
// On Windows, `Path.relativize` will fail if the two paths have different roots.
private static Path relativize(Path path, Path base) {
if (path.isAbsolute() && base.isAbsolute() && !path.getRoot().equals(base.getRoot())) {
return path;
}
return base.relativize(path);
} }
@Override @Override
@@ -15,6 +15,7 @@ import org.pkl.commons.test.FilteringClassLoader
import org.pkl.commons.test.PackageServer import org.pkl.commons.test.PackageServer
import org.pkl.commons.toPath import org.pkl.commons.toPath
import org.pkl.core.Release import org.pkl.core.Release
import java.io.File
import java.nio.file.Files import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.time.Duration import java.time.Duration
@@ -197,9 +198,10 @@ class EmbeddedExecutorTest {
Executors.embedded(listOf("/non/existing".toPath())) Executors.embedded(listOf("/non/existing".toPath()))
} }
val sep = File.separatorChar
assertThat(e.message) assertThat(e.message)
.contains("Cannot find Jar file") .contains("Cannot find Jar file")
.contains("/non/existing") .contains("${sep}non${sep}existing")
} }
@Test @Test
@@ -134,7 +134,7 @@ public abstract class ModulesTask extends BasePklTask {
*/ */
private URI parsedModuleNotationToUri(Object notation) { private URI parsedModuleNotationToUri(Object notation) {
if (notation instanceof File file) { if (notation instanceof File file) {
return IoUtils.createUri(file.getPath()); return IoUtils.createUri(IoUtils.toNormalizedPathString(file.toPath()));
} else if (notation instanceof URI uri) { } else if (notation instanceof URI uri) {
return uri; return uri;
} }
@@ -2,7 +2,9 @@ package org.pkl.gradle
import org.assertj.core.api.Assertions import org.assertj.core.api.Assertions
import org.pkl.commons.readString import org.pkl.commons.readString
import org.pkl.commons.readString
import org.pkl.commons.test.PackageServer import org.pkl.commons.test.PackageServer
import org.pkl.commons.toNormalizedPathString
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir import org.junit.jupiter.api.io.TempDir
@@ -199,8 +201,8 @@ class EvaluatorsTest : AbstractTest() {
@Test @Test
fun `source module URIs`() { fun `source module URIs`() {
val pklFile = writeFile( writeFile(
"test.pkl", """ "testDir/test.pkl", """
person { person {
name = "Pigeon" name = "Pigeon"
age = 20 + 10 age = 20 + 10
@@ -218,7 +220,7 @@ class EvaluatorsTest : AbstractTest() {
evaluators { evaluators {
evalTest { evalTest {
sourceModules = [uri("modulepath:/test.pkl")] sourceModules = [uri("modulepath:/test.pkl")]
modulePath.from "${pklFile.parent}" modulePath.from layout.projectDirectory.dir("testDir")
outputFile = layout.projectDirectory.file("test.pcf") outputFile = layout.projectDirectory.file("test.pcf")
settingsModule = "pkl:settings" settingsModule = "pkl:settings"
} }
@@ -387,7 +389,7 @@ class EvaluatorsTest : AbstractTest() {
@Test @Test
fun `explicitly set cache dir`(@TempDir tempDir: Path) { fun `explicitly set cache dir`(@TempDir tempDir: Path) {
writeBuildFile("pcf", """ writeBuildFile("pcf", """
moduleCacheDir = file("$tempDir") moduleCacheDir = file("${tempDir.toUri()}")
""".trimIndent()) """.trimIndent())
writeFile( writeFile(
"test.pkl", "test.pkl",
@@ -1,6 +1,7 @@
package org.pkl.gradle package org.pkl.gradle
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.pkl.commons.toNormalizedPathString
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.nio.file.Path import java.nio.file.Path
import kotlin.io.path.readText import kotlin.io.path.readText
@@ -46,8 +47,7 @@ class TestsTest : AbstractTest() {
val output = runTask("evalTest", expectFailure = true).output.stripFilesAndLines() val output = runTask("evalTest", expectFailure = true).output.stripFilesAndLines()
assertThat(output.trimStart()).startsWith(""" assertThat(output).contains("""
> Task :evalTest FAILED
module test (file:///file, line x) module test (file:///file, line x)
test ❌ test ❌
Error: Error:
@@ -143,7 +143,7 @@ class TestsTest : AbstractTest() {
val pklFile = writePklFile(contents = bigTest) val pklFile = writePklFile(contents = bigTest)
writeFile("test.pkl-expected.pcf", bigTestExpected) writeFile("test.pkl-expected.pcf", bigTestExpected)
writeBuildFile("junitReportsDir = file('${pklFile.parent}/build')") writeBuildFile("junitReportsDir = file('${pklFile.parent.toNormalizedPathString()}/build')")
runTask("evalTest", expectFailure = true) runTask("evalTest", expectFailure = true)
@@ -58,7 +58,8 @@ class BinaryEvaluatorSnippetTestEngine : InputOutputTestEngine() {
null null
) )
private fun String.stripFilePaths() = replace(snippetsDir.toString(), "/\$snippetsDir") private fun String.stripFilePaths() =
replace(snippetsDir.toUri().toString(), "file:///\$snippetsDir/")
override fun generateOutputFor(inputFile: Path): Pair<Boolean, String> { override fun generateOutputFor(inputFile: Path): Pair<Boolean, String> {
val bytes = evaluator.evaluate(ModuleSource.path(inputFile), null) val bytes = evaluator.evaluate(ModuleSource.path(inputFile), null)
+1 -1
View File
@@ -66,7 +66,7 @@ class VirtualMachine {
/// The operating system of a platform. /// The operating system of a platform.
class OperatingSystem { class OperatingSystem {
/// The name of this operating system. /// The name of this operating system.
name: String name: "macOS"|"Linux"|"Windows"|String
/// The version of this operating system. /// The version of this operating system.
version: String version: String