1449 lines
51 KiB
Bash
Executable file
1449 lines
51 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_audio_libxmp() {
|
|
header "libxmp-lite (DOS / ST audio decoder)"
|
|
local base="${SCRIPT_DIR}/audio"
|
|
local target="${base}/libxmp-lite"
|
|
local key="audio_libxmp"
|
|
local version="4.7.0"
|
|
|
|
if [ "${FORCE_INSTALL}" -eq 1 ]; then
|
|
rm -rf "${target}"
|
|
clear_done "${key}"
|
|
fi
|
|
if is_done "${key}" && [ -f "${target}/include/libxmp-lite/xmp.h" ]; then
|
|
ok "libxmp-lite ${version} already installed"
|
|
STATUS[${key}]="ok"
|
|
return
|
|
fi
|
|
|
|
info "Downloading libxmp-lite ${version} from libxmp.org"
|
|
local tarball="${CACHE_DIR}/libxmp-lite-${version}.tar.gz"
|
|
local url="https://github.com/libxmp/libxmp/releases/download/libxmp-${version}/libxmp-lite-${version}.tar.gz"
|
|
if ! download "${url}" "${tarball}"; then
|
|
STATUS[${key}]="failed"
|
|
INSTRUCTIONS[${key}]="Could not download ${url}"
|
|
return
|
|
fi
|
|
|
|
mkdir -p "${base}"
|
|
rm -rf "${target}"
|
|
if (cd "${base}" && tar xzf "${tarball}") && [ -d "${base}/libxmp-lite-${version}" ]; then
|
|
mv "${base}/libxmp-lite-${version}" "${target}"
|
|
mark_done "${key}"
|
|
ok "libxmp-lite installed at ${target}"
|
|
STATUS[${key}]="ok"
|
|
else
|
|
STATUS[${key}]="failed"
|
|
INSTRUCTIONS[${key}]="Extract of ${tarball} did not produce libxmp-lite-${version}/"
|
|
fi
|
|
}
|
|
|
|
|
|
install_iigs() {
|
|
header "IIgs toolchain"
|
|
local base="${SCRIPT_DIR}/iigs"
|
|
mkdir -p "${base}"
|
|
|
|
# PHP CLI is required by tools/joeymod when converting .MOD files
|
|
# to NinjaTrackerPlus's .NTP runtime format. Installed once here so
|
|
# the IIgs audio asset pipeline works out of the box.
|
|
if command -v php >/dev/null 2>&1; then
|
|
ok "php CLI already on PATH"
|
|
STATUS[iigs_php]="ok"
|
|
else
|
|
info "Installing php CLI for joeymod's MOD->NTP converter"
|
|
local php_pkgs=""
|
|
case "${HOST_OS}" in
|
|
linux) php_pkgs="php-cli" ;;
|
|
macos) php_pkgs="php" ;;
|
|
esac
|
|
if [ -n "${php_pkgs}" ] && pkg_install ${php_pkgs}; then
|
|
ok "php installed"
|
|
STATUS[iigs_php]="ok"
|
|
else
|
|
STATUS[iigs_php]="missing"
|
|
INSTRUCTIONS[iigs_php]=$(cat <<EOF
|
|
php CLI not installed. Required by tools/joeymod for the IIgs
|
|
.MOD -> .NTP conversion path.
|
|
Debian/Ubuntu: sudo apt install php-cli
|
|
macOS Homebrew: brew install php
|
|
EOF
|
|
)
|
|
fi
|
|
fi
|
|
|
|
# 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
|
|
|
|
# NinjaTrackerPlus -- Ninjaforce's IIgs MOD player. Distributed as
|
|
# source (ZIP) plus binary tool disk images on ninjaforce.com.
|
|
# Redistribution is granted by the copyright holder per the user's
|
|
# standing permission. We download the source archive (extracted
|
|
# into ${base}/ntp), the demo disk (kept in cache), and the tool
|
|
# disk (kept in cache) so a fresh checkout can build the IIgs
|
|
# audio HAL without manual intervention.
|
|
if [ "${FORCE_INSTALL}" -eq 1 ]; then
|
|
rm -rf "${base}/ntp"
|
|
clear_done "iigs_ntp"
|
|
fi
|
|
if is_done "iigs_ntp" && [ -f "${base}/ntp/ninjatrackerplus.s" ]; then
|
|
ok "NinjaTrackerPlus already installed"
|
|
STATUS[iigs_ntp]="ok"
|
|
else
|
|
info "Downloading NinjaTrackerPlus from ninjaforce.com"
|
|
local ntp_src_zip="${CACHE_DIR}/ntpsources.zip"
|
|
local ntp_demo="${CACHE_DIR}/ntp.2mg"
|
|
local ntp_tool="${CACHE_DIR}/ninjatrackerplus_tool222_v1.4.2mg"
|
|
local ntp_failed=0
|
|
download "https://www.ninjaforce.com/downloads/ntpsources.zip" "${ntp_src_zip}" || ntp_failed=1
|
|
download "https://www.ninjaforce.com/downloads/ntp.2mg" "${ntp_demo}" || true
|
|
download "https://www.ninjaforce.com/downloads/ninjatrackerplus_tool222_v1.4.2mg" "${ntp_tool}" || true
|
|
if [ "${ntp_failed}" -eq 1 ]; then
|
|
STATUS[iigs_ntp]="failed"
|
|
INSTRUCTIONS[iigs_ntp]="Could not download ntpsources.zip from www.ninjaforce.com"
|
|
else
|
|
mkdir -p "${base}/ntp"
|
|
if (cd "${base}/ntp" && unzip -oq "${ntp_src_zip}") && [ -f "${base}/ntp/ninjatrackerplus.s" ]; then
|
|
# ntpsources.zip ships .s files with CRLF line
|
|
# endings; Merlin32 rejects those as "Unknown line".
|
|
# Normalize once here so the build doesn't have to.
|
|
for f in "${base}/ntp"/*.s; do
|
|
sed -i 's/\r$//' "$f"
|
|
done
|
|
mark_done "iigs_ntp"
|
|
ok "NinjaTrackerPlus installed at ${base}/ntp"
|
|
STATUS[iigs_ntp]="ok"
|
|
else
|
|
STATUS[iigs_ntp]="failed"
|
|
INSTRUCTIONS[iigs_ntp]="Extract of ${ntp_src_zip} did not produce ninjatrackerplus.s"
|
|
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
|
|
|
|
# Audio third-party fetches that span multiple targets. libxmp-lite
|
|
# is used by the DOS HAL today and will back the ST mixer's
|
|
# Protracker decoder later, so it lives in toolchains/audio/ rather
|
|
# than under any one platform's tree.
|
|
if should_install dos || should_install atarist; then
|
|
install_audio_libxmp
|
|
fi
|
|
|
|
if [ "${SKIP_EMULATORS}" -eq 0 ] && [ -z "${ONLY_PLATFORM}" ]; then
|
|
install_emulators
|
|
fi
|
|
|
|
print_status_report
|
|
}
|
|
|
|
main "$@"
|