#!/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 # # check-icons.sh - Host gate for programs' icons: on every port built here, # DRAW (examples/draw) carries the icon make baked it (make/common.mk's # programIcon, tools/iconBake), where its system looks for it. Read with the # systems' own tools where they are installed, otherwise by the format: # # amiga Draw.info beside it: a DiskObject (magic $E310) for a tool, # AMIGA_ICON_STACK bytes of stack, the image iconBake makes # win32 a group icon, number HOSTED_ICON_ID, among its resources # (llvm-readobj) # win16 the same in its NE resource table # sdl3 the window icon linked in (jlgHostedIconRgba) # macos the app's icon: Info.plist naming an ICNS file in its Resources # web icon.png beside the page, the page naming it its icon # psp the EBOOT's ICON0, the baked PNG, 144x80 # haiku, haiku32, beos BEOS:L:STD_ICON and BEOS:M:STD_ICON resources # (xres) # dvx the icon32 resource (dvxres) # iigs DRAW.ICONS in joey.2mg's Icons folder, type $CA: an icon file # for an application named DRAW # # A port not built is left out; none built at all is a SKIP. (Tool output is # taken whole and handed to grep as a here-string: under pipefail a writer # that grep -q leaves behind dies of SIGPIPE and looks like a failure.) Each port's # emulator gate (scripts/verify--icon.sh) checks that its system shows # the icon. # # Usage: scripts/check-icons.sh # Exit: 0 PASS, 1 FAIL, 2 SKIP (no port built) set -uo pipefail repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) source "$repo/scripts/lib/joeyEnv.sh" label=check-icons fails="" checked="" # The Amiga's Workbench icon (workbench.h): a DiskObject's magic and a tool's # do_Type, at their offsets with do_StackSize's and its Image's Width, # Height, and Depth; iconBake's tool icons are 48x24 in 2 planes. WB_DISKMAGIC=$((0xE310)) WBTOOL=3 DO_TYPE_AT=48 DO_STACK_AT=74 IMAGE_SIZE_AT=82 AMIGA_ICON="48 24 2" # Win16's NE header (its offset at 0x3C): the resource and resident-name # tables' offsets, equal when there are no resources; a type's entry is 8 # bytes, a resource's 12 with its ID 6 bytes in; an integer ID has bit 15. NE_HEADER_AT=$((0x3C)) NE_RESOURCES_AT=$((0x24)) NE_NAMES_AT=$((0x26)) NE_TYPE_BYTES=8 NE_RESOURCE_BYTES=12 NE_RESOURCE_ID_AT=6 RT_GROUP_ICON=$((0x800E)) INTEGER_ID=$((0x8000)) # A PNG's signature, and the width and height in its IHDR. PNG_SIGNATURE=89504e470d0a1a0a PNG_WIDTH_AT=16 PNG_HEIGHT_AT=20 # The PSP's EBOOT: "\0PBP", then the parts' offsets from byte 8, ICON0 the # second (and the third's start its end). PBP_MAGIC=00504250 PBP_ICON0_AT=12 PBP_ICON1_AT=16 # The IIgs icon file: its file ID, and in its first record (from byte 26) # the application's name (a Pascal string at 66) and file type (at 82). IIGS_FILE_ID_AT=4 IIGS_RECORD_AT=26 IIGS_NAME_AT=66 IIGS_TYPE_AT=82 IIGS_ICON_FILE_ID=1 IIGS_APPLICATION=$((0xB3)) fail() { fails="${fails:+$fails; }$1: $2" } # makeValue PORT VARIABLE: a variable of make/PORT.mk. makeValue() { make -s -f "$repo/make/$1.mk" --eval "valueOf: ; @echo \$($2)" valueOf 2> /dev/null } # fileNumber FILE OFFSET BYTES be|le: an unsigned number in FILE; fails past # its end. fileNumber() { od -An -tu1 -v -j "$2" -N "$3" "$1" 2> /dev/null | awk -v n="$3" -v endian="$4" ' { for (i = 1; i <= NF; i++) { b[k++] = $i } } END { if (k < n) { exit 1 } v = 0; for (i = 0; i < n; i++) { v = v * 256 + b[endian == "be" ? i : n - 1 - i] } print v }' } # fileHex FILE OFFSET BYTES: those bytes as lower-case hex. fileHex() { od -An -tx1 -v -j "$2" -N "$3" "$1" 2> /dev/null | tr -d ' \n' } # neGroupIcon EXE ID: true when the Win16 EXE's NE resource table holds a # group icon numbered ID. neGroupIcon() { local exe=$1 local want=$((INTEGER_ID | $2)) local ne local table local at local typeId local count local i ne=$(fileNumber "$exe" "$NE_HEADER_AT" 4 le) || return 1 table=$((ne + $(fileNumber "$exe" $((ne + NE_RESOURCES_AT)) 2 le))) [ "$table" -ne $((ne + $(fileNumber "$exe" $((ne + NE_NAMES_AT)) 2 le))) ] || return 1 at=$((table + 2)) while true; do typeId=$(fileNumber "$exe" "$at" 2 le) || return 1 count=$(fileNumber "$exe" $((at + 2)) 2 le) || return 1 [ "$typeId" -ne 0 ] || return 1 at=$((at + NE_TYPE_BYTES)) for ((i = 0; i < count; i++)); do if [ "$typeId" -eq "$RT_GROUP_ICON" ] && [ "$(fileNumber "$exe" $((at + NE_RESOURCE_ID_AT)) 2 le)" = "$want" ]; then return 0 fi at=$((at + NE_RESOURCE_BYTES)) done done } for port in amiga win32 win16 sdl3 macos web psp haiku haiku32 beos dvx iigs; do program=$(joeyExampleProgram draw "$port" 2> /dev/null) || continue [ -f "$program" ] || continue checked="${checked:+$checked }$port" case "$port" in amiga) info=$program.info want="$WB_DISKMAGIC $WBTOOL $(makeValue amiga AMIGA_ICON_STACK) $AMIGA_ICON" got="$(fileNumber "$info" 0 2 be) $(fileNumber "$info" "$DO_TYPE_AT" 1 be) $(fileNumber "$info" "$DO_STACK_AT" 4 be)" got="$got $(fileNumber "$info" "$IMAGE_SIZE_AT" 2 be) $(fileNumber "$info" $((IMAGE_SIZE_AT + 2)) 2 be) $(fileNumber "$info" $((IMAGE_SIZE_AT + 4)) 2 be)" [ "$got" = "$want" ] || fail amiga "Draw.info: magic, type, stack, image $got, want $want" ;; win32) readobj=$(dirname "$WIN32_CC")/llvm-readobj id=$(makeValue win32 HOSTED_ICON_ID) listing=$("$readobj" --coff-resources "$program" 2> /dev/null) awk -v want="Name: (ID $id)" '/Type: GROUP_ICON/ { group = 1; next } group && /Name:/ { found = index($0, want) > 0; exit } END { exit !found }' <<< "$listing" || fail win32 "no group icon $id among DRAW.EXE's resources" ;; win16) neGroupIcon "$program" "$(makeValue win16 HOSTED_ICON_ID)" || fail win16 "no group icon in DRAW.EXE's NE resources" ;; sdl3) symbols=$(nm "$program" 2> /dev/null) grep -q ' jlgHostedIconRgba$' <<< "$symbols" || fail sdl3 "DRAW links no window icon" ;; macos) app=$(dirname "$(dirname "$(dirname "$program")")") iconName=$(awk '/CFBundleIconFile<\/key>/ { getline; gsub(/.*|<\/string>.*/, ""); print; exit }' "$app/Contents/Info.plist" 2> /dev/null) icns=$app/Contents/Resources/$iconName.icns if [ -z "$iconName" ] || [ "$(head -c 4 "$icns" 2> /dev/null)" != icns ] || [ "$(fileNumber "$icns" 4 4 be)" != "$(wc -c < "$icns" | tr -d " ")" ]; then fail macos "DRAW.app's icon" fi ;; web) folder=$(dirname "$program") grep -qE ']*rel=("?)icon\1[^>]*>' "$program" && grep -qE 'href=("?)icon\.png' "$program" || fail web "DRAW.html names no icon.png" [ "$(fileHex "$folder/icon.png" 0 8)" = "$PNG_SIGNATURE" ] || fail web "no icon.png beside DRAW.html" ;; psp) baked=$repo/build/psp/icon/DRAW/icon0.png size=$(makeValue psp PSP_ICON0_SIZE) start=$(fileNumber "$program" "$PBP_ICON0_AT" 4 le) end=$(fileNumber "$program" "$PBP_ICON1_AT" 4 le) if [ "$(fileHex "$program" 0 4)" != "$PBP_MAGIC" ] || ! cmp -s <(tail -c +$((start + 1)) "$program" | head -c $((end - start))) "$baked"; then fail psp "the EBOOT's ICON0 is not the baked icon" elif [ "$(fileNumber "$baked" "$PNG_WIDTH_AT" 4 be)x$(fileNumber "$baked" "$PNG_HEIGHT_AT" 4 be)" != "$size" ]; then fail psp "the EBOOT's ICON0 is not $size" fi ;; haiku|haiku32|beos) listing=$("$HAIKU_TOOLS/xres" -l "$program" 2> /dev/null) for want in "'ICON' *101 .*BEOS:L:STD_ICON" "'MICN' *101 .*BEOS:M:STD_ICON"; do grep -qE "$want" <<< "$listing" || fail "$port" "no $want resource" done ;; dvx) listing=$("$DVX_SDK/bin/dvxres" list "$program" 2> /dev/null) grep -qE '^ +icon32 +icon ' <<< "$listing" || fail dvx "no icon32 resource in draw.app" ;; iigs) disk=$repo/build/iigs/bin/joey.2mg if [ ! -f "$disk" ]; then fail iigs "no joey.2mg (make iigs-disk)" continue fi out=$(mktemp -d -t joeylib-icons.XXXXXX) "$CADIUS" EXTRACTFILE "$disk" /JOEYLIB/Icons/DRAW.ICONS "$out" > /dev/null 2>&1 iconFile=$(find "$out" -iname 'DRAW.ICONS*' -not -name '_FileInformation.txt' -print -quit) listing=$("$CADIUS" CATALOG "$disk" 2> /dev/null) grep -qE '^ +DRAW\.ICONS +ICN ' <<< "$listing" || fail iigs "no DRAW.ICONS of type \$CA in joey.2mg's Icons folder" nameLength=$(fileNumber "$iconFile" $((IIGS_RECORD_AT + IIGS_NAME_AT)) 1 le) got="$(fileNumber "$iconFile" "$IIGS_FILE_ID_AT" 2 le) $(dd if="$iconFile" bs=1 skip=$((IIGS_RECORD_AT + IIGS_NAME_AT + 1)) count="${nameLength:-0}" status=none 2> /dev/null) $(fileNumber "$iconFile" $((IIGS_RECORD_AT + IIGS_TYPE_AT)) 2 le)" [ "$got" = "$IIGS_ICON_FILE_ID DRAW $IIGS_APPLICATION" ] || fail iigs "DRAW.ICONS is no icon file for DRAW (id, name, type $got)" rm -rf "$out" ;; esac done if [ -z "$checked" ]; then echo "$label: SKIP - no port with icons is built" exit 2 fi if [ -n "$fails" ]; then echo "$label: FAIL - $fails" exit 1 fi echo "$label: PASS ($checked: DRAW carries its icon where the system looks)"