joeylib2/make/support/iigs/clang-build.sh

342 lines
16 KiB
Bash
Executable file

#!/usr/bin/env bash
# clang-build.sh - Build an Apple IIgs binary with the llvm-mos w65816
# toolchain (clang + llvm-mc + link816).
#
# Usage:
# clang-build.sh [-I path]... [-Dname[=val]]... [-s stacksize] [-b] \
# [-M depfile] -o OUTPUT MAIN.c [EXTRA.c|.s ...]
#
# -I path extra include dir (repeatable)
# -Dname preprocessor define, passed to clang (probe builds)
# -s SIZE accepted for compatibility; llvm-mos sizes the stack in
# crt0, so this is a no-op here.
# -b accepted for compatibility; no-op (clang pointers are
# already 32-bit).
# -M depfile emit a combined header-dependency file (clang -MM) so make
# rebuilds the binary when a shared header changes.
# -o OUTPUT output path for the GS/OS S16 OMF (ExpressLoad-wrapped,
# relocatable) that the System Loader / Finder can launch.
#
# C sources are compiled with clang; .s / .asm sources are assembled with
# llvm-mc; everything links against the prebuilt w65816 runtime (with the
# GS/OS-aware crt0Gsos). link816 dead-strips unreferenced sections, then
# omfEmit wraps the relocatable image as an S16 OMF so the Loader can place
# it at any Memory-Manager-chosen address.
#
# Requires LLVM816_ROOT (toolchain root) with a built runtime
# ($LLVM816_ROOT/runtime/*.o). make exports it; from a shell, source
# toolchains/env.sh.
set -euo pipefail
: "${LLVM816_ROOT:?clang-build.sh: LLVM816_ROOT unset; run ./install.sh install toolchain:iigs, then use make or source toolchains/env.sh}"
CLANG="$LLVM816_ROOT/tools/llvm-mos-build/bin/clang"
LLVM_MC="$LLVM816_ROOT/tools/llvm-mos-build/bin/llvm-mc"
LINK="$LLVM816_ROOT/tools/link816"
OMFEMIT="$LLVM816_ROOT/tools/omfEmit"
RT="$LLVM816_ROOT/runtime"
RTINC="$RT/include"
for t in "$CLANG" "$LLVM_MC" "$LINK" "$OMFEMIT"; do
[ -x "$t" ] || { echo "clang-build.sh: missing tool: $t" >&2; exit 1; }
done
[ -f "$RT/crt0Gsos.o" ] || { echo "clang-build.sh: runtime not built ($RT/crt0Gsos.o missing); run runtime/build.sh" >&2; exit 1; }
includes=()
output=""
depfile=""
srcs=()
while [ $# -gt 0 ]; do
case "$1" in
-I) includes+=(-I "$2"); shift 2 ;;
-I*) includes+=("$1"); shift ;; # combined -Ipath form
-D*) includes+=("$1"); shift ;; # preprocessor define (probe builds)
-o) output="$2"; shift 2 ;;
-s) shift 2 ;; # stack size: no-op (crt0 handles it)
-b) shift ;; # accepted for compatibility: no-op
-M) depfile="$2"; shift 2 ;;
-*) echo "clang-build.sh: unknown flag $1" >&2; exit 2 ;;
*) srcs+=("$1"); shift ;;
esac
done
[ -n "$output" ] || { echo "clang-build.sh: -o OUTPUT required" >&2; exit 2; }
[ ${#srcs[@]} -gt 0 ] || { echo "clang-build.sh: no source files" >&2; exit 2; }
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
CFLAGS=(--target=w65816 -O2 -ffreestanding -ffunction-sections -I"$RTINC" "${includes[@]}")
# Curated runtime object set for GS/OS-launched OMFs. crt0Gsos.o (the
# System-Loader-aware crt) MUST be first so __start lands at --text-base.
# link816 dead-strips whatever a given binary doesn't use, so offering the
# whole runtime (libc helpers, soft-float, toolbox, GS/OS) is free.
runtimeObjs=(
"$RT/crt0Gsos.o" "$RT/libgcc.o" "$RT/libc.o"
"$RT/strtol.o" "$RT/snprintf.o" "$RT/sscanf.o" "$RT/qsort.o"
"$RT/strtok.o" "$RT/extras.o" "$RT/timeExt.o" "$RT/math.o"
"$RT/softFloat.o" "$RT/softDouble.o"
"$RT/iigsToolbox.o" "$RT/iigsToolboxExtra.o" "$RT/iigsGsos.o"
)
objs=()
depsrcs=()
objIdx=0
for s in "${srcs[@]}"; do
# Unique object name per source. A plain basename collides when two
# sources share a name (e.g. src/core/input.c and src/iigs/
# input.c both -> input.o), silently clobbering one and dropping its
# symbols. Index-prefix to guarantee uniqueness.
b="$(basename "$s")"
o="$tmpdir/${objIdx}_${b%.*}.o"
objIdx=$((objIdx + 1))
case "$s" in
*.c)
# Route through the regalloc fallback so a register-heavy TU
# (greedy "ran out of registers" on the single-accumulator
# 65816 -- e.g. AGI's main()) auto-retries with -regalloc=basic
# instead of breaking the build, exactly like runtime/build.sh.
"$LLVM816_ROOT/scripts/ccRegallocFallback.sh" "$CLANG" "${CFLAGS[@]}" -c "$s" -o "$o"
depsrcs+=("$s")
;;
*.s|*.asm)
"$LLVM_MC" -arch=w65816 -filetype=obj "$s" -o "$o"
;;
*.o|*.a)
# Prebuilt object or static library (e.g. libjoey.a): link816 reads
# both directly and --gc-sections selects what's referenced. Pass
# the real path through untouched.
objs+=("$s")
continue
;;
*)
echo "clang-build.sh: skipping unrecognized source: $s" >&2
continue
;;
esac
objs+=("$o")
done
# Object order is no longer layout-sensitive. The multi-segment crash that
# made it matter was the OMF segment header carrying ALIGN=0, so the System
# Loader placed overflow segments at a non-bank-aligned address (e.g.
# $07:BB11). Code uses 16-bit self-references (computed-goto `rts` tables,
# intra-segment branches) whose operands equal the *link* offset, which only
# resolve when the segment sits at bank:0000. omfEmit now emits ALIGN=0x10000
# so every code segment is bank-aligned and runtime_offset == link_offset in
# ANY split. Objects therefore build in compile order (deterministic from the
# source list); no sort needed. (Root-caused via the MAME debugger: seg2 main
# landed at $07:BB11, jlInit at +0x1E02=$07:D913, computed-gotos jumped wild.)
# Relocatable GS/OS OMF build. The System Loader places the image at a
# Memory-Manager-chosen address, so the OMF carries relocation records
# (--reloc-out -> --relocs). --text-base 0 (NOT 0x1000): the Loader
# bank-aligns KIND=0x1000 segments, so link-time IMM16 operands already
# equal their runtime segment offsets and link816 skips the IMM16 cRELOC
# entries (50-90% fewer relocs -> proportionally faster Loader boot).
# --bss-base 0xA000 keeps BSS above the SHR shadow ($2000-$9FFF in bank 0)
# so global writes don't scribble the screen. crt0Gsos.o (first object)
# puts __start at the segment base.
linkBin="$tmpdir/link.bin"
linkMap="$tmpdir/link.map"
linkReloc="$tmpdir/link.reloc"
linkManifest="$tmpdir/link.json"
linkErr="$tmpdir/link.err"
# The GS/OS load name is the output basename, uppercased to <=8 alnum chars.
omfName="$(basename "$output" | tr '[:lower:]' '[:upper:]' | tr -cd 'A-Z0-9' | cut -c1-8)"
[ -n "$omfName" ] || omfName="JOEYAPP"
# link816 emits an ExpressLoad S16 OMF. Two distinct overflows force the
# --full-bank: a GS/OS OMF image is loaded into a Memory-Manager-chosen
# bank, not bank 0, so there is no IO window or language card to dodge and
# all 64K is usable. link816 defaults to the bank-0-safe layout because its
# other consumers (clang-smoke.sh and the bench/probe scripts) really do
# raw-load into bank 0 -- see the fullBank comment in link816.cpp.
#
# MULTI-segment path: (a) text overrunning the entry bank, or
# (b) BSS overflowing the entry bank's leftover space. crt0Gsos sets
# DBR = the entry segment's bank and C globals are reached DBR-relative, so
# BSS can NOT be moved to its own bank -- instead we push TEXT into overflow
# banks (a low --segment-cap) to free the entry bank for rodata + BSS, and
# raise --bss-base so BSS sits at the top of the entry bank. The GS/OS
# Loader places each bank-aligned segment + applies the per-seg relocations.
emitMultiSeg() { # $1 = segment-cap, $2 = bss-base
"$LINK" -o "$linkBin" --text-base 0 --bss-base "$2" --segment-cap "$1" \
--overflow-cap "$JOEY_OVERFLOW_CAP" --full-bank \
--manifest "$linkManifest" --map "$linkMap" --reloc-out "$linkReloc" \
--multi-seg-expressload "${runtimeObjs[@]}" "${objs[@]}" 2>"$linkErr" || return 1
"$OMFEMIT" --manifest "$linkManifest" --expressload --relocs "$linkReloc" \
--output "$output"
}
# The C heap only has to be LARGE ENOUGH. It used to also have to sit below
# "the bank-0 IO window ($C000)", which was wrong: the program does not run
# in bank 0. Measured under MAME, joeylib's examples execute in banks
# $08-$0B, where there is no IO window and no language card, and link816's
# map is bank-relative (--text-base 0), so that test was comparing a bank
# OFFSET against a bank-0 address. link816's __heap_start comment has the
# full reasoning; the heap now runs to the top of the entry bank.
#
# This used to test only that the heap STARTED below $C000, which let the
# descending-cap search stop at the first layout that merely cleared the
# window -- even when almost nothing was left under it. A program that
# REDUCED its bss could therefore get a worse image: shrinking bss moves
# __heap_start down, so a layout that previously failed the test now
# passes it with a few hundred bytes of heap, and the search stops there.
# Space Taxi hit exactly this, linking a 778-byte heap (vs 4802 before);
# it drew its first screen and then froze, because every allocation after
# that failed. The big buffers (codegen arena, stage pixels, audio) come
# from the Memory Manager, but stdio FILE buffers and codegen arena slots
# come from this heap, so "a few hundred bytes" is never enough.
#
# If no cap reaches the minimum the caller still falls back to the highest
# cap that linked, so this can only reject layouts that were broken.
# Minimum size for the link-time heap WINDOW, in bytes. DEFAULT 0, and
# no target needs to raise it any more.
#
# What this window is: the free space above bss in the ENTRY BANK. In a
# program that links libc it is NOT the heap -- __heapInitMM asks the
# Memory Manager for a handle (which carries no attrBank, so it can land
# in any bank) and malloc runs in that. The window is only addressed
# directly if that allocation fails.
#
# The old measurements below were read as "each program needs its own
# minimum heap", with the odd result that MORE heap could break a program:
#
# STAXI 778 B -> froze on its first allocation
# STAXI 2426 B -> still froze
# AGI 3337 B -> runs KQ3 (19 segments)
# STAXI 4802 B -> runs (25 segments)
# AGI 7689 B -> BROKE: launched, then did nothing
#
# That last row is not mysterious: libc used to pass the window straight
# to NewHandle when it fell in [0x2000, 0xC000] and substitute a 32 KB
# default otherwise. 3337 is below 0x2000, so AGI got 32 KB and ran; 7689
# is inside the range, so AGI asked for 7689 -- less than it needs -- and
# drew nothing. The freezes are the same story with the Memory Manager
# exhausted by a many-segment image, so NewHandle failed and malloc fell
# back to the tiny window. Reproduced exactly while fixing the IO-window
# assumption, and fixed in libc.c: the window is now a floor to beat, not
# the request, so a bigger window can never mean a smaller heap.
JOEY_MIN_HEAP="${JOEY_MIN_HEAP:-0}"
# How much text an OVERFLOW segment may hold. --segment-cap bounds the
# ENTRY bank, which also carries rodata, bss and the heap -- for Space
# Taxi only ~12K of its 64K is left for text. An
# overflow bank carries text ONLY, so capping it the same way wasted ~50K
# per bank: 177K of code needed 17 bank-aligned segments instead of 4.
# Each segment must still fit one bank (16-bit intra-segment addressing),
# so this stays below 0x10000.
#
# DEFAULT 0 = overflow segments use --segment-cap (the historical
# behaviour); each target opts in from make/iigs.mk. AGI links 5 segments
# instead of 19, STAXI 4 instead of 26.
#
# Raising this on STAXI used to produce a dead image, which looked like a
# segment-size limit. It was not -- AGI ships 28798-byte segments and runs.
# The real chain: 26 bank-aligned segments starved the Memory Manager, so
# NTP's 34K NewHandle failed, gNTPReady stayed 0 and every SFX call
# short-circuited. IIgs builds were simply SILENT. Freeing that memory let
# audio initialise, and a latent bug fired the first time NTPstreamsound
# was ever reached (fixed in src/iigs/audio_full.c: SFX now requires a
# running replayer, as jlpAudioStopSfx already did).
#
# Each segment must still fit one bank (16-bit intra-segment addressing),
# so this stays below 0x10000.
JOEY_OVERFLOW_CAP="${JOEY_OVERFLOW_CAP:-0}"
heapUsable() {
local hs he
hs=$(grep -wE '__heap_start' "$linkMap" 2>/dev/null \
| grep -oE '0x[0-9a-fA-F]+' | head -1)
he=$(grep -wE '__heap_end' "$linkMap" 2>/dev/null \
| grep -oE '0x[0-9a-fA-F]+' | head -1)
[ -n "$hs" ] && [ -n "$he" ] || return 1
# No IO-window test: the heap is not in bank 0. See the __heap_start
# comment in link816.cpp -- GS/OS loads bank-aligned segments into
# free banks ($08-$0B here), where the whole 64K is plain RAM, and in
# a program that links libc this window is only a size request to the
# Memory Manager anyway.
[ $(( he - hs )) -ge $(( JOEY_MIN_HEAP )) ]
}
# Multi-seg, choosing the HIGHEST --segment-cap that BOTH links AND leaves a
# usable C heap. A lower cap pushes more TEXT into overflow banks, freeing
# entry-bank space for rodata + BSS + the heap; BSS packs
# tight (--bss-base 0x2000 -- safe, a relocatable multi-seg image loads high,
# never bank 0/1). The highest qualifying cap is preferred: fewest segments,
# fastest Loader boot.
#
# Do NOT "improve" this by falling back to whichever cap yields the BIGGEST
# heap. Tried 2026-09-19: for Space Taxi that selects cap 0x1000, and the
# resulting 49-segment image renders garbage (SCB byte reads back $DD). Low
# caps break for reasons this script cannot see, so the search stays anchored
# to the highest cap that qualifies, and the fallback stays the highest cap
# that merely linked.
# Any of these means seg 1 is too full for bank 0 and a smaller cap may
# fit: BSS, the trampoline table, or rodata + init_array (which follow
# seg 1's text) ran past the ceiling.
ENTRY_BANK_OVERFLOW='bss .*exceeds bank-0 ceiling|trampoline table pushes seg 1 over|rodata .*exceeds bank-0'
emitMultiSegFit() {
local cap
local fallback=""
for cap in 0xB000 0xA000 0x9000 0x8000 0x7000 0x6000 0x5000 0x4000 0x3000 0x2000 0x1000; do
if emitMultiSeg "$cap" 0x2000; then
[ -z "$fallback" ] && fallback="$cap"
heapUsable && return 0
else
# Keep shrinking only on a fixable entry-bank overflow; bail on
# anything else (unsplittable text, an unresolved symbol, ...).
grep -qE "$ENTRY_BANK_OVERFLOW" "$linkErr" || break
fi
done
# Nothing reached JOEY_MIN_HEAP; re-link the highest that linked and say so,
# because the image may well fail at run time for want of heap.
if [ -n "$fallback" ]; then
echo "clang-build: WARNING no segment cap reached a ${JOEY_MIN_HEAP}-byte" \
"heap; using $fallback -- the image may fail to allocate" >&2
emitMultiSeg "$fallback" 0x2000
return $?
fi
# Nothing linked at all. This used to fall off the end and return the
# status of the false `if` above -- 0 -- so the caller never printed
# the linker error and the build "succeeded" with no output file.
return 1
}
if "$LINK" -o "$linkBin" --text-base 0 --bss-base 0xA000 --full-bank \
--map "$linkMap" --reloc-out "$linkReloc" \
"${runtimeObjs[@]}" "${objs[@]}" 2>"$linkErr" && heapUsable; then
# Single-segment AND the heap is big enough: wrap as-is.
"$OMFEMIT" --input "$linkBin" --map "$linkMap" --base 0 --entry __start \
--output "$output" --name "$omfName" --expressload --relocs "$linkReloc"
else
# Single-seg overflowed (text/BSS/trampoline) OR it linked with too
# small a heap -- go multi-seg with a heap-aware descending cap.
emitMultiSegFit || { cat "$linkErr" >&2; exit 1; }
fi
# The link map is the only record of where a symbol landed, and the OMF
# carries no symbols at all, so anything that inspects a running program
# (the PC profiler, the MAME state gates) needs it kept. Off by default.
if [ -n "${JOEY_LINK_MAP:-}" ] && [ -f "$linkMap" ]; then
mkdir -p "$(dirname "$JOEY_LINK_MAP")"
cp "$linkMap" "$JOEY_LINK_MAP"
fi
# Combined header-dependency file: union of clang -MM over the C sources,
# rewritten to a single target ($output) so make rebuilds the binary when
# any transitively-included header changes.
if [ -n "$depfile" ] && [ ${#depsrcs[@]} -gt 0 ]; then
mkdir -p "$(dirname "$depfile")"
{
printf '%s:' "$output"
for s in "${depsrcs[@]}"; do
"$CLANG" "${CFLAGS[@]}" -MM "$s" 2>/dev/null \
| sed -e 's/^[^:]*://' -e 's/\\$//' | tr -d '\n'
done
printf '\n'
} > "$depfile"
fi