diff --git a/.mise/config.coverage.toml b/.mise/config.coverage.toml index 821bb9b1..e5cb754e 100644 --- a/.mise/config.coverage.toml +++ b/.mise/config.coverage.toml @@ -137,7 +137,7 @@ fi host="$(rustc -vV | goawk '/^host:/ { print $2 }')" bin="$(rustc +{{ vars.rust_nightly }} --print sysroot)/lib/rustlib/$host/bin" gut="$covdir/gut.awk" -coreutils cat > "$gut" <<'AWK' +coreutils cat >"$gut" <<'AWK' /^target datalayout/ { next } /^target triple/ { next } /^define/ { print; print "start:"; print " unreachable"; print "}"; skip = 1; next } @@ -145,7 +145,7 @@ skip && /^}/ { skip = 0; next } skip { next } { gsub(/"target-cpu"="[^"]*"/, ""); gsub(/"target-features"="[^"]*"/, ""); print } AWK -: > "$covdir/wasi.lcov" +: >"$covdir/wasi.lcov" for profraw in "$covdir"/*.profraw; do name="$(coreutils basename "$profraw" .profraw)" # Look for the .ll in nightly's new build-dir layout first, then the classic one. @@ -166,25 +166,28 @@ for profraw in "$covdir"/*.profraw; do if [ -z "$ll" ]; then ll="$(find target -path '*/release/deps/*' \\( -name "$name.ll" -o -name "$name-*.ll" \\) -print -quit 2>/dev/null)" fi - if [ -z "$ll" ]; then echo "wasi-cov: no .ll found for $name"; exit 1; fi + if [ -z "$ll" ]; then + echo "wasi-cov: no .ll found for $name" + exit 1 + fi pd="$covdir/$name.profdata" obj="$covdir/$name.o" - goawk -f "$gut" "$ll" > "$covdir/$name.gutted.ll" + goawk -f "$gut" "$ll" >"$covdir/$name.gutted.ll" "$bin/llc" -filetype=obj -mtriple=x86_64-unknown-linux-gnu -o "$obj" "$covdir/$name.gutted.ll" "$bin/llvm-profdata" merge -sparse -o "$pd" "$profraw" - "$bin/llvm-cov" export --format=lcov --instr-profile "$pd" "$obj" >> "$covdir/wasi.lcov" + "$bin/llvm-cov" export --format=lcov --instr-profile "$pd" "$obj" >>"$covdir/wasi.lcov" done # The wasm covmap records reference every source each module linked. # Dependency crates under ~/.cargo/registry and toolchain std under ~/.rustup are not in VCS -- DeepSource # flags them and they skew the aggregate metric -- so keep only workspace records (dropping .cargo/.rustup/rustc # blocks) before merging the wasm lcov into lcov.info. keep="$covdir/keep.awk" -coreutils cat > "$keep" <<'AWK' +coreutils cat >"$keep" <<'AWK' { buf = buf $0 ORS } /^SF:/ { p = substr($0, 4); drop = (index(p, "/.cargo/") || index(p, "/.rustup/") || index(p, "/rustc/")) } /^end_of_record$/ { if (!drop) printf "%s", buf; buf = ""; drop = 0 } AWK -goawk -f "$keep" "$covdir/wasi.lcov" >> lcov.info +goawk -f "$keep" "$covdir/wasi.lcov" >>lcov.info """ shell = "bash -euo pipefail -c" @@ -282,8 +285,14 @@ RUSTFLAGS="-Cinstrument-coverage" cargo build -q -p et-ws-test-server --bin cov- LLVM_PROFILE_FILE="{{ config_root }}/$covdir/cov-server-%p.profraw" ./target/debug/cov-server "$covdir/server-ready" & server_pid=$! trap 'kill "$server_pid" 2>/dev/null || true' EXIT -for _ in $(coreutils seq 1 100); do [ -s "$covdir/server-ready" ] && break; coreutils sleep 0.1; done -[ -s "$covdir/server-ready" ] || { echo "cov-server never became ready"; exit 1; } +for _ in $(coreutils seq 1 100); do + [ -s "$covdir/server-ready" ] && break + coreutils sleep 0.1 +done +[ -s "$covdir/server-ready" ] || { + echo "cov-server never became ready" + exit 1 +} # Export the instrumentation env for the coverage test build. # Trailing-backslash line continuations are banned repo-wide, so each variable is set on its own line; no later @@ -304,7 +313,7 @@ trap - EXIT host="$(rustc +{{ vars.rust_nightly }} -vV | goawk '/^host:/ { print $2 }')" llbin="$(rustc +{{ vars.rust_nightly }} --print sysroot)/lib/rustlib/$host/bin" gut="$covdir/gut.awk" -coreutils cat > "$gut" <<'AWK' +coreutils cat >"$gut" <<'AWK' /^target datalayout/ { next } /^target triple/ { next } /^define/ { print; print "start:"; print " unreachable"; print "}"; skip = 1; next } @@ -336,31 +345,31 @@ fi while IFS= read -r ll; do [ -n "$ll" ] || continue name="$(coreutils basename "$ll" .ll)" - goawk -f "$gut" "$ll" > "$covdir/$name.g.ll" + goawk -f "$gut" "$ll" >"$covdir/$name.g.ll" "$llbin/llc" -filetype=obj -mtriple=x86_64-unknown-linux-gnu -o "$covdir/$name.o" "$covdir/$name.g.ll" objs+=("-object" "$covdir/$name.o") done < "$covdir/all.lcov" +"$llbin/llvm-cov" export --format=lcov --instr-profile "$covdir/agent.profdata" "${objs[@]}" >"$covdir/all.lcov" keep="$covdir/keep.awk" -coreutils cat > "$keep" <<'AWK' +coreutils cat >"$keep" <<'AWK' { buf = buf $0 ORS } /^SF:/ { keep = index($0, want) > 0 } /^end_of_record$/ { if (keep) printf "%s", buf; buf = ""; keep = 0 } AWK coreutils touch lcov.info -goawk -v want="ws-wasm-agent/src/" -f "$keep" "$covdir/all.lcov" >> lcov.info +goawk -v want="ws-wasm-agent/src/" -f "$keep" "$covdir/all.lcov" >>lcov.info # Fold cov-server's own native coverage in from the instrumented run above (its cov-server-*.profraw). # The export object links its whole dep tree, so keep only the cov-server.rs launcher records. csp="$covdir/cov-server" "$llbin/llvm-profdata" merge -sparse -o "$csp.profdata" "$covdir"/cov-server-*.profraw -"$llbin/llvm-cov" export --format=lcov --instr-profile "$csp.profdata" -object target/debug/cov-server > "$csp.lcov" -goawk -v want="ws-test-server/src/bin/cov-server.rs" -f "$keep" "$csp.lcov" >> lcov.info +"$llbin/llvm-cov" export --format=lcov --instr-profile "$csp.profdata" -object target/debug/cov-server >"$csp.lcov" +goawk -v want="ws-test-server/src/bin/cov-server.rs" -f "$keep" "$csp.lcov" >>lcov.info rpt="$covdir/report.txt" -"$llbin/llvm-cov" report --instr-profile "$covdir/agent.profdata" "${objs[@]}" > "$rpt" 2>/dev/null || true +"$llbin/llvm-cov" report --instr-profile "$covdir/agent.profdata" "${objs[@]}" >"$rpt" 2>/dev/null || true rg "ws-wasm-agent/src/lib.rs|Filename|TOTAL" "$rpt" || true """ shell = "bash -euo pipefail -c" @@ -397,7 +406,7 @@ cargo test -p et-ws-pic-viewer --features et-web/coverage --target wasm32-unknow host="$(rustc +{{ vars.rust_nightly }} -vV | goawk '/^host:/ { print $2 }')" llbin="$(rustc +{{ vars.rust_nightly }} --print sysroot)/lib/rustlib/$host/bin" gut="$covdir/gut.awk" -coreutils cat > "$gut" <<'AWK' +coreutils cat >"$gut" <<'AWK' /^target datalayout/ { next } /^target triple/ { next } /^define/ { print; print "start:"; print " unreachable"; print "}"; skip = 1; next } @@ -424,24 +433,24 @@ fi while IFS= read -r ll; do [ -n "$ll" ] || continue name="$(coreutils basename "$ll" .ll)" - goawk -f "$gut" "$ll" > "$covdir/$name.g.ll" + goawk -f "$gut" "$ll" >"$covdir/$name.g.ll" "$llbin/llc" -filetype=obj -mtriple=x86_64-unknown-linux-gnu -o "$covdir/$name.o" "$covdir/$name.g.ll" objs+=("-object" "$covdir/$name.o") done < "$covdir/all.lcov" +"$llbin/llvm-cov" export --format=lcov --instr-profile "$covdir/pic-viewer.profdata" "${objs[@]}" >"$covdir/all.lcov" keep="$covdir/keep.awk" -coreutils cat > "$keep" <<'AWK' +coreutils cat >"$keep" <<'AWK' { buf = buf $0 ORS } /^SF:/ { keep = index($0, want) > 0 } /^end_of_record$/ { if (keep) printf "%s", buf; buf = ""; keep = 0 } AWK coreutils touch lcov.info -goawk -v want="ws-modules/pic-viewer/src/" -f "$keep" "$covdir/all.lcov" >> lcov.info +goawk -v want="ws-modules/pic-viewer/src/" -f "$keep" "$covdir/all.lcov" >>lcov.info rpt="$covdir/report.txt" -"$llbin/llvm-cov" report --instr-profile "$covdir/pic-viewer.profdata" "${objs[@]}" > "$rpt" 2>/dev/null || true +"$llbin/llvm-cov" report --instr-profile "$covdir/pic-viewer.profdata" "${objs[@]}" >"$rpt" 2>/dev/null || true rg "ws-modules/pic-viewer/src/lib.rs|Filename|TOTAL" "$rpt" || true """ shell = "bash -euo pipefail -c" diff --git a/.mise/config.dart.toml b/.mise/config.dart.toml index ae2485df..7ec06c95 100644 --- a/.mise/config.dart.toml +++ b/.mise/config.dart.toml @@ -143,10 +143,10 @@ END { printf("gen:dart-rest storage fixup miss: io=%d get=%d body=%d\\n", io, get, body) > "/dev/stderr" exit 1 } -}' lib/clients/storage.dart | coreutils tr -d '\\r' > "$s/storage.dart" +}' lib/clients/storage.dart | coreutils tr -d '\\r' >"$s/storage.dart" coreutils mv "$s/storage.dart" lib/clients/storage.dart goawk '/^import / && /clients/ { gsub(/_\\.dart/, ".dart") } { print }' lib/rest_client.dart | - coreutils tr -d '\\r' > "$s/rest_client.dart" + coreutils tr -d '\\r' >"$s/rest_client.dart" coreutils mv "$s/rest_client.dart" lib/rest_client.dart dart run build_runner build dart format lib diff --git a/.mise/config.dotnet.toml b/.mise/config.dotnet.toml index 855a5dde..20839535 100644 --- a/.mise/config.dotnet.toml +++ b/.mise/config.dotnet.toml @@ -92,7 +92,7 @@ dotnet workload install wasm-tools --skip-manifest-update # rather than PATH -- verified: the compile succeeds even when cmd can read no PATH whatsoever. publish_args="" if [ "${OS:-}" = "Windows_NT" ]; then - publish_args="-p:PATH=C:\\Windows\\System32" + publish_args="-p:PATH=C:\\Windows\\System32" fi # $publish_args holds zero or one CLI flag; word-splitting is intentional. @@ -115,7 +115,7 @@ dotnet workload install wasm-tools --skip-manifest-update publish_args="" if [ "${OS:-}" = "Windows_NT" ]; then - publish_args="-p:PATH=C:\\Windows\\System32" + publish_args="-p:PATH=C:\\Windows\\System32" fi # $publish_args holds zero or one CLI flag; word-splitting is intentional. diff --git a/.mise/config.linux.toml b/.mise/config.linux.toml index 18c62eed..74d3f3ea 100644 --- a/.mise/config.linux.toml +++ b/.mise/config.linux.toml @@ -101,7 +101,7 @@ fi pkgs="$common $specific" # Azure Linux / openSUSE rename a few DNF_PACKAGES entries. # Remap before the iteration so the checks (and the printed `Install with: ...` hint) refer to the right -# names. Word-by-word in pure bash avoids the regex escape gymnastics that `\b` or awk word-boundary tokens +# names. Word-by-word in pure bash avoids the regex escape gymnastics that `\\b` or awk word-boundary tokens # would need through the TOML basic-string layer. if [ "$arg" = DNF_PACKAGES ] && grep -q '^ID=azurelinux$' /etc/os-release 2>/dev/null; then remapped="" @@ -137,10 +137,11 @@ for pkg in $pkgs; do # Debian/Ubuntu: /etc/ssl/certs/ca-certificates.crt. # Fedora/RHEL/AL2023: /etc/pki/tls/certs/ca-bundle.crt; openSUSE: /etc/ssl/ca-bundle.pem. [ -r /etc/ssl/certs/ca-certificates.crt ] || - [ -r /etc/pki/tls/certs/ca-bundle.crt ] || - [ -r /etc/ssl/ca-bundle.pem ] ;; - libc6-dev|glibc-devel) [ -r /usr/include/stdio.h ] ;; - g++|gcc-c++) command -v g++ >/dev/null 2>&1 ;; + [ -r /etc/pki/tls/certs/ca-bundle.crt ] || + [ -r /etc/ssl/ca-bundle.pem ] + ;; + libc6-dev | glibc-devel) [ -r /usr/include/stdio.h ] ;; + g++ | gcc-c++) command -v g++ >/dev/null 2>&1 ;; # `binutils` provides the assembler/linker (as/ld). # The package itself has no binary called `binutils`. So probe via `as`, which it ships. binutils) command -v as >/dev/null 2>&1 ;; @@ -148,18 +149,20 @@ for pkg in $pkgs; do # Fedora pre-includes it in the base image; Azure Linux base/core does not. So we install it explicitly # on Azure Linux. Debian/Ubuntu get the same headers via libc6-dev's transitive linux-libc-dev. That # path is already checked above. openSUSE names the same package linux-glibc-devel. - kernel-headers|linux-glibc-devel) [ -r /usr/include/linux/limits.h ] ;; - libicu*|icu) + kernel-headers | linux-glibc-devel) [ -r /usr/include/linux/limits.h ] ;; + libicu* | icu) # Try both libicu install layouts. # Debian/Ubuntu install libicu under /usr/lib//. Fedora and Azure Linux put it under /usr/lib64/ directly. coreutils ls /usr/lib/*/libicui18n.so.* >/dev/null 2>&1 || - coreutils ls /usr/lib64/libicui18n.so.* >/dev/null 2>&1 ;; - libatomic*|libgcc-atomic) + coreutils ls /usr/lib64/libicui18n.so.* >/dev/null 2>&1 + ;; + libatomic* | libgcc-atomic) # Fedora and Azure Linux need libatomic explicitly for pnpm's prebuilt binary. # Ubuntu gcc pulls it via libgcc-s1 by default. Same dual-layout split as libicu above. Debian/Ubuntu use # a per-triple subdir vs RPM-family's /usr/lib64. coreutils ls /usr/lib/*/libatomic.so.* >/dev/null 2>&1 || - coreutils ls /usr/lib64/libatomic.so.* >/dev/null 2>&1 ;; + coreutils ls /usr/lib64/libatomic.so.* >/dev/null 2>&1 + ;; *) command -v "$pkg" >/dev/null 2>&1 ;; esac || missing="$missing $pkg" done diff --git a/.mise/config.macos.toml b/.mise/config.macos.toml index cfd07594..9226723b 100644 --- a/.mise/config.macos.toml +++ b/.mise/config.macos.toml @@ -88,7 +88,7 @@ missing="" brewy="" for pkg in $pkgs; do case "$pkg" in - ca-certificates|libc6-dev|libicu*|binutils) cmd="" ;; + ca-certificates | libc6-dev | libicu* | binutils) cmd="" ;; *) cmd="$pkg" ;; esac case "$pkg" in @@ -99,7 +99,10 @@ for pkg in $pkgs; do # The probe via the assembler binary still confirms the assembler is reachable. binutils) command -v as >/dev/null 2>&1 ;; *) command -v "$pkg" >/dev/null 2>&1 ;; - esac || { missing="$missing $pkg"; continue; } + esac || { + missing="$missing $pkg" + continue + } # Reject Homebrew-installed tools, since this repo deliberately avoids Brew. # A tool under a brew prefix means PATH shadowed the Xcode CLT / native one. # Skip the file-based pkgs (no binary to check). @@ -107,8 +110,9 @@ for pkg in $pkgs; do path="$(command -v "$cmd")" || continue real="$(coreutils readlink -f "$path" 2>/dev/null || echo "$path")" case "$real" in - /opt/homebrew/*|/usr/local/Cellar/*|/usr/local/opt/*|/usr/local/Homebrew/*) - brewy="$brewy $cmd($real)" ;; + /opt/homebrew/* | /usr/local/Cellar/* | /usr/local/opt/* | /usr/local/Homebrew/*) + brewy="$brewy $cmd($real)" + ;; esac done if [ -n "$missing" ]; then diff --git a/.mise/config.maint.toml b/.mise/config.maint.toml index 4fde6aed..4e8547be 100644 --- a/.mise/config.maint.toml +++ b/.mise/config.maint.toml @@ -99,7 +99,7 @@ short=$(echo "$commit" | coreutils cut -c1-7) asset="${stem}-${short}.onnx" coreutils mv original.onnx "$asset" -coreutils sha256sum "$asset" > "${asset}.sha256" +coreutils sha256sum "$asset" >"${asset}.sha256" # Sanity-check the computed SHA256 against HF's recorded LFS oid. # The SHA256 we just computed must match HF's recorded LFS oid for the blob; mismatch means the download was @@ -123,7 +123,7 @@ cksum_path="{{ config_root }}/config/upstream-cache/data.toml" mise_path="{{ config_root }}/.mise/config.toml" augtool --transform="Toml incl $cksum_path" --autosave < "${asset}.sha256" +coreutils sha256sum "$asset" >"${asset}.sha256" actual=$(coreutils cut -c1-64 "${asset}.sha256") gh release upload "{{ vars.a_hf_cache_tag }}" --clobber --repo "$GH_REPO" "$asset" "${asset}.sha256" cksum_path="{{ config_root }}/config/upstream-cache/data.toml" augtool --transform="Toml incl $cksum_path" --autosave < "${tarball}.sha256" +coreutils sha256sum "$tarball" >"${tarball}.sha256" actual=$(coreutils cut -c1-64 "${tarball}.sha256") gh release upload "$RP_RELEASE" --clobber --repo "$GH_REPO" "$tarball" "${tarball}.sha256" @@ -240,13 +240,13 @@ gh release upload "$RP_RELEASE" --clobber --repo "$GH_REPO" "$tarball" "${tarbal cksum_path="{{ config_root }}/config/upstream-cache/data.toml" mise_path="{{ config_root }}/.mise/config.toml" augtool --transform="Toml incl $cksum_path" --autosave </dev/null | tr -d '\r' | grep -iE 'MSVC[\\/].*Hostx64' | head -1 || true) + msvc_link=$(where.exe link.exe 2>/dev/null | tr -d '\\r' | grep -iE 'MSVC[\\/].*Hostx64' | head -1 || true) if [ -n "${msvc_link:-}" ]; then export CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER="$msvc_link" fi @@ -348,12 +348,15 @@ else fi case "$triple" in - x86_64-apple-darwin) plat="macos-x64" ;; - aarch64-apple-darwin) plat="macos-arm64" ;; - x86_64-unknown-linux-gnu) plat="linux-x64" ;; - aarch64-unknown-linux-gnu) plat="linux-arm64" ;; - x86_64-pc-windows-*) plat="windows-x64" ;; - *) echo "unsupported host triple: $triple" >&2; exit 1 ;; + x86_64-apple-darwin) plat="macos-x64" ;; + aarch64-apple-darwin) plat="macos-arm64" ;; + x86_64-unknown-linux-gnu) plat="linux-x64" ;; + aarch64-unknown-linux-gnu) plat="linux-arm64" ;; + x86_64-pc-windows-*) plat="windows-x64" ;; + *) + echo "unsupported host triple: $triple" >&2 + exit 1 + ;; esac # Glob the produced binary so the same tar line works on every host. @@ -367,7 +370,7 @@ fi asset="rustpython-${short}-${triple}.tar.gz" tarball="$RP_WORK/$asset" tar -C "$prefix" -czf "$tarball" "$bin_rel" -coreutils sha256sum "$tarball" > "${tarball}.sha256" +coreutils sha256sum "$tarball" >"${tarball}.sha256" actual=$(coreutils cut -c1-64 "${tarball}.sha256") gh release upload "$RP_RELEASE" --clobber --repo "$GH_REPO" "$tarball" "${tarball}.sha256" @@ -387,9 +390,9 @@ if ! command -v augtool >/dev/null 2>&1; then exit 0 fi augtool --transform="Toml incl $mise_path" --autosave < "$shared_venv/bin/pip" <<'PIP_SHIM' + coreutils cat >"$shared_venv/bin/pip" <<'PIP_SHIM' #!/bin/sh exec "$(dirname "$0")/python" -m pip "$@" PIP_SHIM diff --git a/.mise/config.msvc.toml b/.mise/config.msvc.toml index 7af93911..c1d0d885 100644 --- a/.mise/config.msvc.toml +++ b/.mise/config.msvc.toml @@ -70,7 +70,10 @@ if [ ! -f "$script" ]; then rclone copyto "$src" "$script" --http-url https://gist.githubusercontent.com --progress fi actual=$(coreutils sha256sum -b "$script" | coreutils cut -c1-64) -[ "$actual" = "{{ vars.a_pmsvc_sha }}" ] || { echo "portable-msvc.py checksum mismatch: $actual" >&2; exit 1; } +[ "$actual" = "{{ vars.a_pmsvc_sha }}" ] || { + echo "portable-msvc.py checksum mismatch: $actual" >&2 + exit 1 +} # Versions pinned for reproducibility (`--show-versions` lists what the manifest offers). # The rendered msvc.env tracks whatever is installed, so a bump only needs these two flags changed and a # re-run after deleting target/msvc/msvc. diff --git a/.mise/config.python.toml b/.mise/config.python.toml index 80ece8c6..7fc29bab 100644 --- a/.mise/config.python.toml +++ b/.mise/config.python.toml @@ -281,7 +281,7 @@ datamodel-codegen --input ../../target/int-gen/ws.schema.json --output et_ws/mes # Drop the 2-line header that custom-file-header = "#" emits, so the file starts cleanly. # That header is a lone `#` + the blank after it. tail, not sed: datamodel-codegen always emits exactly those # two lines, so a fixed offset is safe. -coreutils tail -n +3 et_ws/messages.py > et_ws/messages.py.tmp +coreutils tail -n +3 et_ws/messages.py >et_ws/messages.py.tmp coreutils mv et_ws/messages.py.tmp et_ws/messages.py # Re-run repo ruff because datamodel-codegen's bundled ruff uses its own defaults. # Those defaults aren't our repo ruff.toml, so re-run repo ruff: `check --fix` diff --git a/.mise/config.r.toml b/.mise/config.r.toml index 5d131cc9..c4cc42c1 100644 --- a/.mise/config.r.toml +++ b/.mise/config.r.toml @@ -27,7 +27,10 @@ description = "Vendor the webR distribution into rdata1's pkg/webr/ (served at / dir = "services/ws-modules/rdata1" run = """ src="$(mise where "http:webr")" -[ -n "$src" ] || { echo "http:webr not installed; run 'MISE_ENV=r mise install' first" >&2; exit 1; } +[ -n "$src" ] || { + echo "http:webr not installed; run 'MISE_ENV=r mise install' first" >&2 + exit 1 +} coreutils rm -rf pkg/webr coreutils mkdir -p pkg/webr coreutils cp -R "$src/." pkg/webr/ @@ -40,7 +43,10 @@ description = "Vendor the webR distribution into rmath1's pkg/webr/ (served at / dir = "services/ws-modules/rmath1" run = """ src="$(mise where "http:webr")" -[ -n "$src" ] || { echo "http:webr not installed; run 'MISE_ENV=r mise install' first" >&2; exit 1; } +[ -n "$src" ] || { + echo "http:webr not installed; run 'MISE_ENV=r mise install' first" >&2 + exit 1 +} coreutils rm -rf pkg/webr coreutils mkdir -p pkg/webr coreutils cp -R "$src/." pkg/webr/ @@ -53,7 +59,10 @@ description = "Vendor the webR distribution into rcomm1's pkg/webr/ (served at / dir = "services/ws-modules/rcomm1" run = """ src="$(mise where "http:webr")" -[ -n "$src" ] || { echo "http:webr not installed; run 'MISE_ENV=r mise install' first" >&2; exit 1; } +[ -n "$src" ] || { + echo "http:webr not installed; run 'MISE_ENV=r mise install' first" >&2 + exit 1 +} coreutils rm -rf pkg/webr coreutils mkdir -p pkg/webr coreutils cp -R "$src/." pkg/webr/ diff --git a/.mise/config.toml b/.mise/config.toml index 124b20ae..38a8d5de 100644 --- a/.mise/config.toml +++ b/.mise/config.toml @@ -59,6 +59,8 @@ action-validator = { version = "latest", os = ["linux", "macos"] } "aqua:EmbarkStudios/cargo-deny" = "latest" "aqua:StyraInc/regal" = "latest" "aqua:koalaman/shellcheck" = "latest" +# shfmt (the `sh` repo's formatter binary) formats the multi-line bash bodies of mise tasks. +"aqua:mvdan/sh" = "latest" "aqua:rhysd/actionlint" = "latest" "aqua:rustwasm/wasm-pack" = "latest" # Vector ships no darwin/amd64 prebuilt, so second-tier macos/x64 builds the same pin from source below. @@ -556,6 +558,11 @@ a_rp_wasm_short = "08a1d6f" # Extra actionlint flags for actionlint-check; empty everywhere except the config.windows.toml override. actionlint_args = "" gh_http = "--http-url https://github.com --progress --ignore-existing" +# Shared shfmt flags for the shfmt-mise-check / shfmt-mise-fmt pair. +# `-ln bash` because a split-out body carries no shebang for shfmt to sniff, `-i 2` to match the indent_size +# .editorconfig sets for every file in the repo, and `-ci` to keep `case` arms indented under their `case` +# (shfmt's default pulls them back to the `case` column, which every body in these configs disagrees with). +shfmt_args = "-ln bash -i 2 -ci" # OpenObserve docker image for the openobserve/o2 dev-observability task. o2_image = "openobserve/openobserve:v0.91.5" # OpenTelemetry Collector image for the o2-hostmetrics / o2-macmon / o2-nvidia / o2-winmetrics tasks. @@ -632,7 +639,7 @@ description = "Format every checked-in .jq file with jqfmt (in-place)" # worth keeping, lift them into the calling task / source-of-truth file before running this. run = """ git ls-files '*.jq' | while read -r f; do - jqfmt -f "$f" -op pipe -ob -ar | goawk '{ sub(/[ \\t]+$/, ""); print }' > "$f.tmp" + jqfmt -f "$f" -op pipe -ob -ar | goawk '{ sub(/[ \\t]+$/, ""); print }' >"$f.tmp" coreutils mv "$f.tmp" "$f" done """ @@ -640,7 +647,7 @@ shell = "bash -euo pipefail -c" [tasks."fmt:rust"] # Rust + universal formatters. Always-loaded, so `fmt`'s glob always matches it. -depends = ["cargo-clippy-fix", "cargo-fmt", "dprint-fmt", "jq-fmt", "taplo-fmt"] +depends = ["cargo-clippy-fix", "cargo-fmt", "dprint-fmt", "jq-fmt", "shfmt-mise-fmt", "taplo-fmt"] description = "Run the Rust + universal repo-wide formatters" [tasks.fmt] @@ -679,6 +686,7 @@ depends = [ "ryl-check", "semgrep-check", "shellcheck-mise-check", + "shfmt-mise-check", "taplo-check", "taplo-fmt-check", "typos-check", @@ -884,7 +892,7 @@ files=$(git ls-files '*Dockerfile' '*Dockerfile.*') conftest test --combine --namespace dockerfile -p config/conftest/policy $files .mise # Filter to Dockerfiles that actually use heredocs before the `ignore` parser. for f in $files; do - if rg -q '^RUN[^\n]*<<' "$f"; then + if rg -q '^RUN[^\\n]*<<' "$f"; then conftest test --parser ignore --namespace dockerfile_heredoc -p config/conftest/policy "$f" fi done @@ -925,13 +933,71 @@ for cfg in .mise/config*.toml; do echo '#!/usr/bin/env bash' echo 'set -euo pipefail' printf '%s' "$b64" | coreutils base64 -d | goawk "$mask" - } | coreutils tr -d '\r' > "$out" + } | coreutils tr -d '\r' >"$out" done done shellcheck --shell=bash --exclude=SC2034,SC2154 "$boot"/*.sh ''' shell = "bash -euo pipefail -c" +[tasks.shfmt-mise-check] +description = "Check that every multi-line bash mise task body is shfmt-formatted" +# Run the shfmt-mise-fmt pipeline against scratch copies and compare, rather than over the configs themselves. +# The `-d` pass is what makes a failure actionable -- shfmt prints the offending body's own diff -- while the +# hash compare additionally catches whatever the merge pass would rewrite outside shfmt's view (the TOML +# escape normalisation .mise/shfmt-mise.awk performs on a `"""` body). +run = ''' +work="{{ vars.config_root_fwd }}/target/scratch/shfmt-mise-check" +coreutils rm -rf "$work" +status=0 +for cfg in .mise/config*.toml; do + d="$work/$(coreutils basename "$cfg" .toml)" + coreutils mkdir -p "$d" + goawk -N raw -v mode=split -v dir="$d" -f .mise/shfmt-mise.awk "$cfg" +done +shfmt {{ vars.shfmt_args }} -d "$work"/*/*.sh || status=1 +shfmt {{ vars.shfmt_args }} -w "$work"/*/*.sh +for cfg in .mise/config*.toml; do + d="$work/$(coreutils basename "$cfg" .toml)" + goawk -N raw -v mode=merge -v dir="$d" -f .mise/shfmt-mise.awk "$cfg" >"$d/merged.toml" + have=$(coreutils sha256sum -b "$cfg" | coreutils cut -c1-64) + want=$(coreutils sha256sum -b "$d/merged.toml" | coreutils cut -c1-64) + if [ "$have" != "$want" ]; then + echo "$cfg: task bodies need reformatting; run 'mise run shfmt-mise-fmt'" >&2 + status=1 + fi +done +exit $status +''' +shell = "bash -euo pipefail -c" + +[tasks.shfmt-mise-fmt] +description = "Format every multi-line bash mise task body with shfmt" +# Split each config's task bodies into scratch scripts, shfmt them, then merge them back over the config. +# .mise/shfmt-mise.awk owns both halves of that trip; the two loops here are split around one shfmt call so +# the glob always has something to match (config.java.toml carries no multi-line body of its own). +run = ''' +work="{{ vars.config_root_fwd }}/target/scratch/shfmt-mise-fmt" +coreutils rm -rf "$work" +for cfg in .mise/config*.toml; do + d="$work/$(coreutils basename "$cfg" .toml)" + coreutils mkdir -p "$d" + goawk -N raw -v mode=split -v dir="$d" -f .mise/shfmt-mise.awk "$cfg" +done +shfmt {{ vars.shfmt_args }} -w "$work"/*/*.sh +for cfg in .mise/config*.toml; do + d="$work/$(coreutils basename "$cfg" .toml)" + goawk -N raw -v mode=merge -v dir="$d" -f .mise/shfmt-mise.awk "$cfg" >"$d/merged.toml" + coreutils cp "$d/merged.toml" "$cfg" +done +''' +shell = "bash -euo pipefail -c" +# taplo-fmt rewrites every .mise/config*.toml in full, and so does this task. +# fmt:rust runs its dependencies as parallel siblings, so with no ordering edge between the two the second +# writer silently drops the first one's work. `wait_for` rather than `depends` because the edge only matters +# when both are already scheduled -- `mise run shfmt-mise-fmt` alone should stay one task, not two. +wait_for = ["taplo-fmt"] + [tasks.taplo-check] alias = "toml-check" # `taplo lint` reads config/taplo.toml's `[[rule]] schema` entries. @@ -1083,7 +1149,7 @@ run = """ echo 'IgnoredVulns = [' rg -oN -r ' { id = $0 },' '"(RUSTSEC-[0-9]{4}-[0-9]{4}|GHSA(-[0-9a-z]{4}){3})"' config/deny.toml | coreutils sort -u echo ']' -} > config/osv-scanner.toml +} >config/osv-scanner.toml """ shell = "bash -euo pipefail -c" @@ -1249,7 +1315,7 @@ mkdir -p "$s" for crate in et-cli et-int-gen et-onnx; do helpmd="utilities/${crate#et-}/HELP.md" echo "[gen-help-all] $crate -> $helpmd" - cargo run --features markdown-help -q -p "$crate" -- --markdown-help | dprint fmt --stdin md > "$s/$crate.md" + cargo run --features markdown-help -q -p "$crate" -- --markdown-help | dprint fmt --stdin md >"$s/$crate.md" coreutils mv "$s/$crate.md" "$helpmd" done """ @@ -1690,7 +1756,10 @@ rclone copyto {{ vars.face1_src }} {{ vars.face1_dst }} {{ vars.gh_http }} export asset='{{ vars.face1_asset }}' sha=$(mise exec -- yq -p toml -oy '.asset[strenv(asset)].sha256' config/upstream-cache/data.toml) actual=$(mise exec -- coreutils sha256sum -b "{{ vars.face1_dst }}" | mise exec -- coreutils cut -c1-64) -[ "$actual" = "$sha" ] || { echo "face1 checksum mismatch: expected=$sha actual=$actual" >&2; exit 1; } +[ "$actual" = "$sha" ] || { + echo "face1 checksum mismatch: expected=$sha actual=$actual" >&2 + exit 1 +} """ shell = "bash -euo pipefail -c" @@ -1703,7 +1772,10 @@ rclone copyto {{ vars.eye1_src }} {{ vars.eye1_dst }} {{ vars.gh_http }} export asset='{{ vars.eye1_asset }}' sha=$(mise exec -- yq -p toml -oy '.asset[strenv(asset)].sha256' config/upstream-cache/data.toml) actual=$(mise exec -- coreutils sha256sum -b "{{ vars.eye1_dst }}" | mise exec -- coreutils cut -c1-64) -[ "$actual" = "$sha" ] || { echo "eye1 checksum mismatch: expected=$sha actual=$actual" >&2; exit 1; } +[ "$actual" = "$sha" ] || { + echo "eye1 checksum mismatch: expected=$sha actual=$actual" >&2 + exit 1 +} """ shell = "bash -euo pipefail -c" @@ -1726,7 +1798,10 @@ rclone copyto {{ vars.speech1_src }} {{ vars.speech1_dst }} $opts export asset='{{ vars.speech1_asset }}' sha=$(mise exec -- yq -p toml -oy '.asset[strenv(asset)].sha256' config/upstream-cache/data.toml) actual=$(mise exec -- coreutils sha256sum -b "{{ vars.speech1_dst }}" | mise exec -- coreutils cut -c1-64) -[ "$actual" = "$sha" ] || { echo "speech1 checksum mismatch: expected=$sha actual=$actual" >&2; exit 1; } +[ "$actual" = "$sha" ] || { + echo "speech1 checksum mismatch: expected=$sha actual=$actual" >&2 + exit 1 +} """ shell = "bash -euo pipefail -c" @@ -1773,14 +1848,14 @@ assets=$(gh release view "$tag" --repo edge-toolkit/core --json assets --jq '.as native_missing=true wasm_missing=true if echo "$assets" | rg -Fxq "$native_asset"; then native_missing=false; fi -if echo "$assets" | rg -Fxq "$wasm_asset"; then wasm_missing=false; fi +if echo "$assets" | rg -Fxq "$wasm_asset"; then wasm_missing=false; fi out="${GITHUB_OUTPUT:-/dev/stdout}" { echo "short=$short" echo "triple=$triple" echo "native_missing=$native_missing" echo "wasm_missing=$wasm_missing" -} >> "$out" +} >>"$out" echo "[rp-detect-missing] triple=$triple short=$short native_missing=$native_missing wasm_missing=$wasm_missing" >&2 """ shell = "bash -euo pipefail -c" diff --git a/.mise/config.windows.toml b/.mise/config.windows.toml index acfcfb91..477bc4cc 100644 --- a/.mise/config.windows.toml +++ b/.mise/config.windows.toml @@ -353,7 +353,7 @@ elif [ "${ENABLE_RUSTPYTHON_PIPX_BOOTSTRAP:-}" = "1" ]; then shared_site="$(coreutils ls -d "$shared_venv"/lib/*/site-packages)" coreutils cp -R "$pip_extracted/pip" "$shared_site/" coreutils cp -R "$pip_extracted"/pip-*.dist-info "$shared_site/" - coreutils cat > "$shared_venv/bin/pip" <<'PIP_SHIM' + coreutils cat >"$shared_venv/bin/pip" <<'PIP_SHIM' #!/bin/sh exec "$(dirname "$0")/python" -m pip "$@" PIP_SHIM diff --git a/.mise/mise.lock b/.mise/mise.lock index a1760535..22137c65 100644 --- a/.mise/mise.lock +++ b/.mise/mise.lock @@ -200,6 +200,35 @@ checksum = "sha256:8a4e35ab0b331c85d73567b12f2a444df187f483e5079ceffa6bda1faa2e7 url = "https://github.com/koalaman/shellcheck/releases/download/v0.11.0/shellcheck-v0.11.0.zip" url_api = "https://api.github.com/repos/koalaman/shellcheck/releases/assets/279056944" +[[tools."aqua:mvdan/sh"]] +version = "3.13.1" +backend = "aqua:mvdan/sh" + +[tools."aqua:mvdan/sh"."platforms.linux-arm64"] +checksum = "sha256:32d92acaa5cd8abb29fc49dac123dc412442d5713967819d8af2c29f1b3857c7" +url = "https://github.com/mvdan/sh/releases/download/v3.13.1/shfmt_v3.13.1_linux_arm64" +url_api = "https://api.github.com/repos/mvdan/sh/releases/assets/390322859" + +[tools."aqua:mvdan/sh"."platforms.linux-x64"] +checksum = "sha256:fb096c5d1ac6beabbdbaa2874d025badb03ee07929f0c9ff67563ce8c75398b1" +url = "https://github.com/mvdan/sh/releases/download/v3.13.1/shfmt_v3.13.1_linux_amd64" +url_api = "https://api.github.com/repos/mvdan/sh/releases/assets/390322866" + +[tools."aqua:mvdan/sh"."platforms.macos-arm64"] +checksum = "sha256:9680526be4a66ea1ffe988ed08af58e1400fe1e4f4aef5bd88b20bb9b3da33f8" +url = "https://github.com/mvdan/sh/releases/download/v3.13.1/shfmt_v3.13.1_darwin_arm64" +url_api = "https://api.github.com/repos/mvdan/sh/releases/assets/390322881" + +[tools."aqua:mvdan/sh"."platforms.macos-x64"] +checksum = "sha256:6feedafc72915794163114f512348e2437d080d0047ef8b8fa2ec63b575f12af" +url = "https://github.com/mvdan/sh/releases/download/v3.13.1/shfmt_v3.13.1_darwin_amd64" +url_api = "https://api.github.com/repos/mvdan/sh/releases/assets/390322886" + +[tools."aqua:mvdan/sh"."platforms.windows-x64"] +checksum = "sha256:60cd368533d0ad73fa86d93d5bbf95ef40587245ce684ed138c1b31557b5fe97" +url = "https://github.com/mvdan/sh/releases/download/v3.13.1/shfmt_v3.13.1_windows_amd64.exe" +url_api = "https://api.github.com/repos/mvdan/sh/releases/assets/390322844" + [[tools."aqua:rhysd/actionlint"]] version = "1.7.12" backend = "aqua:rhysd/actionlint" @@ -370,6 +399,10 @@ backend = "cargo:rustfs/rustfs" version = "0.21.0" backend = "cargo:ryl" +[[tools."cargo:vectordotdev/vector"]] +version = "tag:v0.56.0" +backend = "cargo:vectordotdev/vector" + [[tools."cargo:wasm-opt"]] version = "0.116.1" backend = "cargo:wasm-opt" diff --git a/.mise/shfmt-mise.awk b/.mise/shfmt-mise.awk new file mode 100644 index 00000000..4d9b17b3 --- /dev/null +++ b/.mise/shfmt-mise.awk @@ -0,0 +1,161 @@ +# Split the multi-line bash bodies out of a `.mise/config*.toml` so shfmt can format them, then merge back. +# +# `-v mode=split` writes the Nth body of the config to /NNN.sh, plus /NNN.tok holding the Tera +# tokens it masked out of that body (one per line, in the order they appeared). `-v mode=merge` re-reads the +# same config and re-emits it on stdout with each body replaced by the (by then shfmt-formatted) /NNN.sh. +# Bodies are paired between the two passes positionally, so both passes must see the same input file. +# +# A body is the run of lines between a `run = """` (or `run = '''`) line and the matching closing delimiter on +# its own line -- the shape every multi-line mise task uses, and the only shape a task `run` can take given the +# conftest rule that a multiline run is a bash script. Only the opening delimiter closes a body, so a `"""` +# python docstring inside a `'''` literal block does not end it early. +# +# Two encodings sit between the TOML byte and the byte shfmt must see, and both are reversed on the way back: +# +# Tera `{{ ... }}` / `{% ... %}` masks to `ET_TERA__`, a plain bash word shfmt leaves alone. Without +# it a `{% if os() == 'windows' %}` body fails to parse (`(` where shfmt wants a word) and shfmt +# declines to format it at all. Masking happens before the escape decode and unmasking after the +# re-encode, so a token's own bytes round-trip verbatim however they were spelled. +# escapes A `"""` block is a TOML *basic* string, so `\\` and `\"` in the file are one backslash and one +# quote by the time the shell sees them; shfmt has to be handed that decoded form (raw `\\(` reads +# as escaped-backslash plus a bare `(`, which does not parse), and the merge pass re-escapes each +# backslash on the way out. The decode deliberately rejects every other escape rather than growing +# a table for them: a lone `\n` / `\r` / `\b` in a body is nearly always meant as the two +# characters the shell needs, and TOML quietly folding it into a control character is a bug in the +# body (a `\b` inside a comment here once decoded to a backspace and ate the surrounding text). +# `'''` blocks are TOML *literal* strings -- no escape processing at all -- so they pass untouched. + +function fail(msg) { + printf("%s:%d: %s\n", FILENAME, FNR, msg) > "/dev/stderr" + exit 1 +} + +function placeholder(n) { + return "ET_TERA_" n "_" +} + +# Replace every occurrence of `from` with `to`, byte-wise. +# index/substr rather than sub(): neither argument is a regex, and `&` in an awk replacement is magic. +function subst_all(s, from, to, out, p) { + out = "" + while ((p = index(s, from)) > 0) { + out = out substr(s, 1, p - 1) to + s = substr(s, p + length(from)) + } + return out s +} + +# Swap each Tera token for its placeholder, recording the original in the body's .tok sidecar. +function mask(line, out, rest, tok) { + out = "" + rest = line + while (match(rest, /[{][{][^}]*[}][}]|[{][%][^%]*[%][}]/)) { + tok = substr(rest, RSTART, RLENGTH) + ntok++ + print tok > tokfile + out = out substr(rest, 1, RSTART - 1) placeholder(ntok) + rest = substr(rest, RSTART + RLENGTH) + } + return out rest +} + +function unmask(line, i) { + for (i = 1; i <= ntok; i++) { + line = subst_all(line, placeholder(i), tok[i]) + } + return line +} + +# Undo TOML basic-string escaping, one left-to-right pass so `\\"` decodes as backslash then quote. +function decode(s, out, n, i, c, nx) { + out = "" + n = length(s) + for (i = 1; i <= n; i++) { + c = substr(s, i, 1) + if (c != "\\") { + out = out c + continue + } + nx = substr(s, i + 1, 1) + if (nx == "\\" || nx == "\"") { + out = out nx + i++ + continue + } + fail("unsupported TOML escape \"\\" nx "\" in a task body; double the backslash to pass it to the shell") + } + return out +} + +function encode(s) { + return subst_all(s, "\\", "\\\\") +} + +BEGIN { + if (mode != "split" && mode != "merge") { + printf("shfmt-mise.awk: -v mode= must be split or merge\n") > "/dev/stderr" + exit 1 + } + if (dir == "") { + printf("shfmt-mise.awk: -v dir= must name the body directory\n") > "/dev/stderr" + exit 1 + } +} + +!inbody && ($0 == "run = \"\"\"" || $0 == "run = '''") { + inbody = 1 + delim = ($0 == "run = \"\"\"") ? "\"\"\"" : "'''" + basic = (delim == "\"\"\"") + idx++ + body = sprintf("%s/%03d.sh", dir, idx) + tokfile = sprintf("%s/%03d.tok", dir, idx) + ntok = 0 + if (mode == "split") { + printf("") > body + printf("") > tokfile + next + } + print + while ((getline line < tokfile) > 0) { + tok[++ntok] = line + } + close(tokfile) + while ((getline line < body) > 0) { + if (basic) { + line = encode(line) + if (index(line, "\"\"\"") > 0) { + fail("formatted body contains `\"\"\"`, which would close the TOML string early") + } + } + print unmask(line) + } + close(body) + next +} + +inbody && $0 == delim { + inbody = 0 + if (mode == "split") { + close(body) + close(tokfile) + next + } + print + next +} + +inbody { + if (mode == "merge") { + next + } + line = mask($0) + if (basic) { + line = decode(line) + } + print line > body + next +} + +mode == "merge" { + print +} diff --git a/CLAUDE.md b/CLAUDE.md index 9b959fc0..6e2e38cd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -302,7 +302,8 @@ run every loaded language's row; guest rows need their `MISE_ENV` loaded. `dprint-fmt` / `dprint-check` cover `*.md`, `*.yaml`, `*.json`/`*.jsonc`, `*.ts`/`*.js`, `*.css`, `*.html`, `*.java`, and `Dockerfile*`; `hadolint-check` also lints Dockerfiles, and `link-check` scans `*.md` + `*.rs`. Every file is covered by `editorconfig-check` and `typos-check`, file and directory names by `ls-lint-check`, and `*.yml` is -rejected by `semgrep-check` (use `*.yaml`). +rejected by `semgrep-check` (use `*.yaml`). A multi-line task body inside `.mise/config*.toml` is shell, not TOML, so +it carries two more of its own: `shfmt-mise-fmt` / `shfmt-mise-check` format it, and `shellcheck-mise-check` lints it. For Rust inner-loop iteration on a single crate, use `mise run cargo-clippy-check-pkg ` (alias `clippy-pkg`) instead of `cargo-clippy-check` -- it runs `cargo clippy --keep-going --tests -p ` so you @@ -890,6 +891,12 @@ standalone file or a mise task `run`. The available linters: """` from `.mise/config*.toml`, masks Tera tokens (`{{ ... }}` -> `MISEVAR`, `{% ... %}` -> empty), and shellchecks the lot. This is how shell-quality lints reach mise task bodies (shellcheck itself doesn't read TOML). +- **shfmt on mise task bodies** (`.mise/shfmt-mise.awk`, via `shfmt-mise-fmt` and `shfmt-mise-check`) -- splits those + same bodies out, shfmt-formats each one, and merges the result back over the config, so a task body is held to the + same formatting as any other shell in the repo. The check reports shfmt's own diff and names `shfmt-mise-fmt` as the + fix. Its one demand on how a body is written: inside a `"""` block every backslash escape must be doubled, since a + lone `\n` / `\r` / `\b` is TOML syntax that folds into a control character rather than the two bytes the shell wants + -- the split pass rejects one instead of guessing which was meant. - plus hadolint, ls-lint (file/dir naming), zizmor (Actions security), ryl (YAML), lychee (links), clang-format / clang-tidy / cpplint / flawfinder (C, in the zig config), editorconfig-checker, typos, and action-validator for their domains. diff --git a/services/ws-modules/pic-viewer/tests/show_image.rs b/services/ws-modules/pic-viewer/tests/show_image.rs index 37fd0cf4..23a1c404 100644 --- a/services/ws-modules/pic-viewer/tests/show_image.rs +++ b/services/ws-modules/pic-viewer/tests/show_image.rs @@ -14,10 +14,24 @@ use wasm_bindgen_test::wasm_bindgen_test; wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); -/// The ws-server page favicon: a real 64x64 PNG, staged into `OUT_DIR` by build.rs and embedded here. +/// The ws-server page favicon: a real PNG, staged into `OUT_DIR` by build.rs and embedded here. const FAVICON_PNG: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/favicon.png")); -const FAVICON_WIDTH: u32 = 64; -const FAVICON_HEIGHT: u32 = 64; +/// The fixture's natural size, read out of its own header rather than restated as a literal. +/// A PNG's width and height are the big-endian `u32`s at bytes 16 and 20 (the first two fields of the IHDR +/// chunk's data), so what this test expects on the canvas comes from the very bytes the browser decodes. +/// Deriving it is what keeps the test honest when the favicon is redrawn at a different size. +const FAVICON_WIDTH: u32 = ihdr_u32(16); +const FAVICON_HEIGHT: u32 = ihdr_u32(20); + +/// Read the big-endian `u32` that the PNG header carries at byte `at`. +const fn ihdr_u32(at: usize) -> u32 { + u32::from_be_bytes([ + FAVICON_PNG[at], + FAVICON_PNG[at + 1], + FAVICON_PNG[at + 2], + FAVICON_PNG[at + 3], + ]) +} /// Insert the page's shared output canvas (`show_image` looks it up by this id), hidden like the real page. fn install_output_canvas() -> web_sys::HtmlCanvasElement { @@ -57,15 +71,18 @@ async fn shows_the_favicon_on_the_output_canvas() { assert_eq!(canvas.width(), FAVICON_WIDTH); assert_eq!(canvas.height(), FAVICON_HEIGHT); - // ...and actually carry the drawn pixels: the favicon is an opaque RGB PNG, so every pixel of a real - // draw has full alpha, while an untouched canvas is all-transparent (alpha 0). + // ...and actually carry the drawn pixels: an untouched canvas is all-transparent (alpha 0), so a fully + // opaque pixel can only have come from a real draw. The centre is sampled because the favicon carries an + // alpha channel and only its artwork is guaranteed opaque -- a corner may legitimately be transparent. let context = canvas .get_context("2d") .unwrap() .unwrap() .dyn_into::() .unwrap(); - let center = context.get_image_data(32.0, 32.0, 1.0, 1.0).unwrap(); + let center_x = f64::from(FAVICON_WIDTH) / 2.0; + let center_y = f64::from(FAVICON_HEIGHT) / 2.0; + let center = context.get_image_data(center_x, center_y, 1.0, 1.0).unwrap(); let pixel = center.data(); assert_eq!(pixel[3], 255, "center pixel should be opaque after drawing the favicon"); } diff --git a/services/ws-server/static/favicon.png b/services/ws-server/static/favicon.png index 545ecb00..c37afaed 100644 Binary files a/services/ws-server/static/favicon.png and b/services/ws-server/static/favicon.png differ