diff --git a/.github/workflows/static-renderer.yml b/.github/workflows/static-renderer.yml index e54849b7..4e4b9685 100644 --- a/.github/workflows/static-renderer.yml +++ b/.github/workflows/static-renderer.yml @@ -217,10 +217,13 @@ jobs: timeout-minutes: 12 # The checklist test-graalvm-renderer.yml runs on the GraalVM X11 window, with the same # application: the consumer fixture the input check above built. The table is in the - # job summary; a row that fails is a warning, not a stop. + # job summary; a row that fails is a warning, except the + # resize rows, which are a gate: no frame at a size other than the window's, no size + # without a frame. run: | sudo apt-get install -y xclip - xvfb-run -a -s '-screen 0 1280x1024x24' ./scripts/parity/linux.sh native "$RUNNER_TEMP/parity" -- \ + PARITY_GATE="resize_stale_ratio resize_stretched" \ + xvfb-run -a -s '-screen 0 1280x1024x24' ./scripts/parity/linux.sh native "$RUNNER_TEMP/parity" -- \ "$PWD/target/linux-input-check-static/debug/consumer" --parity - name: Keep the Linux parity run's logs diff --git a/.github/workflows/test-graalvm-renderer.yml b/.github/workflows/test-graalvm-renderer.yml index 2e67065a..66497546 100644 --- a/.github/workflows/test-graalvm-renderer.yml +++ b/.github/workflows/test-graalvm-renderer.yml @@ -370,10 +370,13 @@ jobs: timeout-minutes: 12 # The checklist static-renderer.yml runs on the Kotlin/Native window, with the same # application: the consumer fixture the input check above built against this - # renderer. The table is in the job summary; a row that fails is a warning, not a stop. + # renderer. The table is in the job summary; a row that fails is a warning, except the + # resize rows, which are a gate: no frame at a size other than the window's, no size + # without a frame. run: | sudo apt-get install -y xclip - xvfb-run -a -s '-screen 0 1280x1024x24' ./scripts/parity/linux.sh graalvm "$RUNNER_TEMP/parity" -- \ + PARITY_GATE="resize_stale_ratio resize_stretched" \ + xvfb-run -a -s '-screen 0 1280x1024x24' ./scripts/parity/linux.sh graalvm "$RUNNER_TEMP/parity" -- \ "$PWD/target/linux-input-check-shared/debug/consumer" --parity - name: Keep the Linux parity run's logs diff --git a/renderer/kotlin b/renderer/kotlin index 8c404a97..09763861 100755 --- a/renderer/kotlin +++ b/renderer/kotlin @@ -280,6 +280,48 @@ download_and_extract "Kotlin Toolchain distribution v$kotlin_cli_version" "$kotl # ********** Launch the Kotlin CLI ********** +# Patch Ktor HTTP client's 8192-character header line limit in the extracted toolchain distribution. +# GitHub Releases responses include a 9243-character Content-Security-Policy header line, +# which otherwise triggers io.ktor.utils.io.charsets.TooLongLineException during asset downloads. +patch_flag="$kotlin_cli_target_dir/.ktor-line-limit-patched" +if [ ! -f "$patch_flag" ]; then + python3 -c " +import zipfile, os + +jar_path = '$kotlin_cli_target_dir/lib/ktor-http-cio-jvm-3.4.1.jar' +if os.path.exists(jar_path): + with zipfile.ZipFile(jar_path, 'r') as zin: + entries = {item.filename: zin.read(item.filename) for item in zin.infolist()} + cls_name = 'io/ktor/http/cio/HttpParserKt.class' + if cls_name in entries: + b = bytearray(entries[cls_name]) + old_long = b'\x00\x00\x00\x00\x00\x00\x20\x00' + new_long = b'\x00\x00\x00\x00\x00\x01\x00\x00' + if old_long in b: + b = bytearray(bytes(b).replace(old_long, new_long, 1)) + old_sipush = b'\x11\x20\x00' + new_sipush = b'\x11\x7f\xff' + if old_sipush in b: + b = bytearray(bytes(b).replace(old_sipush, new_sipush, 1)) + entries[cls_name] = bytes(b) + tmp_jar = jar_path + '.tmp' + with zipfile.ZipFile(tmp_jar, 'w', compression=zipfile.ZIP_DEFLATED) as zout: + for name, data in entries.items(): + zout.writestr(name, data) + os.replace(tmp_jar, jar_path) +" 2>/dev/null && touch "$patch_flag" || true +fi + +# Prefetch toolchain downloads hosted on GitHub Releases, because GitHub's response +# headers contain a Content-Security-Policy line longer than Ktor's 8192-character limit, +# which crashes Amper's internal downloader. +_wrapper_dir="$(cd "$(dirname "$0")" && pwd)" +if [ -f "$_wrapper_dir/scripts/prefetch-github-assets.sh" ]; then + "$_wrapper_dir/scripts/prefetch-github-assets.sh" || true +elif [ -f "$_wrapper_dir/prefetch-github-assets.sh" ]; then + "$_wrapper_dir/prefetch-github-assets.sh" || true +fi + launcher_script="$kotlin_cli_target_dir/bin/launcher.sh" KOTLIN_CLI_WRAPPER_PATH="$(realpath "$0")" \ diff --git a/renderer/scripts/build-compose.sh b/renderer/scripts/build-compose.sh index 64c59fdf..cab8b315 100755 --- a/renderer/scripts/build-compose.sh +++ b/renderer/scripts/build-compose.sh @@ -39,7 +39,7 @@ REVISION="127311dc737d8badb68953291d248db8a7071cca" # resize soak (#109), which draws each GraalVM frame in an autorelease pool of its own; it # carries the merge of the coordinates (#119), so the window modules compile against the fork's # Compose coordinates, which the Compose pin introduced, and the window changes before it. -WINDOW_REVISION="1c2dc37c528cd2e52f10542ec1a1921cb4d51f58" +WINDOW_REVISION="57b4a0e102d837ba3271699297b69278fcefedef" # What the pinned commit publishes as: its gradle.properties gives every library line a version # of the form -ext.. The renderer modules ask for these by name, and # scripts/tests/fork-versions.test.sh checks that they do; this script checks the pinned diff --git a/renderer/scripts/fork-dependencies.sh b/renderer/scripts/fork-dependencies.sh index f1751104..9ff41e86 100755 --- a/renderer/scripts/fork-dependencies.sh +++ b/renderer/scripts/fork-dependencies.sh @@ -46,9 +46,9 @@ use File::Find; use JSON::PP; my ($version, $argument, $repository) = @ENV{qw(VERSION ARGUMENT REPOSITORY)}; -# The fork's Compose names its JVM target desktop. jvm is left out: the desktop module's own +# The fork Compose names its JVM target desktop. jvm is left out: the desktop module own # publications are desktop-jvm--, and reading desktop-jvm as a jvm target would make -# every sibling's desktop-jvm publication look missing. +# every sibling desktop-jvm publication look missing. my @targets = qw(desktop android macosarm64 macosx64 linuxx64 linuxarm64 mingwx64 iosarm64 iosx64 iossimulatorarm64 wasmjs js); my $target_pattern = join "|", @targets; diff --git a/renderer/scripts/prefetch-github-assets.sh b/renderer/scripts/prefetch-github-assets.sh new file mode 100755 index 00000000..c16fe98f --- /dev/null +++ b/renderer/scripts/prefetch-github-assets.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# Prefetches toolchain downloads hosted on GitHub Releases directly into Amper's +# download.cache using curl. +# +# GitHub Releases redirects with a Content-Security-Policy header line exceeding +# 8192 characters, which triggers io.ktor.utils.io.charsets.TooLongLineException +# inside Amper's Ktor HTTP client. Placing the archives in download.cache before +# Amper runs lets Amper use the cached files without contacting GitHub Releases. +set -euo pipefail + +case "$(uname -s)" in + Darwin) + cache_dir="${1:-$HOME/Library/Caches/JetBrains/Kotlin/download.cache}" + items=( + "https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.5%2B8/OpenJDK25U-jdk_aarch64_mac_hotspot_25.0.5_8.tar.gz|f8362503fc-OpenJDK25U-jdk_aarch64_mac_hotspot_25.0.5_8.tar.gz" + "https://github.com/pnpm/pnpm/releases/download/v11.9.0/pnpm-darwin-arm64.tar.gz|bfff255e35-pnpm-darwin-arm64.tar.gz" + "https://github.com/cpisciotta/xcbeautify/releases/download/3.2.1/xcbeautify-3.2.1-arm64-apple-macosx.zip|c132514727-xcbeautify-3.2.1-arm64-apple-macosx.zip" + ) + ;; + Linux) + cache_dir="${1:-$HOME/.cache/JetBrains/Kotlin/download.cache}" + case "$(uname -m)" in + x86_64) + items=( + "https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.5%2B8/OpenJDK25U-jdk_x64_linux_hotspot_25.0.5_8.tar.gz|bd467c5b70-OpenJDK25U-jdk_x64_linux_hotspot_25.0.5_8.tar.gz" + "https://github.com/pnpm/pnpm/releases/download/v11.9.0/pnpm-linux-x64.tar.gz|d30df19002-pnpm-linux-x64.tar.gz" + ) + ;; + aarch64) + items=( + "https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.5%2B8/OpenJDK25U-jdk_aarch64_linux_hotspot_25.0.5_8.tar.gz|e259c06f4c-OpenJDK25U-jdk_aarch64_linux_hotspot_25.0.5_8.tar.gz" + ) + ;; + *) + items=() + ;; + esac + ;; + MINGW*|MSYS*|CYGWIN*) + if [ -n "${LOCALAPPDATA:-}" ]; then + win_cache="$(cygpath -u "$LOCALAPPDATA")/JetBrains/Kotlin/download.cache" + else + win_cache="$HOME/AppData/Local/JetBrains/Kotlin/download.cache" + fi + cache_dir="${1:-$win_cache}" + items=( + "https://github.com/adoptium/temurin25-binaries/releases/download/jdk-25.0.5%2B8/OpenJDK25U-jdk_x64_windows_hotspot_25.0.5_8.zip|3f354780d9-OpenJDK25U-jdk_x64_windows_hotspot_25.0.5_8.zip" + ) + ;; + *) + items=() + ;; +esac + +mkdir -p "$cache_dir" + +for item in "${items[@]}"; do + url="${item%%|*}" + file="${item##*|}" + dest="$cache_dir/$file" + if [ ! -s "$dest" ]; then + echo "prefetching $file..." + tmp="$dest.tmp.$$" + if curl -fSL --retry 3 --retry-delay 2 "$url" -o "$tmp"; then + mv "$tmp" "$dest" + else + rm -f "$tmp" + echo "warning: failed to prefetch $file" >&2 + fi + fi +done + +# Patch Ktor HTTP client's 8192-character header line limit in any extracted toolchain distributions. +cli_dir="$(dirname "$cache_dir")/cli" +if [ -d "$cli_dir" ] && command -v python3 >/dev/null 2>&1; then + python3 -c " +import zipfile, os + +cli_dir = '$cli_dir' +for root, dirs, files in os.walk(cli_dir): + for f in files: + if f.startswith('ktor-http-cio') and f.endswith('.jar'): + jar_path = os.path.join(root, f) + try: + with zipfile.ZipFile(jar_path, 'r') as zin: + entries = {item.filename: zin.read(item.filename) for item in zin.infolist()} + cls_name = 'io/ktor/http/cio/HttpParserKt.class' + if cls_name in entries: + b = bytearray(entries[cls_name]) + old_long = b'\x00\x00\x00\x00\x00\x00\x20\x00' + new_long = b'\x00\x00\x00\x00\x00\x01\x00\x00' + old_sipush = b'\x11\x20\x00' + new_sipush = b'\x11\x7f\xff' + changed = False + if old_long in b: + b = bytearray(bytes(b).replace(old_long, new_long, 1)) + changed = True + if old_sipush in b: + b = bytearray(bytes(b).replace(old_sipush, new_sipush, 1)) + changed = True + if changed: + entries[cls_name] = bytes(b) + tmp_jar = jar_path + '.tmp' + with zipfile.ZipFile(tmp_jar, 'w', compression=zipfile.ZIP_DEFLATED) as zout: + for name, data in entries.items(): + zout.writestr(name, data) + os.replace(tmp_jar, jar_path) + except Exception: + pass +" 2>/dev/null || true +fi + diff --git a/renderer/scripts/publish-window.sh b/renderer/scripts/publish-window.sh index 8073e2f1..d213b676 100755 --- a/renderer/scripts/publish-window.sh +++ b/renderer/scripts/publish-window.sh @@ -6,10 +6,11 @@ # The window code is the Compose fork's, at the commit build-compose.sh pins. The renderer # depends on it as org.thisisthepy.compose.window::0.1.0, so the modules are built # from that commit and published under ~/.m2 before the renderer is, the same way the patched -# Compose is. The Kotlin/JVM modules are published on every machine, so a Linux or Windows -# checkout resolves the coordinates the renderer's desktop module asks for. native/macos is -# published on a Mac and native/linux on Linux, because each is built from that system's -# headers. +# Compose is. common is published on every machine. The GraalVM window modules and the scene +# they name are published wherever the fork's desktop Compose is in the local repository, which +# is every build of the renderer's desktop module, so a Linux or Windows checkout resolves the +# coordinates that module asks for. native/macos is published on a Mac and native/linux on +# Linux, because each is built from that system's headers. set -euo pipefail script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -18,7 +19,20 @@ repo="$(cd "$script_dir/../.." && pwd)" fork_dir="$("$repo/scripts/fetch-fork-window.sh")" window="$fork_dir/extended/window" -modules=(common graalvm/graalvm-macos graalvm/graalvm-linux) +# Ensure the toolchain distribution is provisioned and its Ktor line limit is patched +# before entering the fork window directory. +if [[ -x "$repo/renderer/kotlin" ]]; then + "$repo/renderer/kotlin" --version >/dev/null 2>&1 || true +fi + +modules=(common) +# The GraalVM window modules name scene (compile-only, for the window surfaces runComposeWindow +# uses), and scene compiles against the fork's Compose for the desktop, which only a build that +# published it has (build-compose.sh, or the compose-fork-desktop artifact in CI). A +# Kotlin/Native build needs none of the three, and would fail resolving scene's Compose. +if [[ -d "$HOME/.m2/repository/org/thisisthepy/compose/ui/ui-desktop" ]]; then + modules+=(scene graalvm/graalvm-macos graalvm/graalvm-linux) +fi # native/macos compiles against the fork's Compose for macosArm64, which only a build that ran # build-compose.sh --target macosArm64 has; a desktop-only build has no use for it. case "$(uname -s)" in @@ -38,11 +52,41 @@ cp "$window/project.yaml" "$window/project.yaml.all" trap 'mv "$window/project.yaml.all" "$window/project.yaml"' EXIT cd "$window" -# Kotlin/Native has no compiler for an arm64 Linux host, and the Kotlin toolchain has no way to -# publish only some targets of a module, so there the common module is published for the JVM -# alone. That is all the GraalVM image needs, and the fetched checkout is this script's own. -if [[ "$(uname -s)" == "Linux" && "$(uname -m)" != "x86_64" ]]; then - sed -i 's/^ platforms: \[.*\]$/ platforms: [ jvm ]/' "$window/common/module.yaml" +# The common window module lists every platform the fork supports. A host cannot build native +# klibs for other operating systems, and compiling cross-OS native targets triggers native +# distribution commonization which requires those other toolchains. Tailor common's platforms +# to the current host: Darwin builds jvm and macosArm64; Linux builds jvm and linux targets; +# other systems build jvm alone. +case "$(uname -s)" in + Darwin) + if [[ -d "$HOME/.m2/repository/org/thisisthepy/compose/ui/ui-macosarm64" ]]; then + sed -i.orig 's/^ platforms: \[.*\]$/ platforms: [ jvm, macosArm64 ]/' "$window/common/module.yaml" + else + sed -i.orig 's/^ platforms: \[.*\]$/ platforms: [ jvm ]/' "$window/common/module.yaml" + fi + rm -f "$window/common/module.yaml.orig" + ;; + Linux) + if [[ "$(uname -m)" == "x86_64" && -d "$HOME/.m2/repository/org/thisisthepy/compose/ui/ui-linuxx64" ]]; then + sed -i.orig 's/^ platforms: \[.*\]$/ platforms: [ jvm, linuxX64, linuxArm64 ]/' "$window/common/module.yaml" + else + sed -i.orig 's/^ platforms: \[.*\]$/ platforms: [ jvm ]/' "$window/common/module.yaml" + fi + rm -f "$window/common/module.yaml.orig" + ;; + *) + sed -i.orig 's/^ platforms: \[.*\]$/ platforms: [ jvm ]/' "$window/common/module.yaml" + rm -f "$window/common/module.yaml.orig" + ;; +esac +# The GraalVM modules need scene's JVM half alone, and nothing this renderer builds uses its +# other platform (macosArm64, which would also need the fork's Compose for macosArm64), so +# only the JVM half is built. +if [[ " ${modules[*]} " == *" scene "* ]]; then + # A suffix for -i and the copy removed after, because BSD sed on macOS takes the next + # argument to -i as one. + sed -i.orig 's/^ platforms: \[.*\]$/ platforms: [ jvm ]/' "$window/scene/module.yaml" + rm -f "$window/scene/module.yaml.orig" fi # The PATH kotlin.bat runs with on Windows: System32 first, and none of Git's own directories # but Git\cmd (git itself). Started from Git Bash, kotlin.bat would otherwise unpack its toolchain @@ -64,6 +108,7 @@ windows_tool_path() { done echo "$kept" } +"$script_dir/prefetch-github-assets.sh" || true case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) PATH="$(windows_tool_path)" cmd //c kotlin.bat publish mavenLocal ;; *) ./kotlin publish mavenLocal ;; diff --git a/scripts/parity/common.sh b/scripts/parity/common.sh index 6b01c52f..2c9b24a7 100755 --- a/scripts/parity/common.sh +++ b/scripts/parity/common.sh @@ -50,6 +50,23 @@ print_table() { echo } >> "$GITHUB_STEP_SUMMARY" fi + # PARITY_GATE names rows that fail the run whatever PARITY_STRICT says: a resize that + # presented a frame at a size other than the window's, or left a size without a frame of + # its own, breaks the resize rule (every frame drawn at the current size), so on the paths + # that meet it the two resize rows are a gate rather than a warning. + local gated="" + for name in ${PARITY_GATE:-}; do + local result + result="$(awk -F'\t' -v n="$name" '$1 == n { print $2 }' "$table")" + case "$name" in + resize_stretched) [[ "$result" == "0" ]] || gated="$gated $name=${result:-missing}" ;; + *) [[ -n "$result" && "$result" != FAIL* ]] || gated="$gated $name=${result:-missing}" ;; + esac + done + if [[ -n "$gated" ]]; then + echo "::error::parity $label fails its gate:$gated" + exit 1 + fi if grep -q $'\tFAIL' "$table"; then echo "::warning::parity $label has failing rows" [[ "${PARITY_STRICT:-}" == "1" ]] && exit 1 diff --git a/scripts/parity/linux.sh b/scripts/parity/linux.sh index 54036599..261431a6 100755 --- a/scripts/parity/linux.sh +++ b/scripts/parity/linux.sh @@ -69,6 +69,8 @@ export DXC_SYNTH_EXIT_AT="${DXC_SYNTH_EXIT_AT:-20}" export DXC_REPORT_LATENCY=1 export DXC_REPORT_INPUT=1 export DXC_REPORT_RESIZE=1 +# Every requested, confirmed and presented size, so a stale frame can be placed among them. +export DXC_REPORT_RESIZE_DETAIL=1 export GTK_THEME="${GTK_THEME:-Adwaita:dark}" "$@" >"$log" 2>"$err" & diff --git a/scripts/tests/parity-linux.test.sh b/scripts/tests/parity-linux.test.sh index 551d3648..296f5e45 100755 --- a/scripts/tests/parity-linux.test.sh +++ b/scripts/tests/parity-linux.test.sh @@ -62,6 +62,11 @@ done # Both paths on both platforms run the scripts in CI. grep -q 'scripts/parity/linux.sh native' "$repo_root/.github/workflows/static-renderer.yml" || fail "the Kotlin/Native Linux window is not measured in CI" grep -q 'scripts/parity/macos.sh native' "$repo_root/.github/workflows/static-renderer.yml" || fail "the Kotlin/Native macOS window is not measured in CI" +for workflow in static-renderer.yml test-graalvm-renderer.yml; do + grep -q 'PARITY_GATE="resize_stale_ratio resize_stretched"' "$repo_root/.github/workflows/$workflow" \ + || fail "$workflow does not gate the Linux parity run on the resize rows" +done +grep -q 'PARITY_GATE' "$repo_root/scripts/parity/common.sh" || fail "the parity table does not honour PARITY_GATE" grep -q 'scripts/parity/linux.sh graalvm' "$repo_root/.github/workflows/test-graalvm-renderer.yml" || fail "the GraalVM X11 window is not measured in CI" grep -q 'scripts/parity/macos.sh graalvm' "$repo_root/.github/workflows/test-graalvm-renderer.yml" || fail "the GraalVM AppKit window is not measured in CI" diff --git a/scripts/tests/setup-check.test.sh b/scripts/tests/setup-check.test.sh index a68856de..e5e2bc32 100755 --- a/scripts/tests/setup-check.test.sh +++ b/scripts/tests/setup-check.test.sh @@ -57,8 +57,11 @@ assert_run "setup_check_reports_missing_cargo" 1 "cargo not found on PATH" \ # GraalVM is the right one. Not having it is never fatal. It blocks the native image and # nothing else, and on macOS the renderer that ships is not the native image at all. if [[ "$(uname -s)" == "Darwin" ]]; then - assert_run "setup_check_reports_missing_nik" 1 "no Liberica NIK found" \ - env -u GRAALVM_HOME HOME="$empty_home" "$setup_check" + assert_run "setup_check_reports_missing_nik" 0 "no Liberica NIK found" \ + env -u GRAALVM_HOME HOME="$empty_home" \ + RUSTUP_HOME="${RUSTUP_HOME:-$real_home/.rustup}" \ + CARGO_HOME="${CARGO_HOME:-$real_home/.cargo}" \ + "$setup_check" else # Keep rustup discoverable. The empty HOME above exists to make the macOS toolchain # glob find nothing, and that glob is not used here, but rustup does live under HOME,