joeylib2/toolchains/install.sh

1327 lines
46 KiB
Bash
Executable file

#!/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 <<EOF
Merlin32 could not be installed automatically. Install manually:
1. Visit https://brutaldeluxe.fr/products/crossdevtools/merlin/
2. Download the latest Merlin32 zip.
3. Either copy the prebuilt binary for your OS, or build from Source/
with 'gcc -O2 -o merlin32 *.c'.
4. Place the result at ${base}/merlin32/bin/merlin32 and chmod +x it.
5. Re-run ./toolchains/install.sh to verify.
EOF
)
fi
fi
# GoldenGate Unix-side host tools (iix, dumpobj, profuse, ...).
# These are built from the GoldenGate source repo at stuff/GoldenGate/.
# They must NOT live inside the IIgs filesystem tree that
# $GOLDEN_GATE points at, because that tree represents the emulated
# IIgs volume (case-insensitive, with its own bin/ of IIgs utilities).
# Install layout:
# toolchains/iigs/gg-tools/bin/ Unix binaries on host PATH
# toolchains/iigs/goldengate/ $GOLDEN_GATE root (IIgs filesystem)
local gg_tools_dir="${base}/gg-tools"
local gg_root="${base}/goldengate"
local stuff_dir="${REPO_DIR}/stuff"
# Migrate any Unix tools the user may have placed at goldengate/bin/
# over to gg-tools/bin/ so the goldengate directory can be repopulated
# with the IIgs filesystem tree from gg-linux.tgz. iix's siblings gno
# and orca are typically symlinks to iix -- use -e (not -f) so broken
# symlinks from a prior partial migration also match.
if [ -x "${gg_root}/bin/iix" ] && [ ! -x "${gg_tools_dir}/bin/iix" ]; then
info "Relocating Unix host tools from goldengate/bin to gg-tools/bin"
mkdir -p "${gg_tools_dir}/bin"
# Move iix last so the symlinks that point at it resolve during mv.
for f in dumpobj profuse mkfs-profuse opus-extractor orca gno iix; do
if [ -e "${gg_root}/bin/${f}" ] || [ -L "${gg_root}/bin/${f}" ]; then
mv "${gg_root}/bin/${f}" "${gg_tools_dir}/bin/"
fi
done
rmdir "${gg_root}/bin" 2>/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 <<EOF
GoldenGate Unix host tools not found at ${gg_tools_dir}/bin/.
Build from the GoldenGate source repo (e.g. in stuff/GoldenGate):
cd stuff/GoldenGate
git submodule update --init
mkdir -p build && cd build
cmake .. && make iix dumpobj profuse mkfs-profuse opus-extractor
Then copy the built binaries to ${gg_tools_dir}/bin/ and re-run
./toolchains/install.sh to verify.
EOF
)
fi
# Populate the IIgs filesystem at $GOLDEN_GATE. Sources expected in
# stuff/:
# gg-linux.tgz IIgs support tree (bin, etc, Home, lib, System, usr)
# fst.tgz File System Translators and fst-rom
# opus/ Opus II extract with Languages/Libraries/Shell/Utilities/
local gg_linux_tgz="${stuff_dir}/gg-linux.tgz"
local fst_tgz="${stuff_dir}/fst.tgz"
local opus_dir="${stuff_dir}/opus"
if [ "${FORCE_INSTALL}" -eq 1 ]; then
clear_done "iigs_goldengate"
clear_done "iigs_orca"
rm -rf "${gg_root}"
fi
if is_done "iigs_goldengate" && [ -f "${gg_root}/System/rom" ] && [ -d "${gg_root}/bin" ]; then
ok "GoldenGate IIgs filesystem already populated"
STATUS[iigs_goldengate]="ok"
else
if [ ! -f "${gg_linux_tgz}" ]; then
STATUS[iigs_goldengate]="missing"
INSTRUCTIONS[iigs_goldengate]=$(cat <<EOF
Cannot populate the $GOLDEN_GATE tree at ${gg_root}/.
Expected archive: ${gg_linux_tgz}
Place the gg-linux.tgz archive (from the GoldenGate distribution) into
${stuff_dir}/ and re-run ./toolchains/install.sh.
EOF
)
else
info "Extracting gg-linux.tgz into goldengate/"
mkdir -p "${gg_root}"
# The archive roots at gg-linux/GoldenGate/; strip both levels.
tar -xzf "${gg_linux_tgz}" -C "${gg_root}" --strip-components=2 \
gg-linux/GoldenGate/ 2>/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 <<EOF
ORCA source directory not found at ${opus_dir}/.
Extract the Opus II ORCA distribution into ${opus_dir}/ so it contains:
Languages/ Libraries/ Shell/ Utilities/
Then re-run ./toolchains/install.sh.
EOF
)
else
info "Merging Opus II ORCA directories into goldengate/"
local orca_ok=1
for sub in Languages Libraries Shell Utilities; do
if [ -d "${opus_dir}/${sub}" ]; then
cp -rn "${opus_dir}/${sub}" "${gg_root}/" 2>/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 <<EOF
Clone https://github.com/AmigaPorts/m68k-amigaos-gcc manually and build:
git clone https://github.com/AmigaPorts/m68k-amigaos-gcc.git
cd m68k-amigaos-gcc
make all PREFIX=${base}/gcc
Build deps (Debian/Ubuntu):
sudo apt install build-essential bison flex texinfo libgmp-dev libmpfr-dev libmpc-dev
EOF
)
return
fi
fi
(
cd "${src}"
make all PREFIX="${base}/gcc"
) && {
mark_done "amiga_gcc"
ok "m68k-amigaos-gcc installed"
STATUS[amiga_gcc]="ok"
} || {
STATUS[amiga_gcc]="failed"
INSTRUCTIONS[amiga_gcc]=$(cat <<EOF
m68k-amigaos-gcc build failed. Install build dependencies:
Debian/Ubuntu: sudo apt install build-essential bison flex texinfo libgmp-dev libmpfr-dev libmpc-dev
macOS Homebrew: brew install bison flex texinfo gmp mpfr libmpc
Then delete ${src} and re-run ./toolchains/install.sh.
Source: ${src}
EOF
)
}
fi
# vasm -- assembler used by Amiga and ST
if [ "${FORCE_INSTALL}" -eq 1 ]; then
rm -rf "${base}/vasm"
clear_done "vasm"
fi
if is_done "vasm" && [ -x "${base}/vasm/bin/vasmm68k_mot" ]; then
ok "vasm already installed"
STATUS[vasm]="ok"
else
info "Building vasm from source"
local vasm_src="${CACHE_DIR}/vasm"
if [ ! -d "${vasm_src}" ]; then
download "http://sun.hasenbraten.de/vasm/release/vasm.tar.gz" \
"${CACHE_DIR}/vasm.tar.gz"
mkdir -p "${vasm_src}"
tar -xzf "${CACHE_DIR}/vasm.tar.gz" -C "${vasm_src}" --strip-components=1
fi
(
cd "${vasm_src}"
make CPU=m68k SYNTAX=mot
mkdir -p "${base}/vasm/bin"
cp vasmm68k_mot vobjdump "${base}/vasm/bin/" 2>/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 <<EOF
PTPlayer was downloaded but no LHA extractor is available.
Install one and re-run:
Debian/Ubuntu: sudo apt install lhasa
macOS Homebrew: brew install lhasa
Archive at: ${ptp_lha}
Target: ${base}/ptplayer/
EOF
)
else
mkdir -p "${base}/ptplayer"
local extract_ok=0
if [ "${extractor}" = "lhasa" ]; then
(cd "${base}/ptplayer" && lhasa xq "${ptp_lha}") && extract_ok=1
else
(cd "${base}/ptplayer" && lha xq "${ptp_lha}") && extract_ok=1
fi
if [ "${extract_ok}" -eq 1 ] && [ -f "${base}/ptplayer/ptplayer.asm" ]; then
mark_done "amiga_ptplayer"
ok "PTPlayer installed"
STATUS[amiga_ptplayer]="ok"
else
STATUS[amiga_ptplayer]="failed"
INSTRUCTIONS[amiga_ptplayer]="LHA extract of ${ptp_lha} failed or did not produce ptplayer.asm"
fi
fi
fi
fi
}
# ------------------------------------------------------------------------
# Atari ST: m68k-atari-mint-gcc, MiNTLib (vasm shared with Amiga)
# ------------------------------------------------------------------------
install_atarist() {
header "Atari ST toolchain"
local base="${SCRIPT_DIR}/atarist"
mkdir -p "${base}"
# m68k-atari-mint-gcc, binutils, and MiNTLib come as three separate
# tarballs from tho-otto.de (Vincent Riviere's official mirror).
# Each tarball is rooted at 'usr/'; stripping that single component
# places everything into ${base}/gcc/ cleanly, and the relocated
# tree works without -B/--sysroot flags.
local mint_host=""
case "${HOST_OS}" in
linux)
if [ "$(uname -m)" = "x86_64" ]; then
mint_host="linux64"
else
mint_host="linux32"
fi
;;
macos) mint_host="macos" ;;
esac
local mint_base_url="https://tho-otto.de/download/mint"
local mint_tarballs=(
"gcc-15.2.0-mint-20250810-bin-${mint_host}.tar.xz"
"binutils-2.45-mint-20250812-bin-${mint_host}.tar.xz"
)
local mintlib_tarball="mintlib-0.60.1-mint-20240718-dev.tar.xz"
if [ "${FORCE_INSTALL}" -eq 1 ]; then
rm -rf "${base}/gcc"
clear_done "atarist_gcc"
clear_done "atarist_mintlib"
fi
# GCC + binutils
if is_done "atarist_gcc" && [ -x "${base}/gcc/bin/m68k-atari-mint-gcc" ]; then
ok "m68k-atari-mint-gcc already installed"
STATUS[atarist_gcc]="ok"
else
if [ -z "${mint_host}" ]; then
STATUS[atarist_gcc]="missing"
INSTRUCTIONS[atarist_gcc]="No prebuilt m68k-atari-mint-gcc available for host '${HOST_OS}'. Install manually from https://tho-otto.de/download/mint/"
else
mkdir -p "${base}/gcc"
local gcc_ok=1
for tarball in "${mint_tarballs[@]}"; do
local dest="${CACHE_DIR}/${tarball}"
if ! download "${mint_base_url}/${tarball}" "${dest}"; then
gcc_ok=0
break
fi
if ! tar -xJf "${dest}" -C "${base}/gcc" --strip-components=1; then
gcc_ok=0
break
fi
done
if [ "${gcc_ok}" -eq 1 ] && [ -x "${base}/gcc/bin/m68k-atari-mint-gcc" ]; then
mark_done "atarist_gcc"
ok "m68k-atari-mint-gcc installed (prebuilt ${mint_host})"
STATUS[atarist_gcc]="ok"
else
STATUS[atarist_gcc]="failed"
INSTRUCTIONS[atarist_gcc]="m68k-atari-mint-gcc tarball download or extract failed; see ${CACHE_DIR}"
fi
fi
fi
# MiNTLib -- headers and libc for cross-compile
if is_done "atarist_mintlib" && [ -d "${base}/gcc/m68k-atari-mint/sys-root" ]; then
ok "MiNTLib already installed"
STATUS[atarist_mintlib]="ok"
else
if [ ! -x "${base}/gcc/bin/m68k-atari-mint-gcc" ]; then
STATUS[atarist_mintlib]="missing"
INSTRUCTIONS[atarist_mintlib]="Install m68k-atari-mint-gcc first; MiNTLib extracts into the same prefix."
else
local mlib_dest="${CACHE_DIR}/${mintlib_tarball}"
if download "${mint_base_url}/${mintlib_tarball}" "${mlib_dest}" && \
tar -xJf "${mlib_dest}" -C "${base}/gcc" --strip-components=1; then
mark_done "atarist_mintlib"
ok "MiNTLib installed"
STATUS[atarist_mintlib]="ok"
else
STATUS[atarist_mintlib]="failed"
INSTRUCTIONS[atarist_mintlib]="MiNTLib tarball download or extract failed; see ${mlib_dest}"
fi
fi
fi
# vasm -- symlink to the Amiga port's vasm
if [ -L "${base}/vasm" ] || [ -d "${base}/vasm" ]; then
ok "vasm symlink present for ST"
STATUS[atarist_vasm]="ok"
else
if [ -d "${SCRIPT_DIR}/amiga/vasm" ]; then
ln -s "../amiga/vasm" "${base}/vasm"
ok "Linked atarist/vasm -> 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 <<EOF
No prebuilt DJGPP is available for host '${HOST_OS}'. Build from source:
git clone https://github.com/andrewwutw/build-djgpp.git
cd build-djgpp
DJGPP_PREFIX=${base}/djgpp ./build-djgpp.sh 12.2.0
Build deps (Debian/Ubuntu):
sudo apt install build-essential bison flex texinfo zlib1g-dev unzip curl
EOF
)
fi
fi
# NASM
if [ "${FORCE_INSTALL}" -eq 1 ]; then
rm -rf "${base}/nasm"
clear_done "dos_nasm"
fi
if is_done "dos_nasm" && [ -x "${base}/nasm/bin/nasm" ]; then
ok "NASM already installed"
STATUS[dos_nasm]="ok"
else
info "Building NASM"
local nasm_ver="2.16.03"
local nasm_url="https://www.nasm.us/pub/nasm/releasebuilds/${nasm_ver}/nasm-${nasm_ver}.tar.gz"
download "${nasm_url}" "${CACHE_DIR}/nasm.tar.gz"
local nasm_src="${CACHE_DIR}/nasm-src"
rm -rf "${nasm_src}"
mkdir -p "${nasm_src}"
tar -xzf "${CACHE_DIR}/nasm.tar.gz" -C "${nasm_src}" --strip-components=1
(
cd "${nasm_src}"
./configure --prefix="${base}/nasm"
make
make install
) && {
mark_done "dos_nasm"
ok "NASM installed"
STATUS[dos_nasm]="ok"
} || {
STATUS[dos_nasm]="failed"
INSTRUCTIONS[dos_nasm]="NASM build failed; see ${nasm_src}"
}
fi
# CWSDPMI
if [ -f "${base}/cwsdpmi/bin/CWSDSTUB.EXE" ] && [ -f "${base}/cwsdpmi/bin/CWSDPMI.EXE" ]; then
ok "CWSDPMI present"
STATUS[dos_cwsdpmi]="ok"
else
info "Fetching CWSDPMI"
download "https://sandmann.dotster.com/cwsdpmi/csdpmi7b.zip" \
"${CACHE_DIR}/cwsdpmi.zip" || {
STATUS[dos_cwsdpmi]="failed"
INSTRUCTIONS[dos_cwsdpmi]="Download CWSDPMI binaries from https://sandmann.dotster.com/cwsdpmi/ and unzip into ${base}/cwsdpmi/bin/"
return
}
mkdir -p "${base}/cwsdpmi/bin"
(cd "${base}/cwsdpmi/bin" && unzip -o "${CACHE_DIR}/cwsdpmi.zip" >/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 <<EOF
${cmd} was not installed. Install it manually:
Debian/Ubuntu: sudo apt install ${aptpkg}
macOS Homebrew: brew install ${brewpkg}
Windows: install manually and ensure '${cmd}' is on PATH
Re-run ./toolchains/install.sh to verify.
EOF
)
fi
}
install_gsplus() {
local base="${SCRIPT_DIR}/emulators/gsplus"
local bin="${base}/bin/gsplus"
if [ "${FORCE_INSTALL}" -eq 1 ]; then
rm -rf "${base}"
clear_done "emu_gsplus"
fi
if command -v gsplus >/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 <<EOF
GSplus: no build configuration available for host '${HOST_OS}'.
Available configurations in ${gsplus_src_dir}/:
$(ls ${gsplus_src_dir}/vars_* 2>/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 <<EOF
GSplus '${gsplus_target}' target build failed in ${gsplus_src_dir}.
Dependencies expected (Debian/Ubuntu):
sudo apt install build-essential libx11-dev libxext-dev libpulse-dev libpcap-dev libfreetype6-dev
Then delete ${src} and re-run ./toolchains/install.sh.
EOF
)
}
}
install_support_emutos() {
local support_dir="$1"
local dest="${support_dir}/emutos-512k.img"
local key="emu_support_emutos"
if [ "${FORCE_INSTALL}" -eq 1 ]; then
rm -f "${dest}"
clear_done "${key}"
fi
if is_done "${key}" && [ -f "${dest}" ]; then
ok "EmuTOS already staged"
STATUS[${key}]="ok"
return 0
fi
local zip_path="${CACHE_DIR}/emutos-512k-1.4.zip"
local extract_dir="${CACHE_DIR}/emutos-512k-1.4"
local url="https://sourceforge.net/projects/emutos/files/emutos/1.4/emutos-512k-1.4.zip/download"
download "${url}" "${zip_path}" || {
STATUS[${key}]="failed"
return 1
}
rm -rf "${extract_dir}"
(cd "${CACHE_DIR}" && unzip -oq "${zip_path}") || {
STATUS[${key}]="failed"
return 1
}
if [ ! -f "${extract_dir}/etos512us.img" ]; then
err "EmuTOS zip missing etos512us.img"
STATUS[${key}]="failed"
return 1
fi
cp "${extract_dir}/etos512us.img" "${dest}"
mark_done "${key}"
ok "EmuTOS staged at ${dest}"
STATUS[${key}]="ok"
}
install_support_iigs_rom() {
local support_dir="$1"
local dest="${support_dir}/apple-iigs.rom"
local key="emu_support_iigs_rom"
if [ "${FORCE_INSTALL}" -eq 1 ]; then
rm -f "${dest}"
clear_done "${key}"
fi
if is_done "${key}" && [ -f "${dest}" ]; then
ok "Apple IIgs ROM already staged"
STATUS[${key}]="ok"
return 0
fi
local zip_path="${CACHE_DIR}/iigs_rom03.zip"
local url="https://mirrors.apple2.org.za/ftp.apple.asimov.net/emulators/rom_images/iigs_rom03.zip"
download "${url}" "${zip_path}" || {
STATUS[${key}]="failed"
return 1
}
local tmp
tmp=$(mktemp -d)
(cd "${tmp}" && unzip -oq "${zip_path}") || {
rm -rf "${tmp}"
STATUS[${key}]="failed"
return 1
}
local rom_src
rom_src=$(find "${tmp}" -maxdepth 2 -type f \( -iname 'APPLE2GS.ROM*' -o -iname '*.rom' -o -iname '*.bin' \) | head -1)
if [ -z "${rom_src}" ] || [ ! -f "${rom_src}" ]; then
err "Apple IIgs ROM zip did not contain a ROM file"
rm -rf "${tmp}"
STATUS[${key}]="failed"
return 1
fi
cp "${rom_src}" "${dest}"
rm -rf "${tmp}"
mark_done "${key}"
ok "Apple IIgs ROM staged at ${dest}"
STATUS[${key}]="ok"
}
install_support_gsos() {
local support_dir="$1"
local sys_dest="${support_dir}/gsos-system.po"
local tools_dest="${support_dir}/gsos-tools1.po"
local key="emu_support_gsos"
if [ "${FORCE_INSTALL}" -eq 1 ]; then
rm -f "${sys_dest}" "${tools_dest}"
clear_done "${key}"
fi
if is_done "${key}" && [ -f "${sys_dest}" ]; then
ok "GS/OS system disk already staged"
STATUS[${key}]="ok"
return 0
fi
local zip_path="${CACHE_DIR}/Apple_IIGS_System_6.0.4.zip"
local extract_dir="${CACHE_DIR}/Apple_IIGS_System_6.0.4"
local url="https://mirrors.apple2.org.za/ftp.apple.asimov.net/images/gs/os/gsos/Apple_IIGS_System_6.0.4.zip"
download "${url}" "${zip_path}" || {
STATUS[${key}]="failed"
return 1
}
rm -rf "${extract_dir}"
(cd "${CACHE_DIR}" && unzip -oq "${zip_path}") || {
STATUS[${key}]="failed"
return 1
}
local sys_src="${extract_dir}/PO Disk Images/System.Disk.po"
local tools_src="${extract_dir}/PO Disk Images/SystemTools1.po"
if [ ! -f "${sys_src}" ]; then
err "GS/OS zip missing System.Disk.po"
STATUS[${key}]="failed"
return 1
fi
cp "${sys_src}" "${sys_dest}"
[ -f "${tools_src}" ] && cp "${tools_src}" "${tools_dest}"
mark_done "${key}"
ok "GS/OS system disk staged at ${sys_dest}"
STATUS[${key}]="ok"
}
install_support_iigs_null_c600() {
local support_dir="$1"
local dest="${support_dir}/iigs-null-c600.rom"
local key="emu_support_iigs_null_c600"
if [ "${FORCE_INSTALL}" -eq 1 ]; then
rm -f "${dest}"
clear_done "${key}"
fi
if is_done "${key}" && [ -f "${dest}" ]; then
ok "IIgs null slot-6 PROM already staged"
STATUS[${key}]="ok"
return 0
fi
# 256-byte null PROM for slot 6. Leading RTS (so any accidental
# call returns cleanly), then zero-filled -- no Pascal 1.1 firmware
# signature, so the IIgs boot ROM's slot scan skips slot 6 and the
# two empty 5.25 drives never get probed. run-iigs.sh stages this
# into each session's work dir as c600.rom; GSplus picks it up from
# cwd and overrides its built-in Disk II PROM.
{ printf '\x60'; head -c 255 /dev/zero; } > "${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 "$@"