372 lines
18 KiB
Bash
Executable file
372 lines
18 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
|
|
#
|
|
# 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"
|
|
# A kept map (JOEY_LINK_MAP, below) lists the local symbols too: static
|
|
# functions, which a profile otherwise folds into the global before them.
|
|
# link816 only: omfEmit rejects the flag.
|
|
mapLocals=()
|
|
if [ -n "${JOEY_LINK_MAP:-}" ]; then
|
|
mapLocals=(--map-locals)
|
|
fi
|
|
|
|
# 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 "${mapLocals[@]}" "${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); a program opts in with ID_IIGS_OVERFLOW_CAP in its description
|
|
# (make/programs.mk). AGI links 5 segments instead of 19, STAXI 4 instead of
|
|
# 26 (both now separate projects built on JoeyLib).
|
|
#
|
|
# 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" \
|
|
"${mapLocals[@]}" "${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:
|
|
# JOEY_LINK_MAP names a directory, which gets <output name>.map and the
|
|
# segment images beside it, <output name>.seg<N>.bin (segment 1 is
|
|
# link816's main output), with link816's segment manifest, <output
|
|
# name>.json, for a multi-segment one. The Loader puts each segment in whatever bank it
|
|
# likes, and scripts/profile-iigs.sh finds them there by their bytes. A
|
|
# directory, not a file, because one make links several programs (a game
|
|
# disk's Start launcher, say) and each keeps its own.
|
|
if [ -n "${JOEY_LINK_MAP:-}" ] && [ -f "$linkMap" ]; then
|
|
mapBase="$JOEY_LINK_MAP/$(basename "$output")"
|
|
mkdir -p "$JOEY_LINK_MAP"
|
|
cp "$linkMap" "$mapBase.map"
|
|
rm -f "$mapBase".seg*.bin
|
|
cp "$linkBin" "$mapBase.seg1.bin"
|
|
for seg in "$tmpdir"/link.seg*.bin; do
|
|
[ -f "$seg" ] || continue
|
|
cp "$seg" "$mapBase.${seg##*/link.}"
|
|
done
|
|
rm -f "$mapBase.json"
|
|
if [ -f "$linkManifest" ]; then
|
|
cp "$linkManifest" "$mapBase.json"
|
|
fi
|
|
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
|