joeylib2/scripts/verify-be-icon.sh

157 lines
5.7 KiB
Bash
Executable file

#!/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" <<EOF
cd DRAW
./DRAW &
pid=\$!
waited=0
while [ \$waited -lt $attributeSeconds ] && ! catattr BEOS:L:STD_ICON DRAW > /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