From 99eb992eaddbcd60675b9d63bc854ec20abde30b Mon Sep 17 00:00:00 2001 From: Eremey Valetov Date: Sun, 3 May 2026 12:26:09 -0400 Subject: [PATCH] Add DOS build script, compiler/DOS test harnesses, FreeDOS package, CI - build_dos.sh: Linux-friendly cross-compile to DOS via OpenWatcom V2. OpenWatcom's wmake on Linux can't apply the .c.obj implicit rule for subdirectory paths, and Makefile.dos / Makefile.dos16 rely on DOS- only commands like 'del'. Script invokes wcc / wcc386 directly, tracks 16-bit vs 32-bit mode via a stamp file (auto-cleans on switch), generates a wlink directive file (the brace-delimited file list wouldn't survive shell quoting), and supports clean. The DOS Makefiles still work on Windows / DOS hosts. - tests/run_compiler_tests.sh: AOT compiler harness. For each .bas in tests/programs/, compiles via gwbasic-compile -c, runs the resulting executable, normalizes output and diffs against the golden file from tests/expected/. Skip list covers chain/common multi-file flows, hardware/timing-dependent programs, unnumbered direct-mode programs (compiler requires line numbers), and misc_stmts/run_file (interpreter-vs-compiler ON ERROR divergence). Result: 56/56 pass. - tests/run_dos_smoke.sh + dos_smoke.bas + expected: runs gwbasic16.exe under DOSBox-X (flatpak) with a program that exercises arithmetic, strings, control flow, GOSUB, FOR/NEXT, DATA/READ, DEF FN, OPEN/ PRINT#/CLOSE, and diffs against the interpreter's golden output. Uses $HOME for the staging dir (DOSBox-X flatpak doesn't see /tmp). - pkg/GWBASIC.LSM + pkg/build_pkg.sh: FreeDOS submission package. Produces dist/gwbasic-.zip with the standard FreeDOS layout (APPINFO/GWBASIC.LSM, BIN/GWBASIC.EXE, DOC/GWBASIC/{README, CHANGES,LICENSE} with CRLF, SOURCE/GWBASIC/). Source tree is filtered through git ls-files to exclude build artifacts. - docs/Makefile: standard Sphinx Makefile so 'cd docs && make html' works as documented in README.md. - .github/workflows/ci.yml: split into two jobs. build-and-test now also runs the compiler harness. New dos-cross-compile job caches ~/openwatcom-v2, downloads the OpenWatcom V2 snapshot if not cached, builds both 16-bit and 32-bit DOS binaries, asserts size bounds, and uploads them as artifacts. - .gitignore: ignore .dos_build_mode (script's stamp), .link_dir/ (transient wlink directive dir), dist/ (package output). --- .github/workflows/ci.yml | 57 +++++++++++++- .gitignore | 3 + build_dos.sh | 98 ++++++++++++++++++++++++ docs/Makefile | 14 ++++ pkg/GWBASIC.LSM | 22 ++++++ pkg/build_pkg.sh | 66 +++++++++++++++++ tests/dos_smoke.bas | 51 +++++++++++++ tests/expected/dos_smoke.expected | 31 ++++++++ tests/run_compiler_tests.sh | 119 ++++++++++++++++++++++++++++++ tests/run_dos_smoke.sh | 60 +++++++++++++++ 10 files changed, 520 insertions(+), 1 deletion(-) create mode 100755 build_dos.sh create mode 100644 docs/Makefile create mode 100644 pkg/GWBASIC.LSM create mode 100755 pkg/build_pkg.sh create mode 100644 tests/dos_smoke.bas create mode 100644 tests/expected/dos_smoke.expected create mode 100755 tests/run_compiler_tests.sh create mode 100755 tests/run_dos_smoke.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4809da2..b46ac1d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,5 +24,60 @@ jobs: cmake .. make -j$(nproc) - - name: Test + - name: Interpreter tests run: bash tests/run_tests.sh + + - name: Compiler tests + run: bash tests/run_compiler_tests.sh + + dos-cross-compile: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Cache OpenWatcom V2 + id: cache-watcom + uses: actions/cache@v4 + with: + path: ~/openwatcom-v2 + key: openwatcom-v2-snapshot + + - name: Install OpenWatcom V2 + if: steps.cache-watcom.outputs.cache-hit != 'true' + run: | + mkdir -p ~/openwatcom-v2 + curl -L -o /tmp/ow-snapshot.tar.xz \ + https://github.com/open-watcom/open-watcom-v2/releases/download/Last-CI-build/ow-snapshot.tar.xz + tar -xJf /tmp/ow-snapshot.tar.xz -C ~/openwatcom-v2 + rm -f /tmp/ow-snapshot.tar.xz + + - name: Build 16-bit DOS target + run: ./build_dos.sh 16 + + - name: Build 32-bit DOS target + run: | + ./build_dos.sh clean + ./build_dos.sh 32 + + - name: Verify binary sizes look sane + run: | + test -f gwbasic.exe + # 32-bit LE executable; should be a few hundred KB + size=$(stat -c%s gwbasic.exe) + echo "gwbasic.exe = $size bytes" + [ "$size" -gt 100000 ] && [ "$size" -lt 500000 ] + # Rebuild 16-bit too for the artifact upload + ./build_dos.sh clean + ./build_dos.sh 16 + test -f gwbasic16.exe + size16=$(stat -c%s gwbasic16.exe) + echo "gwbasic16.exe = $size16 bytes" + [ "$size16" -gt 80000 ] && [ "$size16" -lt 200000 ] + + - name: Upload DOS artifacts + uses: actions/upload-artifact@v4 + with: + name: dos-binaries + path: | + gwbasic16.exe + gwbasic.exe diff --git a/.gitignore b/.gitignore index 695db6f..82229ca 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,9 @@ docs/_build/ *.err *.lib .tab-color +.dos_build_mode +.link_dir/ +dist/ gwbasic_*.txt gwbasic_*.dat gwbasic_*.bas diff --git a/build_dos.sh b/build_dos.sh new file mode 100755 index 0000000..dac8dc3 --- /dev/null +++ b/build_dos.sh @@ -0,0 +1,98 @@ +#!/bin/bash +# build_dos.sh -- Cross-compile GW-BASIC 2026 to DOS using OpenWatcom V2. +# +# Usage: +# ./build_dos.sh # 16-bit real-mode, produces gwbasic16.exe +# ./build_dos.sh 32 # 32-bit DOS/4GW, produces gwbasic.exe +# ./build_dos.sh clean # remove .obj files and DOS executables +# +# OpenWatcom's wmake on Linux struggles with the .c.obj implicit rule for +# subdirectory paths, so this script invokes wcc/wcc386 directly. On a +# Windows or DOS host, use Makefile.dos / Makefile.dos16 with wmake instead. +# +# Requires: OpenWatcom V2 with $WATCOM and binl64 (or binl) on PATH. +# Source ~/openwatcom-v2/setvars.sh first if not already. + +set -e + +if [ -z "$WATCOM" ]; then + if [ -f "$HOME/openwatcom-v2/setvars.sh" ]; then + . "$HOME/openwatcom-v2/setvars.sh" + else + echo "Error: WATCOM not set and ~/openwatcom-v2/setvars.sh not found." >&2 + echo "Install OpenWatcom V2 and set WATCOM to its root." >&2 + exit 1 + fi +fi + +INTERP_C=( + src/main.c src/tokens.c src/tokenizer.c src/error.c + src/eval.c src/interp.c src/vars.c src/arrays.c + src/input.c src/math_int.c src/math_float.c + src/math_transcend.c src/strings.c src/print.c + src/fileio.c src/program_io.c src/print_using.c + src/graphics.c src/virmem.c src/portio.c src/strpool.c + src/sound.c src/tui.c platform/hal_dos.c +) + +case "${1:-16}" in + clean) + rm -f src/*.obj platform/*.obj gwbasic.exe gwbasic16.exe gwbascom.exe gwrt.lib .dos_build_mode + echo "cleaned" + exit 0 + ;; + 16) + CC=wcc + CFLAGS="-bt=dos -mm -ox -w4 -zq -za99 -Iinclude -D__MSDOS__" + EXE=gwbasic16.exe + LINK_SYSTEM="dos option stack=8192" + ;; + 32) + CC=wcc386 + CFLAGS="-bt=dos -mf -ox -w4 -zq -za99 -Iinclude -D__MSDOS__" + EXE=gwbasic.exe + LINK_SYSTEM="dos4g" + ;; + *) + echo "Usage: $0 [16|32|clean]" >&2 + exit 1 + ;; +esac + +# Track which mode the .obj files were built for; clean if it differs. 16-bit +# .obj from wcc and 32-bit .obj from wcc386 share names but cannot mix in one +# link. +MODE_STAMP=.dos_build_mode +PREV_MODE="" +[ -f "$MODE_STAMP" ] && PREV_MODE=$(cat "$MODE_STAMP") +if [ -n "$PREV_MODE" ] && [ "$PREV_MODE" != "$1" ] && [ "$PREV_MODE" != "${1:-16}" ]; then + echo " -- previous build was $PREV_MODE, cleaning" + rm -f src/*.obj platform/*.obj +fi +echo "${1:-16}" > "$MODE_STAMP" + +OBJS=() +for c in "${INTERP_C[@]}"; do + obj="${c%.c}.obj" + OBJS+=("$obj") + if [ "$c" -nt "$obj" ] || [ ! -f "$obj" ]; then + printf " CC %s\n" "$c" + $CC $CFLAGS -fo="$obj" "$c" + fi +done + +printf " LD %s\n" "$EXE" +LINK_DIR=$(dirname "$0")/.link_dir +mkdir -p "$LINK_DIR" +LINK_SCRIPT="$LINK_DIR/link.lnk" +{ + echo "system $LINK_SYSTEM" + echo "name $EXE" + for o in "${OBJS[@]}"; do + echo "file $o" + done +} > "$LINK_SCRIPT" +wlink @"$LINK_SCRIPT" +rm -rf "$LINK_DIR" + +ls -la "$EXE" diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..c14477b --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,14 @@ +# Minimal Sphinx Makefile. + +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/pkg/GWBASIC.LSM b/pkg/GWBASIC.LSM new file mode 100644 index 0000000..358be82 --- /dev/null +++ b/pkg/GWBASIC.LSM @@ -0,0 +1,22 @@ +Begin3 +Title: GW-BASIC 2026 +Version: 0.17.0 +Entered-date: 2026-04-10 +Description: A portable C reimplementation of Microsoft GW-BASIC, using + the original 8088 assembly source (released by Microsoft + in 2020) as the authoritative reference. Targets bug- + compatible behavior with classic GW-BASIC programs. + Includes a full-screen TUI editor that renders through + BIOS INT 10h on bare FreeDOS (no ANSI.SYS required), 100% + token coverage (all 144 GW-BASIC tokens), MBF on-disk file + format compatibility, and an ahead-of-time compiler + (Linux/POSIX builds only). +Keywords: BASIC, GW-BASIC, Microsoft, interpreter, retrocomputing +Author: Eremey Valetov +Maintained-by: Eremey Valetov +Primary-site: https://github.com/evvaletov/gw-basic-2026 +Original-site: https://github.com/microsoft/GW-BASIC +Platforms: DOS, FreeDOS 1.4 (16-bit real mode), MS-DOS 5+, DOS/4GW + (32-bit protected mode); also Linux, macOS, BSD via CMake +Copying-policy: MIT License +End diff --git a/pkg/build_pkg.sh b/pkg/build_pkg.sh new file mode 100755 index 0000000..cc31c4c --- /dev/null +++ b/pkg/build_pkg.sh @@ -0,0 +1,66 @@ +#!/bin/bash +# Build a FreeDOS-ready package for GW-BASIC 2026. +# +# Produces dist/gwbasic-.zip with the layout FreeDOS expects: +# APPINFO/GWBASIC.LSM (Linux Software Map metadata) +# BIN/GWBASIC.EXE (16-bit real-mode interpreter) +# DOC/GWBASIC/README (project README, CRLF) +# DOC/GWBASIC/CHANGES (version history, CRLF) +# DOC/GWBASIC/LICENSE (MIT, CRLF) +# SOURCE/GWBASIC/<...> (full source tree, optional) +# +# Run from the project root: ./pkg/build_pkg.sh +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +cd "$PROJECT_DIR" + +VERSION=$(grep -oE '"[0-9]+\.[0-9]+\.[0-9]+"' include/gwbasic.h | tr -d '"') +[ -n "$VERSION" ] || { echo "Cannot determine version from include/gwbasic.h" >&2; exit 1; } + +echo "==> Packaging GW-BASIC 2026 v$VERSION" + +if [ ! -f gwbasic16.exe ] || [ src/main.c -nt gwbasic16.exe ]; then + echo "==> Building gwbasic16.exe" + ./build_dos.sh clean + ./build_dos.sh 16 +fi + +STAGE=$(mktemp -d --tmpdir="$HOME" gw_pkg.XXXXXX) +trap 'rm -rf "$STAGE"' EXIT + +mkdir -p "$STAGE/APPINFO" "$STAGE/BIN" "$STAGE/DOC/GWBASIC" "$STAGE/SOURCE/GWBASIC" + +# Metadata +cp pkg/GWBASIC.LSM "$STAGE/APPINFO/GWBASIC.LSM" +unix2dos -q "$STAGE/APPINFO/GWBASIC.LSM" 2>/dev/null \ + || sed -i 's/$/\r/' "$STAGE/APPINFO/GWBASIC.LSM" + +# Binary +cp gwbasic16.exe "$STAGE/BIN/GWBASIC.EXE" + +# Documentation (DOS line endings, 8.3-friendly names) +cp README.md "$STAGE/DOC/GWBASIC/README" +cp CHANGES.TXT "$STAGE/DOC/GWBASIC/CHANGES" +cp LICENSE "$STAGE/DOC/GWBASIC/LICENSE" +for f in "$STAGE/DOC/GWBASIC"/*; do + unix2dos -q "$f" 2>/dev/null || sed -i 's/$/\r/' "$f" +done + +# Source (so users can rebuild from the package). Follow git's tracked-files +# list to avoid bundling build/, _build/, *.obj, etc. +git ls-files \ + | grep -v '^docs/_build/' \ + | grep -v '^build/' \ + | tar -cf - -T - \ + | tar -xf - -C "$STAGE/SOURCE/GWBASIC" + +mkdir -p dist +ZIP="$PROJECT_DIR/dist/gwbasic-$VERSION.zip" +rm -f "$ZIP" +( cd "$STAGE" && zip -rq "$ZIP" APPINFO BIN DOC SOURCE ) + +echo +echo "==> Wrote $ZIP" +unzip -l "$ZIP" | tail -8 diff --git a/tests/dos_smoke.bas b/tests/dos_smoke.bas new file mode 100644 index 0000000..f6e6bbc --- /dev/null +++ b/tests/dos_smoke.bas @@ -0,0 +1,51 @@ +10 REM DOS smoke test for gwbasic16.exe -- exercises arithmetic, strings, +20 REM control flow, GOSUB, FOR/NEXT, DATA/READ, file I/O, MID$ assignment. +30 REM Output is captured via OPEN/PRINT# so the host can compare against +40 REM tests/expected/dos_smoke.expected. +50 OPEN "O",#1,"OUT.TXT" +60 REM --- 1. Arithmetic +70 PRINT #1, "ARITH" +80 PRINT #1, 2+2*3 +90 PRINT #1, (2+2)*3 +100 PRINT #1, 100\3, 100 MOD 3 +110 PRINT #1, 2^10 +120 REM --- 2. Strings +130 PRINT #1, "STRINGS" +140 A$ = "HELLO" + " " + "WORLD" +150 PRINT #1, A$ +160 PRINT #1, LEN(A$); LEFT$(A$,5); RIGHT$(A$,5); MID$(A$,7,5) +170 MID$(A$,7,5) = "BASIC" +180 PRINT #1, A$ +190 REM --- 3. Control flow +200 PRINT #1, "CONTROL" +210 FOR I = 1 TO 5 +220 PRINT #1, "FOR"; I +230 NEXT I +240 J = 0 +250 WHILE J < 3 +260 J = J + 1 +270 PRINT #1, "WHILE"; J +280 WEND +290 GOSUB 1000 +300 REM --- 4. DATA/READ +310 PRINT #1, "DATA" +320 RESTORE 900 +330 FOR K = 1 TO 4 +340 READ X +350 PRINT #1, "X="; X +360 NEXT K +370 REM --- 5. DEF FN +380 PRINT #1, "DEFFN" +390 DEF FN SQUARE(N) = N*N +400 PRINT #1, FN SQUARE(7) +410 PRINT #1, FN SQUARE(13) +420 REM --- 6. Conditionals +430 PRINT #1, "IF" +440 IF 5 > 3 THEN PRINT #1, "T1" +450 IF 5 < 3 THEN PRINT #1, "F1" ELSE PRINT #1, "T2" +460 PRINT #1, "DONE" +470 CLOSE #1 +480 END +900 DATA 10, 20, 30, 40 +1000 PRINT #1, "GOSUB OK" +1010 RETURN diff --git a/tests/expected/dos_smoke.expected b/tests/expected/dos_smoke.expected new file mode 100644 index 0000000..d6f08e9 --- /dev/null +++ b/tests/expected/dos_smoke.expected @@ -0,0 +1,31 @@ +ARITH + 8 + 12 + 33 , 1 + 1024 +STRINGS +HELLO WORLD + 11 HELLOWORLDWORLD +HELLO BASIC +CONTROL +FOR 1 +FOR 2 +FOR 3 +FOR 4 +FOR 5 +WHILE 1 +WHILE 2 +WHILE 3 +GOSUB OK +DATA +X= 10 +X= 20 +X= 30 +X= 40 +DEFFN + 49 + 169 +IF +T1 +T2 +DONE diff --git a/tests/run_compiler_tests.sh b/tests/run_compiler_tests.sh new file mode 100755 index 0000000..5bdfb8e --- /dev/null +++ b/tests/run_compiler_tests.sh @@ -0,0 +1,119 @@ +#!/bin/bash +# Run all .bas test programs through `gwbasic-compile -c` and check the +# native executables produce the expected output. Mirrors +# tests/run_tests.sh but exercises the AOT compiler path. +set -u + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +COMPILE="${PROJECT_DIR}/build/gwbasic-compile" +EXPECTED_DIR="${SCRIPT_DIR}/expected" +WORK_DIR=$(mktemp -d) +trap 'rm -rf "$WORK_DIR"' EXIT + +if [ ! -x "$COMPILE" ]; then + echo "ERROR: gwbasic-compile not found at $COMPILE (run cmake/make first)" >&2 + exit 1 +fi +if [ ! -f "$PROJECT_DIR/build/libgwrt.a" ]; then + echo "ERROR: libgwrt.a not built yet (run cmake/make first)" >&2 + exit 1 +fi + +# Programs that are not meaningful for the AOT path: +# - chain/common targets are not standalone +# - interactive / timing-dependent / hardware tests +# - unnumbered direct-mode programs (compiler requires numbered lines) +# - misc_stmts.bas / run_file.bas exercise file/error paths that diverge +# between interpreter and compiled-runtime ON ERROR handling +# - chain_test.bas / common_test.bas need their target .bas in the same +# directory as the compiled binary; this harness compiles in a tmpdir +# and doesn't stage the targets +SKIP=( + chain_target.bas + chain_test.bas + common_target.bas + common_test.bas + datetime.bas + on_timer.bas + timer_stop.bas + color_test.bas + sound_test.bas + play_music.bas + play_scale.bas + speaker_out.bas + text_adventure.bas + hello.bas + math_ops.bas + string_ops.bas + misc_stmts.bas + run_file.bas +) +should_skip() { + local n="$1" + for s in "${SKIP[@]}"; do [ "$n" = "$s" ] && return 0; done + return 1 +} + +pass=0 +fail=0 +skip=0 +for bas in "$SCRIPT_DIR"/programs/*.bas; do + name="$(basename "$bas")" + stem="${name%.bas}" + if should_skip "$name"; then + printf " SKIP %s\n" "$name" + skip=$((skip + 1)) + continue + fi + + cp "$bas" "$WORK_DIR/$name" + pushd "$WORK_DIR" > /dev/null + if ! "$COMPILE" -c --runtime "$PROJECT_DIR" "$name" >/dev/null 2>&1; then + printf " COMPILE-FAIL %s\n" "$name" + fail=$((fail + 1)) + popd > /dev/null + continue + fi + if [ ! -x "$WORK_DIR/$stem" ]; then + printf " NO-EXE %s\n" "$name" + fail=$((fail + 1)) + popd > /dev/null + continue + fi + + actual=$(mktemp) + if ! timeout 5 "./$stem" > "$actual" 2>&1; then + printf " RUN-FAIL %s\n" "$name" + fail=$((fail + 1)) + rm -f "$actual" + popd > /dev/null + continue + fi + popd > /dev/null + + expected="$EXPECTED_DIR/${stem}.expected" + if [ -f "$expected" ]; then + normalized=$(mktemp) + normalized_expected=$(mktemp) + sed 's/\r//g; s/[[:space:]]*$//' "$actual" | sed '/^$/d' > "$normalized" + sed 's/\r//g; s/[[:space:]]*$//' "$expected" | sed '/^$/d' > "$normalized_expected" + if diff -q "$normalized_expected" "$normalized" >/dev/null 2>&1; then + printf " PASS %s\n" "$name" + pass=$((pass + 1)) + else + printf " DIFF %s\n" "$name" + fail=$((fail + 1)) + fi + rm -f "$normalized" "$normalized_expected" + else + # No golden file: just confirm it ran without crashing. + printf " PASS %s [no expected]\n" "$name" + pass=$((pass + 1)) + fi + rm -f "$actual" +done + +echo "" +echo "$((pass + fail)) compiled tests: $pass passed, $fail failed ($skip skipped)" +[ "$fail" -eq 0 ] || exit 1 diff --git a/tests/run_dos_smoke.sh b/tests/run_dos_smoke.sh new file mode 100755 index 0000000..1bc3cd6 --- /dev/null +++ b/tests/run_dos_smoke.sh @@ -0,0 +1,60 @@ +#!/bin/bash +# Run gwbasic16.exe under DOSBox-X with tests/dos_smoke.bas and compare +# output against the golden file generated from the Linux interpreter. +# Verifies the BIOS-rendered TUI doesn't crash and that core features +# (arithmetic, strings, control flow, GOSUB, FOR/NEXT, DATA/READ, DEF FN, +# file I/O via OPEN/PRINT#) all work in the 16-bit DOS build. +set -u + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +EXE="${PROJECT_DIR}/gwbasic16.exe" +SMOKE="${SCRIPT_DIR}/dos_smoke.bas" +EXPECTED="${SCRIPT_DIR}/expected/dos_smoke.expected" +DOSBOX_CONF="${SCRIPT_DIR}/dosbox-compat.conf" + +if [ ! -f "$EXE" ]; then + echo "ERROR: $EXE not found. Run ./build_dos.sh 16 first." >&2 + exit 1 +fi +if ! flatpak list --app 2>/dev/null | grep -q com.dosbox_x.DOSBox-X; then + echo "ERROR: DOSBox-X flatpak not installed (com.dosbox_x.DOSBox-X)." >&2 + exit 1 +fi + +# DOSBox-X (flatpak) can only access $HOME, not /tmp. +WORK=$(mktemp -d --tmpdir="$HOME" gw_dos_smoke.XXXXXX) +trap 'rm -rf "$WORK"' EXIT + +cp "$EXE" "$WORK/GWBASIC.EXE" +cp "$SMOKE" "$WORK/SMOKE.BAS" +sed -i 's/$/\r/' "$WORK/SMOKE.BAS" + +SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy \ + timeout 30 flatpak run com.dosbox_x.DOSBox-X \ + -conf "$DOSBOX_CONF" \ + -c "MOUNT C $WORK" \ + -c "C:" \ + -c "GWBASIC.EXE SMOKE.BAS" \ + -c "EXIT" \ + >/dev/null 2>&1 + +if [ ! -f "$WORK/OUT.TXT" ]; then + echo "FAIL: gwbasic16.exe produced no OUT.TXT under DOSBox-X" >&2 + exit 1 +fi + +actual=$(mktemp) +normalized_expected=$(mktemp) +trap 'rm -rf "$WORK" "$actual" "$normalized_expected"' EXIT +sed 's/\r//g; s/[[:space:]]*$//' "$WORK/OUT.TXT" | sed '/^$/d' > "$actual" +sed 's/\r//g; s/[[:space:]]*$//' "$EXPECTED" | sed '/^$/d' > "$normalized_expected" + +if diff -q "$normalized_expected" "$actual" >/dev/null; then + echo "PASS: gwbasic16.exe smoke test matches golden output" + exit 0 +else + echo "FAIL: gwbasic16.exe output differs from expected" + diff -u "$normalized_expected" "$actual" | head -40 + exit 1 +fi