#!/usr/bin/env bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT # # verify-be-icon.sh - a Be API program's icon as Tracker shows it. # # make puts every Be API program's icons into its resources (make/be.mk: # tools/iconBake.py be, 32x32 and 16x16 in the BeOS palette, attached by # xres as BEOS:L:STD_ICON and BEOS:M:STD_ICON). Tracker shows a file by the # icons in its attributes, which a program built here does not have; the # backend copies them over from its resources when it starts # (beBackend.cpp, iconAttributesEnsure). In the port's VM # (scripts/lib/beGate.sh) the job runs DRAW (examples/draw) until its icon # attribute is there (attributeSeconds at most), stops it, and opens a # Tracker window on its folder: a screendump must hold the baked icon, one # size or the other, every pixel it leaves opaque in the BeOS palette's # colour (give or take TOLERANCE: BeOS R5 runs in 16-bit colour). # # Usage: scripts/verify-be-icon.sh [haiku|haiku32|beos] (default haiku) # Env: BE_ICON_KEEP keep the last screendump as build/PORT/be-icon.png # Exit: 0 PASS, 1 FAIL, 2 SKIP (the VM not installed, or DRAW not built) # Requires: QEMU and the port's VM (./install.sh install emulator:PORT), # python3 + PIL + numpy; build/PORT/bin/DRAW and its icon # (make PORT). set -uo pipefail repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) source "$repo/scripts/lib/joeyEnv.sh" source "$repo/scripts/lib/beGate.sh" port=${1:-haiku} label="verify-be-icon ($port)" beNeed "$port" program=$(joeyExampleProgram draw "$port") iconDir=$repo/build/$port/icon/DRAW [ -x "$program" ] && [ -f "$iconDir/icon32.raw" ] && [ -f "$iconDir/icon16.raw" ] || { echo "$label: build DRAW first (make $port)" >&2; exit 2; } bootSeconds=120 # Seconds the gate looks for the icon in Tracker's window, and the job keeps # the machine up after (longer: the screendumps must not outlast it). lookSeconds=90 holdSeconds=150 # Seconds DRAW has to give itself its icon attribute, and the attributes # then have to settle. attributeSeconds=60 settleSeconds=5 case "$port" in beos) tracker=/boot/beos/system/Tracker ;; *) tracker=/system/Tracker ;; esac work=$(mktemp -d -t joeylib-be-icon.XXXXXX) cleanup() { beStop rm -rf "$work" } trap cleanup EXIT # iconShown PNG -- true when the screendump PNG holds the icon, either size # (Tracker's icon view shows the 32x32, its list view the 16x16); prints the # verdict. iconShown() { python3 - "$1" "$iconDir" "$repo/tools" "$label" <<'PY' import os import sys import numpy from PIL import Image png, iconDir, tools, label = sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4] sys.path.insert(0, tools) import iconBake # noqa: E402 # BeOS R5's 16-bit screen keeps 5 or 6 bits a gun. TOLERANCE = 8 screen = numpy.asarray(Image.open(png).convert("RGB"), dtype=numpy.int16) sh, sw = screen.shape[:2] palette = iconBake.BE_PALETTE def matches(colour): return numpy.all(numpy.abs(screen - colour) <= TOLERANCE, axis=2) def find(size, raw): """Where the screen holds the baked SIZE x SIZE icon raw, or None.""" indices = open(raw, "rb").read() opaque = [(i // size, i % size) for i, index in enumerate(indices) if index != iconBake.BE_TRANSPARENT] colours = numpy.array([palette[indices[y * size + x] * 3:indices[y * size + x] * 3 + 3] for y, x in opaque], dtype=numpy.int16) opaque = numpy.array(opaque) counts = [int(matches(c).sum()) for c in colours] anchor = min(range(len(opaque)), key=lambda i: counts[i]) ay, ax = opaque[anchor] for y, x in numpy.argwhere(matches(colours[anchor])): top, left = y - ay, x - ax if top < 0 or left < 0 or top + size > sh or left + size > sw: continue shown = screen[top + opaque[:, 0], left + opaque[:, 1]] if numpy.all(numpy.abs(shown - colours) <= TOLERANCE): return left, top, len(opaque) return None for size, name in ((iconBake.BE_LARGE, "icon32.raw"), (iconBake.BE_SMALL, "icon16.raw")): at = find(size, os.path.join(iconDir, name)) if at is not None: print(f"{label}: PASS (Tracker shows DRAW by its icon at {at[0]},{at[1]}: all {at[2]} opaque pixels of the baked {size}x{size})") sys.exit(0) print(f"{label}: FAIL - neither of DRAW's icons is on the screen") sys.exit(1) PY } mkdir -p "$work/job/DRAW" cp "$program" "$work/job/DRAW/DRAW" # The job: DRAW runs until it has given itself its icon attribute, then # Tracker opens a window on its folder (Tracker runs already: a second start # hands it the folder) and the machine stays up a while. cat > "$work/job/job.sh" < /dev/null 2>&1; do sleep 1 waited=\$((waited + 1)) done # All of it: on BeOS R5 a plain kill ends only the main thread, and the # window stays. The registrar writes the other attributes after the first, # and R5's Tracker keeps the icon it first finds: a moment for them. kill -9 \$pid sleep $settleSeconds cd .. $tracker "\$(pwd)/DRAW" & sleep $holdSeconds EOF beJobDisk "$work/job.img" "$work/job" beStart "$work" "$work/job.img" if ! beWaitJob "$work" "$bootSeconds"; then echo "$label: FAIL - the VM did not start the job within ${bootSeconds} s" >&2 exit 1 fi lookEnd=$((SECONDS + lookSeconds)) while [ "$SECONDS" -lt "$lookEnd" ]; do sleep 2 beQmp "$work" screendump "$work/screen.png" [ -n "${BE_ICON_KEEP:-}" ] && cp "$work/screen.png" "$repo/build/$port/be-icon.png" if iconShown "$work/screen.png" > "$work/verdict"; then cat "$work/verdict" exit 0 fi done cat "$work/verdict" exit 1