#!/bin/bash # The MIT License (MIT) # # Copyright (C) 2026 Scott Duensing # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to # deal in the Software without restriction, including without limitation the # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or # sell copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # installToolchain.sh -- Install the DOS cross toolchain and test tools into toolchains/ # # Usage: ./installToolchain.sh [--force] # # Downloads and unpacks, into toolchains/ next to this script: # toolchains/djgpp DJGPP cross compiler, GCC 12.2.0 (prebuilt # linux64 release of andrewwutw/build-djgpp v3.4) # toolchains/cwsdpmi CWSDPMI r7 (csdpmi7b.zip); src/common.mk uses # toolchains/cwsdpmi/bin/bin/CWSDSTUB.EXE # toolchains/86box 86Box v6.0 (build 9001) x86_64 Linux AppImage, # extracted once into app/ (no FUSE needed), and # the matching 86Box ROM set (tag v6.0) in roms/ # toolchains/freedos FreeDOS 1.3 master boot record code, FAT16 boot # sector, KERNEL.SYS and COMMAND.COM (from the # LiteUSB disk image) and CuteMouse 2.1b4 # CTMOUSE.EXE: the DOS the emulator test disk boots # (src/test/dos) # toolchains/dosbox DOSBox Staging v0.83.0 (the quick emulator) # toolchains/cppcheck cppcheck 2.21, built from its source release # (src/test/lint/staticGate.sh) # toolchains/lrzsz lrzsz 0.12.20 (src/test/terminal/installLrzsz.sh, # which pins it), the Terminal tests' ZMODEM peer # toolchains/pktdrv NE2000.COM packet driver (src/test/net/ # installPktDrv.sh, which pins it) for the DOS # network tests # # Every download is checked against a pinned SHA-256 before it is unpacked, # so a changed or corrupted archive is refused instead of installed. # Archives are cached in toolchains/cache and reused on later runs. # An existing, working install is left alone unless --force is given. # After installing 86Box the script runs it once with its isolated # environment (src/test/dos/emuEnv.sh) and fails if anything appeared in # the per-user folders of the home directory. # toolchains/ is gitignored; run this after a fresh clone. set -euo pipefail DJGPP_URL="https://github.com/andrewwutw/build-djgpp/releases/download/v3.4/djgpp-linux64-gcc1220.tar.bz2" DJGPP_SHA256="8464f17017d6ab1b2bb2df4ed82357b5bf692e6e2b7fee37e315638f3d505f00" DJGPP_VERSION="12.2.0" CWSDPMI_URL="https://sandmann.dotster.com/cwsdpmi/csdpmi7b.zip" CWSDPMI_SHA256="deacda0488e1cdd7c4a9f32fab45662b34c0ed6b2d7d4d13bc07041b62004a8c" BOX86_URL="https://github.com/86Box/86Box/releases/download/v6.0/86Box-Linux-x86_64-b9001.AppImage" BOX86_FILE="86Box-Linux-x86_64-b9001.AppImage" BOX86_SHA256="535d3c329f8c32b601103970c86d80aa80298f345452c3524ea2f433624727a4" ROMS_URL="https://github.com/86Box/roms/archive/refs/tags/v6.0.tar.gz" ROMS_FILE="86box-roms-v6.0.tar.gz" ROMS_SHA256="38fccaf4a7b48e581ccdfee2dc3cc0545e7d3a778bc25611e902a117eec571e6" FREEDOS_URL="https://www.ibiblio.org/pub/micro/pc-stuff/freedos/files/distributions/1.3/official/FD13-LiteUSB.zip" FREEDOS_FILE="FD13-LiteUSB.zip" FREEDOS_SHA256="64a934585087ccd91a18c55e20ee01f5f6762be712eeaa5f456be543778f9f7e" FREEDOS_DISK_IMG="FD13LITE.img" FREEDOS_PART_SECTOR=63 # first sector of the image's FAT16 partition MBR_CODE_BYTES=446 # boot code before the partition table CTMOUSE_URL="https://www.ibiblio.org/pub/micro/pc-stuff/freedos/files/repositories/1.3/base/ctmouse.zip" CTMOUSE_FILE="ctmouse.zip" CTMOUSE_SHA256="a891124cd5b13e8141778fcae718f3b2667b0a49727ce92b782ab11a8c4bb63a" DOSBOX_URL="https://github.com/dosbox-staging/dosbox-staging/releases/download/v0.83.0/dosbox-staging-linux-x86_64-v0.83.0.tar.xz" DOSBOX_FILE="dosbox-staging-linux-x86_64-v0.83.0.tar.xz" DOSBOX_SHA256="d3a94f7f1c3e68a47ec88d61145506c7904452adb0c9c5928cb8cfe2331d6c5c" CPPCHECK_URL="https://github.com/danmar/cppcheck/archive/refs/tags/2.21.0.tar.gz" CPPCHECK_FILE="cppcheck-2.21.0.tar.gz" CPPCHECK_SHA256="f028ff75ca5372738f3737c8b3e8611426a6526b6aea2ef01301ab0f5902f044" CPPCHECK_VERSION="2.21.0" # Seconds the isolation check lets 86Box run BOX86_CHECK_SECONDS=8 ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" TOOLCHAINS="${ROOT}/toolchains" CACHE="${TOOLCHAINS}/cache" DJGPP_DIR="${TOOLCHAINS}/djgpp" CWSDPMI_DIR="${TOOLCHAINS}/cwsdpmi" DJGPP_GCC="${DJGPP_DIR}/bin/i586-pc-msdosdjgpp-gcc" CWSDSTUB="${CWSDPMI_DIR}/bin/bin/CWSDSTUB.EXE" # Emulator, FreeDOS and cppcheck locations and the isolated run environment DVX_ROOT="${ROOT}" # shellcheck source=src/test/dos/emuEnv.sh . "${ROOT}/src/test/dos/emuEnv.sh" # Each install records the SHA-256 of the archive(s) it came from here, so # a changed pin reinstalls. PIN_FILE=".installed" FORCE=0 for arg in "$@"; do case "${arg}" in --force) FORCE=1 ;; -h|--help) sed -n '/^# Usage/,/^# toolchains\/ is gitignored/p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//' exit 0 ;; *) echo "installToolchain: unknown option '${arg}' (try --help)" >&2 exit 2 ;; esac done if [ "$(uname -s)" != "Linux" ] || [ "$(uname -m)" != "x86_64" ]; then echo "installToolchain: the pinned DJGPP build is for x86_64 Linux; this host is $(uname -s) $(uname -m)" >&2 exit 1 fi for tool in curl sha256sum tar bzip2 xz unzip make g++ mcopy xvfb-run; do if ! command -v "${tool}" >/dev/null 2>&1; then echo "installToolchain: required tool '${tool}' is not installed" >&2 exit 1 fi done mkdir -p "${CACHE}" # fetch URL FILE SHA256 -- download FILE into the cache (unless a cached copy # already matches) and verify it. fetch() { local url="$1" local file="${CACHE}/$2" local sum="$3" if [ -f "${file}" ] && echo "${sum} ${file}" | sha256sum -c --status; then echo "installToolchain: using cached $2" return 0 fi echo "installToolchain: downloading $2" curl -fL --retry 3 -o "${file}.part" "${url}" mv "${file}.part" "${file}" if ! echo "${sum} ${file}" | sha256sum -c --status; then echo "installToolchain: checksum mismatch for $2 (expected ${sum}); refusing to install" >&2 rm -f "${file}" exit 1 fi } # pinned DIR PIN -- true when DIR holds an install made from PIN. pinned() { [ -f "$1/${PIN_FILE}" ] && [ "$(cat "$1/${PIN_FILE}")" = "$2" ] } # DJGPP if [ "${FORCE}" -eq 0 ] && [ -x "${DJGPP_GCC}" ] && [ "$("${DJGPP_GCC}" -dumpfullversion 2>/dev/null)" = "${DJGPP_VERSION}" ]; then echo "installToolchain: DJGPP ${DJGPP_VERSION} already installed" else fetch "${DJGPP_URL}" "djgpp-linux64-gcc1220.tar.bz2" "${DJGPP_SHA256}" rm -rf "${DJGPP_DIR}.new" mkdir -p "${DJGPP_DIR}.new" tar -xjf "${CACHE}/djgpp-linux64-gcc1220.tar.bz2" -C "${DJGPP_DIR}.new" --strip-components=1 rm -rf "${DJGPP_DIR}" mv "${DJGPP_DIR}.new" "${DJGPP_DIR}" echo "installToolchain: DJGPP installed in ${DJGPP_DIR}" fi # CWSDPMI if [ "${FORCE}" -eq 0 ] && [ -f "${CWSDSTUB}" ]; then echo "installToolchain: CWSDPMI already installed" else fetch "${CWSDPMI_URL}" "csdpmi7b.zip" "${CWSDPMI_SHA256}" rm -rf "${CWSDPMI_DIR}.new" mkdir -p "${CWSDPMI_DIR}.new/bin" (cd "${CWSDPMI_DIR}.new/bin" && unzip -qo "${CACHE}/csdpmi7b.zip") rm -rf "${CWSDPMI_DIR}" mv "${CWSDPMI_DIR}.new" "${CWSDPMI_DIR}" echo "installToolchain: CWSDPMI installed in ${CWSDPMI_DIR}" fi # 86Box AppImage, extracted once so it runs without FUSE if [ "${FORCE}" -eq 0 ] && pinned "${BOX86_APP}" "${BOX86_SHA256}" && [ -x "${BOX86_BIN}" ]; then echo "installToolchain: 86Box already installed" else fetch "${BOX86_URL}" "${BOX86_FILE}" "${BOX86_SHA256}" chmod +x "${CACHE}/${BOX86_FILE}" rm -rf "${BOX86_APP}.new" mkdir -p "${BOX86_APP}.new" (cd "${BOX86_APP}.new" && "${CACHE}/${BOX86_FILE}" --appimage-extract >/dev/null) mv "${BOX86_APP}.new/squashfs-root" "${BOX86_APP}.tmp" rm -rf "${BOX86_APP}.new" "${BOX86_APP}" mv "${BOX86_APP}.tmp" "${BOX86_APP}" echo "${BOX86_SHA256}" > "${BOX86_APP}/${PIN_FILE}" echo "installToolchain: 86Box installed in ${BOX86_APP}" fi # 86Box ROM set if [ "${FORCE}" -eq 0 ] && pinned "${BOX86_ROMS}" "${ROMS_SHA256}"; then echo "installToolchain: 86Box ROM set already installed" else fetch "${ROMS_URL}" "${ROMS_FILE}" "${ROMS_SHA256}" rm -rf "${BOX86_ROMS}.new" mkdir -p "${BOX86_ROMS}.new" tar -xzf "${CACHE}/${ROMS_FILE}" -C "${BOX86_ROMS}.new" --strip-components=1 rm -rf "${BOX86_ROMS}" mv "${BOX86_ROMS}.new" "${BOX86_ROMS}" echo "${ROMS_SHA256}" > "${BOX86_ROMS}/${PIN_FILE}" echo "installToolchain: 86Box ROM set installed in ${BOX86_ROMS}" fi # FreeDOS boot files and CuteMouse FREEDOS_PIN="${FREEDOS_SHA256} ${CTMOUSE_SHA256}" if [ "${FORCE}" -eq 0 ] && pinned "${FREEDOS_DIR}" "${FREEDOS_PIN}"; then echo "installToolchain: FreeDOS boot files already installed" else fetch "${FREEDOS_URL}" "${FREEDOS_FILE}" "${FREEDOS_SHA256}" fetch "${CTMOUSE_URL}" "${CTMOUSE_FILE}" "${CTMOUSE_SHA256}" rm -rf "${FREEDOS_DIR}.new" mkdir -p "${FREEDOS_DIR}.new" unzip -p "${CACHE}/${FREEDOS_FILE}" "${FREEDOS_DISK_IMG}" > "${FREEDOS_DIR}.new/disk.img" dd if="${FREEDOS_DIR}.new/disk.img" of="${FREEDOS_DIR}.new/MBR.BIN" bs="${MBR_CODE_BYTES}" count=1 status=none dd if="${FREEDOS_DIR}.new/disk.img" of="${FREEDOS_DIR}.new/BOOTSECT.BIN" bs=512 skip="${FREEDOS_PART_SECTOR}" count=1 status=none MTOOLS_SKIP_CHECK=1 mcopy -n -i "${FREEDOS_DIR}.new/disk.img@@$((FREEDOS_PART_SECTOR * 512))" ::KERNEL.SYS ::COMMAND.COM "${FREEDOS_DIR}.new/" unzip -p "${CACHE}/${CTMOUSE_FILE}" BIN/CTMOUSE.EXE > "${FREEDOS_DIR}.new/CTMOUSE.EXE" rm -f "${FREEDOS_DIR}.new/disk.img" rm -rf "${FREEDOS_DIR}" mv "${FREEDOS_DIR}.new" "${FREEDOS_DIR}" echo "${FREEDOS_PIN}" > "${FREEDOS_DIR}/${PIN_FILE}" echo "installToolchain: FreeDOS boot files installed in ${FREEDOS_DIR}" fi # DOSBox Staging if [ "${FORCE}" -eq 0 ] && pinned "${DOSBOX_DIR}" "${DOSBOX_SHA256}" && [ -x "${DOSBOX_BIN}" ]; then echo "installToolchain: DOSBox Staging already installed" else fetch "${DOSBOX_URL}" "${DOSBOX_FILE}" "${DOSBOX_SHA256}" rm -rf "${DOSBOX_DIR}.new" mkdir -p "${DOSBOX_DIR}.new" tar -xJf "${CACHE}/${DOSBOX_FILE}" -C "${DOSBOX_DIR}.new" --strip-components=1 rm -rf "${DOSBOX_DIR}" mv "${DOSBOX_DIR}.new" "${DOSBOX_DIR}" echo "${DOSBOX_SHA256}" > "${DOSBOX_DIR}/${PIN_FILE}" echo "installToolchain: DOSBox Staging installed in ${DOSBOX_DIR}" fi # cppcheck, built from source (no system package, no sudo) if [ "${FORCE}" -eq 0 ] && pinned "${CPPCHECK_DIR}" "${CPPCHECK_SHA256}" && [ -x "${CPPCHECK_BIN}" ]; then echo "installToolchain: cppcheck ${CPPCHECK_VERSION} already installed" else fetch "${CPPCHECK_URL}" "${CPPCHECK_FILE}" "${CPPCHECK_SHA256}" rm -rf "${CPPCHECK_DIR}.build" mkdir -p "${CPPCHECK_DIR}.build" tar -xzf "${CACHE}/${CPPCHECK_FILE}" -C "${CPPCHECK_DIR}.build" --strip-components=1 echo "installToolchain: building cppcheck ${CPPCHECK_VERSION} (a few minutes)" make -C "${CPPCHECK_DIR}.build" -j"$(nproc)" CXXFLAGS="-O2" FILESDIR="${CPPCHECK_DIR}/share/cppcheck" >"${CPPCHECK_DIR}.build/build.log" 2>&1 || { echo "installToolchain: cppcheck build failed; see ${CPPCHECK_DIR}.build/build.log" >&2 exit 1 } # FILESDIR is compiled in, so install straight to its final place. rm -rf "${CPPCHECK_DIR}" make -C "${CPPCHECK_DIR}.build" CXXFLAGS="-O2" PREFIX="${CPPCHECK_DIR}" FILESDIR="${CPPCHECK_DIR}/share/cppcheck" install >>"${CPPCHECK_DIR}.build/build.log" 2>&1 rm -rf "${CPPCHECK_DIR}.build" echo "${CPPCHECK_SHA256}" > "${CPPCHECK_DIR}/${PIN_FILE}" echo "installToolchain: cppcheck installed in ${CPPCHECK_DIR}" fi # lrzsz (host ZMODEM peer for the Terminal tests), pinned in its own script if [ "${FORCE}" -eq 1 ]; then "${ROOT}/src/test/terminal/installLrzsz.sh" --force else "${ROOT}/src/test/terminal/installLrzsz.sh" fi # NE2000 packet driver for the emulator network tests, pinned in its own script if [ "${FORCE}" -eq 1 ]; then "${ROOT}/src/test/net/installPktDrv.sh" --force else "${ROOT}/src/test/net/installPktDrv.sh" fi # Verify what the build needs. if [ "$("${DJGPP_GCC}" -dumpfullversion)" != "${DJGPP_VERSION}" ]; then echo "installToolchain: ${DJGPP_GCC} is not GCC ${DJGPP_VERSION}" >&2 exit 1 fi for f in "${DJGPP_DIR}/i586-pc-msdosdjgpp/bin/dxe3gen" "${DJGPP_DIR}/i586-pc-msdosdjgpp/bin/exe2coff" "${CWSDSTUB}" "${BOX86_BIN}" "${BOX86_ROMS}/machines" "${FREEDOS_DIR}/MBR.BIN" "${FREEDOS_DIR}/BOOTSECT.BIN" "${FREEDOS_DIR}/KERNEL.SYS" "${FREEDOS_DIR}/COMMAND.COM" "${FREEDOS_DIR}/CTMOUSE.EXE" "${DOSBOX_BIN}" "${CPPCHECK_BIN}" "${TOOLCHAINS}/lrzsz/bin/lsz" "${TOOLCHAINS}/pktdrv/NE2000.COM"; do if [ ! -e "${f}" ]; then echo "installToolchain: expected file missing after install: ${f}" >&2 exit 1 fi done case "$("${CPPCHECK_BIN}" --version)" in "Cppcheck ${CPPCHECK_VERSION}"|"Cppcheck ${CPPCHECK_VERSION%.0}") ;; *) echo "installToolchain: ${CPPCHECK_BIN} is not cppcheck ${CPPCHECK_VERSION}" >&2 exit 1 ;; esac # Run 86Box once, isolated, and prove nothing reached the home directory. checkDir="${BOX86_DIR}/check" rm -rf "${checkDir}" mkdir -p "${checkDir}/vm" printf '[Machine]\nmachine = ibmpc\nmem_size = 256\n' > "${checkDir}/vm/86box.cfg" touch "${checkDir}/stamp" emuHomeProbe "${checkDir}/stamp" > "${checkDir}/before.txt" emuIsolatedEnv "${BOX86_HOME}" setsid env "${EMU_ENV[@]}" xvfb-run -a "${BOX86_BIN}" -P "${checkDir}/vm" -R "${BOX86_ROMS}" -O "${checkDir}/global.cfg" -L "${checkDir}/86box.log" -N >/dev/null 2>&1 & checkPid=$! sleep "${BOX86_CHECK_SECONDS}" emuStopGroup "${checkPid}" emuHomeProbe "${checkDir}/stamp" > "${checkDir}/after.txt" if ! diff -u "${checkDir}/before.txt" "${checkDir}/after.txt"; then echo "installToolchain: 86Box changed the home directory (above); refusing to use it" >&2 exit 1 fi # 86Box rewrites the machine config when it starts. if ! grep -q "^emu_build_num" "${checkDir}/vm/86box.cfg"; then echo "installToolchain: the 86Box check run did not start" >&2 exit 1 fi rm -rf "${checkDir}" echo "installToolchain: 86Box ran isolated (nothing written outside ${DVX_ROOT})" echo "installToolchain: toolchain ready (GCC ${DJGPP_VERSION}, CWSDPMI r7, 86Box 6.0, FreeDOS 1.3, DOSBox Staging 0.83.0, cppcheck ${CPPCHECK_VERSION})"