#!/usr/bin/env bash # # JoeyLib toolchain installer. # # Installs all cross-compilers, assemblers, and libraries needed to build # JoeyLib for Apple IIgs, Amiga, Atari ST, and MS-DOS into the local # toolchains/ folder. Nothing is installed system-wide. Free tools are # fetched and built; non-free tools (ORCA/C, GoldenGate) print clear # placement instructions and the script verifies they were placed # correctly on a subsequent run. # # Usage: # ./toolchains/install.sh install everything we can get # ./toolchains/install.sh --only iigs install only one platform # ./toolchains/install.sh --force wipe and reinstall # ./toolchains/install.sh --skip-emulators skip emulators (cross tools only) # ./toolchains/install.sh --help show this message # # Emulators: dosbox, fs-uae (Amiga), hatari (Atari ST), GSplus (Apple IIgs). # On Linux with apt, the first three are installed via 'sudo apt install'. # On macOS with Homebrew, they are installed via 'brew install'. GSplus is # always built from source into toolchains/emulators/gsplus/. # set -euo pipefail # Never prompt for git credentials. Missing / private GitHub repos # otherwise ask for a username/password on HTTPS clones; we want those # to fail fast so the installer reports clear instructions instead. export GIT_TERMINAL_PROMPT=0 export GIT_ASKPASS=/bin/true # ------------------------------------------------------------------------ # Paths and globals # ------------------------------------------------------------------------ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" CACHE_DIR="${SCRIPT_DIR}/cache" STATE_DIR="${SCRIPT_DIR}/.install_state" ONLY_PLATFORM="" FORCE_INSTALL=0 SKIP_EMULATORS=0 # Status tracking. Keys are component identifiers; values are "ok", # "missing", or "failed". declare -A STATUS declare -A INSTRUCTIONS # ------------------------------------------------------------------------ # Output helpers (ANSI color when stdout is a TTY) # ------------------------------------------------------------------------ if [ -t 1 ]; then C_RESET=$'\033[0m' C_BOLD=$'\033[1m' C_RED=$'\033[31m' C_GREEN=$'\033[32m' C_YELLOW=$'\033[33m' C_CYAN=$'\033[36m' else C_RESET="" C_BOLD="" C_RED="" C_GREEN="" C_YELLOW="" C_CYAN="" fi info() { printf '%s[*]%s %s\n' "${C_CYAN}" "${C_RESET}" "$*"; } ok() { printf '%s[ok]%s %s\n' "${C_GREEN}" "${C_RESET}" "$*"; } warn() { printf '%s[!!]%s %s\n' "${C_YELLOW}" "${C_RESET}" "$*"; } err() { printf '%s[xx]%s %s\n' "${C_RED}" "${C_RESET}" "$*" 1>&2; } header() { printf '\n%s== %s ==%s\n' "${C_BOLD}" "$*" "${C_RESET}"; } # ------------------------------------------------------------------------ # Utility functions # ------------------------------------------------------------------------ usage() { sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//' exit 0 } require_tool() { local tool="$1" if ! command -v "${tool}" >/dev/null 2>&1; then err "Required host tool not found: ${tool}" err "Install it via your system package manager and re-run." exit 1 fi } detect_host_os() { case "$(uname -s)" in Linux*) HOST_OS="linux" ;; Darwin*) HOST_OS="macos" ;; MINGW*|MSYS*|CYGWIN*) HOST_OS="windows" ;; *) err "Unsupported host OS: $(uname -s)" exit 1 ;; esac info "Host OS: ${HOST_OS}" } ensure_dirs() { mkdir -p "${CACHE_DIR}" "${STATE_DIR}" } mark_done() { touch "${STATE_DIR}/$1.done" } is_done() { [ -f "${STATE_DIR}/$1.done" ] } clear_done() { rm -f "${STATE_DIR}/$1.done" } download() { local url="$1" local dest="$2" if [ -f "${dest}" ]; then info "Cached: $(basename "${dest}")" return 0 fi info "Downloading $(basename "${dest}")" if command -v curl >/dev/null 2>&1; then curl -fL --retry 3 -o "${dest}" "${url}" elif command -v wget >/dev/null 2>&1; then wget -O "${dest}" "${url}" else err "Neither curl nor wget is installed" return 1 fi } # ------------------------------------------------------------------------ # Argument parsing # ------------------------------------------------------------------------ while [ $# -gt 0 ]; do case "$1" in --only) ONLY_PLATFORM="$2" shift 2 ;; --force) FORCE_INSTALL=1 shift ;; --skip-emulators) SKIP_EMULATORS=1 shift ;; --help|-h) usage ;; *) err "Unknown argument: $1" usage ;; esac done should_install() { local platform="$1" if [ -z "${ONLY_PLATFORM}" ]; then return 0 fi [ "${ONLY_PLATFORM}" = "${platform}" ] } # ------------------------------------------------------------------------ # IIgs: Merlin32 (free), ORCA/C (manual), GoldenGate (manual) # ------------------------------------------------------------------------ install_iigs() { header "IIgs toolchain" local base="${SCRIPT_DIR}/iigs" mkdir -p "${base}" # Merlin32 -- 65816 cross-assembler from Brutal Deluxe. # The canonical distribution is a zip on brutaldeluxe.fr that bundles # prebuilt binaries for Linux / macOS / Windows plus the C source. # We prefer the host's prebuilt binary; if not present, we build # from source with plain gcc (the source has no Makefile). if [ "${FORCE_INSTALL}" -eq 1 ]; then rm -rf "${base}/merlin32" clear_done "iigs_merlin32" fi if is_done "iigs_merlin32" && [ -x "${base}/merlin32/bin/merlin32" ]; then ok "Merlin32 already installed" STATUS[iigs_merlin32]="ok" else local merlin_installed=0 local merlin_urls=( "https://brutaldeluxe.fr/products/crossdevtools/merlin/Merlin32_v1.1.zip" "https://brutaldeluxe.fr/products/crossdevtools/merlin/Merlin32.zip" ) local merlin_zip="${CACHE_DIR}/merlin32.zip" local merlin_extract="${CACHE_DIR}/merlin32-extract" local prebuilt_subdir="" case "${HOST_OS}" in linux) prebuilt_subdir="Linux" ;; macos) prebuilt_subdir="MacOs" ;; *) prebuilt_subdir="" ;; esac for url in "${merlin_urls[@]}"; do if download "${url}" "${merlin_zip}" 2>/dev/null; then info "Extracting Merlin32" rm -rf "${merlin_extract}" mkdir -p "${merlin_extract}" if ! unzip -o "${merlin_zip}" -d "${merlin_extract}" >/dev/null 2>&1; then continue fi mkdir -p "${base}/merlin32/bin" # 1. Try prebuilt binary for the host OS. Name may be # capitalized ("Merlin32") or lowercase. No executable # bit in the zip, so we chmod after copying. local mbin="" if [ -n "${prebuilt_subdir}" ]; then mbin=$(find "${merlin_extract}" -type d -name "${prebuilt_subdir}" -print 2>/dev/null | head -n1) if [ -n "${mbin}" ]; then mbin=$(find "${mbin}" -maxdepth 1 -type f -iname "merlin32" 2>/dev/null | head -n1) fi fi if [ -n "${mbin}" ] && [ -f "${mbin}" ]; then cp "${mbin}" "${base}/merlin32/bin/merlin32" chmod +x "${base}/merlin32/bin/merlin32" mark_done "iigs_merlin32" ok "Merlin32 installed (prebuilt ${prebuilt_subdir} binary)" STATUS[iigs_merlin32]="ok" merlin_installed=1 break fi # 2. No prebuilt for this host; build from Source/. The # source has no Makefile; 'gcc -O2 *.c' produces the # binary directly. local msrc msrc=$(find "${merlin_extract}" -type d -name "Source" 2>/dev/null | head -n1) if [ -n "${msrc}" ] && [ -f "${msrc}/Main.c" ]; then info "Building Merlin32 from source" ( cd "${msrc}" gcc -O2 -o merlin32 *.c ) && { if [ -x "${msrc}/merlin32" ]; then cp "${msrc}/merlin32" "${base}/merlin32/bin/merlin32" mark_done "iigs_merlin32" ok "Merlin32 built from source and installed" STATUS[iigs_merlin32]="ok" merlin_installed=1 break fi } fi fi done if [ "${merlin_installed}" -eq 0 ]; then STATUS[iigs_merlin32]="missing" INSTRUCTIONS[iigs_merlin32]=$(cat </dev/null || true fi # Clean up any dangling symlinks that reference a now-moved iix. if [ -d "${gg_root}/bin" ]; then find "${gg_root}/bin" -maxdepth 1 -type l -xtype l -delete 2>/dev/null || true fi # Recreate gno/orca symlinks next to iix in gg-tools if they're # missing (iix behaves differently when invoked as 'gno' or 'orca'). if [ -x "${gg_tools_dir}/bin/iix" ]; then for alias in gno orca; do if [ ! -e "${gg_tools_dir}/bin/${alias}" ]; then ln -s iix "${gg_tools_dir}/bin/${alias}" fi done fi # Verify Unix tools if [ -x "${gg_tools_dir}/bin/iix" ]; then ok "GoldenGate host tools present at ${gg_tools_dir}/bin/" STATUS[iigs_gg_tools]="ok" else STATUS[iigs_gg_tools]="missing" INSTRUCTIONS[iigs_gg_tools]=$(cat </dev/null || { STATUS[iigs_goldengate]="failed" INSTRUCTIONS[iigs_goldengate]="Extract of ${gg_linux_tgz} failed" } # Extract fix-filetypes.sh separately (it sits at gg-linux/ root). if [ ! -f "${CACHE_DIR}/fix-filetypes.sh" ]; then tar -xzf "${gg_linux_tgz}" -C "${CACHE_DIR}" --strip-components=1 \ gg-linux/fix-filetypes.sh 2>/dev/null || true fi # FST tarball: merge fst/orca/ subtree into goldengate/. if [ -f "${fst_tgz}" ]; then info "Merging fst.tgz into goldengate/System/" tar -xzf "${fst_tgz}" -C "${gg_root}" --strip-components=2 \ fst/orca/ 2>/dev/null || warn "fst.tgz extract reported issues" else warn "${fst_tgz} not found; FSTs will be missing" fi if [ -f "${gg_root}/System/rom" ] && [ -d "${gg_root}/bin" ]; then mark_done "iigs_goldengate" ok "GoldenGate IIgs filesystem populated" STATUS[iigs_goldengate]="ok" else STATUS[iigs_goldengate]="failed" INSTRUCTIONS[iigs_goldengate]="goldengate/ did not end up with System/rom and bin/; check ${gg_linux_tgz}" fi fi fi # Merge ORCA (from Opus II) directories into goldengate/. if is_done "iigs_orca" && [ -f "${gg_root}/Languages/cc" ]; then ok "ORCA/C merged into goldengate/" STATUS[iigs_orca]="ok" else if [ "${STATUS[iigs_goldengate]}" != "ok" ]; then STATUS[iigs_orca]="missing" INSTRUCTIONS[iigs_orca]="Install GoldenGate IIgs filesystem first." elif [ ! -d "${opus_dir}" ]; then STATUS[iigs_orca]="missing" INSTRUCTIONS[iigs_orca]=$(cat </dev/null || orca_ok=0 else warn "${opus_dir}/${sub} missing" orca_ok=0 fi done # Run fix-filetypes.sh which sets IIgs filetype xattrs via chtyp. # chtyp is an IIgs binary inside goldengate/bin and is invoked # by fix-filetypes through iix, so we need iix on PATH and # GOLDEN_GATE exported first. if [ "${orca_ok}" -eq 1 ] && [ -f "${CACHE_DIR}/fix-filetypes.sh" ] \ && [ -x "${gg_tools_dir}/bin/iix" ]; then info "Running fix-filetypes.sh" ( cd "${gg_root}" export GOLDEN_GATE="${gg_root}" export PATH="${gg_tools_dir}/bin:${PATH}" bash "${CACHE_DIR}/fix-filetypes.sh" >/dev/null 2>&1 ) || warn "fix-filetypes.sh reported issues; proceeding" fi if [ "${orca_ok}" -eq 1 ] && [ -f "${gg_root}/Languages/cc" ]; then mark_done "iigs_orca" ok "ORCA/C merged and filetypes fixed" STATUS[iigs_orca]="ok" else STATUS[iigs_orca]="failed" INSTRUCTIONS[iigs_orca]="ORCA merge did not produce ${gg_root}/Languages/cc; check ${opus_dir}/" fi fi fi } # ------------------------------------------------------------------------ # Amiga: Bebbo m68k-amigaos-gcc, vasm, NDK, PTPlayer # ------------------------------------------------------------------------ install_amiga() { header "Amiga toolchain" local base="${SCRIPT_DIR}/amiga" mkdir -p "${base}" # Bebbo amiga-gcc -- build from source; long but reliable if [ "${FORCE_INSTALL}" -eq 1 ]; then rm -rf "${base}/gcc" clear_done "amiga_gcc" fi if is_done "amiga_gcc" && [ -x "${base}/gcc/bin/m68k-amigaos-gcc" ]; then ok "m68k-amigaos-gcc already installed" STATUS[amiga_gcc]="ok" else ensure_build_deps "m68k-amigaos-gcc" \ "build-essential bison flex m4 texinfo libgmp-dev libmpfr-dev libmpc-dev autoconf automake lhasa" \ "bison flex m4 texinfo gmp mpfr libmpc autoconf automake lhasa" || true info "Cloning AmigaPorts/m68k-amigaos-gcc (this build takes a while)" local src="${CACHE_DIR}/amiga-gcc-src" if [ ! -d "${src}" ]; then if ! git clone https://github.com/AmigaPorts/m68k-amigaos-gcc.git "${src}"; then STATUS[amiga_gcc]="failed" INSTRUCTIONS[amiga_gcc]=$(cat </dev/null || true ) && { mark_done "vasm" ok "vasm installed" STATUS[vasm]="ok" } || { STATUS[vasm]="failed" INSTRUCTIONS[vasm]="vasm build failed; see ${vasm_src}" } fi # NDK -- AmigaOS includes (Bebbo's repo bundles these via 'make all') if [ -d "${base}/gcc/m68k-amigaos/sys-include" ] || [ -d "${base}/gcc/m68k-amigaos/include" ]; then ok "Amiga NDK headers present (bundled with amiga-gcc)" STATUS[amiga_ndk]="ok" else STATUS[amiga_ndk]="missing" INSTRUCTIONS[amiga_ndk]="Amiga NDK headers should be installed by Bebbo's amiga-gcc 'make all'. Re-run installer after fixing amiga-gcc." fi # PTPlayer -- MOD replayer by Frank Wille, distributed as an LHA # archive on Aminet. Extract with lhasa (installed as part of the # Amiga GCC build deps above). if [ "${FORCE_INSTALL}" -eq 1 ]; then rm -rf "${base}/ptplayer" clear_done "amiga_ptplayer" fi if is_done "amiga_ptplayer" && [ -f "${base}/ptplayer/ptplayer.asm" ]; then ok "PTPlayer already installed" STATUS[amiga_ptplayer]="ok" else info "Downloading PTPlayer from Aminet" local ptp_lha="${CACHE_DIR}/ptplayer.lha" if ! download "http://aminet.net/mus/play/ptplayer.lha" "${ptp_lha}"; then STATUS[amiga_ptplayer]="failed" INSTRUCTIONS[amiga_ptplayer]="Could not download http://aminet.net/mus/play/ptplayer.lha" else local extractor="" if command -v lhasa >/dev/null 2>&1; then extractor="lhasa" elif command -v lha >/dev/null 2>&1; then extractor="lha" fi if [ -z "${extractor}" ]; then STATUS[amiga_ptplayer]="missing" INSTRUCTIONS[amiga_ptplayer]=$(cat < amiga/vasm" STATUS[atarist_vasm]="ok" else STATUS[atarist_vasm]="missing" INSTRUCTIONS[atarist_vasm]="vasm not yet installed for Amiga; install Amiga toolchain first." fi fi } # ------------------------------------------------------------------------ # DOS: DJGPP, NASM, CWSDPMI # ------------------------------------------------------------------------ install_dos() { header "DOS toolchain" local base="${SCRIPT_DIR}/dos" mkdir -p "${base}" # DJGPP -- the canonical zip set if [ "${FORCE_INSTALL}" -eq 1 ]; then rm -rf "${base}/djgpp" clear_done "dos_djgpp" fi if is_done "dos_djgpp" && [ -x "${base}/djgpp/bin/i586-pc-msdosdjgpp-gcc" ]; then ok "DJGPP already installed" STATUS[dos_djgpp]="ok" else # Use prebuilt DJGPP binaries from andrewwutw/build-djgpp # releases. Much faster than source build and avoids a long list # of build-dep prerequisites. info "Downloading DJGPP prebuilt binaries" local djgpp_arch="" local djgpp_url="" case "${HOST_OS}" in linux) if [ "$(uname -m)" = "x86_64" ]; then djgpp_arch="linux64" else djgpp_arch="linux32" fi ;; macos) djgpp_arch="osx" ;; esac if [ -n "${djgpp_arch}" ]; then djgpp_url="https://github.com/andrewwutw/build-djgpp/releases/download/v3.4/djgpp-${djgpp_arch}-gcc1220.tar.bz2" fi local djgpp_tar="${CACHE_DIR}/djgpp.tar.bz2" if [ -n "${djgpp_url}" ] && download "${djgpp_url}" "${djgpp_tar}" 2>/dev/null; then mkdir -p "${base}/djgpp" # The tarball extracts to a 'djgpp' directory at the top level. # Strip one component so contents land directly in ${base}/djgpp. tar -xjf "${djgpp_tar}" -C "${base}/djgpp" --strip-components=1 && { if [ -x "${base}/djgpp/bin/i586-pc-msdosdjgpp-gcc" ]; then mark_done "dos_djgpp" ok "DJGPP installed (prebuilt ${djgpp_arch})" STATUS[dos_djgpp]="ok" else STATUS[dos_djgpp]="failed" INSTRUCTIONS[dos_djgpp]="DJGPP tarball extracted but i586-pc-msdosdjgpp-gcc not found under ${base}/djgpp/bin/" fi } || { STATUS[dos_djgpp]="failed" INSTRUCTIONS[dos_djgpp]="Failed to extract DJGPP tarball ${djgpp_tar}" } else STATUS[dos_djgpp]="failed" INSTRUCTIONS[dos_djgpp]=$(cat </dev/null) && { ok "CWSDPMI installed" STATUS[dos_cwsdpmi]="ok" } || { STATUS[dos_cwsdpmi]="failed" } fi } # ------------------------------------------------------------------------ # Emulators: DOSBox, FS-UAE, Hatari, GSplus # # Package-manager-installable emulators (DOSBox, FS-UAE, Hatari) are # installed via apt on Linux / brew on macOS and exist on the host PATH # after install. GSplus has no package and is always built from source # into toolchains/emulators/gsplus/. # ------------------------------------------------------------------------ pkg_install() { # Install a list of package names on the current host. Returns 0 on # success, non-zero on any failure. Uses apt on Linux, brew on macOS. case "${HOST_OS}" in linux) if command -v apt-get >/dev/null 2>&1; then sudo apt-get install -y "$@" return $? fi ;; macos) if command -v brew >/dev/null 2>&1; then brew install "$@" return $? fi ;; esac return 1 } # Install build dependencies. Args: label, apt-list, brew-list. # The label identifies what we're about to build; the lists are the # space-separated package names for each package manager. On failure, # prints instructions but does not abort the installer (the build step # will then fail with a clearer message). ensure_build_deps() { local label="$1" local apt_pkgs="$2" local brew_pkgs="$3" local pkgs="" case "${HOST_OS}" in linux) pkgs="${apt_pkgs}" ;; macos) pkgs="${brew_pkgs}" ;; esac if [ -z "${pkgs}" ]; then return 0 fi info "Installing ${label} build dependencies" if ! pkg_install ${pkgs}; then warn "Failed to install ${label} dependencies via package manager." warn "You may need to install manually: ${pkgs}" return 1 fi return 0 } install_apt_emulator() { local key="$1" local cmd="$2" local aptpkg="$3" local brewpkg="$4" if command -v "${cmd}" >/dev/null 2>&1; then ok "${cmd} already on PATH" STATUS[${key}]="ok" return fi info "Installing ${cmd} via host package manager" local pkgs case "${HOST_OS}" in linux) pkgs="${aptpkg}" ;; macos) pkgs="${brewpkg}" ;; *) pkgs="" ;; esac if [ -n "${pkgs}" ] && pkg_install ${pkgs}; then ok "${cmd} installed" STATUS[${key}]="ok" else STATUS[${key}]="missing" INSTRUCTIONS[${key}]=$(cat </dev/null 2>&1; then ok "gsplus already on PATH" STATUS[emu_gsplus]="ok" return fi if is_done "emu_gsplus" && [ -x "${bin}" ]; then ok "GSplus already built at ${bin}" STATUS[emu_gsplus]="ok" return fi ensure_build_deps "GSplus" \ "build-essential libx11-dev libxext-dev libpulse-dev libpcap-dev libfreetype6-dev" \ "libpcap freetype pulseaudio" || true info "Building GSplus from source (Apple IIgs emulator)" local src="${CACHE_DIR}/gsplus-src" if [ ! -d "${src}" ]; then if ! git clone --depth 1 https://github.com/digarok/gsplus.git "${src}"; then STATUS[emu_gsplus]="failed" INSTRUCTIONS[emu_gsplus]="git clone https://github.com/digarok/gsplus.git failed" return fi fi # GSplus build layout: source lives at gsplus/src/ and expects one # of the vars_* config files to be copied into 'vars' before make. # On Linux we use the X11/PulseAudio build; on macOS we use the # Mac X11 build for command-line use (the Swift/clang .app build # needs Xcode and is skipped here). local gsplus_src_dir="${src}/gsplus/src" local gsplus_target="" local vars_file="" case "${HOST_OS}" in linux) vars_file="vars_x86linux" gsplus_target="gsplus-x" ;; macos) vars_file="vars_mac_x" gsplus_target="gsplus-x" ;; esac if [ -z "${vars_file}" ] || [ ! -f "${gsplus_src_dir}/${vars_file}" ]; then STATUS[emu_gsplus]="failed" INSTRUCTIONS[emu_gsplus]=$(cat </dev/null | xargs -n1 basename 2>/dev/null | tr '\n' ' ') Copy the appropriate vars_* file to 'vars' and run make manually. EOF ) return fi ( cd "${gsplus_src_dir}" cp "${vars_file}" vars make clean >/dev/null 2>&1 || true make "${gsplus_target}" ) && { # Built binary lands at gsplus/src/../gsplus per the Makefile # (it's moved up one level after linking). local gsbin="${src}/gsplus/gsplus" if [ ! -f "${gsbin}" ]; then # Fallback: find anywhere under the tree gsbin=$(find "${src}" -maxdepth 4 -name gsplus -type f 2>/dev/null | head -n1) fi if [ -n "${gsbin}" ] && [ -f "${gsbin}" ]; then mkdir -p "${base}/bin" cp "${gsbin}" "${base}/bin/gsplus" chmod +x "${base}/bin/gsplus" # Also copy the support files GSplus reads at runtime if [ -d "${src}/gsplus/lib" ]; then mkdir -p "${base}/lib" cp -r "${src}/gsplus/lib/." "${base}/lib/" fi mark_done "emu_gsplus" ok "GSplus built and installed" STATUS[emu_gsplus]="ok" else STATUS[emu_gsplus]="failed" INSTRUCTIONS[emu_gsplus]="GSplus build succeeded but binary not found; check ${src}" fi } || { STATUS[emu_gsplus]="failed" INSTRUCTIONS[emu_gsplus]=$(cat < "${dest}" mark_done "${key}" ok "IIgs null slot-6 PROM staged at ${dest}" STATUS[${key}]="ok" } install_emulator_support() { header "Emulator support files" local support_dir="${SCRIPT_DIR}/emulators/support" mkdir -p "${support_dir}" install_support_emutos "${support_dir}" install_support_iigs_rom "${support_dir}" install_support_iigs_null_c600 "${support_dir}" install_support_gsos "${support_dir}" } install_emulators() { header "Emulators" install_apt_emulator emu_dosbox dosbox dosbox dosbox-x install_apt_emulator emu_fsuae fs-uae fs-uae fs-uae install_apt_emulator emu_hatari hatari hatari hatari install_gsplus install_emulator_support } # ------------------------------------------------------------------------ # Final status report # ------------------------------------------------------------------------ print_status_report() { header "Installation status" local any_missing=0 local any_failed=0 local key local s # First pass: compute overall status. Do this in the current shell # (not inside a pipeline) so variable assignments stick. for key in "${!STATUS[@]}"; do s="${STATUS[$key]}" case "$s" in missing) any_missing=1 ;; failed) any_failed=1 ;; esac done # Second pass: render sorted output. { for key in "${!STATUS[@]}"; do s="${STATUS[$key]}" case "$s" in ok) printf ' %s[ok]%s %s\n' "${C_GREEN}" "${C_RESET}" "$key" ;; missing) printf ' %s[--]%s %s (manual install required)\n' "${C_YELLOW}" "${C_RESET}" "$key" ;; failed) printf ' %s[xx]%s %s (install failed)\n' "${C_RED}" "${C_RESET}" "$key" ;; esac done } | sort if [ "${any_missing}" -eq 1 ] || [ "${any_failed}" -eq 1 ]; then header "Action required" for key in "${!INSTRUCTIONS[@]}"; do if [ "${STATUS[$key]}" != "ok" ]; then printf '\n%s---- %s ----%s\n%s\n' "${C_BOLD}" "$key" "${C_RESET}" "${INSTRUCTIONS[$key]}" fi done echo "" warn "Some components are not yet installed. Re-run after addressing the items above." exit 1 else echo "" ok "All requested toolchains are ready." info "Now: source ${SCRIPT_DIR}/env.sh" fi } # ------------------------------------------------------------------------ # Main # ------------------------------------------------------------------------ main() { detect_host_os require_tool make require_tool tar require_tool unzip ensure_dirs if should_install iigs; then install_iigs; fi if should_install amiga; then install_amiga; fi if should_install atarist; then install_atarist; fi if should_install dos; then install_dos; fi if [ "${SKIP_EMULATORS}" -eq 0 ] && [ -z "${ONLY_PLATFORM}" ]; then install_emulators fi print_status_report } main "$@"