Native SDK: the complete toolkit for building native desktop apps (#67)
zero-native becomes the Native SDK. Apps are authored as native markup plus Zig on a deterministic runtime and rendered by the toolkit's own engine into real OS windows — no browser, no WebView, no interpreter in the binary. - Desktop is complete on macOS, Windows, and Linux: native rendering with per-platform titlebar fidelity, audio playback with streaming, a verified track cache, and real spectrum analysis, native context menus, packaging with sealed code signing, and a deterministic automation and record-replay story. - Experimental iOS and Android host tiers ship behind the same app manifest: simulator and emulator dev loops, archive-ready packaging, real platform tab bars and push navigation, with embedding over the C ABI underneath. - The docs site, component catalog, theme packs, showcase apps, and CHANGELOG carry the full account.
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 79 KiB |
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env bash
|
||||
# Linux canvas smoke under Xvfb.
|
||||
#
|
||||
# Exercises the Linux gpu_surface software path against system WebKitGTK
|
||||
# without a display server: builds examples/ui-inbox with -Dplatform=linux
|
||||
# -Dweb-engine=system -Dautomation=true, runs it under Xvfb, and asserts
|
||||
# against the automation snapshot:
|
||||
#
|
||||
# 1. snapshot ready=true (app booted, automation server live)
|
||||
# 2. gpu_backend=software (the software present path is active)
|
||||
# 3. gpu_nonblank=true (real pixels were presented)
|
||||
# 4. widget-click "Add task" -> '4 open' (automation input mutates state)
|
||||
# 5. automate screenshot renders a non-empty PNG
|
||||
#
|
||||
# Deliberately NOT `set -e` (same as windows-canvas-smoke.sh): grep exits 1
|
||||
# on zero matches, and under `set -e` an assignment like `x=$(grep ...)` or
|
||||
# a swallowed `$(cli 2>&1)` capture dies with NO output — this job failed
|
||||
# three times with nothing in the log but the exit code. Every assertion
|
||||
# goes through fail(), which dumps the snapshot and the app log.
|
||||
set -u
|
||||
|
||||
# WebKitGTK's bubblewrap sandbox needs unprivileged user namespaces, which
|
||||
# ubuntu-24.04 runners restrict via AppArmor; without this the web process
|
||||
# dies launching xdg-dbus-proxy and the app never publishes an automation
|
||||
# snapshot (reproduced in a local container: ready=true never lands; with
|
||||
# the sandbox disabled the full smoke passes). The sandbox is not what
|
||||
# this smoke tests.
|
||||
export WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS="${WEBKIT_DISABLE_SANDBOX_THIS_IS_DANGEROUS:-1}"
|
||||
|
||||
# GTK_A11Y=none: under Xvfb there is no session bus providing org.a11y.Bus,
|
||||
# and GTK4's a11y init blocks ~25 s on the GDBus name lookup before warning
|
||||
# and continuing — the app's first runtime event landed after the readiness
|
||||
# window had already expired (reproduced in a local container: without this
|
||||
# the wait times out at startup; with it the full smoke passes).
|
||||
# Accessibility is not what this smoke tests.
|
||||
export GTK_A11Y="${GTK_A11Y:-none}"
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
app_dir="$repo_root/examples/ui-inbox"
|
||||
snap="$app_dir/.zig-cache/native-sdk-automation/snapshot.txt"
|
||||
cli="$repo_root/zig-out/bin/native"
|
||||
app_log="${TMPDIR:-/tmp}/linux-canvas-smoke-app.log"
|
||||
|
||||
# Readiness budget. Even with GTK_A11Y=none, shared ubuntu-24.04 runners
|
||||
# show a consistent ~27 s stall between EGL init and the app's first
|
||||
# runtime event (measured in runs 28690855597 pass / 28691951139 fail —
|
||||
# the SAME stall in both; the old hard 30 s `automate wait` window flipped
|
||||
# green/red on one or two seconds of runner noise). Local containers show
|
||||
# no stall at all. Widen the budget here instead of weakening the CLI
|
||||
# default; every correctness assertion stays strict.
|
||||
ready_timeout_ms=90000
|
||||
|
||||
app_pid=""
|
||||
cleanup() {
|
||||
[ -n "$app_pid" ] && kill "$app_pid" >/dev/null 2>&1
|
||||
# xvfb-run does not forward signals to an already-detached app; reap the
|
||||
# app and its Xvfb directly so local runs exit clean (CI would otherwise
|
||||
# rely on the runner's orphan sweep).
|
||||
pkill -f "$app_dir/zig-out/bin/ui-inbox" >/dev/null 2>&1
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
diagnostics() {
|
||||
echo "---- diagnostics ----"
|
||||
echo "-- snapshot ($snap):"
|
||||
if [ -f "$snap" ]; then tr '|' '\n' < "$snap" | sed 's/^/ /'; else echo " (missing)"; fi
|
||||
echo "-- app log head ($app_log):"
|
||||
head -20 "$app_log" 2>/dev/null | sed 's/^/ /'
|
||||
echo "-- app log tail ($app_log):"
|
||||
tail -40 "$app_log" 2>/dev/null | sed 's/^/ /'
|
||||
echo "---------------------"
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $1"
|
||||
diagnostics
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ---- build ----------------------------------------------------------------
|
||||
(cd "$repo_root" && zig build) || fail "root zig build (CLI) failed"
|
||||
(cd "$app_dir" && zig build -Dplatform=linux -Dweb-engine=system -Dautomation=true) \
|
||||
|| fail "ui-inbox Linux build failed"
|
||||
|
||||
# ---- launch ---------------------------------------------------------------
|
||||
cd "$app_dir" || fail "missing $app_dir"
|
||||
rm -rf .zig-cache/native-sdk-automation
|
||||
xvfb-run -a "$app_dir/zig-out/bin/ui-inbox" > "$app_log" 2>&1 &
|
||||
app_pid=$!
|
||||
|
||||
# ---- 1: automation snapshot becomes ready ---------------------------------
|
||||
# `automate assert` self-reports on timeout (missing patterns + snapshot
|
||||
# tail) and prints the measured latency on success, so green logs carry
|
||||
# the readiness margin.
|
||||
"$cli" automate assert --timeout-ms "$ready_timeout_ms" 'ready=true' \
|
||||
|| fail "snapshot never became ready"
|
||||
|
||||
# ---- 2 + 3: software backend presented non-blank pixels --------------------
|
||||
"$cli" automate assert --timeout-ms 30000 'gpu_nonblank=true' \
|
||||
|| fail "gpu_nonblank never became true"
|
||||
grep -q 'gpu_backend=software' "$snap" || fail "gpu_backend is not software"
|
||||
echo "== canvas: $(grep -o 'gpu_backend=[a-z]*' "$snap" | head -1)" \
|
||||
"$(grep -o 'gpu_nonblank=[a-z]*' "$snap" | head -1)"
|
||||
|
||||
# ---- 4: automation widget-click mutates the model --------------------------
|
||||
echo "== open before click: $(grep -oE '[0-9]+ open' "$snap" | head -1)"
|
||||
add_id=$(grep -o 'widget @w1/inbox-canvas#[0-9]* role=button name="Add task"' "$snap" \
|
||||
| grep -o '#[0-9]*' | tr -d '#')
|
||||
[ -n "$add_id" ] || fail "Add task button not found in snapshot"
|
||||
"$cli" automate widget-click inbox-canvas "$add_id" || fail "CLI widget-click failed"
|
||||
"$cli" automate assert --timeout-ms 30000 '4 open' \
|
||||
|| fail "widget-click did not reach '4 open'"
|
||||
echo "== open after click: $(grep -oE '[0-9]+ open' "$snap" | head -1)"
|
||||
|
||||
# ---- 5: screenshot renders a non-empty PNG ---------------------------------
|
||||
"$cli" automate screenshot inbox-canvas || fail "CLI screenshot failed"
|
||||
test -s .zig-cache/native-sdk-automation/screenshot-inbox-canvas.png \
|
||||
|| fail "screenshot PNG missing or empty"
|
||||
|
||||
echo "PASS: linux canvas smoke"
|
||||
exit 0
|
||||
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env bash
|
||||
# Windows canvas smoke under Wine.
|
||||
#
|
||||
# Exercises the Windows gpu_surface software path (src/platform/windows/
|
||||
# webview2_host.cpp: child HWND, WM_TIMER frame events, SetDIBitsToDevice
|
||||
# blits) without Windows hardware: cross-compiles examples/ui-inbox for
|
||||
# x86_64-windows-gnu, runs the .exe under Xvfb + Wine, and asserts against
|
||||
# the automation snapshot:
|
||||
#
|
||||
# 1. snapshot ready=true (app booted, automation server live)
|
||||
# 2. gpu_backend=software (the SetDIBitsToDevice path is active)
|
||||
# 3. gpu_nonblank=true (real pixels were presented)
|
||||
# 4. widget-click "Add task" -> '4 open' (automation input mutates state)
|
||||
# 5. real X11 click + typing lands in the draft textbox (XTEST -> Wine ->
|
||||
# WM_LBUTTONDOWN/WM_CHAR -> runtime)
|
||||
#
|
||||
# Step 5 (xdotool) is deliberately included: it is the only coverage of the
|
||||
# Win32 pointer/keyboard input mapping in webview2_host.cpp. It is also the
|
||||
# flakiest step (window lookup, focus without a window manager), so every
|
||||
# failure path dumps the X window list, the snapshot, and the app log.
|
||||
#
|
||||
# Deliberately NOT `set -e`: grep exits 1 on zero matches inside the poll
|
||||
# loops, and we want explicit, diagnosable failures instead of silent early
|
||||
# exits. Every assertion goes through fail(), which dumps diagnostics.
|
||||
set -u
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
app_dir="$repo_root/examples/ui-inbox"
|
||||
snap="$app_dir/.zig-cache/native-sdk-automation/snapshot.txt"
|
||||
cli="$repo_root/zig-out/bin/native"
|
||||
app_log="${TMPDIR:-/tmp}/windows-canvas-smoke-app.log"
|
||||
|
||||
# Wine needs an X display; when none is present (CI), re-exec the whole
|
||||
# script under a private Xvfb server so the app and xdotool share it. The
|
||||
# explicit screen size beats xvfb-run's 640x480x8 default: the app window is
|
||||
# 720x520 and Wine wants a 24-bit visual.
|
||||
if [ -z "${DISPLAY:-}" ]; then
|
||||
exec xvfb-run -a --server-args="-screen 0 1280x800x24" "$0" "$@"
|
||||
fi
|
||||
|
||||
export WINEPREFIX="${WINEPREFIX:-$repo_root/.zig-cache/wineprefix}"
|
||||
export WINEDEBUG="${WINEDEBUG:--all}"
|
||||
|
||||
app_pid=""
|
||||
cleanup() {
|
||||
[ -n "$app_pid" ] && kill "$app_pid" >/dev/null 2>&1
|
||||
wineserver -k >/dev/null 2>&1
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
diagnostics() {
|
||||
echo "---- diagnostics ----"
|
||||
echo "-- X windows:"
|
||||
xdotool search --name "." 2>/dev/null | while read -r w; do
|
||||
echo " $w: $(xdotool getwindowname "$w" 2>/dev/null)"
|
||||
done
|
||||
echo "-- snapshot ($snap):"
|
||||
if [ -f "$snap" ]; then tr '|' '\n' < "$snap" | sed 's/^/ /'; else echo " (missing)"; fi
|
||||
echo "-- app log tail ($app_log):"
|
||||
tail -40 "$app_log" 2>/dev/null | sed 's/^/ /'
|
||||
echo "---------------------"
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $1"
|
||||
diagnostics
|
||||
exit 1
|
||||
}
|
||||
|
||||
# poll <seconds> <pattern>: wait until $snap contains <pattern>.
|
||||
poll() {
|
||||
local deadline=$((SECONDS + $1))
|
||||
while [ "$SECONDS" -lt "$deadline" ]; do
|
||||
[ -f "$snap" ] && grep -q "$2" "$snap" && return 0
|
||||
sleep 0.5
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---- build ----------------------------------------------------------------
|
||||
(cd "$repo_root" && zig build) || fail "root zig build (CLI) failed"
|
||||
(cd "$app_dir" && zig build -Dtarget=x86_64-windows-gnu -Dplatform=windows -Dweb-engine=system -Dautomation=true) \
|
||||
|| fail "ui-inbox Windows cross-compile failed"
|
||||
|
||||
# ---- wineprefix -----------------------------------------------------------
|
||||
# First run initializes the prefix (measured ~10-30s on CI-class machines);
|
||||
# subsequent runs are instant, so no cache step is needed.
|
||||
start=$SECONDS
|
||||
wineboot --init >/dev/null 2>&1
|
||||
wineserver --wait >/dev/null 2>&1
|
||||
echo "== wineprefix ready in $((SECONDS - start))s ($WINEPREFIX)"
|
||||
|
||||
# ---- launch ---------------------------------------------------------------
|
||||
cd "$app_dir" || fail "missing $app_dir"
|
||||
rm -rf .zig-cache/native-sdk-automation
|
||||
mkdir -p .zig-cache/native-sdk-automation
|
||||
wine zig-out/bin/ui-inbox.exe > "$app_log" 2>&1 &
|
||||
app_pid=$!
|
||||
|
||||
# ---- 1: automation snapshot becomes ready ---------------------------------
|
||||
poll 180 'ready=true' || fail "snapshot never became ready"
|
||||
echo "== ready: $(head -1 "$snap" | cut -d'|' -f1)"
|
||||
|
||||
# ---- 2 + 3: software backend presented non-blank pixels --------------------
|
||||
poll 60 'gpu_nonblank=true' || fail "gpu_nonblank never became true"
|
||||
grep -q 'gpu_backend=software' "$snap" || fail "gpu_backend is not software"
|
||||
echo "== canvas: $(grep -o 'gpu_backend=[a-z]*' "$snap" | head -1)" \
|
||||
"$(grep -o 'gpu_nonblank=[a-z]*' "$snap" | head -1)" \
|
||||
"$(grep -o 'gpu_sample=0x[0-9a-f]*' "$snap" | head -1)" \
|
||||
"$(grep -o 'gpu_present_mode=[a-z]*' "$snap" | head -1)"
|
||||
|
||||
# ---- 4: automation widget-click mutates the model --------------------------
|
||||
echo "== open before click: $(grep -oE '[0-9]+ open' "$snap" | head -1)"
|
||||
add_id=$(grep -o 'widget @w1/inbox-canvas#[0-9]* role=button name="Add task"' "$snap" \
|
||||
| grep -o '#[0-9]*' | tr -d '#')
|
||||
[ -n "$add_id" ] || fail "Add task button not found in snapshot"
|
||||
"$cli" automate widget-click inbox-canvas "$add_id" || fail "CLI widget-click failed"
|
||||
poll 30 '4 open' || fail "widget-click did not reach '4 open'"
|
||||
echo "== open after click: $(grep -oE '[0-9]+ open' "$snap" | head -1)"
|
||||
|
||||
# ---- 5: real X11 input through the Win32 path ------------------------------
|
||||
# Root-coordinate math. Hidden-titlebar windows keep the full overlapped
|
||||
# frame and reclaim the caption band through WM_NCCALCSIZE, so the Win32
|
||||
# client area starts at the very top of the window. Under a WM-less Wine
|
||||
# the X11 driver still places (and reports) the client X window at the
|
||||
# DEFAULT frame offset - one caption band lower - so the X origin sits a
|
||||
# band BELOW where Win32 client coordinates actually map, and the reported
|
||||
# X height is short by exactly that band (measured: X window 718x489 at
|
||||
# y=30 for a 718x519 client whose clicks land at y=0). The snapshot knows
|
||||
# the true client height, so the height shortfall IS the y correction; a
|
||||
# standard-frame window reports matching heights and corrects by zero.
|
||||
win=""
|
||||
for w in $(xdotool search --name "." 2>/dev/null); do
|
||||
case "$(xdotool getwindowname "$w" 2>/dev/null)" in
|
||||
*[Ii]nbox*) win="$w" ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$win" ] || fail "app X window not found"
|
||||
eval "$(xdotool getwindowgeometry --shell "$win")"
|
||||
client_h=$(grep -o 'window @w1 "[^"]*" bounds=([^)]*)' "$snap" | head -1 \
|
||||
| sed -n 's/.*x\([0-9]*\)[^x]*$/\1/p')
|
||||
[ -n "$client_h" ] || client_h=$HEIGHT
|
||||
y_off=$((client_h - HEIGHT))
|
||||
[ "$y_off" -ge 0 ] 2>/dev/null || y_off=0
|
||||
echo "== x window $win: pos=($X,$Y) size=${WIDTH}x${HEIGHT} client_h=$client_h y_off=$y_off"
|
||||
xdotool windowactivate "$win" >/dev/null 2>&1 || xdotool windowfocus "$win" >/dev/null 2>&1
|
||||
|
||||
draft_line=$(grep -o 'widget @w1/inbox-canvas#[0-9]* role=textbox[^|]*' "$snap" | head -1)
|
||||
[ -n "$draft_line" ] || fail "draft textbox not found in snapshot"
|
||||
bounds=$(echo "$draft_line" | grep -o 'bounds=([^)]*)')
|
||||
bx=$(echo "$bounds" | sed -n 's/bounds=(\([0-9.]*\),.*/\1/p')
|
||||
by=$(echo "$bounds" | sed -n 's/bounds=([0-9.]*,\([0-9.]*\) .*/\1/p')
|
||||
bw=$(echo "$bounds" | sed -n 's/.* \([0-9.]*\)x[0-9.]*).*/\1/p')
|
||||
bh=$(echo "$bounds" | sed -n 's/.* [0-9.]*x\([0-9.]*\)).*/\1/p')
|
||||
[ -n "$bx" ] && [ -n "$by" ] && [ -n "$bw" ] && [ -n "$bh" ] || fail "could not parse draft bounds: $draft_line"
|
||||
cx=$(awk "BEGIN{printf \"%d\", $X + $bx + $bw / 2}")
|
||||
cy=$(awk "BEGIN{printf \"%d\", $Y - $y_off + $by + $bh / 2}")
|
||||
echo "== clicking draft field $bounds at ($cx,$cy)"
|
||||
xdotool mousemove "$cx" "$cy" click 1
|
||||
# The click must move widget focus into the textbox before any keys are
|
||||
# sent: spaces in the typed string would otherwise activate whatever
|
||||
# widget held focus (a button press adds a task and the real failure -
|
||||
# input landing in the wrong widget - would read as missing text).
|
||||
poll 10 'role=textbox[^|]*focused=true' || fail "draft textbox did not take focus from the click"
|
||||
sleep 1
|
||||
xdotool type --delay 120 "hi from wine"
|
||||
poll 30 'hi from wine' || fail "typed text never appeared in the snapshot"
|
||||
echo "== draft after typing: $(grep -o 'widget @w1/inbox-canvas#[0-9]* role=textbox[^|]*' "$snap" | head -1 | cut -c1-160)"
|
||||
echo "== input latency: $(grep -o 'gpu_input_latency_ns=[0-9]*' "$snap" | head -1)"
|
||||
|
||||
echo "PASS: windows canvas smoke"
|
||||
exit 0
|
||||
@@ -0,0 +1,165 @@
|
||||
#!/usr/bin/env bash
|
||||
# Windows effects smoke under Wine.
|
||||
#
|
||||
# Exercises the effect system's live Windows path (src/runtime/effects.zig
|
||||
# worker threads -> PostMessageW wake in src/platform/windows/
|
||||
# webview2_host.cpp -> loop-thread drain) without Windows hardware:
|
||||
# cross-compiles examples/effects-probe for x86_64-windows-gnu, runs the
|
||||
# .exe under Xvfb + Wine, and asserts against the automation snapshot and
|
||||
# the app's trace log:
|
||||
#
|
||||
# 1. snapshot ready=true (app booted, automation server live)
|
||||
# 2. gpu_backend=software + nonblank (the canvas presented real pixels)
|
||||
# 3. widget-click "Start stream" (fx.spawn launches cmd.exe under
|
||||
# Wine; streamed lines land in the
|
||||
# model and grow the snapshot)
|
||||
# 4. app log shows event=effects_wake (the worker's PostMessageW wake was
|
||||
# marshalled through the message
|
||||
# loop -- the wake path itself, not
|
||||
# just the frame-tick drain)
|
||||
# 5. widget-click "Cancel" (fx.cancel terminates the child;
|
||||
# status shows "cancelled")
|
||||
# 6. the line count freezes (no lines arrive after cancel,
|
||||
# sampled across ~5 more would-be
|
||||
# line intervals)
|
||||
#
|
||||
# Known caveat: the timer present mode also drains effect completions on
|
||||
# every frame tick (ui_app.zig handleFrame), so line delivery alone cannot
|
||||
# isolate the wake; that is why step 4 checks the trace log for the wake
|
||||
# events directly instead of inferring the wake from model updates.
|
||||
#
|
||||
# Deliberately NOT `set -e`: grep exits 1 on zero matches inside the poll
|
||||
# loops, and we want explicit, diagnosable failures instead of silent early
|
||||
# exits. Every assertion goes through fail(), which dumps diagnostics.
|
||||
set -u
|
||||
|
||||
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
app_dir="$repo_root/examples/effects-probe"
|
||||
snap="$app_dir/.zig-cache/native-sdk-automation/snapshot.txt"
|
||||
cli="$repo_root/zig-out/bin/native"
|
||||
app_log="${TMPDIR:-/tmp}/windows-effects-smoke-app.log"
|
||||
|
||||
# Wine needs an X display; when none is present (CI), re-exec the whole
|
||||
# script under a private Xvfb server. The explicit screen size beats
|
||||
# xvfb-run's 640x480x8 default: the app window is 560x480 and Wine wants a
|
||||
# 24-bit visual.
|
||||
if [ -z "${DISPLAY:-}" ]; then
|
||||
exec xvfb-run -a --server-args="-screen 0 1280x800x24" "$0" "$@"
|
||||
fi
|
||||
|
||||
export WINEPREFIX="${WINEPREFIX:-$repo_root/.zig-cache/wineprefix}"
|
||||
export WINEDEBUG="${WINEDEBUG:--all}"
|
||||
|
||||
app_pid=""
|
||||
cleanup() {
|
||||
[ -n "$app_pid" ] && kill "$app_pid" >/dev/null 2>&1
|
||||
wineserver -k >/dev/null 2>&1
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
diagnostics() {
|
||||
echo "---- diagnostics ----"
|
||||
echo "-- snapshot ($snap):"
|
||||
if [ -f "$snap" ]; then tr '|' '\n' < "$snap" | sed 's/^/ /'; else echo " (missing)"; fi
|
||||
echo "-- app log tail ($app_log):"
|
||||
tail -40 "$app_log" 2>/dev/null | sed 's/^/ /'
|
||||
echo "---------------------"
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "FAIL: $1"
|
||||
diagnostics
|
||||
exit 1
|
||||
}
|
||||
|
||||
# poll <seconds> <pattern>: wait until $snap contains <pattern>.
|
||||
poll() {
|
||||
local deadline=$((SECONDS + $1))
|
||||
while [ "$SECONDS" -lt "$deadline" ]; do
|
||||
[ -f "$snap" ] && grep -q "$2" "$snap" && return 0
|
||||
sleep 0.5
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# The status bar renders "{N} lines total · {M} dropped"; extract N.
|
||||
total_lines() {
|
||||
grep -o '[0-9]* lines total' "$snap" 2>/dev/null | head -1 | grep -o '^[0-9]*'
|
||||
}
|
||||
|
||||
# widget_id <name>: find a widget id in the snapshot by accessible name.
|
||||
widget_id() {
|
||||
grep -o "widget @w1/probe-canvas#[0-9]* role=button name=\"$1\"" "$snap" \
|
||||
| grep -o '#[0-9]*' | tr -d '#'
|
||||
}
|
||||
|
||||
# ---- build ----------------------------------------------------------------
|
||||
(cd "$repo_root" && zig build) || fail "root zig build (CLI) failed"
|
||||
# effects-probe is a zero-config app (app.zon + src, no build.zig): the CLI
|
||||
# synthesizes its build graph. -Doptimize=Debug keeps the smoke binary at
|
||||
# the debug shape (`native build` alone would inject ReleaseFast).
|
||||
"$cli" build "$app_dir" -Dtarget=x86_64-windows-gnu -Dplatform=windows -Dweb-engine=system -Dautomation=true -Doptimize=Debug \
|
||||
|| fail "effects-probe Windows cross-compile failed"
|
||||
|
||||
# ---- wineprefix -----------------------------------------------------------
|
||||
start=$SECONDS
|
||||
wineboot --init >/dev/null 2>&1
|
||||
wineserver --wait >/dev/null 2>&1
|
||||
echo "== wineprefix ready in $((SECONDS - start))s ($WINEPREFIX)"
|
||||
|
||||
# ---- launch ---------------------------------------------------------------
|
||||
cd "$app_dir" || fail "missing $app_dir"
|
||||
rm -rf .zig-cache/native-sdk-automation
|
||||
mkdir -p .zig-cache/native-sdk-automation
|
||||
wine zig-out/bin/effects-probe.exe > "$app_log" 2>&1 &
|
||||
app_pid=$!
|
||||
|
||||
# ---- 1: automation snapshot becomes ready ---------------------------------
|
||||
poll 180 'ready=true' || fail "snapshot never became ready"
|
||||
echo "== ready: $(head -1 "$snap" | cut -d'|' -f1)"
|
||||
|
||||
# ---- 2: software backend presented non-blank pixels ------------------------
|
||||
poll 60 'gpu_nonblank=true' || fail "gpu_nonblank never became true"
|
||||
grep -q 'gpu_backend=software' "$snap" || fail "gpu_backend is not software"
|
||||
echo "== canvas: $(grep -o 'gpu_backend=[a-z]*' "$snap" | head -1)" \
|
||||
"$(grep -o 'gpu_nonblank=[a-z]*' "$snap" | head -1)"
|
||||
grep -q 'idle' "$snap" || fail "probe did not start idle"
|
||||
|
||||
# ---- 3: Start spawns the stream and lines arrive ---------------------------
|
||||
start_id=$(widget_id "Start stream")
|
||||
[ -n "$start_id" ] || fail "Start stream button not found in snapshot"
|
||||
"$cli" automate widget-click probe-canvas "$start_id" || fail "CLI widget-click Start failed"
|
||||
poll 30 'streaming:' || fail "status never showed streaming (spawn failed under Wine?)"
|
||||
# The Windows stream paces ~1 line/s (cmd for /L + ping); wait for at
|
||||
# least 2 visible lines so cancel provably interrupts an active stream.
|
||||
poll 60 'stream line 2' || fail "stream lines never reached the model"
|
||||
echo "== streaming: $(grep -o 'streaming: [0-9]* lines' "$snap" | head -1), status-bar: $(total_lines) lines total"
|
||||
|
||||
# ---- 4: the PostMessage wake path fired ------------------------------------
|
||||
# handleFrame also drains completions on every frame tick, so lines in the
|
||||
# model alone cannot isolate the wake. The runner's default -Dtrace=events
|
||||
# sink prints every runtime event; effects_wake records prove the worker's
|
||||
# PostMessageW -> kWakeMessage -> kWake -> .effects_wake marshalling ran.
|
||||
grep -q 'event="effects_wake"' "$app_log" || fail "no effects_wake events in the app log (PostMessage wake never fired)"
|
||||
echo "== effects_wake events so far: $(grep -c 'event="effects_wake"' "$app_log")"
|
||||
|
||||
# ---- 5: Cancel terminates the child ----------------------------------------
|
||||
cancel_id=$(widget_id "Cancel")
|
||||
[ -n "$cancel_id" ] || fail "Cancel button not found in snapshot"
|
||||
"$cli" automate widget-click probe-canvas "$cancel_id" || fail "CLI widget-click Cancel failed"
|
||||
poll 30 'cancelled: code' || fail "status never showed cancelled"
|
||||
frozen=$(total_lines)
|
||||
[ -n "$frozen" ] || fail "could not read line count after cancel"
|
||||
echo "== cancelled at $frozen lines: $(grep -o 'cancelled: code [0-9-]* after [0-9]* lines' "$snap" | head -1)"
|
||||
|
||||
# ---- 6: the line count is frozen -------------------------------------------
|
||||
# ~5 more lines would have arrived at the ~1s cadence if the child were
|
||||
# still alive or queued lines were still draining.
|
||||
sleep 6
|
||||
after=$(total_lines)
|
||||
[ "$after" = "$frozen" ] || fail "line count moved after cancel ($frozen -> $after)"
|
||||
grep -q 'streaming:' "$snap" && fail "status went back to streaming after cancel"
|
||||
echo "== count frozen at $after lines across 6s"
|
||||
|
||||
echo "PASS: windows effects smoke"
|
||||
exit 0
|
||||
@@ -96,13 +96,13 @@ jobs:
|
||||
esac
|
||||
find "$CEF_ROOT/build/libcef_dll_wrapper" -name "$wrapper" -print -quit | xargs -I{} cp "{}" "$CEF_ROOT/libcef_dll_wrapper/$wrapper"
|
||||
|
||||
- name: Prepare zero-native runtime archive
|
||||
- name: Prepare native-sdk runtime archive
|
||||
if: ${{ inputs.source == 'official' }}
|
||||
shell: bash
|
||||
run: |
|
||||
zig build
|
||||
cli="zig-out/bin/zero-native"
|
||||
if [[ "${{ runner.os }}" == "Windows" ]]; then cli="zig-out/bin/zero-native.exe"; fi
|
||||
cli="zig-out/bin/native"
|
||||
if [[ "${{ runner.os }}" == "Windows" ]]; then cli="zig-out/bin/native.exe"; fi
|
||||
"$cli" cef prepare-release --dir "$CEF_ROOT" --output zig-out/cef --version "${{ inputs.cef_version }}"
|
||||
|
||||
- name: Build CEF from source and prepare runtime
|
||||
@@ -116,7 +116,7 @@ jobs:
|
||||
--platform "${{ matrix.platform }}"
|
||||
--version "${{ inputs.cef_version }}"
|
||||
--output zig-out/cef
|
||||
--zero-native-bin zig-out/bin/zero-native
|
||||
--native-sdk-bin zig-out/bin/native
|
||||
)
|
||||
if [ -n "${{ inputs.cef_branch }}" ]; then
|
||||
args+=(--cef-branch "${{ inputs.cef_branch }}")
|
||||
@@ -129,5 +129,5 @@ jobs:
|
||||
tag_name: cef-${{ inputs.cef_version }}
|
||||
name: CEF ${{ inputs.cef_version }}
|
||||
files: |
|
||||
zig-out/cef/zero-native-cef-${{ inputs.cef_version }}-${{ matrix.platform }}.tar.gz
|
||||
zig-out/cef/zero-native-cef-${{ inputs.cef_version }}-${{ matrix.platform }}.tar.gz.sha256
|
||||
zig-out/cef/native-sdk-cef-${{ inputs.cef_version }}-${{ matrix.platform }}.tar.gz
|
||||
zig-out/cef/native-sdk-cef-${{ inputs.cef_version }}-${{ matrix.platform }}.tar.gz.sha256
|
||||
|
||||
@@ -31,6 +31,44 @@ jobs:
|
||||
version: 0.16.0
|
||||
- run: zig build test-webview-system-link
|
||||
- run: zig build test-webview-smoke
|
||||
# Signed-package seal pin: an ad-hoc signed package must pass
|
||||
# codesign --verify --strict (macOS runners are the only tier with
|
||||
# codesign; the step skips loudly anywhere else).
|
||||
- run: zig build test-package-signing
|
||||
# Shared macos-14 runners are far noisier than a dev box (the second
|
||||
# CI run measured a 576 ms automation-ready against the 500 ms local
|
||||
# ceiling), so widen the smoke budgets here instead of weakening the
|
||||
# local defaults. NATIVE_SDK_SMOKE_BUDGET_MS raises the first-frame latency
|
||||
# budget and the automation-ready ceiling together; every correctness
|
||||
# assertion in the smokes stays strict.
|
||||
- run: zig build test-gpu-dashboard-smoke
|
||||
env:
|
||||
NATIVE_SDK_SMOKE_BUDGET_MS: "1500"
|
||||
- run: zig build test-gpu-components-smoke
|
||||
env:
|
||||
NATIVE_SDK_SMOKE_BUDGET_MS: "1500"
|
||||
|
||||
macos-gpu-perf:
|
||||
name: macOS GPU Perf
|
||||
runs-on: macos-14
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: 0.16.0
|
||||
# Percentile perf check: 5 cold launches asserting p90
|
||||
# first-frame latency under NATIVE_SDK_PERF_BUDGET_MS, then 5 steady-state
|
||||
# widget clicks asserting p90 input latency under NATIVE_SDK_PERF_INPUT_BUDGET_MS.
|
||||
# Its own job so a shared-runner slowdown is visible in isolation and
|
||||
# never blocks the correctness smokes.
|
||||
# Shared macos-14 runners are far noisier than a dev box (first CI run
|
||||
# measured a 581 ms cold-start outlier against the 300 ms default), so
|
||||
# widen the budgets here instead of weakening the local defaults: this
|
||||
# job exists to catch step-function regressions, not runner noise.
|
||||
- run: zig build test-gpu-dashboard-perf
|
||||
env:
|
||||
NATIVE_SDK_PERF_BUDGET_MS: "1500"
|
||||
NATIVE_SDK_PERF_INPUT_BUDGET_MS: "500"
|
||||
|
||||
linux-webkitgtk:
|
||||
name: Linux WebKitGTK
|
||||
@@ -72,8 +110,79 @@ jobs:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 24
|
||||
- run: npm --prefix packages/zero-native run version:check
|
||||
- run: npm --prefix packages/zero-native run scripts:check
|
||||
- run: npm --prefix packages/native-sdk run version:check
|
||||
- run: npm --prefix packages/native-sdk run scripts:check
|
||||
|
||||
native-examples:
|
||||
name: Native Examples
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: 0.16.0
|
||||
- name: Install GTK dependencies
|
||||
run: sudo apt-get update && sudo apt-get install -y libgtk-4-dev libwebkitgtk-6.0-dev
|
||||
- run: zig build test-examples-native
|
||||
|
||||
linux-canvas-smoke:
|
||||
name: Linux Canvas Smoke
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: 0.16.0
|
||||
- name: Install GTK and Xvfb
|
||||
run: sudo apt-get update && sudo apt-get install -y libgtk-4-dev libwebkitgtk-6.0-dev xvfb
|
||||
# Drives the gpu_surface software path against system WebKitGTK under
|
||||
# Xvfb: snapshot ready, gpu_backend=software, gpu_nonblank=true,
|
||||
# automation widget-click, and a rendered screenshot. Sandbox/a11y
|
||||
# env, the widened cold-start readiness budget (shared runners stall
|
||||
# ~27 s before the first runtime event), and failure forensics (dump
|
||||
# snapshot + app log) all live in the script.
|
||||
- name: Build and drive ui-inbox headless
|
||||
run: .github/scripts/linux-canvas-smoke.sh
|
||||
|
||||
windows-canvas-smoke:
|
||||
name: Windows Canvas Smoke (Wine)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: 0.16.0
|
||||
- name: Install Wine, Xvfb, and xdotool
|
||||
run: sudo apt-get update && sudo apt-get install -y wine xvfb xdotool
|
||||
# Cross-compiles ui-inbox for x86_64-windows-gnu and drives the
|
||||
# gpu_surface software path (child HWND + WM_TIMER + SetDIBitsToDevice)
|
||||
# under Wine: snapshot ready, gpu_backend=software, gpu_nonblank=true,
|
||||
# automation widget-click, and real XTEST pointer/keyboard input.
|
||||
# Wineprefix init happens inline in the script (measured 21s from
|
||||
# scratch in an ubuntu-24.04 container, so no cache step).
|
||||
- name: Build and drive ui-inbox.exe under Wine
|
||||
run: .github/scripts/windows-canvas-smoke.sh
|
||||
|
||||
windows-effects-smoke:
|
||||
name: Windows Effects Smoke (Wine)
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: mlugg/setup-zig@v2
|
||||
with:
|
||||
version: 0.16.0
|
||||
- name: Install Wine and Xvfb
|
||||
run: sudo apt-get update && sudo apt-get install -y wine xvfb
|
||||
# Cross-compiles examples/effects-probe for x86_64-windows-gnu and
|
||||
# proves the effect system's live Windows path under Wine: fx.spawn
|
||||
# launches cmd.exe, streamed lines land in the model, the worker's
|
||||
# PostMessageW wake shows up as effects_wake events in the trace
|
||||
# log (frame ticks also drain, so the log is the wake's evidence),
|
||||
# and fx.cancel terminates the child with the line count frozen.
|
||||
- name: Build and drive effects-probe.exe under Wine
|
||||
run: .github/scripts/windows-effects-smoke.sh
|
||||
|
||||
frontend-examples:
|
||||
name: Frontend Examples
|
||||
@@ -104,16 +213,49 @@ jobs:
|
||||
with:
|
||||
version: 0.16.0
|
||||
- run: zig build
|
||||
- name: Scaffold and test the zero-config native app
|
||||
run: |
|
||||
set -euo pipefail
|
||||
app=".zig-cache/scaffold-native-slim"
|
||||
rm -rf "$app"
|
||||
./zig-out/bin/native init "$app"
|
||||
# Slim scaffold: no build files — the CLI's generated graph drives it.
|
||||
test ! -f "$app/build.zig"
|
||||
test ! -f "$app/build.zig.zon"
|
||||
./zig-out/bin/native test "$app" -Dplatform=null
|
||||
./zig-out/bin/native check "$app"
|
||||
./zig-out/bin/native eject "$app"
|
||||
(cd "$app" && zig build test -Dplatform=null)
|
||||
- name: Scaffold and test frontend templates
|
||||
run: |
|
||||
set -euo pipefail
|
||||
for frontend in next vite react svelte vue; do
|
||||
for frontend in native next vite react svelte vue; do
|
||||
app=".zig-cache/scaffold-${frontend}"
|
||||
rm -rf "$app"
|
||||
./zig-out/bin/zero-native init "$app" --frontend "$frontend"
|
||||
(cd "$app" && zig build test -Dplatform=null && ../../zig-out/bin/zero-native validate app.zon)
|
||||
./zig-out/bin/native init "$app" --frontend "$frontend" --full
|
||||
(cd "$app" && zig build test -Dplatform=null && ../../zig-out/bin/native validate app.zon)
|
||||
# Every scaffold ships a CI workflow; parse it as real YAML.
|
||||
test -s "$app/.github/workflows/ci.yml"
|
||||
python3 -c 'import sys, yaml; yaml.safe_load(open(sys.argv[1]))' "$app/.github/workflows/ci.yml"
|
||||
done
|
||||
|
||||
evals-typecheck:
|
||||
name: Evals Typecheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 10.23.0
|
||||
package_json_file: evals/package.json
|
||||
- run: pnpm install --frozen-lockfile
|
||||
working-directory: evals
|
||||
- run: pnpm typecheck
|
||||
working-directory: evals
|
||||
|
||||
docs:
|
||||
name: Docs
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -32,10 +32,10 @@ jobs:
|
||||
- name: Compare package.json version to npm and check GitHub release
|
||||
id: check
|
||||
run: |
|
||||
LOCAL_VERSION=$(node -p "require('./packages/zero-native/package.json').version")
|
||||
LOCAL_VERSION=$(node -p "require('./packages/native-sdk/package.json').version")
|
||||
echo "Local version: $LOCAL_VERSION"
|
||||
|
||||
NPM_VERSION=$(npm view zero-native version 2>/dev/null || echo "0.0.0")
|
||||
NPM_VERSION=$(npm view @native-sdk/cli version 2>/dev/null || echo "0.0.0")
|
||||
echo "npm version: $NPM_VERSION"
|
||||
|
||||
if [ "$LOCAL_VERSION" != "$NPM_VERSION" ]; then
|
||||
@@ -51,14 +51,14 @@ jobs:
|
||||
missing=0
|
||||
for asset in \
|
||||
CHECKSUMS.txt \
|
||||
zero-native-darwin-arm64 \
|
||||
zero-native-darwin-x64 \
|
||||
zero-native-linux-arm64 \
|
||||
zero-native-linux-x64 \
|
||||
zero-native-linux-musl-arm64 \
|
||||
zero-native-linux-musl-x64 \
|
||||
zero-native-win32-arm64.exe \
|
||||
zero-native-win32-x64.exe
|
||||
native-sdk-darwin-arm64 \
|
||||
native-sdk-darwin-x64 \
|
||||
native-sdk-linux-arm64 \
|
||||
native-sdk-linux-x64 \
|
||||
native-sdk-linux-musl-arm64 \
|
||||
native-sdk-linux-musl-x64 \
|
||||
native-sdk-win32-arm64.exe \
|
||||
native-sdk-win32-x64.exe
|
||||
do
|
||||
if ! printf '%s\n' "$EXISTING_ASSETS" | grep -Fx "$asset" >/dev/null; then
|
||||
echo "Missing release asset: $asset"
|
||||
@@ -108,35 +108,11 @@ jobs:
|
||||
fi
|
||||
echo "Extracted release notes for $VERSION ($LINES lines)"
|
||||
|
||||
- name: Build native release asset
|
||||
- name: Build native release assets (all platforms)
|
||||
run: |
|
||||
mkdir -p /tmp/zero-native-release
|
||||
|
||||
build_asset() {
|
||||
target="$1"
|
||||
name="$2"
|
||||
rm -rf zig-out
|
||||
zig build -Dtarget="$target" -Doptimize=ReleaseSmall
|
||||
|
||||
src="zig-out/bin/zero-native"
|
||||
case "$name" in
|
||||
*.exe) src="${src}.exe" ;;
|
||||
esac
|
||||
|
||||
cp "$src" "/tmp/zero-native-release/$name"
|
||||
chmod 755 "/tmp/zero-native-release/$name"
|
||||
}
|
||||
|
||||
build_asset aarch64-macos zero-native-darwin-arm64
|
||||
build_asset x86_64-macos zero-native-darwin-x64
|
||||
build_asset aarch64-linux-gnu zero-native-linux-arm64
|
||||
build_asset x86_64-linux-gnu zero-native-linux-x64
|
||||
build_asset aarch64-linux-musl zero-native-linux-musl-arm64
|
||||
build_asset x86_64-linux-musl zero-native-linux-musl-x64
|
||||
build_asset aarch64-windows zero-native-win32-arm64.exe
|
||||
build_asset x86_64-windows zero-native-win32-x64.exe
|
||||
|
||||
(cd /tmp/zero-native-release && shasum -a 256 zero-native-* > CHECKSUMS.txt)
|
||||
# Cross-compiles the CLI for all eight platforms and writes the
|
||||
# flat release assets + CHECKSUMS.txt into zig-out/release/.
|
||||
bash packages/native-sdk/scripts/build-binaries.sh
|
||||
|
||||
- name: Create GitHub Release
|
||||
run: |
|
||||
@@ -153,8 +129,8 @@ jobs:
|
||||
fi
|
||||
|
||||
gh release upload "$TAG" \
|
||||
/tmp/zero-native-release/zero-native-* \
|
||||
/tmp/zero-native-release/CHECKSUMS.txt \
|
||||
zig-out/release/native-sdk-* \
|
||||
zig-out/release/CHECKSUMS.txt \
|
||||
--clobber
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -167,7 +143,7 @@ jobs:
|
||||
&& needs.check-release.outputs.should_release == 'true'
|
||||
&& (needs.github-release.result == 'success' || needs.github-release.result == 'skipped')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
timeout-minutes: 15
|
||||
environment: Release
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -182,17 +158,73 @@ jobs:
|
||||
node-version: "24"
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
# Publishing uses npm trusted publishing (OIDC): the job's id-token
|
||||
# permission lets npm mint short-lived credentials, so no npm token
|
||||
# secret exists anywhere in this repo. All nine packages —
|
||||
# @native-sdk/cli plus the eight @native-sdk/cli-* platform packages
|
||||
# under packages/native-sdk/npm/ — must each be configured on
|
||||
# npmjs.com with a GitHub Actions trusted publisher pointing at
|
||||
# repository vercel-labs/zero-native, workflow release.yml,
|
||||
# environment Release. If a package is missing that configuration,
|
||||
# npm publish fails loudly with an OIDC authentication error before
|
||||
# anything is uploaded for that package. Trusted publishing requires
|
||||
# npm >= 11.5.1, satisfied by the npm bundled with Node 24.
|
||||
|
||||
- name: Check version sync
|
||||
run: npm --prefix packages/zero-native run version:check
|
||||
run: npm --prefix packages/native-sdk run version:check
|
||||
|
||||
- name: Check package scripts
|
||||
run: npm --prefix packages/zero-native run scripts:check
|
||||
run: npm --prefix packages/native-sdk run scripts:check
|
||||
|
||||
- name: Download release binaries
|
||||
run: |
|
||||
VERSION="${{ needs.check-release.outputs.version }}"
|
||||
mkdir -p /tmp/native-sdk-release
|
||||
gh release download "v$VERSION" \
|
||||
--pattern 'native-sdk-*' \
|
||||
--pattern 'CHECKSUMS.txt' \
|
||||
--dir /tmp/native-sdk-release
|
||||
(cd /tmp/native-sdk-release && sha256sum -c CHECKSUMS.txt)
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Stage binaries into the platform packages
|
||||
run: |
|
||||
# Release-asset name -> npm platform package (same table as
|
||||
# packages/native-sdk/scripts/build-binaries.sh).
|
||||
stage() {
|
||||
asset="$1"; key="$2"; ext="$3"
|
||||
mkdir -p "packages/native-sdk/npm/$key/bin"
|
||||
cp "/tmp/native-sdk-release/$asset" "packages/native-sdk/npm/$key/bin/native$ext"
|
||||
chmod 755 "packages/native-sdk/npm/$key/bin/native$ext"
|
||||
}
|
||||
stage native-sdk-darwin-arm64 darwin-arm64 ""
|
||||
stage native-sdk-darwin-x64 darwin-x64 ""
|
||||
stage native-sdk-linux-arm64 linux-arm64-gnu ""
|
||||
stage native-sdk-linux-x64 linux-x64-gnu ""
|
||||
stage native-sdk-linux-musl-arm64 linux-arm64-musl ""
|
||||
stage native-sdk-linux-musl-x64 linux-x64-musl ""
|
||||
stage native-sdk-win32-arm64.exe win32-arm64 ".exe"
|
||||
stage native-sdk-win32-x64.exe win32-x64 ".exe"
|
||||
|
||||
- name: Publish to npm
|
||||
run: |
|
||||
if [ "${{ github.event.repository.visibility }}" = "public" ]; then
|
||||
npm publish --provenance --access public
|
||||
else
|
||||
npm publish --access public
|
||||
fi
|
||||
working-directory: packages/zero-native
|
||||
VERSION="${{ needs.check-release.outputs.version }}"
|
||||
|
||||
# Platform packages first, so the main package's
|
||||
# optionalDependencies pins are resolvable the moment it lands.
|
||||
# Re-runs skip anything already on the registry at this version.
|
||||
publish_dir() {
|
||||
dir="$1"
|
||||
name=$(node -p "require('./$dir/package.json').name")
|
||||
if npm view "$name@$VERSION" version >/dev/null 2>&1; then
|
||||
echo "$name@$VERSION already published, skipping"
|
||||
return 0
|
||||
fi
|
||||
(cd "$dir" && npm publish --provenance --access public)
|
||||
}
|
||||
|
||||
for dir in packages/native-sdk/npm/*/; do
|
||||
publish_dir "${dir%/}"
|
||||
done
|
||||
publish_dir packages/native-sdk
|
||||
|
||||
@@ -1,12 +1,25 @@
|
||||
.DS_Store
|
||||
.zig-cache/
|
||||
zig-out/
|
||||
# CLI-generated build graph for zero-config apps (the examples here) — the
|
||||
# same entry `native init` writes into a new app's .gitignore.
|
||||
.native/
|
||||
|
||||
# Native binaries in zero-native npm package (built by CI or locally)
|
||||
packages/zero-native/bin/zero-native-*
|
||||
packages/zero-native/src/
|
||||
packages/zero-native/skills/
|
||||
packages/zero-native/skill-data/
|
||||
# Native binaries in the @native-sdk/cli npm packages (built by CI or locally)
|
||||
packages/native-sdk/bin/native-sdk-*
|
||||
packages/native-sdk/npm/*/bin/
|
||||
# SDK payload mirrored into the npm package at pack time (copy-framework.js)
|
||||
packages/native-sdk/src/
|
||||
packages/native-sdk/skills/
|
||||
packages/native-sdk/skill-data/
|
||||
packages/native-sdk/build/
|
||||
packages/native-sdk/build.zig
|
||||
packages/native-sdk/build.zig.zon
|
||||
packages/native-sdk/app.zon
|
||||
packages/native-sdk/LICENSE
|
||||
# npm pack output
|
||||
packages/native-sdk/*.tgz
|
||||
packages/native-sdk/npm/*/*.tgz
|
||||
|
||||
# Downloaded/prepared CEF runtimes are large local artifacts.
|
||||
third_party/cef/macos/
|
||||
@@ -18,3 +31,4 @@ third_party/cef/linux/
|
||||
|
||||
# TypeScript build info (generated)
|
||||
docs/tsconfig.tsbuildinfo
|
||||
.claude/
|
||||
|
||||
@@ -1,16 +1,31 @@
|
||||
# Agent Rules
|
||||
# Agent Guide
|
||||
|
||||
## Releasing
|
||||
Guidance for agents (and humans) working on this repository.
|
||||
|
||||
Releases are manual, single-PR affairs. The maintainer controls the changelog voice and format.
|
||||
## Build, test, and gate
|
||||
|
||||
To prepare a release:
|
||||
```bash
|
||||
zig build test # root engine + runtime suites
|
||||
zig build validate # sample app.zon manifest check
|
||||
zig build test-example-<name> # one example's suite (e.g. test-example-notes)
|
||||
scripts/gate.sh fast [ref] # affected-only local gate for your diff (default base: main)
|
||||
scripts/gate.sh full # everything CI-shaped that runs locally
|
||||
```
|
||||
|
||||
1. Create a branch (e.g. `prepare-v1.2.0`)
|
||||
2. Bump the version in `packages/zero-native/package.json`
|
||||
3. Run `npm --prefix packages/zero-native run version:sync` to update all version references
|
||||
4. Write the changelog entry in `CHANGELOG.md`, wrapped in `<!-- release:start -->` and `<!-- release:end -->` markers
|
||||
5. Remove the `<!-- release:start -->` and `<!-- release:end -->` markers from the previous release entry; only the latest release should have markers
|
||||
6. Open a PR and merge to `main`
|
||||
Run `scripts/gate.sh fast` before finishing any change; it maps your diff to the suites that cover it. The docs site checks with `pnpm --dir docs check` (the gate runs it only when `docs/` changed).
|
||||
|
||||
CI compares the version in `packages/zero-native/package.json` to what's on npm. If it differs, it publishes the CLI package and creates the GitHub release automatically. If npm already has the version but the GitHub release is missing, CI creates the GitHub release from the marked changelog entry.
|
||||
Pinned goldens (pixel signatures, schema fingerprints, command counts) are updated deliberately: review the rendered output or the counted commands first, and keep the pin's comment a self-contained description of what the value represents.
|
||||
|
||||
## Changelog
|
||||
|
||||
Do not edit `CHANGELOG.md` directly. Each user-visible change ships a fragment in `changelog.d/` — see `changelog.d/README.md` for the format and voice. Internal-only polish needs no fragment.
|
||||
|
||||
## Where things live
|
||||
|
||||
- `src/` — the engine and runtime; `src/primitives/canvas/` holds the widget, markup, and vector core.
|
||||
- `examples/` — the showcase apps, most zero-config (`app.zon` + `src/`).
|
||||
- `docs/` — the documentation site; `docs/AGENTS.md` has its MDX conventions.
|
||||
- `skills/` and `skill-data/` — the agent skills the CLI ships (`native skills list`).
|
||||
- `tools/` and `scripts/` — dev tooling and the local gate.
|
||||
|
||||
Releases are maintainer-run; see [RELEASING.md](./RELEASING.md).
|
||||
|
||||
@@ -1,13 +1,110 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to zero-native will be documented in this file.
|
||||
All notable changes to the Native SDK (formerly zero-native) will be documented in this file.
|
||||
|
||||
## 0.3.0
|
||||
## 0.4.0
|
||||
|
||||
<!-- release:start -->
|
||||
|
||||
### New Features
|
||||
|
||||
- **zero-native is now the Native SDK**: The toolkit, CLI, and packages are renamed end to end — the CLI binary is `native`, the Zig module and build helper are `native_sdk` (`native_sdk.addApp`, `native_sdk.addMobileLib`), the embed C ABI prefix is `native_sdk_*`, and the npm CLI package is `@native-sdk/cli`.
|
||||
- **Native-rendered apps by default**: `native init` scaffolds a native-rendered app — a declarative `.native` markup view plus Zig logic on the `UiApp` runtime (a `Model`, a `Msg` union, `update`, and a view) — with web frontends still available via `--frontend next|vite|react|svelte|vue`.
|
||||
- Native markup: HTML-inspired views with flex layout, `{bindings}` to model fields and functions, typed `on-*` message dispatch, `for`/`if`/`else` structure tags (multi-child `for` bodies, `<else>` empty states), and keyed identity; a deliberately closed grammar keeps logic in Zig.
|
||||
- Comptime compilation: views compile at build time into direct field access — release binaries carry no parser, and markup or binding mistakes are compile errors with line and column.
|
||||
- Hot reload: dev builds watch every `.native` file — imported components and fragments embedded in Zig views included — and update the running window in place, preserving model state, selection, and widget identity.
|
||||
- Expressions in bindings: arithmetic, comparisons, boolean logic, string concatenation, and a closed 17-function formatting library (`fixed`, `thousands`, `date`/`time`, `pad`, `plural`, ...), evaluated bit-identically by both markup engines; string-producing model functions bind directly through the build arena.
|
||||
- Cross-file components: `<import>` splices template files (transitively, with cycle and duplicate diagnostics), template args take literal defaults, `<slot/>` marks where use-site children land, and `native eject component` transfers a library composite's canonical source into your app exactly once.
|
||||
- `canvas.Ui`, the programmatic builder under the markup: structural widget identity, typed message handlers, flex-first layout, and per-element `opacity`/`transform` render channels for animated composition.
|
||||
- **The model–view contract, checked in both directions**: `native check` verifies every binding path, iterable, key, message tag, payload type, and expression in every `.native` file against the app's reflected `Model`/`Msg` surface in milliseconds — with did-you-mean suggestions and a dead-state lint for model fields and messages no view uses.
|
||||
- **Markup tooling**: `native markup check` (instant validation with positions), a language server (diagnostics, completion, hover), a TextMate grammar with editor setup, `native markup dump` over the canonical serialized document format, and the `native-ui` agent skill — the complete authoring reference, served through the skills CLI.
|
||||
- **Two-way tooling**: `native automate provenance` reports where a live widget was authored (file, byte span, template instantiation chain), and `native automate edit` writes minimal-diff attribute and text edits back into the markup source — validated before anything touches the file, with hot reload closing the loop.
|
||||
- **Full component catalog**: every built-in component is expressible in markup — tabs, tables, dialogs, drawers, sheets, selects, comboboxes, accordions, menus, badges, avatars, tooltips, inputs, and more — implemented in both engines with parity tests, alongside new composites in markup and Zig:
|
||||
- Charts (`<chart>` / `ui.chart`): line, area, bar, and band series drawn through the vector path pipeline with design-token colors, deterministic downsampling past 256 points, axis labels on a nice-step lattice, and pointer hover details.
|
||||
- Markdown (`<markdown>` / `native_sdk.markdown`): a GitHub-flavored subset — headings, inline styles, links, lists, task lists, fenced code, blockquotes, pipe tables, autolinks, and model-driven collapsibles — that degrades malformed input to text and never fails a build.
|
||||
- Disclosure trees with the full ARIA tree keymap, steppers and timeline items, input groups with focus-within rings, chat bubbles with reaction pills and thread-width caps, and a `ui.nav` push/pop page container with stable per-page state.
|
||||
- Resizable split panes with model-owned fractions, keyboard and assistive resize, and optional eased animation on model-driven moves.
|
||||
- Windowed virtual lists: viewport-sized widget budgets at 100,000 items, variable row extents that converge to measured truth without visible jumps, tail anchoring for chat transcripts, and `on_reach_end`/`on_reach_start` for infinite fetch and history loading.
|
||||
- Anchored floating surfaces (dropdowns, selects, popovers) that float above the tree with edge auto-flip; dismissal (Escape, click-outside, assistive dismiss) is a Msg the model owns, and focused selects get the full open/navigate/commit keymap.
|
||||
- Vector icons: an SVG stroke-icon subset parser, 50 curated built-in icons, leading or trailing icon slots on buttons, toggle chips, list and menu rows, badges, and timeline items, app-registered icons comptime-parsed from your own SVGs, model-bound icon names, and a loud missing-icon fallback.
|
||||
- **Text engine**:
|
||||
- Inline styled spans — weight (resolved to real faces), italic, monospace, color tokens, underline, strikethrough, size scale, per-span backgrounds, and hit-testable links — wrap as one paragraph in Zig and markup alike.
|
||||
- Honest single-line text: unwrapped text elides with a trailing ellipsis by default, an `overflow` policy knob keeps the deliberate hard cut available, and word wrap is an explicit opt-in — paint always agrees with measurement.
|
||||
- `heading`/`display` typography rungs on the token ladder, first-class text alignment, and fixed grid column counts.
|
||||
- **Selection and clipboard**: cmd/ctrl+C/X/V in editable fields through the platform clipboard, click-drag selection with copy on static text (surviving rebuilds, exposed to semantics and automation), and clipboard effects for app code.
|
||||
- **Interaction model**:
|
||||
- Presses fall through to the nearest pressable ancestor, so any element with a handler is a real hit target — nested pressables resolve to the deepest one, and text selection still works inside pressable rows.
|
||||
- Press-and-hold, double-click, Enter as a list row's primary action, and an app-level key fallback (`Options.on_key`) with pinned precedence — quiet list rows stay transparent to app-owned selection models.
|
||||
- Source-driven `autofocus`, observable typed scroll events (`on_scroll`), a built-in search-field clear affordance, and a quiet-hover style knob for content tiles.
|
||||
- **Effect system**: the update loop's command half — `update` gains an effects channel of bounded, key-addressed effects that deliver exactly one terminal Msg each and are fully testable against a deterministic fake executor:
|
||||
- `fx.spawn` runs subprocesses with streamed lines or whole-output collect mode (stderr tail included), raisable per-effect line bounds, and cancellation; `fx.fetch` runs HTTP(S) requests with an explicit failure taxonomy, timeouts, and a streaming response mode for line-oriented endpoints.
|
||||
- `fx.readFile`/`fx.writeFile` persistence, `fx.startTimer`/`fx.cancelTimer`, `fx.writeClipboard`/`fx.readClipboard`, `fx.registerImageBytes` for runtime images, `fx.closeWindow`/`fx.minimizeWindow`, and the `init_fx` boot hook so loading states are in the very first paint.
|
||||
- A facade time API (`nowMs`, `monotonicMs`) plus `Clock`/`TestClock` seams for deterministic time-dependent logic.
|
||||
- **Audio, end to end on five platforms**: `fx.playAudio` with full transport (pause, resume, stop, seek, volume), real decoded durations, position ticks, and honest completion and failure reports — AVFoundation on macOS, Media Foundation on Windows, GStreamer on Linux, and the experimental mobile hosts on iOS (AVFoundation) and Android (MediaPlayer).
|
||||
- Streaming with a verified track cache: URL sources resolve local file, then size-verified cache, then progressive stream (filling the cache in parallel for the next play), with honest `buffering` states and explicit failures — never a silent stall.
|
||||
- Real spectrum analysis on macOS, Windows, and Linux: 32 log-spaced bands at ~25 Hz from the app's own playback, journaled at the effect boundary so record/replay repaints identical bars; hosts that cannot analyze report the capability honestly instead of fabricating bands.
|
||||
- **Images**: a platform decode seam (CGImageSource, gdk-pixbuf, WIC) so the toolkit bundles no image decoders; runtime image registration renders through every path — GPU packets, software presentation, and screenshots — with pixels riding an out-of-band upload channel so image-bearing frames stay on the GPU path; avatars take a bound image with initials fallback.
|
||||
- **Windowing and chrome**: model-declared secondary windows (presence is visibility; a user close dispatches a Msg), enforced window minimum sizes, and present-before-show so a canvas window never appears blank.
|
||||
- Titlebar control on all three desktops: `hidden_inset`, a tall unified-toolbar variant, and fully `chromeless` styles; markup `window-drag` regions; and an `on_chrome` hook carrying the real overlay insets and control-cluster frames — with real system window controls preserved on Linux client-side decorations and Windows DWM caption buttons.
|
||||
- Native context menus, declared per widget in Zig or markup (`<context-menu>`): the real OS menu where one exists, an anchored canvas surface elsewhere, editable-text cut/copy/paste defaults, and full automation support for enumerating and invoking items.
|
||||
- A menu-bar status item with model-driven title and menu; canvas and WebView panes composed in one window; adoption of app-owned native views into the layout (`adoptViewSurface`); and native scroll drivers on macOS that give every scroll region OS momentum, rubber-band overscroll, and the system overlay scrollbar with zero app code.
|
||||
- **Experimental iOS and Android host tiers — the toolkit owns the entire mobile app**: complete UIKit and Android hosts ship in the SDK over the embed C ABI, an app project carries zero host code, and embedding a hand-written host stays first-class.
|
||||
- `native dev --target ios|android [--device name]` builds, installs, and launches on a simulator or emulator and streams the app log; `native package --target ios` emits an archive-ready Xcode project and `--target android` a complete generated host project plus a debug-signed APK — no build-system project, no plugin matrix.
|
||||
- Touch, soft keyboard, and IME forwarding; safe-area and keyboard insets on the window-chrome channel plus host-reported form factor; platform text metrics; platform audio and image decoding; and damage-rect rendering so a keystroke repaints and uploads only the changed region instead of the whole screen.
|
||||
- Declared platform chrome: apps project a tab set and primary action as a real system tab bar, and a model-owned page stack drives real push/pop transitions with the system edge-swipe back gesture — navigation state stays in the model and replays deterministically from the Msg journal.
|
||||
- The soundboard ships the proof: one codebase, a desktop composition plus a compact phone shell selected by the host-reported form factor, running on the simulator via `native dev --target ios`.
|
||||
- **Theme packs and design tokens**: named packs — the default register plus `geist`, the design register of the bundled Geist type family — compose with the live system appearance; interaction-state formulas, control metrics, and focus-ring geometry are all token-stated; new `success`/`warning`/`info` semantic color tokens; the stock theme follows the OS light/dark, high-contrast, and reduce-motion settings live; modal scrims blur the content behind them for real; app-registered TrueType fonts resolve everywhere a font id rides.
|
||||
- **Deterministic rendering core**: a bounded, std-only TTF parser inks real anti-aliased glyphs (bundled Geist and Geist Mono) on every headless path — screenshots, mobile embeds, pixel goldens — while layout measures exactly what gets inked; an allocation-free vector rasterizer with bit-identical cross-platform coverage draws paths, icons, and charts.
|
||||
- **Automation and testing**: `native automate` gains `assert` (regex polling against the accessibility snapshot), deterministic PNG screenshots, per-stage frame profiling (`profile on`), and widget verbs for hold, secondary click, context-menu invocation, drag, wheel, and tray actions.
|
||||
- Deterministic session record and replay: journal every platform event and effect result, then re-run headlessly with checkpoint verification (`native automate record` / `replay --verify`).
|
||||
- `native init` scaffolds a CI workflow: null-platform tests for every frontend plus a Linux automation smoke that drives the app's real binary under Xvfb.
|
||||
- **Accessibility as machine checks**: unnamed interactive controls, icon-only controls without labels, and misused roles are validation errors (degradations report as warnings; `--strict` promotes); a deterministic tree-level audit catches labels that resolve empty at runtime, focus-unreachable widgets, and duplicate sibling labels; and assistive actions actuate through the same activation paths keyboard users take instead of reporting success on nothing.
|
||||
- **Showcase examples**: calculator, notes (folders, trash, context menus), soundboard (a real music library with playback and search), deck (a radically re-skinned sibling proving theme packs and chrome passes), system-monitor (live effects-driven sampling), markdown-viewer (split-pane editor and preview), and feed (a 100,000-post virtual list) — each with a deterministic test suite, and a prepared real-music catalog that streams out of the box.
|
||||
- **Docs site**: a full Components section (34 pages) where every preview is rendered offscreen by the engine itself and upgrades on hover to a live engine instance running in-page via a ~306 KB (gzip) wasm build; attribute tables generate from the validator's own vocabulary so docs cannot drift; the whole site restructured native-first with new State & Data Flow, App & Runtime, Theming, and Testing in CI pages.
|
||||
- **Zero-config toolchain and distribution**: `native dev|build|test|check` work in a directory holding only `app.zon` and `src/` (`native eject` writes the build files exactly once when you want to own them); the pinned Zig toolchain downloads on consent with checksum verification; and `@native-sdk/cli` installs from npm with zero scripts — eight platform binaries plus the SDK source, so `native init && native dev` work offline right after install.
|
||||
- **One-image app icons**: drop a single square PNG or SVG in `assets/`, and `native package` generates everything — a masked, grid-correct macOS `.icns`, a multi-size Windows `.ico`, Linux hicolor PNGs, and iOS/Android catalog icons — with exact linear-light downscales, teaching errors for bad sources, and no external tools.
|
||||
|
||||
### Improvements
|
||||
|
||||
- **Performance — frame cost scales with what changed, not view size**:
|
||||
- GPU packets ride a compact binary encoding (~10x smaller than JSON, ~40x effective capacity — text-heavy frames no longer silently fall back to software rendering), steady-state frames ship incremental patches (~20x less wire per interaction), and repaints derive per-change dirty-rect lists so pixels between two far-apart changes stay retained.
|
||||
- Per-command raster caches stop re-rasterizing unchanged content (host draw p50 dropped an order of magnitude on animated views); frame planning and widget reconciliation moved from quadratic scans to indexed lookups (end-to-end interaction p50 improved ~2.3-3.2x on large views); backdrop blur cost no longer scales with radius; a click emits one display list instead of three.
|
||||
- Launch to glass: the first canvas frame presents before the event loop starts, first paint rasterizes across cores, the main WebView is created lazily, and warm launches measured 150→120 ms on the heaviest showcase app; `NATIVE_SDK_WINDOW_TIMING=1` prints a per-phase launch breakdown.
|
||||
- Occluded windows throttle to a ~1 Hz heartbeat instead of spinning the frame clock (spectrum reports pause too); accessibility publishes only when the tree actually changed and defer off the input-to-glass path; frame pacing delivers exactly one event per display interval; input latency is measured to the responding present, honestly.
|
||||
- `zig build bench-render` runs deterministic interaction scenarios against committed per-scenario budgets, and a percentile GPU perf check gates first-frame and input-to-present latency in CI.
|
||||
- **Component fidelity**: the built-in components land a refined default look, verified pixel-for-pixel in CI under both theme packs.
|
||||
- Measured control geometry and state washes, ring-offset focus rings, flat buttons with a quiet destructive treatment, segmented button groups rendered as one bar with collapsed seams, compact badges, and hairline tables.
|
||||
- Reworked accordion, tabs, alert, and card treatments with sensible per-kind layout defaults; skeletons pulse and the caret blinks; select menus read like menus (row highlight, trailing checkmark for the committed option).
|
||||
- Native cursor conventions (the pointing hand is reserved for true links), flat list rows, axis-aware separators, and edge-pinned scrolling with opt-in rubber-band overscroll.
|
||||
- **Capacity and honesty**: per-view widget budgets quadrupled to 1024 nodes (command, glyph, and text budgets raised to match) with headroom telemetry in every snapshot; explicit `width`/`height` are definite bounds; layout overflow is diagnosed, dispatch errors degrade and record instead of exiting the app, and every effect-facing type and constant is exported from the `native_sdk` facade.
|
||||
- **Teaching validation**: handlers on elements that can never receive them, `gap` on stacking containers, `wrap` on non-text elements, and literal glyphs outside the bundled font's coverage are all positioned teaching errors, enforced identically by the validator, both engines, and the language server.
|
||||
- **Desktop parity**: the Linux and Windows hosts reach the macOS seam contract — app timers, appearance events, window options at create, interactive window moves, IME composition on Windows, and hidden-titlebar fidelity with real system controls; CI gains Windows canvas and effects smokes under Wine, a headless Linux canvas smoke, and a containerized Linux live-truth harness driving every showcase app on real GTK.
|
||||
- **Observability**: automation snapshots report the live present path and mode, patch sizes, fallback reasons with byte counts, budget headroom, audio state, tray contents, and per-stage frame percentiles while profiling; `NATIVE_SDK_GPU_DRAW_TRACE=1` attributes every present.
|
||||
- **Docs and skill accuracy**: the code-signing page documents the real ad-hoc Gatekeeper experience, form-control and picker docs match what the engine ships, the keyboard and interaction seams are documented where developers look, and stale commands and API shapes were fixed across the site.
|
||||
- **Example polish**: showcase headers carry only working controls under hidden-inset titlebars, the soundboard adopts desktop list-selection conventions, notes gains Recently Deleted and dialog autofocus, the deck refined its hardware identity across feedback passes, system-monitor lands the standard settings flow, and every showcase app ships the zero-config scaffold shape with a real neutral default app icon.
|
||||
- **Contributor workflow**: changelog fragments (`changelog.d/`) end merge conflicts on this file, and `scripts/gate.sh` runs a tiered local gate that scales with the diff.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **Input and focus**: clicked and tabbed-into fields always show a caret (drawn in the field's own ink, readable in every scheme); Escape dismisses surfaces opened from non-focusable triggers; Enter inserts a newline in textareas (the primary chord submits); programmatic focus is quiet on non-editables; composite rows hover, point, and press as one surface; cross-centered overflow distributes evenly.
|
||||
- **Model-driven control state**: sliders, exclusive selections, and toggle-button chips follow the model when the source moves (a live drag is never yanked); disabled selection controls render disabled; idle disabled buttons no longer wear an accent outline.
|
||||
- **Rendering correctness**: pixel snapping no longer wraps exact-fit text or elides exact-fit badges; packet text honors engine line breaks; text bounds cover glyph ink; mono runs read as monospace on every headless path; avatar initials center; the spinner actually spins and sizes to the icon register; offscreen screenshots clear with live tokens; render animations invalidate only the affected commands; one invalid UTF-8 byte can no longer hang the renderer; budget overflows apply atomically instead of tearing the retained tree.
|
||||
- **macOS**: Debug builds no longer abort at launch on an SDK sanitizer trap; `resizable = false` is honored; frames keep pumping during live resize and menu tracking; occluded windows keep presenting and flush instantly on reveal; quitting mid-playback no longer crashes; the Chromium (CEF) host builds and runs again, verified live with child WebViews.
|
||||
- **Windows and Linux**: Windows apps launch on real Windows (common-controls manifest, dynamic task-dialog resolution) and builds link again; embed input timestamps and network error classification fixed on Windows; Linux audio no longer sticks in a buffering state; a saturated frame loop no longer freezes GTK windows; runtimes heap-allocate in every runner, fixing startup crashes under default stack limits; GTK initial allocation and overlay z-order fixed.
|
||||
- **Packaging**: signed bundles keep a valid code signature; packaged apps read their bundled assets and show their display name in the menu bar; archives are labeled with the real optimize mode; unbundled dev runs fall back to the embedded default Dock icon.
|
||||
- **Automation and CLI reliability**: commands queue with delivery acknowledgments instead of overwriting a single slot; a landing command wakes an idle app (~4 ms consumption); CLI and app handshake on a protocol version, and stale publishers or binaries are refused loudly; parseable payloads land on stdout; clicks aim at the rendered control, not its stretched box; `native dev` runs Debug so hot reload is actually compiled in; no CLI verb exits silently, and `--help` exits 0 everywhere.
|
||||
- **Hardening**: the markdown renderer survives hostile input (three quadratic blowups fixed, a fuzz corpus added); large models neither exhaust the comptime branch quota nor ride the stack (`UiApp.create` constructs in place); mobile embed libraries stage per target so cross-target builds cannot poison each other; oversized inline window sources fail loudly instead of leaving a blank window; docs live previews build, lay out with the selected pack's tokens, animate, and route keyboard shortcuts correctly.
|
||||
- **Measured-label controls no longer elide under pixel snapping**: a control sized exactly to its measured label — toggle chips (the system monitor's "PID" sort chip painted "PI…"), buttons, segmented controls and tab triggers, menu and list rows, tooltips, checkbox/radio/switch labels, hug-sized status bars — could lose a fraction of a pixel to render-time geometry snapping and swap real glyphs for an ellipsis. Every measured-label intrinsic width now rounds UP to the snap grid (the badge rule from the previous round), the switch additionally reserves its snapped track extent, and themes without geometry snapping stay bit-identical.
|
||||
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
<!-- release:end -->
|
||||
|
||||
## 0.3.0
|
||||
|
||||
### New Features
|
||||
|
||||
- **Keyboard shortcuts**: Add app-level keyboard shortcuts with manifest and runtime configuration, native delivery to Zig `Event.shortcut`, and typed JavaScript `window.zero` shortcut events (#62).
|
||||
- **Manifest-driven runner shortcuts**: Load `app.zon` shortcuts automatically in generated runners, with a `RunOptions.shortcuts` override for apps that build shortcut lists in Zig (#62).
|
||||
|
||||
@@ -23,7 +120,6 @@ All notable changes to zero-native will be documented in this file.
|
||||
### Contributors
|
||||
|
||||
- @ctate
|
||||
<!-- release:end -->
|
||||
|
||||
## 0.2.0
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Contributing
|
||||
|
||||
Thanks for helping improve zero-native. This guide is for maintainers and contributors working on the framework repository itself.
|
||||
Thanks for helping improve the Native SDK. This guide is for maintainers and contributors working on the toolkit repository itself.
|
||||
|
||||
For app author documentation, start at [zero-native.dev](https://zero-native.dev).
|
||||
|
||||
@@ -14,7 +14,7 @@ For app author documentation, start at [zero-native.dev](https://zero-native.dev
|
||||
|
||||
## Local Checks
|
||||
|
||||
Run the framework tests:
|
||||
Run the toolkit tests:
|
||||
|
||||
```bash
|
||||
zig build test
|
||||
@@ -41,8 +41,8 @@ zig build run-webview
|
||||
Check the npm CLI package:
|
||||
|
||||
```bash
|
||||
npm --prefix packages/zero-native run version:check
|
||||
npm --prefix packages/zero-native run scripts:check
|
||||
npm --prefix packages/native-sdk run version:check
|
||||
npm --prefix packages/native-sdk run scripts:check
|
||||
```
|
||||
|
||||
Check the documentation site:
|
||||
@@ -63,7 +63,7 @@ zig build run-webview -Dweb-engine=system
|
||||
For Chromium on macOS, install CEF and run with the Chromium engine:
|
||||
|
||||
```bash
|
||||
zero-native cef install
|
||||
native cef install
|
||||
zig build run-webview -Dweb-engine=chromium
|
||||
```
|
||||
|
||||
@@ -85,11 +85,17 @@ zig build package
|
||||
Package explicitly through the CLI:
|
||||
|
||||
```bash
|
||||
zero-native package --target macos --manifest app.zon --assets assets --binary zig-out/lib/libzero-native.a
|
||||
native package --target macos --manifest app.zon --assets assets --binary zig-out/lib/libnative-sdk.a
|
||||
```
|
||||
|
||||
For Chromium packages, configure `.web_engine = "chromium"` and `.cef` in `app.zon`, or use temporary `--web-engine` and `--cef-dir` overrides while testing.
|
||||
|
||||
Verify an ad-hoc signed package's code signature survives packaging intact (macOS; skips loudly on hosts without `codesign`):
|
||||
|
||||
```bash
|
||||
zig build test-package-signing
|
||||
```
|
||||
|
||||
## Automation Development
|
||||
|
||||
Enable automation in a build:
|
||||
@@ -101,19 +107,22 @@ zig build run-webview -Dautomation=true
|
||||
Interact with the running app:
|
||||
|
||||
```bash
|
||||
zero-native automate wait
|
||||
zero-native automate list
|
||||
zero-native automate bridge '{"id":"ping","command":"native.ping","payload":null}'
|
||||
native automate wait
|
||||
native automate list
|
||||
native automate bridge '{"id":"ping","command":"native.ping","payload":null}'
|
||||
```
|
||||
|
||||
Automation writes artifacts under `.zig-cache/zero-native-automation`.
|
||||
Automation writes artifacts under `.zig-cache/native-sdk-automation`.
|
||||
|
||||
|
||||
## Making a Pull Request
|
||||
Thank you for your contribution! Please follow these steps to ensure a smooth review process:
|
||||
1. Fork the repository and create a new branch for your feature or bug fix.
|
||||
2. Make your changes and commit them with clear, descriptive messages.
|
||||
3. Push your branch to your forked repository.
|
||||
4. Open a pull request against the main repository's `main` branch.
|
||||
|
||||
Please cryptographically sign your commits so they show as **Verified** on GitHub. This requires a GPG or SSH signing key added to your GitHub account — see [GitHub's guide](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification). Note: the `Signed-off-by` trailer (`git commit -s`) is a DCO attestation and does **not** produce the Verified badge; you need `git commit -S` (uppercase) or `commit.gpgsign = true` in your git config.
|
||||
Branch from `main` (fork first if you don't have push access), keep the change focused, and run the tiered local gate before opening the PR:
|
||||
|
||||
```bash
|
||||
scripts/gate.sh fast # root suites + the example suites your diff touches
|
||||
```
|
||||
|
||||
If the change is user-visible, add a changelog fragment in `changelog.d/` (see [changelog.d/README.md](./changelog.d/README.md)) instead of editing `CHANGELOG.md`. Open the PR against `main` describing what changed and why; for larger changes, open an issue first so the design can be discussed.
|
||||
|
||||
Commits must be cryptographically signed (`git commit -S`, or set `commit.gpgsign = true`) so they show as **Verified** — the `Signed-off-by` trailer from `git commit -s` is a DCO attestation, not a signature.
|
||||
@@ -1,139 +1,132 @@
|
||||
# zero-native
|
||||
# Native SDK
|
||||
|
||||
Build native desktop apps with Zig, secure WebView surfaces, native controls, and OS capabilities. Tiny binaries. Minimal memory. Instant rebuilds.
|
||||
**Native SDK is the complete toolkit for building native desktop applications.**
|
||||
|
||||
zero-native is a native app framework where WebView content is one first-class surface, not the whole app model. Use native windows, menus, shortcuts, controls, dialogs, and OS services around rich web product UI. Use the platform WebView when you want the smallest possible app, or bundle Chromium through CEF when rendering consistency matters.
|
||||
Native SDK exists because expressive UI and native performance should not be competing goals. Developers often choose web-based runtimes because they offer freedom, speed and control over the product experience. But that freedom often comes with a heavy runtime. Native SDK keeps the expressive authoring model and replaces the runtime with native rendering.
|
||||
|
||||
## Quick Start
|
||||
Views are declarative markup in `.native` files, logic is plain Zig, and Native SDK's own engine draws every pixel into real OS windows — no browser, no WebView, no interpreter in the binary.
|
||||
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset=".github/assets/soundboard-dark.webp">
|
||||
<img src=".github/assets/soundboard-light.webp" alt="The Soundboard example app rendered by the Native SDK engine: a music library with album cover art, search, and a playback bar" width="100%">
|
||||
</picture>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="70%">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset=".github/assets/notes-dark.webp">
|
||||
<img src=".github/assets/notes-light.webp" alt="The Notes example app rendered by the Native SDK engine: a three-pane notes manager with folders, a note list, and an open note" width="640">
|
||||
</picture>
|
||||
</td>
|
||||
<td width="30%">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset=".github/assets/calculator-dark.webp">
|
||||
<img src=".github/assets/calculator-light.webp" alt="The Calculator example app rendered by the Native SDK engine: a finished calculation above a full keypad" width="270">
|
||||
</picture>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<sub>Soundboard, Notes, and Calculator from <a href="./examples">examples/</a> — every pixel drawn by the Native SDK engine, captured through its deterministic reference renderer. The images follow your color scheme.</sub>
|
||||
|
||||
## Quick start
|
||||
|
||||
Install the CLI:
|
||||
|
||||
```bash
|
||||
npm install -g zero-native
|
||||
npm install -g @native-sdk/cli
|
||||
```
|
||||
|
||||
Create and run an app:
|
||||
|
||||
```bash
|
||||
zero-native init my_app --frontend next
|
||||
native init my_app
|
||||
cd my_app
|
||||
zig build run
|
||||
native dev
|
||||
```
|
||||
|
||||
The first run installs frontend dependencies, builds the generated native shell, and opens a desktop window rendering your WebView content.
|
||||
A native window opens with a working counter. The whole view is `src/app.native` — a markup file that binds values and dispatches messages:
|
||||
|
||||
Read the full guide at [zero-native.dev/quick-start](https://zero-native.dev/quick-start).
|
||||
```html
|
||||
<column gap="12" padding="16">
|
||||
<row gap="8" main="center" cross="center" grow="1">
|
||||
<button variant="secondary" on-press="decrement">-</button>
|
||||
<text>{count}</text>
|
||||
<button variant="primary" on-press="increment">+</button>
|
||||
</row>
|
||||
<status-bar>count: {count}</status-bar>
|
||||
</column>
|
||||
```
|
||||
|
||||
## Why zero-native
|
||||
|
||||
### Native shell, web where it fits
|
||||
|
||||
Build app chrome and trusted utility surfaces with native views, while keeping WebViews available for rich product UI, frontend framework workflows, and rendering consistency when you need it.
|
||||
|
||||
### Tiny and fast
|
||||
|
||||
System WebView apps do not bundle a browser runtime, so the native shell stays small and starts quickly. Your app uses WKWebView on macOS and WebKitGTK on Linux.
|
||||
|
||||
### Choose your web engine
|
||||
|
||||
Pick the engine that fits the product. System WebView gives you a lightweight native footprint. Chromium through CEF gives you predictable rendering and a pinned web platform on supported targets.
|
||||
|
||||
### Fast native rebuilds
|
||||
|
||||
The native layer is Zig, so app logic, bridge commands, and platform integrations rebuild quickly. Your frontend can still use the web tooling you already know.
|
||||
|
||||
### OS power without heavy glue
|
||||
|
||||
Zig calls C directly, which keeps platform SDKs, native libraries, codecs, and local system integrations within reach when the WebView layer needs to do real native work.
|
||||
|
||||
### Explicit security model
|
||||
|
||||
The WebView is treated as untrusted by default. Native commands, permissions, navigation, external links, and window APIs are opt-in and policy controlled.
|
||||
|
||||
## Status
|
||||
|
||||
zero-native is pre-release. Desktop support now covers macOS 11+, Linux, and Windows build paths, native controls on system-WebView hosts, and Chromium/CEF distributed as platform-specific runtimes.
|
||||
|
||||
## Core Concepts
|
||||
|
||||
`App` is the small Zig object that describes your application: name, WebView source, lifecycle hooks, an optional native scene, and native services.
|
||||
|
||||
`Runtime` owns the event loop, windows, native views, WebViews, command routing, bridge dispatch, automation hooks, tracing, and platform services.
|
||||
|
||||
`ShellConfig` declares native-first windows and view trees: toolbars, sidebars, status bars, split panes, stacks, controls, WebViews, and future surface kinds.
|
||||
|
||||
`WebViewSource` tells the runtime what a WebView should load: inline HTML, a URL, or packaged frontend assets served from a local app origin.
|
||||
|
||||
`app.zon` is the app manifest. It declares app metadata, icons, windows, native shell views, frontend assets, web engine selection, security policy, bridge permissions, and packaging inputs.
|
||||
|
||||
`window.zero.*` is the guarded JavaScript-to-native bridge for commands, windows, views, WebViews, dialogs, clipboard, credentials, and OS services. Calls are size-limited, origin checked, permission checked, and routed only to allowed handlers.
|
||||
|
||||
## Configuration
|
||||
|
||||
Most project-level behavior lives in `app.zon`:
|
||||
All logic lives in `src/main.zig`: a `Model` struct, a `Msg` union, and one `update` function — the only place state changes:
|
||||
|
||||
```zig
|
||||
.{
|
||||
.id = "com.example.my-app",
|
||||
.name = "my-app",
|
||||
.display_name = "My App",
|
||||
.version = "0.1.0",
|
||||
.web_engine = "system",
|
||||
.permissions = .{},
|
||||
.capabilities = .{ "webview" },
|
||||
.security = .{
|
||||
.navigation = .{
|
||||
.allowed_origins = .{ "zero://app", "http://127.0.0.1:5173" },
|
||||
},
|
||||
},
|
||||
.windows = .{
|
||||
.{ .label = "main", .title = "My App", .width = 960, .height = 640 },
|
||||
},
|
||||
pub fn update(model: *Model, msg: Msg) void {
|
||||
switch (msg) {
|
||||
.increment => model.count += 1,
|
||||
.decrement => model.count -= 1,
|
||||
.reset => model.count = 0,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use `.web_engine = "system"` for the platform WebView. On supported macOS builds, use `.web_engine = "chromium"` with a `.cef` config when you want to bundle Chromium.
|
||||
Edit `src/app.native` while `native dev` runs and the window updates in place, keeping your state. `native check` validates every view in milliseconds without building, `native test` runs full-loop UI tests headlessly, and `native build` produces an optimized release binary.
|
||||
|
||||
Read the full guide at [zero-native.dev/quick-start](https://zero-native.dev/quick-start).
|
||||
|
||||
## What you get
|
||||
|
||||
**Beautiful by default** — Great software should not start from a blank slate. The built-in component catalog — buttons, tabs, text fields, dialogs, charts, virtual lists, and more — ships with considered typography, spacing, and color, so the app `native init` scaffolds already looks intentional the first time its window opens.
|
||||
|
||||
**Customizable by design** — Your app should have its own identity, not ours. Styling is design tokens end to end: color, radius, and typography resolve by name, re-resolve live when the theme changes, and can be replaced wholesale — `examples/soundboard` and `examples/deck` are the same music player separated only by tokens and a chrome pass.
|
||||
|
||||
**Native from the start** — Every interface is rendered without a browser or WebView. The engine draws into real OS windows while scroll physics, menus, dialogs, the tray, and text input stay with the operating system, and markup compiles into the executable at build time, so a release build carries no parser or interpreter — the scaffolded counter app builds to a single binary a few megabytes small.
|
||||
|
||||
**Predictable state** — State changes should be explicit, inspectable and easy to reason about. Events produce messages, messages update state, and state renders the interface; markup can bind and dispatch but never mutate. The loop is so deterministic that `native automate record` journals a session and `replay` reproduces it headlessly, verified frame by frame against state fingerprints.
|
||||
|
||||
**Simple authoring** — Interfaces should be easy to read, easy to write and easy to generate. Views are elements, flex layout, `{bindings}`, and expressions like `selected="{f == filter}"`, and `native check` validates every view against your app's actual `Model` and `Msg` — bindings, iterables, message tags — in milliseconds, with `file:line:column` errors that teach.
|
||||
|
||||
**AI is part of the workflow** — Native SDK is designed for a world where humans and AI agents build software together. Every app embeds an automation server, so any agent can read accessibility snapshots, drive widgets, assert on live state, and take deterministic screenshots of the running window; accessibility findings are machine-checked in `native check`; and the CLI ships the agent skills that teach all of it (`native skills list`).
|
||||
|
||||
## Examples
|
||||
|
||||
The apps pictured above live in [examples/](./examples), most as zero-config projects — `app.zon` plus `src/`, no build files — run straight from their directory with `native dev`.
|
||||
|
||||
| Example | What it shows |
|
||||
| --- | --- |
|
||||
| [`calculator`](./examples/calculator) | A complete small app: markup keypad, keyboard input, chrome shortcuts, theming. |
|
||||
| [`notes`](./examples/notes) | Persistence through the effects channel: debounced writes, restore on boot, dialogs, search. |
|
||||
| [`soundboard`](./examples/soundboard) | Album grid with decoded cover art, context menus, timers, and a custom theme. |
|
||||
| [`deck`](./examples/deck) | The soundboard player rebuilt as a dense hardware chassis: two windows, same widgets, different tokens. |
|
||||
| [`feed`](./examples/feed) | A 100,000-row list, virtualized with runtime-owned scrolling. |
|
||||
|
||||
The full catalog in [examples/README.md](./examples/README.md) also covers guarded OS capabilities, GPU surfaces, WebView composition, web-frontend shells, and the iOS/Android embed hosts.
|
||||
|
||||
## Platforms
|
||||
|
||||
macOS is the primary development platform and carries the deepest support: Metal presentation, OS scroll physics, native context menus, app menus, tray, and dialogs. Linux runs the full showcase through the deterministic software renderer in real windows, with pointer, keyboard, scroll, IME composition, and HiDPI; Windows runs on a Win32 host with IME composition and is exercised in CI, including real input injection. Mobile support is experimental: iOS is simulator-proven through the embed library and Android cross-compiles with the full embed ABI, but APIs and tooling on both are still evolving — desktop is the mature surface. WebView surfaces coexist on every desktop platform. The [platform support matrix](https://zero-native.dev/platform-support) documents exactly what each host supports today.
|
||||
|
||||
## Documentation
|
||||
|
||||
The full documentation is at [zero-native.dev](https://zero-native.dev).
|
||||
|
||||
- [Quick Start](https://zero-native.dev/quick-start)
|
||||
- [Web Engines](https://zero-native.dev/web-engines)
|
||||
- [App Model](https://zero-native.dev/app-model)
|
||||
- [Native Surfaces](https://zero-native.dev/native-surfaces)
|
||||
- [Native Controls](https://zero-native.dev/native-controls)
|
||||
- [Commands](https://zero-native.dev/commands)
|
||||
- [Capabilities](https://zero-native.dev/capabilities)
|
||||
- [Platform Support](https://zero-native.dev/platform-support)
|
||||
- [Bridge](https://zero-native.dev/bridge)
|
||||
- [Security](https://zero-native.dev/security)
|
||||
- [Packaging](https://zero-native.dev/packaging)
|
||||
- [Quick Start](https://zero-native.dev/quick-start) — install to a running, tested app
|
||||
- [Philosophy](https://zero-native.dev/philosophy) — the six principles behind the toolkit
|
||||
- [App Model](https://zero-native.dev/app-model) — the model/message/update loop, wiring, and hot reload
|
||||
- [Native UI](https://zero-native.dev/native-ui) — every element, attribute, and pattern in the markup
|
||||
- [Components](https://zero-native.dev/components) — the component catalog
|
||||
- [State & Data Flow](https://zero-native.dev/state) — derive-don't-store, bindings, and text editing
|
||||
- [Testing](https://zero-native.dev/testing) — full-loop UI tests, headless on any machine
|
||||
- [Automation](https://zero-native.dev/automation) — snapshots, widget driving, record/replay, screenshots
|
||||
- [Capabilities](https://zero-native.dev/capabilities) — guarded OS services: notifications, clipboard, dialogs, credentials
|
||||
- [Packaging](https://zero-native.dev/packaging) — from binary to distributable app
|
||||
- [Platform Support](https://zero-native.dev/platform-support) — what each host supports today
|
||||
|
||||
## Examples
|
||||
## Contributing
|
||||
|
||||
Framework-specific starter examples live in `examples/`:
|
||||
Native SDK is pre-1.0: APIs still move, and the toolkit is evolving quickly. Bug reports and focused pull requests are welcome — for larger changes, open an issue first so the design can be discussed. See [CONTRIBUTING.md](./CONTRIBUTING.md) for the development setup and local checks.
|
||||
|
||||
- `examples/next`
|
||||
- `examples/react`
|
||||
- `examples/svelte`
|
||||
- `examples/vue`
|
||||
## License
|
||||
|
||||
Each example is a complete zero-native app with `app.zon`, a Zig shell, and a minimal frontend project. Run one with `zig build run` from its directory.
|
||||
|
||||
Native-first examples are available too:
|
||||
|
||||
- `examples/command-app` - shared command routing across native controls, menus, shortcuts, tray, and bridge calls
|
||||
- `examples/native-shell` - native toolbar/sidebar/statusbar chrome around WebView content
|
||||
- `examples/native-panels` - split/stack native panel composition with WebView content
|
||||
- `examples/capabilities` - guarded OS services such as notifications, clipboard, dialogs, credentials, file drops, and recent documents
|
||||
- `examples/mobile-shell` - shared metadata for the iOS and Android native shell hosts
|
||||
|
||||
Mobile embedding examples are available too:
|
||||
|
||||
- `examples/ios`
|
||||
- `examples/android`
|
||||
|
||||
These show how an iOS or Android host app links the zero-native C ABI from `libzero-native.a`.
|
||||
|
||||
For local framework development, see [CONTRIBUTING.md](./CONTRIBUTING.md).
|
||||
[Apache-2.0](./LICENSE)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# Releasing
|
||||
|
||||
Releases are manual, single-PR affairs. The maintainer controls the changelog voice and format.
|
||||
|
||||
To prepare a release:
|
||||
|
||||
1. Create a branch (e.g. `prepare-v1.2.0`)
|
||||
2. Bump the version in `packages/native-sdk/package.json`
|
||||
3. Run `npm --prefix packages/native-sdk run version:sync` to update all version references
|
||||
4. Run `scripts/changelog-merge.sh` to fold any pending `changelog.d/` fragments into the `## Unreleased` section
|
||||
5. Write the changelog entry in `CHANGELOG.md`, wrapped in `<!-- release:start -->` and `<!-- release:end -->` markers
|
||||
6. Remove the `<!-- release:start -->` and `<!-- release:end -->` markers from the previous release entry; only the latest release should have markers
|
||||
7. Open a PR and merge to `main`
|
||||
|
||||
CI compares the version in `packages/native-sdk/package.json` to what's on npm. If it differs, it cross-builds the CLI for every platform, creates the GitHub release with the binaries, publishes the per-platform binary packages (`packages/native-sdk/npm/*`), and publishes `@native-sdk/cli` last — so the main package only lands once every binary package it pins is live. If npm already has the version but the GitHub release is missing assets, CI recreates the GitHub release from the marked changelog entry.
|
||||
|
||||
Publishing uses npm trusted publishing (OIDC) — there is no npm token secret. One-time setup: on npmjs.com, each of the nine packages (`@native-sdk/cli` plus the eight `@native-sdk/cli-*` platform packages under `packages/native-sdk/npm/*`) must have a GitHub Actions trusted publisher configured with repository `vercel-labs/zero-native`, workflow `release.yml`, and environment `Release`. Every publish runs with `--provenance`. If a package is missing its trusted-publisher configuration, `npm publish` fails loudly with an OIDC authentication error for that package.
|
||||
@@ -1,7 +1,7 @@
|
||||
.{
|
||||
.id = "dev.zero_native",
|
||||
.name = "zero-native",
|
||||
.display_name = "zero-native",
|
||||
.id = "dev.native_sdk",
|
||||
.name = "native-sdk",
|
||||
.display_name = "Native SDK",
|
||||
.version = "0.1.0",
|
||||
.icons = .{ "assets/icon.icns", "assets/icon.ico" },
|
||||
.platforms = .{ "macos" },
|
||||
@@ -10,10 +10,10 @@
|
||||
.bridge = .{
|
||||
.commands = .{
|
||||
.{ .name = "native.ping", .origins = .{ "zero://inline", "zero://app" } },
|
||||
.{ .name = "zero-native.window.list", .permissions = .{ "window" }, .origins = .{ "zero://inline", "zero://app" } },
|
||||
.{ .name = "zero-native.window.create", .permissions = .{ "window" }, .origins = .{ "zero://inline", "zero://app" } },
|
||||
.{ .name = "zero-native.window.focus", .permissions = .{ "window" }, .origins = .{ "zero://inline", "zero://app" } },
|
||||
.{ .name = "zero-native.window.close", .permissions = .{ "window" }, .origins = .{ "zero://inline", "zero://app" } },
|
||||
.{ .name = "native-sdk.window.list", .permissions = .{ "window" }, .origins = .{ "zero://inline", "zero://app" } },
|
||||
.{ .name = "native-sdk.window.create", .permissions = .{ "window" }, .origins = .{ "zero://inline", "zero://app" } },
|
||||
.{ .name = "native-sdk.window.focus", .permissions = .{ "window" }, .origins = .{ "zero://inline", "zero://app" } },
|
||||
.{ .name = "native-sdk.window.close", .permissions = .{ "window" }, .origins = .{ "zero://inline", "zero://app" } },
|
||||
},
|
||||
},
|
||||
.security = .{
|
||||
@@ -25,6 +25,6 @@
|
||||
.web_engine = "system",
|
||||
.cef = .{ .dir = "third_party/cef/macos", .auto_install = false },
|
||||
.windows = .{
|
||||
.{ .label = "main", .title = "zero-native", .width = 720, .height = 480, .restore_state = true },
|
||||
.{ .label = "main", .title = "native-sdk", .width = 720, .height = 480, .restore_state = true },
|
||||
},
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 35 KiB |
@@ -0,0 +1,15 @@
|
||||
<!-- Generated by `zig build generate-icon` (tools/generate_app_icon.zig). Edit the tool, not this file. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
|
||||
<defs>
|
||||
<linearGradient id="plate" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#262626"/>
|
||||
<stop offset="1" stop-color="#171717"/>
|
||||
</linearGradient>
|
||||
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feDropShadow dx="0" dy="12" stdDeviation="16" flood-color="#000000" flood-opacity="0.3"/>
|
||||
</filter>
|
||||
</defs>
|
||||
<rect x="100" y="100" width="824" height="824" rx="185.4" fill="url(#plate)" filter="url(#shadow)"/>
|
||||
<rect x="372" y="272" width="380" height="380" rx="84" fill="#ffffff" fill-opacity="0.52"/>
|
||||
<rect x="272" y="372" width="380" height="380" rx="84" fill="#ffffff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 853 B |
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<!-- Application manifest embedded into Windows app executables. Declaring
|
||||
the common-controls v6 side-by-side dependency activates the modern
|
||||
control styling and the v6-only exports (TaskDialogIndirect); without
|
||||
it the loader binds the system-default v5 assembly. -->
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
</assembly>
|
||||
@@ -1,6 +1,6 @@
|
||||
.{
|
||||
.name = .zero_native,
|
||||
.fingerprint = 0x338d08a1e3dd81aa,
|
||||
.name = .native_sdk,
|
||||
.fingerprint = 0xc309966142f33087,
|
||||
.version = "0.1.0",
|
||||
.minimum_zig_version = "0.16.0",
|
||||
.dependencies = .{},
|
||||
@@ -11,6 +11,7 @@
|
||||
"SECURITY.md",
|
||||
"app.zon",
|
||||
"assets",
|
||||
"build",
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"docs",
|
||||
|
||||
@@ -0,0 +1,716 @@
|
||||
//! Framework build helper: `addApp` gives a markup/builder app a complete
|
||||
//! build (exe, run, test) from a ~5-line build.zig. The app supplies
|
||||
//! src/main.zig, app.zon, and assets; the runner and all framework modules
|
||||
//! come from the native-sdk dependency.
|
||||
|
||||
const std = @import("std");
|
||||
|
||||
const PlatformOption = enum {
|
||||
auto,
|
||||
null,
|
||||
macos,
|
||||
linux,
|
||||
windows,
|
||||
};
|
||||
|
||||
const TraceOption = enum {
|
||||
off,
|
||||
events,
|
||||
runtime,
|
||||
all,
|
||||
};
|
||||
|
||||
const WebEngineOption = enum {
|
||||
system,
|
||||
chromium,
|
||||
};
|
||||
|
||||
pub const AppOptions = struct {
|
||||
name: []const u8,
|
||||
/// App entry point; defaults to src/main.zig (relative to `app_root`).
|
||||
main: []const u8 = "src/main.zig",
|
||||
/// Root of the app source tree, relative to the build root. "." for a
|
||||
/// build.zig that lives in the app directory (every ejected app). The
|
||||
/// CLI's generated build graph under `<app>/.native/build/` passes
|
||||
/// "../.." so `src/`, `app.zon`, and `assets/` keep resolving in the
|
||||
/// app directory rather than the cache directory.
|
||||
app_root: []const u8 = ".",
|
||||
};
|
||||
|
||||
/// The `native_sdk_app_*` C ABI every embed static library exports.
|
||||
pub const mobile_export_symbol_names = [_][]const u8{
|
||||
"native_sdk_app_create",
|
||||
"native_sdk_app_destroy",
|
||||
"native_sdk_app_start",
|
||||
"native_sdk_app_activate",
|
||||
"native_sdk_app_deactivate",
|
||||
"native_sdk_app_stop",
|
||||
"native_sdk_app_resize",
|
||||
"native_sdk_app_viewport",
|
||||
"native_sdk_app_viewport_state",
|
||||
"native_sdk_app_gpu_frame_state",
|
||||
"native_sdk_app_text_input_state",
|
||||
"native_sdk_app_set_text_measure",
|
||||
"native_sdk_app_set_audio_service",
|
||||
"native_sdk_app_audio_event",
|
||||
"native_sdk_app_set_image_service",
|
||||
"native_sdk_app_set_automation_dir",
|
||||
"native_sdk_app_touch",
|
||||
"native_sdk_app_scroll",
|
||||
"native_sdk_app_key",
|
||||
"native_sdk_app_text",
|
||||
"native_sdk_app_ime",
|
||||
"native_sdk_app_command",
|
||||
"native_sdk_app_frame",
|
||||
"native_sdk_app_chrome_tab_count",
|
||||
"native_sdk_app_chrome_tab_at",
|
||||
"native_sdk_app_chrome_primary_action",
|
||||
"native_sdk_app_chrome_selected_tab",
|
||||
"native_sdk_app_chrome_navigation_depth",
|
||||
"native_sdk_app_chrome_navigation_back_command",
|
||||
"native_sdk_app_chrome_icon_pixels",
|
||||
"native_sdk_app_set_form_factor",
|
||||
"native_sdk_app_set_chrome_tabs_projected",
|
||||
"native_sdk_app_set_asset_root",
|
||||
"native_sdk_app_set_asset_entry",
|
||||
"native_sdk_app_last_command_count",
|
||||
"native_sdk_app_last_command_name",
|
||||
"native_sdk_app_last_error_name",
|
||||
"native_sdk_app_widget_semantics_count",
|
||||
"native_sdk_app_widget_semantics_at",
|
||||
"native_sdk_app_widget_semantics_by_id",
|
||||
"native_sdk_app_widget_text_geometry",
|
||||
"native_sdk_app_widget_action",
|
||||
"native_sdk_app_render_pixel_size",
|
||||
"native_sdk_app_render_pixels",
|
||||
"native_sdk_app_render_pixels_damage",
|
||||
};
|
||||
|
||||
pub const MobileSceneOption = enum {
|
||||
/// The user app's UiApp on a gpu_surface view (window 1,
|
||||
/// "mobile-surface"), pumped by the host's frame callback.
|
||||
canvas,
|
||||
/// The fixed WebView shell the ios/android/mobile-shell examples embed
|
||||
/// today; the app module is not compiled in.
|
||||
webview,
|
||||
};
|
||||
|
||||
pub const MobileLibOptions = struct {
|
||||
name: []const u8,
|
||||
/// Mobile app entry (the `"app"` module the embed host drives); must
|
||||
/// declare `Model`, `Msg`, `initModel`, and `mobileOptions` — see
|
||||
/// `src/embed/ui_host.zig`. Ignored for `.scene = .webview`.
|
||||
main: []const u8 = "src/main.zig",
|
||||
scene: MobileSceneOption = .canvas,
|
||||
};
|
||||
|
||||
/// Mobile counterpart of `addApp`: produce the embed static library
|
||||
/// (`native_sdk_app_*` C ABI) compiled with the user's UiApp. Call it from
|
||||
/// a standalone build.zig (it registers the standard `target`/`optimize`
|
||||
/// options itself).
|
||||
pub fn addMobileLib(b: *std.Build, dep: *std.Build.Dependency, options: MobileLibOptions) void {
|
||||
const target = nativeSdkTarget(b);
|
||||
const optimize_request = b.option(std.builtin.OptimizeMode, "optimize", "Prioritize performance, safety, or binary size");
|
||||
const optimize = exampleOptimizeMode(b, optimize_request, .Debug);
|
||||
addMobileLibWithTarget(b, dep, target, optimize, options);
|
||||
}
|
||||
|
||||
/// The mobile-lib wiring behind `addMobileLib`, for builds that already
|
||||
/// resolved `target`/`optimize` (`addAppArtifacts` registers the `lib`
|
||||
/// step through this for iOS/Android targets, so every standard app —
|
||||
/// generated graph or ejected `addApp` — can produce the embed library
|
||||
/// with nothing but `-Dtarget`).
|
||||
fn addMobileLibWithTarget(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, options: MobileLibOptions) void {
|
||||
const native_sdk_mod = nativeSdkModule(b, dep, target, optimize);
|
||||
// Android hosts load the embed lib inside a shared object
|
||||
// (System.loadLibrary / NativeActivity), so every object must be PIC —
|
||||
// without it Zig emits local-exec TLS relocations (R_AARCH64_TLSLE_*)
|
||||
// that the NDK linker rejects when producing the shim .so. Imported
|
||||
// modules leave `pic` null and inherit this from the root module.
|
||||
const pic: ?bool = if (target.result.abi.isAndroid()) true else null;
|
||||
const exports_mod = b.createModule(.{
|
||||
.root_source_file = dep.path(switch (options.scene) {
|
||||
.canvas => "src/embed/app_exports.zig",
|
||||
.webview => "src/embed/c_exports.zig",
|
||||
}),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
.pic = pic,
|
||||
});
|
||||
exports_mod.addImport("native_sdk", native_sdk_mod);
|
||||
if (options.scene == .canvas) {
|
||||
const app_mod = localModule(b, target, optimize, options.main);
|
||||
app_mod.addImport("native_sdk", native_sdk_mod);
|
||||
exports_mod.addImport("app", app_mod);
|
||||
}
|
||||
exports_mod.export_symbol_names = &mobile_export_symbol_names;
|
||||
|
||||
const lib = b.addLibrary(.{
|
||||
.linkage = .static,
|
||||
.name = options.name,
|
||||
.root_module = exports_mod,
|
||||
// The embed C ABI (`native_sdk_app_viewport`) is exactly the
|
||||
// f32-heavy SysV signature Zig 0.16.0's self-hosted x86_64 backend
|
||||
// miscompiles (see useLlvmWorkaround in the framework build.zig):
|
||||
// clang-compiled hosts calling a self-hosted Debug lib receive
|
||||
// corrupted inset/keyboard floats on x86_64 (Android emulators,
|
||||
// Intel simulators). Force LLVM there; Release already uses it.
|
||||
.use_llvm = useLlvmWorkaround(target),
|
||||
});
|
||||
b.installArtifact(lib);
|
||||
|
||||
const lib_step = b.step("lib", "Build the mobile embed static library");
|
||||
lib_step.dependOn(&b.addInstallArtifact(lib, .{}).step);
|
||||
}
|
||||
|
||||
/// The pieces `addApp` wires, for callers that extend the standard app
|
||||
/// build (extra native sources, frameworks, post-build steps such as
|
||||
/// entitlement signing). `install` is the artifact-install step behind the
|
||||
/// default `zig build`; append dependencies to it and to `run` to order
|
||||
/// work between the emitted binary and its consumers.
|
||||
pub const AppArtifacts = struct {
|
||||
exe: *std.Build.Step.Compile,
|
||||
tests: *std.Build.Step.Compile,
|
||||
install: *std.Build.Step.InstallArtifact,
|
||||
run: *std.Build.Step.Run,
|
||||
};
|
||||
|
||||
pub fn addApp(b: *std.Build, dep: *std.Build.Dependency, app_options: AppOptions) void {
|
||||
_ = addAppArtifacts(b, dep, app_options);
|
||||
}
|
||||
|
||||
pub fn addAppArtifacts(b: *std.Build, dep: *std.Build.Dependency, app_options: AppOptions) AppArtifacts {
|
||||
const target = nativeSdkTarget(b);
|
||||
const optimize_request = b.option(std.builtin.OptimizeMode, "optimize", "Prioritize performance, safety, or binary size");
|
||||
const optimize = exampleOptimizeMode(b, optimize_request, .Debug);
|
||||
const app_optimize = exampleOptimizeMode(b, optimize_request, .ReleaseFast);
|
||||
|
||||
// Mobile targets get the embed static library as a `lib` step: the
|
||||
// artifact the toolkit-owned iOS host (and any hand-written shim)
|
||||
// links, so `native dev|package --target ios` works against every
|
||||
// standard app build — generated graph or ejected — with nothing but
|
||||
// `-Dtarget`. Desktop targets keep the step absent.
|
||||
if (target.result.os.tag == .ios or target.result.abi.isAndroid()) {
|
||||
addMobileLibWithTarget(b, dep, target, optimize, .{
|
||||
.name = app_options.name,
|
||||
.main = appPath(b, app_options.app_root, app_options.main),
|
||||
});
|
||||
}
|
||||
const platform_option = b.option(PlatformOption, "platform", "Desktop backend: auto, null, macos, linux, windows") orelse .auto;
|
||||
const trace_option = b.option(TraceOption, "trace", "Trace output: off, events, runtime, all") orelse .events;
|
||||
const debug_overlay = b.option(bool, "debug-overlay", "Enable debug overlay output") orelse false;
|
||||
const automation_enabled = b.option(bool, "automation", "Enable Native SDK automation artifacts") orelse false;
|
||||
const js_bridge_enabled = b.option(bool, "js-bridge", "Enable optional JavaScript bridge stubs") orelse false;
|
||||
const web_engine_override = b.option(WebEngineOption, "web-engine", "Override app.zon web engine: system, chromium");
|
||||
const cef_dir_override = b.option([]const u8, "cef-dir", "Override CEF root directory for Chromium builds");
|
||||
const cef_auto_install_override = b.option(bool, "cef-auto-install", "Override app.zon CEF auto-install setting");
|
||||
const selected_platform: PlatformOption = switch (platform_option) {
|
||||
.auto => if (target.result.os.tag == .macos) .macos else if (target.result.os.tag == .linux) .linux else if (target.result.os.tag == .windows) .windows else .null,
|
||||
else => platform_option,
|
||||
};
|
||||
if (selected_platform == .macos and target.result.os.tag != .macos) {
|
||||
@panic("-Dplatform=macos requires a macOS target");
|
||||
}
|
||||
if (selected_platform == .linux and target.result.os.tag != .linux) {
|
||||
@panic("-Dplatform=linux requires a Linux target");
|
||||
}
|
||||
if (selected_platform == .windows and target.result.os.tag != .windows) {
|
||||
@panic("-Dplatform=windows requires a Windows target");
|
||||
}
|
||||
const app_web_engine = appWebEngineConfig(b, app_options.app_root);
|
||||
const web_engine = web_engine_override orelse app_web_engine.web_engine;
|
||||
const cef_dir = cef_dir_override orelse defaultCefDir(selected_platform, app_web_engine.cef_dir);
|
||||
const cef_auto_install = cef_auto_install_override orelse app_web_engine.cef_auto_install;
|
||||
if (web_engine == .chromium and selected_platform != .macos) {
|
||||
@panic("-Dweb-engine=chromium currently requires -Dplatform=macos");
|
||||
}
|
||||
|
||||
const options = b.addOptions();
|
||||
options.addOption([]const u8, "platform", switch (selected_platform) {
|
||||
.auto => unreachable,
|
||||
.null => "null",
|
||||
.macos => "macos",
|
||||
.linux => "linux",
|
||||
.windows => "windows",
|
||||
});
|
||||
options.addOption([]const u8, "trace", @tagName(trace_option));
|
||||
options.addOption([]const u8, "web_engine", @tagName(web_engine));
|
||||
options.addOption(bool, "debug_overlay", debug_overlay);
|
||||
options.addOption(bool, "automation", automation_enabled);
|
||||
options.addOption(bool, "js_bridge", js_bridge_enabled);
|
||||
const options_mod = options.createModule();
|
||||
|
||||
const app_mod = appModule(b, dep, target, app_optimize, app_options, options_mod);
|
||||
const exe = b.addExecutable(.{
|
||||
.name = app_options.name,
|
||||
.root_module = app_mod,
|
||||
});
|
||||
linkPlatform(b, dep, target, app_mod, exe, selected_platform, web_engine, cef_dir, cef_auto_install);
|
||||
const install = b.addInstallArtifact(exe, .{});
|
||||
b.getInstallStep().dependOn(&install.step);
|
||||
|
||||
const run = b.addRunArtifact(exe);
|
||||
addCefRuntimeRunFiles(b, target, run, exe, web_engine, cef_dir);
|
||||
const run_step = b.step("run", "Run the app");
|
||||
run_step.dependOn(&run.step);
|
||||
|
||||
const test_app_mod = if (app_optimize == optimize) app_mod else appModule(b, dep, target, optimize, app_options, options_mod);
|
||||
const tests = b.addTest(.{ .root_module = test_app_mod, .use_llvm = useLlvmWorkaround(target) });
|
||||
const test_step = b.step("test", "Run tests");
|
||||
test_step.dependOn(&b.addRunArtifact(tests).step);
|
||||
|
||||
// `zig build model-contract`: reflect the app's Model/Msg into
|
||||
// zig-out/model-contract.zon so `native check` can verify markup
|
||||
// bindings against the app's real surface without compiling the app.
|
||||
// The artifact carries a hash over the app's Zig sources; the checker
|
||||
// degrades to structural checking when it goes stale. Apps without a
|
||||
// pub Model/Msg pair make this a silent no-op. The test step refreshes
|
||||
// the artifact too, so CI-checked apps always hold a fresh one.
|
||||
const contract_root = b.addWriteFiles().add("model_contract_emit.zig",
|
||||
\\//! Generated by the app build: emits the model contract artifact
|
||||
\\//! (see the toolkit's ui_markup_contract.zig).
|
||||
\\const std = @import("std");
|
||||
\\const native_sdk = @import("native_sdk");
|
||||
\\const app = @import("app");
|
||||
\\
|
||||
\\pub fn main(init: std.process.Init) !void {
|
||||
\\ try native_sdk.canvas.emitModelContractMain(app, init);
|
||||
\\}
|
||||
\\
|
||||
);
|
||||
// The emit root must share the app module's native_sdk instance so
|
||||
// the Msg payload types it classifies are the same types the app
|
||||
// declares its variants with.
|
||||
const contract_mod = b.createModule(.{
|
||||
.root_source_file = contract_root,
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
contract_mod.addImport("app", test_app_mod);
|
||||
if (test_app_mod.import_table.get("native_sdk")) |sdk_mod| {
|
||||
contract_mod.addImport("native_sdk", sdk_mod);
|
||||
}
|
||||
const contract_exe = b.addExecutable(.{
|
||||
.name = b.fmt("{s}-model-contract", .{app_options.name}),
|
||||
.root_module = contract_mod,
|
||||
.use_llvm = useLlvmWorkaround(target),
|
||||
});
|
||||
const contract_run = b.addRunArtifact(contract_exe);
|
||||
contract_run.setCwd(b.path(app_options.app_root));
|
||||
contract_run.addArgs(&.{ "--src", "src", "--out", "zig-out/model-contract.zon" });
|
||||
contract_run.has_side_effects = true;
|
||||
const contract_step = b.step("model-contract", "Emit zig-out/model-contract.zon for `native check`");
|
||||
contract_step.dependOn(&contract_run.step);
|
||||
test_step.dependOn(&contract_run.step);
|
||||
|
||||
// `zig build package`: bundle the built binary through the `native`
|
||||
// CLI (built from the native_sdk dependency), so a scaffolded app can
|
||||
// package itself without locating the CLI by hand.
|
||||
const host_os = b.graph.host.result.os.tag;
|
||||
const package_target: ?[]const u8 = switch (host_os) {
|
||||
.macos => "macos",
|
||||
.linux => "linux",
|
||||
.windows => "windows",
|
||||
else => null,
|
||||
};
|
||||
if (package_target) |package_target_name| {
|
||||
const package_run = b.addRunArtifact(dep.artifact("native"));
|
||||
package_run.addArgs(&.{ "package", "--target", package_target_name, "--manifest", "app.zon", "--output" });
|
||||
package_run.addArg(if (host_os == .macos)
|
||||
b.fmt("zig-out/package/{s}.app", .{app_options.name})
|
||||
else
|
||||
b.fmt("zig-out/package/{s}", .{package_target_name}));
|
||||
package_run.addArg("--binary");
|
||||
package_run.addFileArg(exe.getEmittedBin());
|
||||
// The archive and report names carry an optimize label; this
|
||||
// build graph knows the packaged binary's REAL mode, so forward
|
||||
// it instead of letting the CLI assume one.
|
||||
package_run.addArgs(&.{ "--optimize", @tagName(app_optimize) });
|
||||
package_run.has_side_effects = true;
|
||||
const package_step = b.step("package", "Create a distributable package via the native CLI");
|
||||
package_step.dependOn(&package_run.step);
|
||||
}
|
||||
|
||||
return .{ .exe = exe, .tests = tests, .install = install, .run = run };
|
||||
}
|
||||
|
||||
/// Zig 0.16.0's self-hosted x86_64 backend miscompiles the SysV C calling
|
||||
/// convention for f32-heavy signatures with interleaved pointer arguments
|
||||
/// (`native_sdk_app_viewport`: 11 f32s + 2 pointers): both the caller and
|
||||
/// the callee place/read the wrong registers and stack slots, so safe-area
|
||||
/// insets arrive as garbage on x86_64 Debug builds while every LLVM-backed
|
||||
/// build is correct. Minimal repro (fails under `zig test`, passes with
|
||||
/// `-fllvm` on x86_64-linux):
|
||||
///
|
||||
/// fn take(a: ?*anyopaque, w: f32, h: f32, s: f32, p: ?*anyopaque,
|
||||
/// t: f32, r: f32, bo: f32, l: f32, kt: f32, kr: f32, kb: f32,
|
||||
/// kl: f32) callconv(.c) void { ... }
|
||||
///
|
||||
/// Force the LLVM backend on x86_64 until the upstream backend is fixed;
|
||||
/// Release modes already default to LLVM, so this only changes Debug.
|
||||
pub fn useLlvmWorkaround(target: std.Build.ResolvedTarget) ?bool {
|
||||
return if (target.result.cpu.arch == .x86_64) true else null;
|
||||
}
|
||||
|
||||
fn exampleOptimizeMode(b: *std.Build, requested: ?std.builtin.OptimizeMode, default_mode: std.builtin.OptimizeMode) std.builtin.OptimizeMode {
|
||||
if (requested) |mode| return mode;
|
||||
return switch (b.release_mode) {
|
||||
.off => default_mode,
|
||||
.any, .fast => .ReleaseFast,
|
||||
.safe => .ReleaseSafe,
|
||||
.small => .ReleaseSmall,
|
||||
};
|
||||
}
|
||||
|
||||
fn appModule(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, app_options: AppOptions, options_mod: *std.Build.Module) *std.Build.Module {
|
||||
const native_sdk_mod = nativeSdkModule(b, dep, target, optimize);
|
||||
const runner_mod = b.createModule(.{
|
||||
.root_source_file = dep.path("src/app_runner/root.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
runner_mod.addImport("native_sdk", native_sdk_mod);
|
||||
runner_mod.addImport("build_options", options_mod);
|
||||
runner_mod.addImport("app_manifest_zon", b.createModule(.{ .root_source_file = b.path(appPath(b, app_options.app_root, "app.zon")) }));
|
||||
|
||||
const app_mod = localModule(b, target, optimize, appPath(b, app_options.app_root, app_options.main));
|
||||
app_mod.addImport("native_sdk", native_sdk_mod);
|
||||
app_mod.addImport("runner", runner_mod);
|
||||
return app_mod;
|
||||
}
|
||||
|
||||
fn nativeSdkTarget(b: *std.Build) std.Build.ResolvedTarget {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
if (target.result.os.tag != .macos) return target;
|
||||
|
||||
if (b.sysroot == null) {
|
||||
b.sysroot = macosSdkPath(b) orelse b.sysroot;
|
||||
}
|
||||
|
||||
var query = target.query;
|
||||
query.os_tag = .macos;
|
||||
query.os_version_min = .{ .semver = .{ .major = 11, .minor = 0, .patch = 0 } };
|
||||
return b.resolveTargetQuery(query);
|
||||
}
|
||||
|
||||
fn macosSdkPath(b: *std.Build) ?[]const u8 {
|
||||
if (b.graph.environ_map.get("SDKROOT")) |sdkroot| {
|
||||
if (sdkroot.len > 0) return sdkroot;
|
||||
}
|
||||
|
||||
const result = std.process.run(b.allocator, b.graph.io, .{
|
||||
.argv = &.{ "xcrun", "--sdk", "macosx", "--show-sdk-path" },
|
||||
.stdout_limit = .limited(4096),
|
||||
.stderr_limit = .limited(4096),
|
||||
}) catch return null;
|
||||
defer b.allocator.free(result.stderr);
|
||||
if (result.term != .exited or result.term.exited != 0) {
|
||||
b.allocator.free(result.stdout);
|
||||
return null;
|
||||
}
|
||||
return std.mem.trimEnd(u8, result.stdout, "\r\n");
|
||||
}
|
||||
|
||||
fn localModule(b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, path: []const u8) *std.Build.Module {
|
||||
return b.createModule(.{
|
||||
.root_source_file = b.path(path),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
}
|
||||
|
||||
fn nativeSdkModule(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) *std.Build.Module {
|
||||
const geometry_mod = externalModule(b, dep, target, optimize, "src/primitives/geometry/root.zig");
|
||||
const assets_mod = externalModule(b, dep, target, optimize, "src/primitives/assets/root.zig");
|
||||
const app_dirs_mod = externalModule(b, dep, target, optimize, "src/primitives/app_dirs/root.zig");
|
||||
const trace_mod = externalModule(b, dep, target, optimize, "src/primitives/trace/root.zig");
|
||||
const app_manifest_mod = externalModule(b, dep, target, optimize, "src/primitives/app_manifest/root.zig");
|
||||
const diagnostics_mod = externalModule(b, dep, target, optimize, "src/primitives/diagnostics/root.zig");
|
||||
const platform_info_mod = externalModule(b, dep, target, optimize, "src/primitives/platform_info/root.zig");
|
||||
const json_mod = externalModule(b, dep, target, optimize, "src/primitives/json/root.zig");
|
||||
const canvas_mod = externalModule(b, dep, target, optimize, "src/primitives/canvas/root.zig");
|
||||
canvas_mod.addImport("geometry", geometry_mod);
|
||||
canvas_mod.addImport("json", json_mod);
|
||||
const debug_mod = externalModule(b, dep, target, optimize, "src/debug/root.zig");
|
||||
debug_mod.addImport("app_dirs", app_dirs_mod);
|
||||
debug_mod.addImport("trace", trace_mod);
|
||||
|
||||
const native_sdk_mod = externalModule(b, dep, target, optimize, "src/root.zig");
|
||||
native_sdk_mod.addImport("geometry", geometry_mod);
|
||||
native_sdk_mod.addImport("assets", assets_mod);
|
||||
native_sdk_mod.addImport("app_dirs", app_dirs_mod);
|
||||
native_sdk_mod.addImport("trace", trace_mod);
|
||||
native_sdk_mod.addImport("app_manifest", app_manifest_mod);
|
||||
native_sdk_mod.addImport("diagnostics", diagnostics_mod);
|
||||
native_sdk_mod.addImport("platform_info", platform_info_mod);
|
||||
native_sdk_mod.addImport("json", json_mod);
|
||||
native_sdk_mod.addImport("canvas", canvas_mod);
|
||||
return native_sdk_mod;
|
||||
}
|
||||
|
||||
fn externalModule(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, path: []const u8) *std.Build.Module {
|
||||
return b.createModule(.{
|
||||
.root_source_file = dep.path(path),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
}
|
||||
|
||||
// -fno-sanitize=builtin on every ObjC compile: Zig 0.16.0's Debug UBSan
|
||||
// aborts any process whose first dispatch_once runs — the macOS SDK's
|
||||
// inline `_dispatch_once` ends in `__builtin_assume(*predicate == ~0l)`
|
||||
// (dispatch/once.h), Zig's bundled clang instruments that builtin, and the
|
||||
// check fires spuriously at startup; zig's ubsan_rt then cannot even decode
|
||||
// the report ("invalid enum value" / "passing zero to clz()" panics).
|
||||
// Reproduced with a 10-line `zig cc` program against both the 14.5 and
|
||||
// 26.0 SDKs. Release builds never hit it (no UBSan), which is why only
|
||||
// Debug-built examples (standardOptimizeOption default) crashed.
|
||||
fn linkPlatform(b: *std.Build, dep: *std.Build.Dependency, target: std.Build.ResolvedTarget, app_mod: *std.Build.Module, exe: *std.Build.Step.Compile, platform: PlatformOption, web_engine: WebEngineOption, cef_dir: []const u8, cef_auto_install: bool) void {
|
||||
if (platform == .macos) {
|
||||
switch (web_engine) {
|
||||
.system => {
|
||||
const sdk_include = if (b.sysroot) |sysroot| b.fmt("-I{s}/usr/include", .{sysroot}) else "";
|
||||
const flags: []const []const u8 = if (b.sysroot) |sysroot| &.{ "-fobjc-arc", "-fno-sanitize=builtin", "-ObjC", "-mmacosx-version-min=11.0", "-isysroot", sysroot, sdk_include } else &.{ "-fobjc-arc", "-fno-sanitize=builtin", "-ObjC", "-mmacosx-version-min=11.0" };
|
||||
app_mod.addCSourceFile(.{ .file = dep.path("src/platform/macos/appkit_host.m"), .flags = flags });
|
||||
app_mod.linkFramework("WebKit", .{});
|
||||
},
|
||||
.chromium => {
|
||||
const cef_check = addCefCheck(b, target, cef_dir);
|
||||
if (cef_auto_install) {
|
||||
const cef_auto = b.addSystemCommand(&.{ "native", "cef", "install", "--dir", cef_dir });
|
||||
cef_check.step.dependOn(&cef_auto.step);
|
||||
}
|
||||
exe.step.dependOn(&cef_check.step);
|
||||
const include_arg = b.fmt("-I{s}", .{cef_dir});
|
||||
const define_arg = b.fmt("-DNATIVE_SDK_CEF_DIR=\"{s}\"", .{cef_dir});
|
||||
// The SDK's usr/include must stay a system include dir (searched after zig's
|
||||
// bundled libc++/libc headers). A plain -I shadows libc++'s <string.h>/<math.h>
|
||||
// wrappers in ObjC++ and surfaces SDK nullability gaps as a diagnostic flood.
|
||||
const sdk_include = if (b.sysroot) |sysroot| b.fmt("-isystem{s}/usr/include", .{sysroot}) else "";
|
||||
const flags: []const []const u8 = if (b.sysroot) |sysroot| &.{ "-fobjc-arc", "-fno-sanitize=builtin", "-ObjC++", "-std=c++17", "-stdlib=libc++", "-mmacosx-version-min=11.0", "-isysroot", sysroot, sdk_include, include_arg, define_arg } else &.{ "-fobjc-arc", "-fno-sanitize=builtin", "-ObjC++", "-std=c++17", "-stdlib=libc++", "-mmacosx-version-min=11.0", include_arg, define_arg };
|
||||
app_mod.addCSourceFile(.{ .file = dep.path("src/platform/macos/cef_host.mm"), .flags = flags });
|
||||
app_mod.addObjectFile(b.path(b.fmt("{s}/libcef_dll_wrapper/libcef_dll_wrapper.a", .{cef_dir})));
|
||||
app_mod.addFrameworkPath(b.path(b.fmt("{s}/Release", .{cef_dir})));
|
||||
app_mod.linkFramework("Chromium Embedded Framework", .{});
|
||||
app_mod.addRPath(.{ .cwd_relative = "@executable_path/Frameworks" });
|
||||
},
|
||||
}
|
||||
if (b.sysroot) |sysroot| {
|
||||
app_mod.addFrameworkPath(.{ .cwd_relative = b.pathJoin(&.{ sysroot, "System/Library/Frameworks" }) });
|
||||
}
|
||||
app_mod.linkFramework("AppKit", .{});
|
||||
// The audio playback service (the AppKit host's single AVPlayer).
|
||||
app_mod.linkFramework("AVFoundation", .{});
|
||||
// Spectrum analysis of the app's own playback: the MediaToolbox
|
||||
// audio tap hands the player's PCM to the host, and Accelerate
|
||||
// (vDSP) turns it into band magnitudes.
|
||||
app_mod.linkFramework("MediaToolbox", .{});
|
||||
app_mod.linkFramework("Accelerate", .{});
|
||||
app_mod.linkFramework("Foundation", .{});
|
||||
app_mod.linkFramework("CoreText", .{});
|
||||
app_mod.linkFramework("UniformTypeIdentifiers", .{});
|
||||
app_mod.linkFramework("Security", .{});
|
||||
app_mod.linkFramework("Metal", .{});
|
||||
app_mod.linkFramework("QuartzCore", .{});
|
||||
app_mod.linkSystemLibrary("c", .{});
|
||||
if (web_engine == .chromium) app_mod.linkSystemLibrary("c++", .{});
|
||||
} else if (platform == .linux) {
|
||||
switch (web_engine) {
|
||||
.system => {
|
||||
app_mod.addCSourceFile(.{ .file = dep.path("src/platform/linux/gtk_host.c"), .flags = &.{} });
|
||||
app_mod.linkSystemLibrary("gtk4", .{});
|
||||
app_mod.linkSystemLibrary("webkitgtk-6.0", .{});
|
||||
app_mod.linkSystemLibrary("dl", .{});
|
||||
},
|
||||
.chromium => {
|
||||
const cef_check = addCefCheck(b, target, cef_dir);
|
||||
if (cef_auto_install) {
|
||||
const cef_auto = b.addSystemCommand(&.{ "native", "cef", "install", "--dir", cef_dir });
|
||||
cef_check.step.dependOn(&cef_auto.step);
|
||||
}
|
||||
exe.step.dependOn(&cef_check.step);
|
||||
const include_arg = b.fmt("-I{s}", .{cef_dir});
|
||||
const define_arg = b.fmt("-DNATIVE_SDK_CEF_DIR=\"{s}\"", .{cef_dir});
|
||||
app_mod.addCSourceFile(.{ .file = dep.path("src/platform/linux/cef_host.cpp"), .flags = &.{ "-std=c++17", include_arg, define_arg } });
|
||||
app_mod.addObjectFile(b.path(b.fmt("{s}/libcef_dll_wrapper/libcef_dll_wrapper.a", .{cef_dir})));
|
||||
app_mod.addLibraryPath(b.path(b.fmt("{s}/Release", .{cef_dir})));
|
||||
app_mod.linkSystemLibrary("cef", .{});
|
||||
app_mod.addRPath(.{ .cwd_relative = "$ORIGIN" });
|
||||
},
|
||||
}
|
||||
app_mod.linkSystemLibrary("c", .{});
|
||||
if (web_engine == .chromium) app_mod.linkSystemLibrary("stdc++", .{});
|
||||
} else if (platform == .windows) {
|
||||
// Common-controls v6 side-by-side dependency: without this
|
||||
// manifest the loader binds the system-default v5 assembly, which
|
||||
// renders classic-styled controls and lacks the v6-only exports.
|
||||
exe.win32_manifest = dep.path("assets/native-sdk.manifest");
|
||||
switch (web_engine) {
|
||||
.system => app_mod.addCSourceFile(.{ .file = dep.path("src/platform/windows/webview2_host.cpp"), .flags = &.{"-std=c++17"} }),
|
||||
.chromium => {
|
||||
const cef_check = addCefCheck(b, target, cef_dir);
|
||||
if (cef_auto_install) {
|
||||
const cef_auto = b.addSystemCommand(&.{ "native", "cef", "install", "--dir", cef_dir });
|
||||
cef_check.step.dependOn(&cef_auto.step);
|
||||
}
|
||||
exe.step.dependOn(&cef_check.step);
|
||||
const include_arg = b.fmt("-I{s}", .{cef_dir});
|
||||
const define_arg = b.fmt("-DNATIVE_SDK_CEF_DIR=\"{s}\"", .{cef_dir});
|
||||
app_mod.addCSourceFile(.{ .file = dep.path("src/platform/windows/cef_host.cpp"), .flags = &.{ "-std=c++17", include_arg, define_arg } });
|
||||
app_mod.addObjectFile(b.path(b.fmt("{s}/libcef_dll_wrapper/libcef_dll_wrapper.lib", .{cef_dir})));
|
||||
app_mod.addLibraryPath(b.path(b.fmt("{s}/Release", .{cef_dir})));
|
||||
},
|
||||
}
|
||||
app_mod.linkSystemLibrary("c", .{});
|
||||
app_mod.linkSystemLibrary("c++", .{});
|
||||
app_mod.linkSystemLibrary("user32", .{});
|
||||
app_mod.linkSystemLibrary("gdi32", .{});
|
||||
app_mod.linkSystemLibrary("imm32", .{});
|
||||
app_mod.linkSystemLibrary("comctl32", .{});
|
||||
app_mod.linkSystemLibrary("ole32", .{});
|
||||
app_mod.linkSystemLibrary("oleacc", .{});
|
||||
app_mod.linkSystemLibrary("shell32", .{});
|
||||
// The audio backend: Media Foundation (session + source resolver
|
||||
// + streaming audio renderer) and WinHTTP (the cache fill).
|
||||
app_mod.linkSystemLibrary("mf", .{});
|
||||
app_mod.linkSystemLibrary("mfplat", .{});
|
||||
app_mod.linkSystemLibrary("winhttp", .{});
|
||||
if (web_engine == .chromium) app_mod.linkSystemLibrary("libcef", .{});
|
||||
}
|
||||
}
|
||||
|
||||
fn addCefRuntimeRunFiles(b: *std.Build, target: std.Build.ResolvedTarget, run: *std.Build.Step.Run, exe: *std.Build.Step.Compile, web_engine: WebEngineOption, cef_dir: []const u8) void {
|
||||
if (web_engine != .chromium) return;
|
||||
if (target.result.os.tag != .macos) return;
|
||||
const copy = b.addSystemCommand(&.{
|
||||
"sh", "-c",
|
||||
b.fmt(
|
||||
\\set -e
|
||||
\\exe="$0"
|
||||
\\exe_dir="$(dirname "$exe")"
|
||||
\\rm -rf "zig-out/Frameworks/Chromium Embedded Framework.framework" "zig-out/bin/Frameworks/Chromium Embedded Framework.framework" ".zig-cache/o/Frameworks/Chromium Embedded Framework.framework" &&
|
||||
\\mkdir -p "zig-out/Frameworks" "zig-out/bin/Frameworks" ".zig-cache/o/Frameworks" "$exe_dir" &&
|
||||
\\cp -R "{s}/Release/Chromium Embedded Framework.framework" "zig-out/Frameworks/" &&
|
||||
\\cp -R "{s}/Release/Chromium Embedded Framework.framework" "zig-out/bin/Frameworks/" &&
|
||||
\\cp -R "{s}/Release/Chromium Embedded Framework.framework" ".zig-cache/o/Frameworks/" &&
|
||||
\\cp "{s}/Release/Chromium Embedded Framework.framework/Libraries/libEGL.dylib" "$exe_dir/" &&
|
||||
\\cp "{s}/Release/Chromium Embedded Framework.framework/Libraries/libGLESv2.dylib" "$exe_dir/" &&
|
||||
\\cp "{s}/Release/Chromium Embedded Framework.framework/Libraries/libvk_swiftshader.dylib" "$exe_dir/" &&
|
||||
\\cp "{s}/Release/Chromium Embedded Framework.framework/Libraries/vk_swiftshader_icd.json" "$exe_dir/"
|
||||
, .{ cef_dir, cef_dir, cef_dir, cef_dir, cef_dir, cef_dir, cef_dir }),
|
||||
});
|
||||
copy.addFileArg(exe.getEmittedBin());
|
||||
run.step.dependOn(©.step);
|
||||
}
|
||||
|
||||
fn addCefCheck(b: *std.Build, target: std.Build.ResolvedTarget, cef_dir: []const u8) *std.Build.Step.Run {
|
||||
const script = switch (target.result.os.tag) {
|
||||
.macos => b.fmt(
|
||||
\\test -f "{s}/include/cef_app.h" &&
|
||||
\\test -d "{s}/Release/Chromium Embedded Framework.framework" &&
|
||||
\\test -f "{s}/libcef_dll_wrapper/libcef_dll_wrapper.a" || {{
|
||||
\\ echo "missing CEF dependency for -Dweb-engine=chromium" >&2
|
||||
\\ echo "Fix with: native cef install --dir {s}" >&2
|
||||
\\ exit 1
|
||||
\\}}
|
||||
, .{ cef_dir, cef_dir, cef_dir, cef_dir }),
|
||||
.linux => b.fmt(
|
||||
\\test -f "{s}/include/cef_app.h" &&
|
||||
\\test -f "{s}/Release/libcef.so" &&
|
||||
\\test -f "{s}/libcef_dll_wrapper/libcef_dll_wrapper.a" || {{
|
||||
\\ echo "missing CEF dependency for -Dweb-engine=chromium" >&2
|
||||
\\ echo "Fix with: native cef install --dir {s}" >&2
|
||||
\\ exit 1
|
||||
\\}}
|
||||
, .{ cef_dir, cef_dir, cef_dir, cef_dir }),
|
||||
.windows => b.fmt(
|
||||
\\test -f "{s}/include/cef_app.h" &&
|
||||
\\test -f "{s}/Release/libcef.dll" &&
|
||||
\\test -f "{s}/libcef_dll_wrapper/libcef_dll_wrapper.lib" || {{
|
||||
\\ echo "missing CEF dependency for -Dweb-engine=chromium" >&2
|
||||
\\ echo "Fix with: native cef install --dir {s}" >&2
|
||||
\\ exit 1
|
||||
\\}}
|
||||
, .{ cef_dir, cef_dir, cef_dir, cef_dir }),
|
||||
else => "echo unsupported CEF target >&2; exit 1",
|
||||
};
|
||||
return b.addSystemCommand(&.{ "sh", "-c", script });
|
||||
}
|
||||
|
||||
const AppWebEngineConfig = struct {
|
||||
web_engine: WebEngineOption = .system,
|
||||
cef_dir: []const u8 = "third_party/cef/macos",
|
||||
cef_auto_install: bool = false,
|
||||
};
|
||||
|
||||
fn defaultCefDir(platform: PlatformOption, configured: []const u8) []const u8 {
|
||||
if (!std.mem.eql(u8, configured, "third_party/cef/macos")) return configured;
|
||||
return switch (platform) {
|
||||
.linux => "third_party/cef/linux",
|
||||
.windows => "third_party/cef/windows",
|
||||
else => configured,
|
||||
};
|
||||
}
|
||||
|
||||
/// Resolve an app-relative path against `app_root` (see AppOptions). Kept
|
||||
/// lexical: `b.path` rejects absolute paths and the generated build graph
|
||||
/// hands us "../..", which openat/b.path both resolve fine.
|
||||
fn appPath(b: *std.Build, app_root: []const u8, sub_path: []const u8) []const u8 {
|
||||
if (app_root.len == 0 or std.mem.eql(u8, app_root, ".")) return sub_path;
|
||||
return b.pathJoin(&.{ app_root, sub_path });
|
||||
}
|
||||
|
||||
fn appWebEngineConfig(b: *std.Build, app_root: []const u8) AppWebEngineConfig {
|
||||
const source = b.build_root.handle.readFileAlloc(b.graph.io, appPath(b, app_root, "app.zon"), b.allocator, .limited(1024 * 1024)) catch return .{};
|
||||
var config: AppWebEngineConfig = .{};
|
||||
if (stringField(source, ".web_engine")) |value| {
|
||||
config.web_engine = parseWebEngine(value) orelse .system;
|
||||
}
|
||||
if (objectSection(source, ".cef")) |cef| {
|
||||
if (stringField(cef, ".dir")) |value| config.cef_dir = value;
|
||||
if (boolField(cef, ".auto_install")) |value| config.cef_auto_install = value;
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
fn parseWebEngine(value: []const u8) ?WebEngineOption {
|
||||
if (std.mem.eql(u8, value, "system")) return .system;
|
||||
if (std.mem.eql(u8, value, "chromium")) return .chromium;
|
||||
return null;
|
||||
}
|
||||
|
||||
fn stringField(source: []const u8, field: []const u8) ?[]const u8 {
|
||||
const field_index = std.mem.indexOf(u8, source, field) orelse return null;
|
||||
const equals = std.mem.indexOfScalarPos(u8, source, field_index, '=') orelse return null;
|
||||
const start_quote = std.mem.indexOfScalarPos(u8, source, equals, '"') orelse return null;
|
||||
const end_quote = std.mem.indexOfScalarPos(u8, source, start_quote + 1, '"') orelse return null;
|
||||
return source[start_quote + 1 .. end_quote];
|
||||
}
|
||||
|
||||
fn objectSection(source: []const u8, field: []const u8) ?[]const u8 {
|
||||
const field_index = std.mem.indexOf(u8, source, field) orelse return null;
|
||||
const open = std.mem.indexOfScalarPos(u8, source, field_index, '{') orelse return null;
|
||||
var depth: usize = 0;
|
||||
var index = open;
|
||||
while (index < source.len) : (index += 1) {
|
||||
switch (source[index]) {
|
||||
'{' => depth += 1,
|
||||
'}' => {
|
||||
depth -= 1;
|
||||
if (depth == 0) return source[open + 1 .. index];
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
fn boolField(source: []const u8, field: []const u8) ?bool {
|
||||
const field_index = std.mem.indexOf(u8, source, field) orelse return null;
|
||||
const equals = std.mem.indexOfScalarPos(u8, source, field_index, '=') orelse return null;
|
||||
var index = equals + 1;
|
||||
while (index < source.len and std.ascii.isWhitespace(source[index])) : (index += 1) {}
|
||||
if (std.mem.startsWith(u8, source[index..], "true")) return true;
|
||||
if (std.mem.startsWith(u8, source[index..], "false")) return false;
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
# Changelog fragments
|
||||
|
||||
Agents and feature branches do not edit `CHANGELOG.md` directly — concurrent work would conflict on every merge. Instead, each change lands with a small fragment in this directory, and `scripts/changelog-merge.sh` folds all fragments into the `## Unreleased` section of `CHANGELOG.md` (typically during release prep, see RELEASING.md).
|
||||
|
||||
## Writing a fragment
|
||||
|
||||
Add `changelog.d/<slug>.md`, where `<slug>` names your change (e.g. `gpu-dashboard-smoke-budget.md`). The file holds a bullet or two for one changelog section:
|
||||
|
||||
- The first line starts with a section tag: `feature:`, `improvement:`, or `fix:`, followed by the first bullet's text.
|
||||
- Any further lines are additional bullets (start them with `- `; bare lines get `- ` prefixed for you).
|
||||
- One tag per fragment. A change that touches multiple sections ships multiple fragments.
|
||||
- Match the CHANGELOG voice: bold lead-in, then the story. One line per bullet — never hard-wrap.
|
||||
|
||||
Example (`changelog.d/faster-frobnication.md`):
|
||||
|
||||
```
|
||||
improvement: **Faster frobnication**: the frobnicator now memoizes per-frame, cutting rebuild time ~40% on the kanban example.
|
||||
- **Frobnication telemetry**: automation snapshots report `frob_cache_hits=`.
|
||||
```
|
||||
|
||||
Tags map to sections: `feature:` → `### New Features`, `improvement:` → `### Improvements`, `fix:` → `### Bug Fixes`.
|
||||
|
||||
## Merging
|
||||
|
||||
```sh
|
||||
scripts/changelog-merge.sh
|
||||
```
|
||||
|
||||
appends every fragment's bullets to the end of its section under `## Unreleased` (creating the section — or the whole `## Unreleased` block — when missing), then deletes the merged fragments. This `README.md` is never merged or deleted. The script refuses unknown tags loudly instead of guessing.
|
||||
@@ -1,3 +1,5 @@
|
||||
node_modules/
|
||||
.next/
|
||||
next-env.d.ts
|
||||
.next-gate/
|
||||
.next-agent/
|
||||
|
||||
@@ -1,10 +1,40 @@
|
||||
import createMDX from "@next/mdx";
|
||||
import { createRequire } from "node:module";
|
||||
|
||||
const withMDX = createMDX();
|
||||
// Resolve the plugin to an absolute path (still a string, so the config
|
||||
// stays serializable for Turbopack). A bare "remark-gfm" is require()d
|
||||
// from the MDX loader's own package context, which under pnpm's strict
|
||||
// module isolation cannot see this app's dependencies — production
|
||||
// builds resolved it, the Turbopack dev server did not.
|
||||
const require = createRequire(import.meta.url);
|
||||
|
||||
const withMDX = createMDX({
|
||||
options: {
|
||||
// GFM is what gives .mdx pages pipe tables (plus autolinks and
|
||||
// strikethrough) — without it, table markdown renders as a plain
|
||||
// paragraph of pipes.
|
||||
remarkPlugins: [[require.resolve("remark-gfm")]],
|
||||
},
|
||||
});
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
pageExtensions: ["ts", "tsx", "md", "mdx"],
|
||||
// CI-style builds set NEXT_DIST_DIR so `pnpm check` never shares .next
|
||||
// with a running dev server (a shared dist dir corrupts the dev cache).
|
||||
distDir: process.env.NEXT_DIST_DIR || ".next",
|
||||
// The gate builds into .next-gate INSIDE this dir; without an ignore,
|
||||
// the dev watcher sees every one of those build files land and
|
||||
// recompiles continuously whenever a gate runs.
|
||||
watchOptions: {
|
||||
ignored: ["**/.next-gate/**", "**/.next-check/**"],
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
// The Philosophy page became the Introduction, the opening page of the docs.
|
||||
{ source: "/philosophy", destination: "/introduction", permanent: true },
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
export default withMDX(nextConfig);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "@zero-native/docs",
|
||||
"name": "@native-sdk/docs",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
@@ -22,6 +22,7 @@
|
||||
"radix-ui": "^1.4.3",
|
||||
"react": "^19",
|
||||
"react-dom": "^19",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"shiki": "^4.0.2",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
@@ -40,4 +41,4 @@
|
||||
"postcss@<8.5.10": ">=8.5.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,9 @@ importers:
|
||||
react-dom:
|
||||
specifier: ^19
|
||||
version: 19.2.6(react@19.2.6)
|
||||
remark-gfm:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1
|
||||
shiki:
|
||||
specifier: ^4.0.2
|
||||
version: 4.0.2
|
||||
@@ -1302,6 +1305,10 @@ packages:
|
||||
esast-util-from-js@2.0.1:
|
||||
resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==}
|
||||
|
||||
escape-string-regexp@5.0.0:
|
||||
resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
estree-util-attach-comments@3.0.0:
|
||||
resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==}
|
||||
|
||||
@@ -1460,9 +1467,33 @@ packages:
|
||||
resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
markdown-table@3.0.4:
|
||||
resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==}
|
||||
|
||||
mdast-util-find-and-replace@3.0.2:
|
||||
resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==}
|
||||
|
||||
mdast-util-from-markdown@2.0.3:
|
||||
resolution: {integrity: sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==}
|
||||
|
||||
mdast-util-gfm-autolink-literal@2.0.1:
|
||||
resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==}
|
||||
|
||||
mdast-util-gfm-footnote@2.1.0:
|
||||
resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==}
|
||||
|
||||
mdast-util-gfm-strikethrough@2.0.0:
|
||||
resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==}
|
||||
|
||||
mdast-util-gfm-table@2.0.0:
|
||||
resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==}
|
||||
|
||||
mdast-util-gfm-task-list-item@2.0.0:
|
||||
resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==}
|
||||
|
||||
mdast-util-gfm@3.1.0:
|
||||
resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==}
|
||||
|
||||
mdast-util-mdx-expression@2.0.1:
|
||||
resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==}
|
||||
|
||||
@@ -1490,6 +1521,27 @@ packages:
|
||||
micromark-core-commonmark@2.0.3:
|
||||
resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==}
|
||||
|
||||
micromark-extension-gfm-autolink-literal@2.1.0:
|
||||
resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==}
|
||||
|
||||
micromark-extension-gfm-footnote@2.1.0:
|
||||
resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==}
|
||||
|
||||
micromark-extension-gfm-strikethrough@2.1.0:
|
||||
resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==}
|
||||
|
||||
micromark-extension-gfm-table@2.1.1:
|
||||
resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==}
|
||||
|
||||
micromark-extension-gfm-tagfilter@2.0.0:
|
||||
resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==}
|
||||
|
||||
micromark-extension-gfm-task-list-item@2.1.0:
|
||||
resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==}
|
||||
|
||||
micromark-extension-gfm@3.0.0:
|
||||
resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==}
|
||||
|
||||
micromark-extension-mdx-expression@3.0.1:
|
||||
resolution: {integrity: sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==}
|
||||
|
||||
@@ -1712,6 +1764,9 @@ packages:
|
||||
rehype-recma@1.0.0:
|
||||
resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==}
|
||||
|
||||
remark-gfm@4.0.1:
|
||||
resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==}
|
||||
|
||||
remark-mdx@3.1.1:
|
||||
resolution: {integrity: sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==}
|
||||
|
||||
@@ -1721,6 +1776,9 @@ packages:
|
||||
remark-rehype@11.1.2:
|
||||
resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==}
|
||||
|
||||
remark-stringify@11.0.0:
|
||||
resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==}
|
||||
|
||||
scheduler@0.27.0:
|
||||
resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==}
|
||||
|
||||
@@ -3050,6 +3108,8 @@ snapshots:
|
||||
esast-util-from-estree: 2.0.0
|
||||
vfile-message: 4.0.3
|
||||
|
||||
escape-string-regexp@5.0.0: {}
|
||||
|
||||
estree-util-attach-comments@3.0.0:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.9
|
||||
@@ -3228,6 +3288,15 @@ snapshots:
|
||||
|
||||
markdown-extensions@2.0.0: {}
|
||||
|
||||
markdown-table@3.0.4: {}
|
||||
|
||||
mdast-util-find-and-replace@3.0.2:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
escape-string-regexp: 5.0.0
|
||||
unist-util-is: 6.0.1
|
||||
unist-util-visit-parents: 6.0.2
|
||||
|
||||
mdast-util-from-markdown@2.0.3:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
@@ -3245,6 +3314,63 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mdast-util-gfm-autolink-literal@2.0.1:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
ccount: 2.0.1
|
||||
devlop: 1.1.0
|
||||
mdast-util-find-and-replace: 3.0.2
|
||||
micromark-util-character: 2.1.1
|
||||
|
||||
mdast-util-gfm-footnote@2.1.0:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
devlop: 1.1.0
|
||||
mdast-util-from-markdown: 2.0.3
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
micromark-util-normalize-identifier: 2.0.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mdast-util-gfm-strikethrough@2.0.0:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
mdast-util-from-markdown: 2.0.3
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mdast-util-gfm-table@2.0.0:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
devlop: 1.1.0
|
||||
markdown-table: 3.0.4
|
||||
mdast-util-from-markdown: 2.0.3
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mdast-util-gfm-task-list-item@2.0.0:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
devlop: 1.1.0
|
||||
mdast-util-from-markdown: 2.0.3
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mdast-util-gfm@3.1.0:
|
||||
dependencies:
|
||||
mdast-util-from-markdown: 2.0.3
|
||||
mdast-util-gfm-autolink-literal: 2.0.1
|
||||
mdast-util-gfm-footnote: 2.1.0
|
||||
mdast-util-gfm-strikethrough: 2.0.0
|
||||
mdast-util-gfm-table: 2.0.0
|
||||
mdast-util-gfm-task-list-item: 2.0.0
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
mdast-util-mdx-expression@2.0.1:
|
||||
dependencies:
|
||||
'@types/estree-jsx': 1.0.5
|
||||
@@ -3346,6 +3472,64 @@ snapshots:
|
||||
micromark-util-symbol: 2.0.1
|
||||
micromark-util-types: 2.0.2
|
||||
|
||||
micromark-extension-gfm-autolink-literal@2.1.0:
|
||||
dependencies:
|
||||
micromark-util-character: 2.1.1
|
||||
micromark-util-sanitize-uri: 2.0.1
|
||||
micromark-util-symbol: 2.0.1
|
||||
micromark-util-types: 2.0.2
|
||||
|
||||
micromark-extension-gfm-footnote@2.1.0:
|
||||
dependencies:
|
||||
devlop: 1.1.0
|
||||
micromark-core-commonmark: 2.0.3
|
||||
micromark-factory-space: 2.0.1
|
||||
micromark-util-character: 2.1.1
|
||||
micromark-util-normalize-identifier: 2.0.1
|
||||
micromark-util-sanitize-uri: 2.0.1
|
||||
micromark-util-symbol: 2.0.1
|
||||
micromark-util-types: 2.0.2
|
||||
|
||||
micromark-extension-gfm-strikethrough@2.1.0:
|
||||
dependencies:
|
||||
devlop: 1.1.0
|
||||
micromark-util-chunked: 2.0.1
|
||||
micromark-util-classify-character: 2.0.1
|
||||
micromark-util-resolve-all: 2.0.1
|
||||
micromark-util-symbol: 2.0.1
|
||||
micromark-util-types: 2.0.2
|
||||
|
||||
micromark-extension-gfm-table@2.1.1:
|
||||
dependencies:
|
||||
devlop: 1.1.0
|
||||
micromark-factory-space: 2.0.1
|
||||
micromark-util-character: 2.1.1
|
||||
micromark-util-symbol: 2.0.1
|
||||
micromark-util-types: 2.0.2
|
||||
|
||||
micromark-extension-gfm-tagfilter@2.0.0:
|
||||
dependencies:
|
||||
micromark-util-types: 2.0.2
|
||||
|
||||
micromark-extension-gfm-task-list-item@2.1.0:
|
||||
dependencies:
|
||||
devlop: 1.1.0
|
||||
micromark-factory-space: 2.0.1
|
||||
micromark-util-character: 2.1.1
|
||||
micromark-util-symbol: 2.0.1
|
||||
micromark-util-types: 2.0.2
|
||||
|
||||
micromark-extension-gfm@3.0.0:
|
||||
dependencies:
|
||||
micromark-extension-gfm-autolink-literal: 2.1.0
|
||||
micromark-extension-gfm-footnote: 2.1.0
|
||||
micromark-extension-gfm-strikethrough: 2.1.0
|
||||
micromark-extension-gfm-table: 2.1.1
|
||||
micromark-extension-gfm-tagfilter: 2.0.0
|
||||
micromark-extension-gfm-task-list-item: 2.1.0
|
||||
micromark-util-combine-extensions: 2.0.1
|
||||
micromark-util-types: 2.0.2
|
||||
|
||||
micromark-extension-mdx-expression@3.0.1:
|
||||
dependencies:
|
||||
'@types/estree': 1.0.9
|
||||
@@ -3746,6 +3930,17 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
remark-gfm@4.0.1:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
mdast-util-gfm: 3.1.0
|
||||
micromark-extension-gfm: 3.0.0
|
||||
remark-parse: 11.0.0
|
||||
remark-stringify: 11.0.0
|
||||
unified: 11.0.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
remark-mdx@3.1.1:
|
||||
dependencies:
|
||||
mdast-util-mdx: 3.0.0
|
||||
@@ -3770,6 +3965,12 @@ snapshots:
|
||||
unified: 11.0.5
|
||||
vfile: 6.0.3
|
||||
|
||||
remark-stringify@11.0.0:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
mdast-util-to-markdown: 2.1.2
|
||||
unified: 11.0.5
|
||||
|
||||
scheduler@0.27.0: {}
|
||||
|
||||
semver@7.7.4:
|
||||
|
||||
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
After Width: | Height: | Size: 6.4 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 614 B |
|
After Width: | Height: | Size: 634 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 854 B |
|
After Width: | Height: | Size: 864 B |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 822 B |
|
After Width: | Height: | Size: 832 B |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 5.2 KiB |
|
After Width: | Height: | Size: 8.3 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 1.3 KiB |