chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:29:49 +08:00
commit 505bac6b53
1470 changed files with 457757 additions and 0 deletions
+32
View File
@@ -0,0 +1,32 @@
# Linux truth loop: build + drive every showcase app on real Linux (GTK4)
# under Xvfb, with the repo mounted read-only at /src and all build output
# kept container-local under /work (shared-mount .zig-cache causes host/
# container path and permission skew).
#
# Matches the CI Linux jobs' package set (libgtk-4-dev, libwebkitgtk-6.0-dev,
# xvfb) plus capture/drive tools: x11-apps (xwd) and imagemagick (xwd -> png)
# for window-chrome-level captures alongside the engine screenshot channel.
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl xz-utils rsync \
libgtk-4-dev libwebkitgtk-6.0-dev \
xvfb xauth x11-apps x11-utils imagemagick \
dbus \
&& rm -rf /var/lib/apt/lists/*
ARG ZIG_VERSION=0.16.0
RUN curl -fsSL "https://ziglang.org/download/${ZIG_VERSION}/zig-aarch64-linux-${ZIG_VERSION}.tar.xz" \
| tar -xJ -C /opt \
&& ln -s "/opt/zig-aarch64-linux-${ZIG_VERSION}/zig" /usr/local/bin/zig
# The web process sandbox needs unprivileged user namespaces that container
# seccomp/AppArmor policies restrict; the sandbox is not what this loop
# tests (same setting as the Linux canvas smoke in CI).
ENV WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS=1
# No session bus provides an accessibility bus under Xvfb; without this the
# toolkit's accessibility init blocks ~25 s on a bus-name lookup at startup.
ENV GTK_A11Y=none
WORKDIR /work
+13
View File
@@ -0,0 +1,13 @@
# linux-truth
The Linux live-truth loop: drives the toolkit's showcase apps in real X11 windows under Xvfb inside a container, capturing what Linux actually does — layout, input, IME, resize clamps, screenshots — instead of trusting the null platform.
Prerequisites: a container runtime on the host (the image bundles Zig, GTK4, WebKitGTK dev headers, and Xvfb). The repo mounts read-only at `/src`, builds happen in the container-local `/work` volume, and artifacts land in the container's `/out` (`docker cp native-sdk-linux-truth:/out <dest>` copies them home).
One command runs everything, or one named step, from the repo root on the host:
```sh
tools/linux-truth/run-all.sh [image|up|sync|recon|drive|suites|all]
```
Steps: `image` builds the container image and `up` starts the long-lived container; `sync.sh` rsyncs `/src` into `/work` after local edits (never writing back); `recon.sh` builds and launches every showcase app, dumping snapshots, widget inventories, and both screenshot channels; `drive.sh` replays per-app interaction scenarios (clicks, text input, wheel, resize including the min-size clamp); `suites` runs the engine and example test suites plus the webview link check on real Linux. `lib.sh` holds the shared helpers and `send-wm-delete.c` closes windows the way a window manager would.
+292
View File
@@ -0,0 +1,292 @@
#!/usr/bin/env bash
# Interaction drive pass: launch each showcase app on Linux under Xvfb and
# run a representative scenario (clicks, text input, wheel scrolling, window
# resize incl. minimum-size clamp), screenshotting at defined points.
# Results land in /out/drive/<app>/: drive.log (ok:/MISS:/WARN: lines),
# engine *.png screenshots, final snapshot, and the app's stderr log.
#
# Run recon.sh first (it builds the apps); this script only launches.
set -u
source /src/tools/linux-truth/lib.sh
OUT=/out/drive
mkdir -p "$OUT"
start_xvfb
# window_bounds — echo the current @w1 bounds string, e.g. "0,0 320x490".
window_bounds() {
snapshot_lines | sed -n 's/.*window @w1 "[^"]*" bounds=(\([^)]*\)).*/\1/p' | head -1
}
# set_text <canvas> <role> <name> <text> — focus + replace through the
# real input-event path (select-all + text_input).
set_text() {
local id
id=$(widget_id "$1" "$2" "$3")
[ -n "$id" ] || { echo "WARN: widget $2 \"$3\" not found for set-text"; return 1; }
send widget-action "$1" "$id" set-text "$4"
}
# wheel <canvas> <role> <name> <delta-y> — scroll at a widget's position.
wheel() {
local id
id=$(widget_id "$1" "$2" "$3")
[ -n "$id" ] || { echo "WARN: widget $2 \"$3\" not found for wheel"; return 1; }
send widget-wheel "$1" "$id" "$4"
}
# check_min_size — ask for an absurdly small window and report the clamp.
check_min_size() {
send resize 50 50
sleep 1
echo "bounds after resize 50x50 request: $(window_bounds)"
}
finish() {
local out="$1" canvas="$2"
sleep 0.5
snapshot_lines > "$out/final-snapshot.txt"
echo "final: $(grep -o 'dispatch_errors=[0-9]*' "$out/final-snapshot.txt" | head -1)"
grep -icE "error|critical|warning|assert" /tmp/app.log >/dev/null 2>&1 \
&& echo "app log flagged lines: $(grep -icE 'error|critical|warning|assert' /tmp/app.log)" \
|| echo "app log flagged lines: 0"
grep -iE "error|critical|warning|assert" /tmp/app.log | head -10 > "$out/log-flags.txt" 2>/dev/null
cp /tmp/app.log "$out/app.log" 2>/dev/null
stop_app
}
drive_calculator() {
local out="$OUT/calculator" c=calc-canvas
cd /work/examples/calculator && launch_app zig-out/bin/calculator || return 1
click $c button "All clear"; click $c button 7; click $c button Multiply
click $c button 8; click $c button Equals
expect 'name="56"'
shot $c "$out/1-compute.png"
# Keyboard path: digits and enter through widget-key on the keypad.
id=$(widget_id $c textbox "Expression")
send widget-action $c "$id" focus
send widget-key $c 1 1; send widget-key $c 2 2; send widget-key $c plus +
send widget-key $c 3 3; send widget-key $c equal =
expect 'name="15"' 5000 || echo "note: keyboard entry path did not produce 15"
shot $c "$out/2-keyboard.png"
echo "bounds before resize: $(window_bounds)"
send resize 420 640; sleep 1
echo "bounds after resize 420x640: $(window_bounds)"
check_min_size
shot $c "$out/3-resized.png"
finish "$out" $c
}
drive_notes() {
local out="$OUT/notes" c=notes-canvas
cd /work/examples/notes && launch_app zig-out/bin/notes || return 1
shot $c "$out/1-initial.png"
# Notes persists its store across launches (app-dirs state), so assert
# the count DELTA rather than an absolute count.
count_before=$(snapshot_lines | grep -oE '[0-9]+ notes' | head -1 | grep -oE '[0-9]+')
click $c button "New note"
expect "$((count_before + 1)) notes" 5000
id=$(widget_id $c textbox "Note editor")
send widget-action $c "$id" focus
set_text $c textbox "Note editor" "Linux-live-truth"
expect 'Linux-live-truth' 5000
shot $c "$out/2-typed.png"
set_text $c textbox "Search notes" "Piranesi"
expect '1 shown' 5000
shot $c "$out/3-search.png"
set_text $c textbox "Search notes" ""
wheel $c button "New folder" 40
send resize 900 600; sleep 1
echo "bounds after resize 900x600: $(window_bounds)"
check_min_size
shot $c "$out/4-resized.png"
finish "$out" $c
}
drive_soundboard() {
local out="$OUT/soundboard" c=soundboard-canvas
cd /work/examples/soundboard && launch_app zig-out/bin/soundboard || return 1
shot $c "$out/1-initial.png"
click $c tab "Songs"
selected=no
for _ in $(seq 1 50); do
snapshot_lines | grep -oE 'role=tab name="Songs"[^|]*' | grep -qE 'state=\[[a-z,]*selected' \
&& { selected=yes; break; }
sleep 0.1
done
[ "$selected" = yes ] && echo "ok: Songs tab selected" || echo "MISS: Songs tab not selected"
shot $c "$out/2-songs.png"
click $c button "Play or pause"
sleep 1
shot $c "$out/3-playing.png"
set_text $c textbox "Search library" "glass"
sleep 1
shot $c "$out/4-search.png"
wheel $c tab "Albums" -40
send resize 1200 800; sleep 1
echo "bounds after resize 1200x800: $(window_bounds)"
check_min_size
finish "$out" $c
}
drive_markdown_viewer() {
local out="$OUT/markdown-viewer" c=viewer-canvas
cd /work/examples/markdown-viewer && launch_app zig-out/bin/markdown-viewer || return 1
shot $c "$out/1-initial.png"
id=$(widget_id $c textbox "Markdown source")
send widget-action $c "$id" focus
send widget-key $c end
# Real typing into the source pane; the preview should keep up.
send widget-key $c z
sleep 1
shot $c "$out/2-typed.png"
# Scroll the preview pane.
wheel $c link "https://github.com" 60
sleep 1
shot $c "$out/3-scrolled.png"
send resize 1400 800; sleep 1
echo "bounds after resize 1400x800: $(window_bounds)"
check_min_size
finish "$out" $c
}
drive_system_monitor() {
local out="$OUT/system-monitor" c=monitor-canvas
cd /work/examples/system-monitor && launch_app zig-out/bin/system-monitor || return 1
sleep 3
shot $c "$out/1-initial.png"
click $c button "Sort by Memory"
sleep 1
shot $c "$out/2-sort-memory.png"
set_text $c textbox "Filter processes" "zig"
sleep 1
shot $c "$out/3-filter.png"
click $c button "Pause or resume sampling"
sleep 1
# Settings opens a second window; the snapshot should grow a @w2.
click $c button "Open settings window"
sleep 2
snapshot_lines | grep -q 'window @w2' && echo "ok: settings window @w2 appeared" \
|| echo "MISS: settings window @w2"
snapshot_lines | grep -o 'window @w2 "[^"]*"' | head -1
xshot "$out/4-two-windows-x11.png"
send resize 1300 800; sleep 1
echo "bounds after resize 1300x800: $(window_bounds)"
check_min_size
finish "$out" $c
}
drive_gpu_dashboard() {
local out="$OUT/gpu-dashboard" c=dashboard-canvas
cd /work/examples/gpu-dashboard && launch_app zig-out/bin/gpu-dashboard || return 1
expect 'gpu_nonblank=true' 15000
shot $c "$out/1-initial.png"
id=$(widget_id $c switch "Auto refresh")
send widget-click $c "$id"
expect 'Auto refresh off.' 10000
set_text $c textbox "Segment search" "native-engine"
sleep 1
id=$(widget_id $c slider "Confidence threshold")
send widget-action $c "$id" increment
sleep 1
shot $c "$out/2-interacted.png"
send resize 1120 700; sleep 1
echo "bounds after resize 1120x700: $(window_bounds)"
snapshot_lines | grep -q 'view @w1/dashboard-canvas kind=gpu_surface.*bounds=(0,0 1120x700)' \
&& echo "ok: canvas relayout 1120x700" || echo "MISS: canvas relayout after resize"
check_min_size
shot $c "$out/3-resized.png"
finish "$out" $c
}
drive_deck() {
local out="$OUT/deck" c=deck-canvas
cd /work/examples/deck && launch_app zig-out/bin/deck || return 1
shot $c "$out/1-initial.png"
click $c button "Play or pause"
sleep 1
shot $c "$out/2-playing.png"
id=$(widget_id $c slider "Volume")
send widget-action $c "$id" decrement
# Playlist opens a second window.
click $c button "Playlist window"
sleep 2
snapshot_lines | grep -q 'window @w2' && echo "ok: playlist window @w2 appeared" \
|| echo "MISS: playlist window @w2"
xshot "$out/3-two-windows-x11.png"
check_min_size
finish "$out" $c
}
drive_feed() {
local out="$OUT/feed" c=feed-canvas
cd /work/examples/feed && launch_app zig-out/bin/feed || return 1
shot $c "$out/1-initial.png"
click $c button "Like post 0"
sleep 0.5
shot $c "$out/2-liked.png"
# Windowed list scroll: wheel down hard, the visible post range must move.
before=$(snapshot_lines | grep -o 'posts [0-9]*[0-9]*' | head -1)
wheel $c button "Like post 3" 400
sleep 1
wheel $c button "Like post 6" 400 2>/dev/null || true
sleep 1
after=$(snapshot_lines | grep -o 'posts [0-9]*[0-9]*' | head -1)
echo "scroll: '$before' -> '$after'"
[ "$before" != "$after" ] && echo "ok: windowed scroll moved" || echo "MISS: scroll did not move visible range"
shot $c "$out/3-scrolled.png"
send resize 700 900; sleep 1
echo "bounds after resize 700x900: $(window_bounds)"
check_min_size
finish "$out" $c
}
drive_kanban() {
local out="$OUT/kanban" c=kanban-canvas
cd /work/examples/kanban && launch_app zig-out/bin/kanban || return 1
shot $c "$out/1-initial.png"
click $c button "Add card"
expect '3 todo' 5000
# Move the first card right.
id=$(snapshot_lines | grep -o 'widget @w1/kanban-canvas#[0-9]* role=button name=">"' | head -1 | grep -o '#[0-9]*' | tr -d '#')
[ -n "$id" ] && send widget-click $c "$id"
expect '2 doing' 5000
shot $c "$out/2-moved.png"
send resize 1100 700; sleep 1
echo "bounds after resize 1100x700: $(window_bounds)"
check_min_size
finish "$out" $c
}
drive_ui_inbox() {
local out="$OUT/ui-inbox" c=inbox-canvas
cd /work/examples/ui-inbox && launch_app zig-out/bin/ui-inbox || return 1
shot $c "$out/1-initial.png"
id=$(snapshot_lines | grep -o 'widget @w1/inbox-canvas#[0-9]* role=textbox name=""' | head -1 | grep -o '#[0-9]*' | tr -d '#')
send widget-action $c "$id" set-text "verify-linux-truth"
click $c button "Add task"
expect '4 open' 5000
# Complete the first task via its checkbox, then filter to done.
id=$(snapshot_lines | grep -o 'widget @w1/inbox-canvas#[0-9]* role=checkbox name=""' | head -1 | grep -o '#[0-9]*' | tr -d '#')
[ -n "$id" ] && send widget-click $c "$id"
expect '1 done' 5000
click $c tab "done"
sleep 0.5
shot $c "$out/2-done-filter.png"
click $c button "Clear done"
expect '0 done' 5000
send resize 900 700; sleep 1
echo "bounds after resize 900x700: $(window_bounds)"
check_min_size
shot $c "$out/3-resized.png"
finish "$out" $c
}
APPS="${APPS:-calculator notes soundboard markdown-viewer system-monitor gpu-dashboard deck feed kanban ui-inbox}"
for app in $APPS; do
echo "==== drive $app ===="
mkdir -p "$OUT/$app"
fn="drive_$(echo "$app" | tr '-' '_')"
"$fn" > >(tee "$OUT/$app/drive.log") 2>&1
done
echo "drive complete"
+124
View File
@@ -0,0 +1,124 @@
#!/usr/bin/env bash
# Shared helpers for driving automation-enabled showcase apps on Linux
# under Xvfb. Sourced by the per-app drive scripts and run-all.sh; runs
# INSIDE the container (see Dockerfile), never on the host.
#
# Transport: the app (built with -Dautomation=true) publishes
# .zig-cache/native-sdk-automation/snapshot.txt and consumes a bounded
# queue of command-<n>.txt entries, oldest first, DELETING each entry as
# its consumption ack. The CLI already waits for its own entry's
# deletion before exiting; wait_done below is the belt-and-braces check
# that the whole queue drained.
CLI=/work/zig-out/bin/native
# :77 avoids colliding with any xvfb-run-owned :99 from ad-hoc runs.
DISPLAY_NUM=:77
AUTOMATION_DIR=.zig-cache/native-sdk-automation
SNAP="$AUTOMATION_DIR/snapshot.txt"
# One shared Xvfb with access control off so xwd can capture the root
# window without the per-run cookie dance xvfb-run would require.
start_xvfb() {
if ! xdpyinfo -display "$DISPLAY_NUM" >/dev/null 2>&1; then
Xvfb "$DISPLAY_NUM" -ac -screen 0 1600x1000x24 >/tmp/xvfb.log 2>&1 &
for _ in $(seq 1 50); do
xdpyinfo -display "$DISPLAY_NUM" >/dev/null 2>&1 && break
sleep 0.1
done
fi
export DISPLAY="$DISPLAY_NUM"
}
APP_PID=""
# launch_app <binary> [ready-timeout-ms] — clear the dropbox, start the
# app on the shared display, wait for the automation snapshot.
launch_app() {
local bin="$1" timeout="${2:-30000}"
# A straggler from an earlier run would keep publishing snapshots into
# its own dropbox and steal the display's focus truth; clear the field.
# Kill by exact process name (comm), never by command-line pattern: a
# pattern like the binary's path also matches the CALLING shell when
# the script text mentions it, and the sweep would kill its own driver.
for app_bin in /work/examples/*/zig-out/bin/*; do
[ -x "$app_bin" ] && pkill -KILL -x "$(basename "$app_bin")" 2>/dev/null
done
rm -rf "$AUTOMATION_DIR"
"$bin" >/tmp/app.log 2>&1 &
APP_PID=$!
"$CLI" automate assert --timeout-ms "$timeout" 'ready=true' >/dev/null || {
echo "LAUNCH FAIL: snapshot never ready"
tail -20 /tmp/app.log
return 1
}
}
stop_app() {
[ -n "$APP_PID" ] && kill "$APP_PID" >/dev/null 2>&1
wait "$APP_PID" 2>/dev/null
APP_PID=""
}
# Pacing: block until the app has consumed every queued command (the
# app deletes each command-<n>.txt entry as it consumes it, so an empty
# queue means everything dispatched).
wait_done() {
for _ in $(seq 1 200); do
if ! ls "$AUTOMATION_DIR"/command-*.txt >/dev/null 2>&1; then return 0; fi
sleep 0.05
done
echo "WARN: command queue not drained within 10s"
return 1
}
# send <automate-subcommand...> — queue one command and wait for consumption.
send() {
"$CLI" automate "$@" >/dev/null || { echo "WARN: send $* failed"; return 1; }
wait_done
}
snapshot_lines() { tr '|' '\n' < "$SNAP"; }
# widget_id <canvas> <role> <name> — resolve a widget id from the snapshot.
# Retries briefly: the app rewrites snapshot.txt on every published frame
# (constantly, while an animation runs), so a single read can catch a
# partially written file.
widget_id() {
local id
for _ in $(seq 1 20); do
id=$(snapshot_lines \
| grep -o "widget @w1/$1#[0-9]* role=$2 name=\"$3\"" \
| head -1 | grep -o '#[0-9]*' | tr -d '#')
[ -n "$id" ] && { echo "$id"; return 0; }
sleep 0.1
done
return 1
}
# click <canvas> <role> <name>
click() {
local id
id=$(widget_id "$1" "$2" "$3")
[ -n "$id" ] || { echo "WARN: widget $2 \"$3\" not found on $1"; return 1; }
send widget-click "$1" "$id"
}
# expect <pattern> [timeout-ms] — assert the snapshot reaches a state.
expect() {
"$CLI" automate assert --timeout-ms "${2:-10000}" "$1" >/dev/null 2>&1 \
&& echo "ok: $1" || { echo "MISS: $1"; return 1; }
}
# shot <canvas-label> <out.png> — engine screenshot (platform-honest pixels).
shot() {
send screenshot "$1" || return 1
local name="$AUTOMATION_DIR/screenshot-$1.png"
for _ in $(seq 1 100); do [ -s "$name" ] && break; sleep 0.05; done
cp "$name" "$2" 2>/dev/null || echo "WARN: engine screenshot $1 missing"
}
# xshot <out.png> — X-root capture (window chrome included).
xshot() {
xwd -display "$DISPLAY_NUM" -root -silent | convert xwd:- "$1" 2>/dev/null \
|| echo "WARN: xwd capture failed"
}
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
# Recon pass: build every showcase app for Linux, launch each under Xvfb,
# and dump its automation snapshot, widget inventory, log tail, and both
# screenshot channels to /out/<app>/. This is the discovery half of the
# loop; drive.sh replays real interaction scenarios on top of it.
set -u
source /src/tools/linux-truth/lib.sh
APPS="${APPS:-calculator notes soundboard markdown-viewer system-monitor gpu-dashboard deck feed kanban ui-inbox}"
OUT=/out
mkdir -p "$OUT"
start_xvfb
# The CLI drives every app build below (most showcase apps are zero-config:
# app.zon + src only, no build.zig — the CLI synthesizes their build graph
# into .native/build). Build it first; automation commands need it anyway.
echo "==== build native CLI ===="
(cd /work && zig build) >/tmp/cli-build.log 2>&1 || {
echo "CLI BUILD FAIL"
tail -30 /tmp/cli-build.log
exit 1
}
for app in $APPS; do
dir="/work/examples/$app"
out="$OUT/$app"
mkdir -p "$out"
echo "==== $app ===="
# -Doptimize=Debug: keep recon binaries at the debug shape (`native
# build` alone would inject ReleaseFast).
(cd "$dir" && "$CLI" build -Dplatform=linux -Dweb-engine=system -Dautomation=true -Doptimize=Debug) \
>"$out/build.log" 2>&1
if [ $? -ne 0 ]; then
echo "BUILD FAIL"
tail -30 "$out/build.log"
echo "build=FAIL" > "$out/status.txt"
continue
fi
echo "build=OK" > "$out/status.txt"
cd "$dir" || continue
bin="zig-out/bin/$app"
[ -x "$bin" ] || bin=$(ls zig-out/bin/* 2>/dev/null | head -1)
if ! launch_app "$bin" 30000; then
echo "launch=FAIL" >> "$out/status.txt"
cp /tmp/app.log "$out/app.log" 2>/dev/null
stop_app
continue
fi
echo "launch=OK" >> "$out/status.txt"
sleep 2
snapshot_lines > "$out/snapshot.txt"
grep -o 'widget @w1/[^#]*#[0-9]* role=[a-z]* name="[^"]*"' "$out/snapshot.txt" > "$out/widgets.txt"
grep -o 'view @w1/[^ ]* kind=[a-z_]*' "$out/snapshot.txt" > "$out/views.txt"
grep -o 'runtime_uptime_ns=[0-9]*' "$out/snapshot.txt" | head -1 >> "$out/status.txt"
grep -o 'dispatch_errors=[0-9]*' "$out/snapshot.txt" | head -1 >> "$out/status.txt"
grep -o 'gpu_backend=[a-z]*' "$out/snapshot.txt" | head -1 >> "$out/status.txt"
grep -o 'gpu_nonblank=[a-z]*' "$out/snapshot.txt" | head -1 >> "$out/status.txt"
canvas=$(sed -n 's|.*view @w1/\([^ ]*\) kind=gpu_surface.*|\1|p' "$out/views.txt" | head -1)
[ -n "$canvas" ] && shot "$canvas" "$out/engine.png"
xshot "$out/x11.png"
cp /tmp/app.log "$out/app.log" 2>/dev/null
stop_app
echo "done"
done
echo "recon complete"
+92
View File
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
# Linux live-truth loop, end to end. Run from the repo root on the host:
#
# tools/linux-truth/run-all.sh [image|up|sync|recon|drive|suites|all]
#
# What it does (container-side, repo mounted read-only at /src, all build
# output in the container-local /work volume):
# image - build the container image (Zig + GTK4 + WebKitGTK dev + Xvfb)
# up - start (or restart) the long-lived container and sync sources
# sync - rsync /src -> /work (run after local edits)
# recon - build every showcase app for Linux, launch under Xvfb, dump
# snapshot/widgets/views + engine and X screenshots to /out
# drive - per-app interaction scenarios (clicks, text input, wheel
# scrolling, resize incl. min-size probe) with screenshots
# suites - engine suite, example suites, and the webview link check,
# all on real Linux
# all - everything above, in order
#
# Artifacts land in the container's /out; copy them out with
# docker cp native-sdk-linux-truth:/out <dest>
set -eu
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd "$here/../.." && pwd)"
image=native-sdk-linux-truth
container=native-sdk-linux-truth
step="${1:-all}"
build_image() {
docker build -t "$image" "$here"
}
up() {
docker rm -f "$container" >/dev/null 2>&1 || true
docker volume create linux-truth-work >/dev/null
docker run -d --name "$container" \
-v "$repo_root":/src:ro \
-v linux-truth-work:/work \
"$image" sleep infinity >/dev/null
docker exec "$container" mkdir -p /out
sync_sources
}
sync_sources() {
# A verdict from this loop is only as honest as the tree it ran on.
# The long-lived container binds /src at CREATE time, so a container
# started from one checkout (say, an agent worktree) keeps testing
# THAT tree forever — a later invocation from a different checkout
# would sync and pass against the wrong sources while looking green.
# Refuse loudly instead of reporting another tree's truth.
local mounted
mounted="$(docker inspect "$container" --format '{{range .Mounts}}{{if eq .Destination "/src"}}{{.Source}}{{end}}{{end}}' 2>/dev/null || true)"
if [ -z "$mounted" ]; then
echo "linux-truth: container '$container' is not running; start it with '$0 up'" >&2
exit 1
fi
if [ "$mounted" != "$repo_root" ]; then
echo "linux-truth: container '$container' mounts /src from" >&2
echo " $mounted" >&2
echo "but this invocation is from" >&2
echo " $repo_root" >&2
echo "so its results would describe a different tree. Recreate it with '$0 up'." >&2
exit 1
fi
docker exec "$container" bash /src/tools/linux-truth/sync.sh
}
recon() {
docker exec "$container" bash /src/tools/linux-truth/recon.sh
}
drive() {
docker exec "$container" bash /src/tools/linux-truth/drive.sh
}
suites() {
docker exec -w /work "$container" zig build test
docker exec -w /work "$container" zig build validate
docker exec -w /work "$container" zig build test-webview-system-link -Dplatform=linux
docker exec -w /work "$container" zig build test-examples-native
}
case "$step" in
image) build_image ;;
up) up ;;
sync) sync_sources ;;
recon) sync_sources; recon ;;
drive) sync_sources; drive ;;
suites) sync_sources; suites ;;
all) build_image; up; recon; drive; suites ;;
*) echo "usage: $0 [image|up|sync|recon|drive|suites|all]" >&2; exit 2 ;;
esac
+40
View File
@@ -0,0 +1,40 @@
/* send-wm-delete <window-id>: deliver a WM_DELETE_WINDOW client message
* to an X11 window — the graceful close request a window manager would
* send. Bare Xvfb has no window manager, and XDestroyWindow-style tools
* kill the window out from under the toolkit (which aborts); this is the
* honest way to exercise an app's own close path headlessly.
*
* Build: zig cc -o send-wm-delete send-wm-delete.c -lX11
*/
#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv) {
if (argc != 2) {
fprintf(stderr, "usage: send-wm-delete <window-id>\n");
return 2;
}
Display *dpy = XOpenDisplay(NULL);
if (!dpy) {
fprintf(stderr, "cannot open display\n");
return 1;
}
Window win = (Window)strtoul(argv[1], NULL, 0);
XEvent ev = {0};
ev.xclient.type = ClientMessage;
ev.xclient.window = win;
ev.xclient.message_type = XInternAtom(dpy, "WM_PROTOCOLS", False);
ev.xclient.format = 32;
ev.xclient.data.l[0] = (long)XInternAtom(dpy, "WM_DELETE_WINDOW", False);
ev.xclient.data.l[1] = CurrentTime;
if (!XSendEvent(dpy, win, False, NoEventMask, &ev)) {
fprintf(stderr, "XSendEvent failed\n");
XCloseDisplay(dpy);
return 1;
}
XFlush(dpy);
XCloseDisplay(dpy);
return 0;
}
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env bash
# Sync the read-only source mount (/src) into the container-local build tree
# (/work). Build artifacts and caches are excluded so a re-sync never clobbers
# container-local state, and nothing here ever writes back to /src. `.native`
# stays container-local too: the CLI regenerates each zero-config app's build
# graph in /work/examples/<app>/.native/build and its zig cache lives inside.
set -eu
rsync -a --delete \
--exclude '.git' \
--exclude '.zig-cache' \
--exclude '.native' \
--exclude 'zig-out' \
--exclude 'node_modules' \
--exclude 'evals/' \
--exclude '.claude/' \
/src/ /work/
echo "synced /src -> /work"