joeylib2/scripts/make-iigs-disk.sh

250 lines
9.5 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
#
# make-iigs-disk.sh - Build the JOEYLIB ProDOS disk image (joey.2mg) that
# the gates and joey-run mount as the data disk beside GS/OS. It holds
# the launchable example binaries as ProDOS type $B3 (S16 / GS-OS apps) in a
# volume named JOEYLIB, so GS/OS Finder lists and launches them.
#
# A 3.5" floppy caps at 800KB, which cannot hold every example at once.
# Examples are added in priority order (small primitive demos first); any that
# would overflow the volume are skipped with a warning rather than corrupting
# the image. Override the set with JOEY_DISK_EXAMPLES="DRAW SPRITE ...".
#
# A program's data goes on with it, into the volume's DATA folder (programs
# share it): make stages it per program in $JOEY_DISK_DATA/<NAME>/DATA, and the
# files the program runs without (say a game's per-level sprite banks, which it
# JIT-compiles at level entry when one is missing) in <NAME>/OPTIONAL, laid out
# the same way. Those go on last, and only while there is room. Last of all,
# while there is room, each program's Finder icon goes into the volume's
# Icons folder as NAME.ICONS (type $CA), from what make bakes in
# $JOEY_DISK_ICONS/<NAME>/finder.icons: a program shown without its icon
# still runs.
#
# Usage: scripts/make-iigs-disk.sh [output.2mg]
# Env: BINDIR the programs (default build/iigs/bin)
# JOEY_DISK_DATA their staged data (default BINDIR/../data)
# JOEY_DISK_ICONS their Finder icons (default BINDIR/../icon)
# NTP_BIN the NTP replayer (default build/iigs/audio/ntpplayer.bin)
# Requires: ./install.sh install toolchain:iigs; built IIgs examples.
set -euo pipefail
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
source "$repo/scripts/lib/joeyEnv.sh"
joeyNeed LLVM816_ROOT toolchain:iigs
joeyNeed CADIUS cadius
BINDIR="${BINDIR:-$repo/build/iigs/bin}"
DATA_ROOT="${JOEY_DISK_DATA:-$BINDIR/../data}"
ICON_ROOT="${JOEY_DISK_ICONS:-$BINDIR/../icon}"
# The NTP replayer. DEFAULTED, not optional: without it on the volume every
# IIgs example is silent except the ROM boot beep, because jlpAudioInit
# fopen()s "ntpplayer" and returns before ntpArm() when that read fails --
# with no diagnostic. Callers used to have to pass NTP_BIN and one of them
# passed the wrong path, which is exactly how a game ended up mute.
NTP_BIN="${NTP_BIN:-$repo/build/iigs/audio/ntpplayer.bin}"
OUT="${1:-$BINDIR/joey.2mg}"
VOL=JOEYLIB
# Volume size passed to cadius CREATEVOLUME. Defaults to an 800KB 3.5"
# floppy (the classic bootable image); set JOEY_DISK_SIZE to a larger
# ProDOS size (e.g. "5MB", "32MB") to build a hard-disk image that holds
# the full example set, which does not fit an 800KB floppy.
VOLSIZE="${JOEY_DISK_SIZE:-800KB}"
[ -x "$CADIUS" ] || { echo "make-iigs-disk.sh: cadius not found at $CADIUS" >&2; exit 2; }
# Priority order: small primitive demos first so DRAW/PATTERN/etc. always fit;
# the larger adventures land last and are dropped if the floppy is full.
order=(${JOEY_DISK_EXAMPLES:-DRAW PATTERN KEYS JOY SPRITE UBER ADV2 ADV})
work=$(mktemp -d -t joeylib-disk.XXXXXX)
trap 'rm -rf "$work"' EXIT
rm -f "$OUT"
"$CADIUS" CREATEVOLUME "$OUT" "$VOL" "$VOLSIZE" >/dev/null
# Writable save area for the save-file API (joey/file.h -> jlSave*). GS/OS's
# libc has no mkdir, but the IIgs save HAL creates SAVES/ at runtime via GS/OS
# Create; prestaging it here as well means saves work even before that call and
# on a first run. Set JOEY_SKIP_SAVES_PRESTAGE=1 to omit it (tests runtime
# creation in isolation).
if [ -z "${JOEY_SKIP_SAVES_PRESTAGE:-}" ]; then
"$CADIUS" CREATEFOLDER "$OUT" "/$VOL/SAVES" >/dev/null
fi
freeBlocks() {
"$CADIUS" CATALOG "$OUT" 2>/dev/null \
| grep -oE 'Free : [0-9]+' | grep -oE '[0-9]+' | head -1
}
# fileBlocks FILE -- the blocks FILE takes on the volume: its data blocks, one
# index block per 256 of them, and a little directory slack.
fileBlocks() {
local sz
sz=$(stat -c%s "$1")
printf '%s\n' $(( (sz + 511) / 512 + (sz + 131071) / 131072 + 2 ))
}
# treeFiles DIR -- every file under DIR, sorted (nothing when DIR is absent).
treeFiles() {
[ -d "$1" ] || return 0
find "$1" -type f | sort
}
# treeBlocks DIR -- the blocks every file under DIR takes.
treeBlocks() {
local f
local total=0
while IFS= read -r f; do
total=$(( total + $(fileBlocks "$f") ))
done < <(treeFiles "$1")
printf '%s\n' "$total"
}
# addDataFile FILE FOLDER -- add FILE to FOLDER on the volume as a plain ProDOS
# BIN ($06) file under its upper-cased name. GS/OS ProDOS lookups are
# case-insensitive, so a program's lowercase fopen paths still match.
addDataFile() {
local base
base=$(basename "$1")
cp "$1" "$work/${base^^}#060000"
"$CADIUS" ADDFILE "$OUT" "$2" "$work/${base^^}#060000" >/dev/null
rm -f "$work/${base^^}#060000"
}
# addIcon NAME -- NAME's Finder icon file, when make baked one, into the
# volume's Icons folder (the folder the Finder reads icon files from) as
# NAME.ICONS, ProDOS type $CA, when it and the folder fit.
addIcon() {
local icon="$ICON_ROOT/$1/finder.icons"
[ -f "$icon" ] || return 0
[ "$(( $(fileBlocks "$icon") + 1 ))" -le "$(freeBlocks)" ] || return 1
"$CADIUS" CREATEFOLDER "$OUT" "/$VOL/Icons" >/dev/null
cp "$icon" "$work/$1.ICONS#CA0000"
"$CADIUS" ADDFILE "$OUT" "/$VOL/Icons" "$work/$1.ICONS#CA0000" >/dev/null
rm -f "$work/$1.ICONS#CA0000"
}
# addToData FILE ROOT -- add FILE, which lies under ROOT, to the volume's DATA
# folder at the same place (subfolders upper-cased). CREATEFOLDER makes the
# missing parents and leaves a folder another program's data made alone.
addToData() {
local rel
local dst="/$VOL/DATA"
rel=$(dirname "${1#"$2/"}")
if [ "$rel" != "." ]; then
dst="$dst/${rel^^}"
fi
"$CADIUS" CREATEFOLDER "$OUT" "$dst" >/dev/null
addDataFile "$1" "$dst"
}
added=()
skipped=()
programs=()
# Blocks promised to the data of the programs already on: it goes on after
# them and NTP, so every later fit check leaves it room.
reserved=0
for name in "${order[@]}"; do
bin="$BINDIR/$name"
[ -f "$bin" ] || continue
dataNeed=$(treeBlocks "$DATA_ROOT/$name/DATA")
need=$(( $(fileBlocks "$bin") + dataNeed ))
if [ "$need" -gt $(( $(freeBlocks) - reserved )) ]; then
skipped+=("$name")
continue
fi
# ProDOS type $B3 (S16/GS-OS application), aux $0000.
cp "$bin" "$work/$name#B30000"
"$CADIUS" ADDFILE "$OUT" "/$VOL" "$work/$name#B30000" >/dev/null
rm -f "$work/$name#B30000"
if "$CADIUS" CATALOG "$OUT" 2>/dev/null | grep -qE "^ $name +S16"; then
added+=("$name")
programs+=("$name")
reserved=$((reserved + dataNeed))
else
skipped+=("$name")
fi
done
# The NTP replayer binary (audio): jlpAudioInit fopen()s "ntpplayer" from
# the volume at runtime (see src/iigs/audio_full.c NTP_PATH). Plain ProDOS
# BIN ($06) file. Skipped with a warning if it doesn't fit or wasn't built.
ntpOk=0
if [ -f "$NTP_BIN" ]; then
ntpSz=$(stat -c%s "$NTP_BIN")
ntpNeed=$(( (ntpSz + 511) / 512 + 2 ))
if [ "$ntpNeed" -le $(( $(freeBlocks) - reserved )) ]; then
cp "$NTP_BIN" "$work/NTPPLAYER#060000"
"$CADIUS" ADDFILE "$OUT" "/$VOL" "$work/NTPPLAYER#060000" >/dev/null
rm -f "$work/NTPPLAYER#060000"
# Never trust the exit code here (see the launch check below).
if "$CADIUS" CATALOG "$OUT" 2>/dev/null | grep -qE '^ +NTPPLAYER +BIN'; then
added+=(NTPPLAYER)
ntpOk=1
fi
fi
fi
if [ "$ntpOk" -eq 0 ]; then
skipped+=(NTPPLAYER)
cat >&2 <<EOF
make-iigs-disk.sh: WARNING - NTPPLAYER is NOT on $OUT.
Every example built against this volume will be SILENT (you will hear the
ROM boot beep and nothing else). jlpAudioInit fopen()s "ntpplayer" and
gives up quietly when it is missing; \$E1002C keeps its ROM-bank default.
Looked for: $NTP_BIN
Build it with 'make -f make/iigs.mk \$(BUILD)/audio/ntpplayer.bin', or set
NTP_BIN to its location.
EOF
fi
# Each packed program's DATA, then whatever of its optional data fits.
for name in "${programs[@]}"; do
count=0
while IFS= read -r f; do
addToData "$f" "$DATA_ROOT/$name/DATA"
count=$((count + 1))
done < <(treeFiles "$DATA_ROOT/$name/DATA")
if [ "$count" -gt 0 ]; then
added+=("$name/DATA[$count files]")
fi
done
for name in "${programs[@]}"; do
count=0
left=0
while IFS= read -r f; do
if [ "$(fileBlocks "$f")" -gt "$(freeBlocks)" ]; then
left=$((left + 1))
continue
fi
addToData "$f" "$DATA_ROOT/$name/OPTIONAL"
count=$((count + 1))
done < <(treeFiles "$DATA_ROOT/$name/OPTIONAL")
if [ "$count" -gt 0 ]; then
added+=("$name/OPTIONAL[$count files]")
fi
if [ "$left" -gt 0 ]; then
echo " note: $left optional $name data file(s) left off (volume full; $name runs without them)" >&2
fi
done
# The programs' icons, while they fit.
icons=()
for name in "${programs[@]}"; do
if addIcon "$name"; then
[ -f "$ICON_ROOT/$name/finder.icons" ] && icons+=("$name")
else
echo " note: no room for $name's Finder icon (volume full; it shows the Finder's own)" >&2
fi
done
if [ ${#icons[@]} -gt 0 ]; then
added+=("Icons[${icons[*]}]")
fi
echo "iigs-disk: $OUT (volume /$VOL)"
echo " added: ${added[*]:-none}"
if [ ${#skipped[@]} -gt 0 ]; then
echo " SKIPPED ($VOLSIZE volume full): ${skipped[*]}"
echo " -> mount a single-example disk via JOEY_DISK_EXAMPLES=\"NAME\" $0"
fi