From 233f0bc0888515230121fb855ccda3ad71845f15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 16 Aug 2026 20:22:11 +0200 Subject: [PATCH 01/12] docs: add design spec for build scripts and js-dos Pages player Records the approved design: JWasm-based reproducible build of MARS.ASM into MARS.COM, and a GitHub Pages site that runs it via a self-hosted, build-time-pinned js-dos 8.4.1. Includes verified findings that shaped the design: MARS.ASM is MASM dialect (not NASM), JWasm assembles it to 1550 bytes with no errors, and js-dos 8.4.1 needs no SharedArrayBuffer, so it deploys on Pages without COOP/COEP headers. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014Cv1JoZyJdqYiCXiVSKDQP --- .../2026-08-16-mars-com-jsdos-pages-design.md | 181 ++++++++++++++++++ 1 file changed, 181 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-16-mars-com-jsdos-pages-design.md diff --git a/docs/superpowers/specs/2026-08-16-mars-com-jsdos-pages-design.md b/docs/superpowers/specs/2026-08-16-mars-com-jsdos-pages-design.md new file mode 100644 index 0000000..ab52eb3 --- /dev/null +++ b/docs/superpowers/specs/2026-08-16-mars-com-jsdos-pages-design.md @@ -0,0 +1,181 @@ +# MARS.COM — build scripts and js-dos GitHub Pages player + +**Date:** 2026-08-16 +**Repo:** `dtz-labs/MARS.COM` (fork of `matrix-toolbox/MARS.COM`, GPL-3.0) +**Status:** approved design + +## Purpose + +The upstream repository ships the MARS landscape renderer as assembly source +only. There is no binary, no build instructions, and no way to see the demo +without a DOS machine or a local DOSBox install. + +This project adds two things: + +1. A reproducible build that turns `MARS.ASM` into a runnable `MARS.COM`. +2. A GitHub Pages site that runs that binary in the browser via js-dos, so the + demo can be seen by clicking a link. + +## Background + +`MARS.ASM` is a 778-line MASM/TASM-dialect source: `.model tiny`, `.code`, +`.386`, `org 100h`, `end start`. Two lines that look like corruption (bare `#` +at lines 615 and 777) are MASM `COMMENT #` block delimiters around a disabled +jump-table dispatch. The dialect is not NASM-compatible and cannot be ported +without a rewrite. + +Because the model is tiny, the assembler emits a `.COM` memory image directly. +No linker or `exe2bin` step is required. + +The source comments record two runtime requirements: DOSBox must run with +`-machine vgaonly`, and the demo needs a mouse (it drives the camera). The +`vgaonly` requirement is about rendering correctness, not preference — the +renderer writes to VGA mode 13h and reprograms the palette DAC, and DOSBox's +default `svga_s3` emulation produces visual artifacts. + +## Decisions + +| Decision | Choice | Rationale | +|---|---|---| +| Repo relationship | True GitHub fork | Preserves upstream attribution link; allows upstream PRs | +| Assembler | JWasm, pinned version | MASM-compatible, open source, builds from source with gcc | +| Built binary | CI-only, never committed | `MARS.ASM` stays the single source of truth | +| js-dos delivery | Downloaded at Pages build time, pinned version | Self-hosted at runtime, lean repo, reproducible | +| js-dos version | 8.4.1 | Current release; verified self-hostable | +| Emulator backend | `dosbox` (`wdosbox`) | 1.5 MB vs 7.9 MB for dosbox-x; `vgaonly` is supported | +| Page scope | Framed player plus context | Credits, usage hint, size story, source and download links | + +## Verified findings + +These were established by direct investigation, not assumption: + +- JWasm 2.21 assembles `MARS.ASM` cleanly: 778 lines, 3 passes, 0 warnings, + 0 errors, producing a **1550-byte** `MARS.COM`. The upstream README claims + 1517 bytes; the discrepancy is documented rather than hidden. +- js-dos 8.4.1's `dist/` contains **no** references to `SharedArrayBuffer` or + `crossOriginIsolated`. It therefore does not require COOP/COEP headers, which + GitHub Pages cannot set. Self-hosting on Pages is viable. +- `js-dos.js` contains no hardcoded absolute base path. The CDN URL appears only + as a default for the `pathPrefix` option, which we override. +- The js-dos npm tarball (14 MB) contains `dist/js-dos.js`, `dist/js-dos.css`, + and `dist/emulators/*`. Only a subset is needed for this deployment. + +## Architecture + +### Components + +**`scripts/build.sh`** — assembles `MARS.ASM` into `MARS.COM`. + +Resolves a toolchain in order: `$JWASM` environment variable, then `jwasm` on +`PATH`, then builds JWasm from a pinned upstream tag into `.toolchain/`. A +`--docker` flag runs the whole build in a container for hosts with no compiler. +The same script runs locally and in CI, so the two cannot drift. + +Output: `build/MARS.COM`. + +**`scripts/bundle.sh`** — produces the js-dos bundle. + +Creates `build/mars.jsdos`, a ZIP containing `MARS.COM` and `.jsdos/dosbox.conf`. +The config pins `machine=vgaonly`, enables mouse autolock, and autoexecs the +program. + +**`scripts/fetch-jsdos.sh`** — downloads the pinned js-dos release. + +Fetches the js-dos npm tarball at the pinned version, extracts only the files +the page needs (`js-dos.js`, `js-dos.css`, `emulators/emulators.js`, +`emulators/wdosbox.js`, `emulators/wdosbox.wasm`), and places them under the +site output. Never runs at page-view time. + +**`scripts/build-site.sh`** — assembles the deployable site. + +Composes `site/` sources, the built `MARS.COM`, the `.jsdos` bundle, and the +fetched js-dos assets into `_site/`. + +**`scripts/serve.sh`** — local preview. + +Builds the site and serves `_site/` over HTTP. HTTP rather than `file://` is +required because bundle loading uses `fetch`. + +**`Makefile`** — thin targets over the scripts: `build`, `bundle`, `site`, +`serve`, `clean`. + +### Site + +Static HTML and CSS, no framework, no build step beyond file composition. + +`site/index.html` frames the js-dos canvas with: + +- Credit to Tim J. Clarke (original, 1993) and Wojciech Bruzda (disassembly and + reduction, 2021) +- A usage hint: click to capture the mouse; the mouse drives the camera +- The size story: 5649 bytes reduced to roughly 1.5 KB +- Links to `MARS.ASM`, the `MARS.COM` download, and the upstream repository + +js-dos is initialised with `pathPrefix` pointing at the self-hosted emulator +directory, `backend: "dosbox"`, and `url` pointing at `mars.jsdos`. + +### Workflows + +**`.github/workflows/ci.yml`** — on push and pull request. + +Runs `scripts/build.sh`, asserts the output is non-empty and under 64 KB, and +records size and SHA-256 in the job summary. Uploads `MARS.COM` as a workflow +artifact. + +**`.github/workflows/pages.yml`** — on push to `main` and manual dispatch. + +Builds the binary, fetches pinned js-dos, assembles the site, uploads a Pages +artifact, and deploys. Requires `pages: write` and `id-token: write`. + +### Repository configuration + +Two settings are applied via the GitHub API, since forks do not inherit them: + +- GitHub Actions enabled on the fork +- Pages source set to `github-actions` + +## Data flow + +``` +MARS.ASM ──[JWasm -bin]──> build/MARS.COM ──┐ + ├──[zip]──> build/mars.jsdos ──┐ +.jsdos/dosbox.conf ─────────────────────────┘ │ + ├──> _site/ ──> Pages +site/index.html, site/style.css ───────────────────────────────────────────┤ + │ +js-dos 8.4.1 npm tarball ──[fetch, extract subset]──> _site/js-dos/ ───────┘ +``` + +At page view time nothing is fetched from outside the Pages origin. + +## Error handling + +- **Assembler unavailable.** `build.sh` falls back through its toolchain chain + and fails with an explicit message naming the options, rather than a bare + `command not found`. +- **Assembly failure.** JWasm's non-zero exit fails the build; its diagnostics + are surfaced verbatim. CI does not deploy a stale binary. +- **Size regression.** CI fails if the output is empty or exceeds 64 KB, which + would mean the tiny-model assumption has broken. +- **js-dos fetch failure.** The Pages build fails rather than deploying a page + with missing emulator assets. +- **Browser-side load failure.** The page shows an inline error message instead + of an indefinitely blank canvas. + +## Testing + +- `scripts/build.sh` produces a `MARS.COM` of the expected size, from a clean + checkout, on a machine with no assembler pre-installed. +- The same script succeeds in CI on `ubuntu-latest`. +- `scripts/serve.sh` renders the demo locally: the landscape draws, the palette + is correct, and mouse movement pans the camera. +- The deployed Pages URL loads with no requests to third-party origins + (verifiable in the browser network panel). + +## Out of scope + +- Modifying `MARS.ASM` itself +- Mobile or touch controls +- Reconciling the 1517-byte figure by reproducing the author's original + toolchain; the discrepancy is documented, not resolved +- Upstreaming the build to `matrix-toolbox` From 2807a6c45ba6bf120f78f80586ea1be67f3164a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 16 Aug 2026 20:31:15 +0200 Subject: [PATCH 02/12] docs: add implementation plan and reconcile it with the spec Adds the task-by-task implementation plan, then fixes seven defects found in a self-review of both documents: Plan: corrected assertion counts in three tasks (they under- and over-counted, which would send an executor hunting a phantom bug); closed a hole where test-lib.sh recorded nothing when die() behaved correctly; replaced a brittle basename check with a file-existence check so the repo can be cloned to any directory name; and rewrote a self-referential note about the third-party-origin assertion. Spec: documented versions.env, scripts/lib.sh and tests/ which the plan introduced but the spec never named; replaced the ambiguous "under 64 KB" with the true .COM ceiling of 65280 bytes (0xFF00, PSP-adjusted); and aligned the verification section with the plan, which enforces the pinned size and SHA-256 in CI rather than merely recording them. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014Cv1JoZyJdqYiCXiVSKDQP --- .../plans/2026-08-16-jsdos-pages-build.md | 1357 +++++++++++++++++ .../2026-08-16-mars-com-jsdos-pages-design.md | 23 +- 2 files changed, 1377 insertions(+), 3 deletions(-) create mode 100644 docs/superpowers/plans/2026-08-16-jsdos-pages-build.md diff --git a/docs/superpowers/plans/2026-08-16-jsdos-pages-build.md b/docs/superpowers/plans/2026-08-16-jsdos-pages-build.md new file mode 100644 index 0000000..8124502 --- /dev/null +++ b/docs/superpowers/plans/2026-08-16-jsdos-pages-build.md @@ -0,0 +1,1357 @@ +# MARS.COM Build Scripts and js-dos Pages Player — Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Assemble `MARS.ASM` into a runnable `MARS.COM` with a reproducible script, and publish a GitHub Pages site that runs that binary in the browser via a self-hosted js-dos. + +**Architecture:** Plain POSIX-ish bash scripts under `scripts/`, each with one responsibility, sharing `scripts/lib.sh`. Version pins live in a single `versions.env`. A bash test harness under `tests/` exercises each script. Two GitHub Actions workflows: one verifies the build on every push, one builds and deploys the site to Pages. Nothing is fetched from a third-party origin at page-view time. + +**Tech Stack:** JWasm v2.20 (MASM-compatible assembler, built from source), js-dos 8.4.1 (fetched from npm at build time), GitHub Actions, GitHub Pages. No package manager, no framework, no runtime dependencies. + +**Spec:** `docs/superpowers/specs/2026-08-16-mars-com-jsdos-pages-design.md` + +## Global Constraints + +- **Assembler:** JWasm, pinned to tag `v2.20`, repo `https://github.com/Baron-von-Riedesel/JWasm.git`. Build with `make -f GccUnix.mak`; binary lands at `build/GccUnixR/jwasm` inside the JWasm checkout. +- **js-dos:** pinned to version `8.4.1`, fetched via `npm pack js-dos@8.4.1`. +- **Emulator backend:** `dosbox` (`wdosbox.js` + `wdosbox.wasm`, ~1.5 MB). Do **not** ship `wdosbox-x*` (~15.5 MB combined). +- **Expected build output:** `MARS.COM`, exactly **1550 bytes**, SHA-256 `10a1bb6c319296dd8628e8fd2d705b50fca97b82d6d9a811c91c30c06199d773`. Verified byte-identical under JWasm v2.20 and v2.21. +- **`.COM` size ceiling:** 65280 bytes (0xFF00). CI fails above this. +- **DOSBox machine:** `machine=vgaonly` is mandatory — the renderer uses VGA mode 13h and reprograms the palette DAC; the default `svga_s3` produces artifacts. +- **Bundle layout:** a ZIP named `mars.jsdos` containing `MARS.COM` at the root and `.jsdos/dosbox.conf`. js-dos does **not** auto-mount — the `[autoexec]` section must contain `mount c .` then `c:` then the command. (Confirmed against js-dos's own bundle generator in `emulators.js`.) +- **js-dos asset path:** `pathPrefix` must point at the *emulators* directory and end with a slash (the built-in default is `https://v8.js-dos.com/latest/emulators/`). +- **No third-party runtime requests.** All assets served from the Pages origin. +- **Never commit** `build/`, `_site/`, `.toolchain/`, or `MARS.COM`. +- **Attribution:** original by Tim J. Clarke (1993); disassembly and reduction by Wojciech Bruzda (2021). GPL-3.0 preserved. +- **Shell style:** every script starts `#!/usr/bin/env bash` and sources `scripts/lib.sh`, which sets `set -euo pipefail`. + +--- + +### Task 1: Scaffolding — version pins, shared library, test harness + +**Files:** +- Create: `versions.env` +- Create: `scripts/lib.sh` +- Create: `tests/lib.sh` +- Create: `tests/run-tests.sh` +- Create: `tests/test-lib.sh` +- Create: `.gitignore` + +**Interfaces:** +- Consumes: nothing. +- Produces: `scripts/lib.sh` exporting `REPO_ROOT`, `BUILD_DIR`, `SITE_OUT`, `TOOLCHAIN_DIR` (all absolute paths) and functions `log(msg)`, `die(msg)` (exit 1), `need(tool)` (die if absent); plus every variable from `versions.env`. `tests/lib.sh` exports `assert_eq(actual, expected, label)`, `assert_file_exists(path)`, `assert_contains(haystack_file, needle)`, `assert_success(cmd...)`, and `finish_tests()`. + +- [ ] **Step 1: Write the failing test** + +Create `tests/test-lib.sh`: + +```bash +#!/usr/bin/env bash +# Verifies scripts/lib.sh exposes the contract the other scripts rely on. +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" +. "$(dirname "${BASH_SOURCE[0]}")/../scripts/lib.sh" + +assert_file_exists "$REPO_ROOT/MARS.ASM" # REPO_ROOT points at the repo +assert_eq "$BUILD_DIR" "$REPO_ROOT/build" "BUILD_DIR derived from REPO_ROOT" +assert_eq "$SITE_OUT" "$REPO_ROOT/_site" "SITE_OUT derived from REPO_ROOT" +assert_eq "$JWASM_TAG" "v2.20" "JWasm pin loaded from versions.env" +assert_eq "$JSDOS_VERSION" "8.4.1" "js-dos pin loaded from versions.env" +assert_eq "$MARS_COM_SIZE" "1550" "expected binary size loaded" + +# die must exit non-zero and print to stderr +if ( die "boom" ) 2>/dev/null; then + fail "die should exit non-zero" +else + pass "die exits non-zero" +fi + +finish_tests +``` + +- [ ] **Step 2: Run it to make sure it fails** + +Run: `bash tests/test-lib.sh` +Expected: FAIL — `tests/lib.sh: No such file or directory`. + +- [ ] **Step 3: Write the test harness library** + +Create `tests/lib.sh`: + +```bash +#!/usr/bin/env bash +# Minimal assertion helpers. No external test framework dependency. +TESTS_RUN=0 +TESTS_FAILED=0 + +pass() { TESTS_RUN=$((TESTS_RUN + 1)); printf ' \033[0;32mok\033[0m %s\n' "$1"; } + +fail() { + TESTS_RUN=$((TESTS_RUN + 1)) + TESTS_FAILED=$((TESTS_FAILED + 1)) + printf ' \033[0;31mFAIL\033[0m %s\n' "$1" +} + +assert_eq() { + local actual="$1" expected="$2" label="$3" + if [ "$actual" = "$expected" ]; then + pass "$label" + else + fail "$label (expected '$expected', got '$actual')" + fi +} + +assert_file_exists() { + if [ -f "$1" ]; then pass "file exists: $1"; else fail "file missing: $1"; fi +} + +assert_contains() { + local file="$1" needle="$2" + if grep -qF -- "$needle" "$file" 2>/dev/null; then + pass "$file contains '$needle'" + else + fail "$file does not contain '$needle'" + fi +} + +assert_success() { + if "$@" >/dev/null 2>&1; then pass "command succeeded: $*"; else fail "command failed: $*"; fi +} + +finish_tests() { + printf '\n %d assertion(s), %d failure(s)\n' "$TESTS_RUN" "$TESTS_FAILED" + [ "$TESTS_FAILED" -eq 0 ] || exit 1 +} +``` + +- [ ] **Step 4: Write the version pins** + +Create `versions.env`: + +```sh +# Single source of truth for pinned versions. +# Sourced by scripts/lib.sh; plain KEY=value so CI can read it too. + +# JWasm — MASM-compatible assembler used to build MARS.COM +JWASM_TAG=v2.20 +JWASM_REPO=https://github.com/Baron-von-Riedesel/JWasm.git + +# js-dos — browser DOS emulator, fetched at site build time +JSDOS_VERSION=8.4.1 + +# Expected build output (JWasm v2.20 and v2.21 agree byte-for-byte) +MARS_COM_SIZE=1550 +MARS_COM_SHA256=10a1bb6c319296dd8628e8fd2d705b50fca97b82d6d9a811c91c30c06199d773 + +# Hard ceiling for a DOS .COM image (0xFF00) +MARS_COM_MAX_SIZE=65280 +``` + +- [ ] **Step 5: Write the shared script library** + +Create `scripts/lib.sh`: + +```bash +#!/usr/bin/env bash +# Shared helpers for every script in this repo. Source it, don't execute it. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +BUILD_DIR="$REPO_ROOT/build" +SITE_OUT="$REPO_ROOT/_site" +TOOLCHAIN_DIR="$REPO_ROOT/.toolchain" + +# shellcheck source=/dev/null +. "$REPO_ROOT/versions.env" + +log() { printf '\033[1;34m==>\033[0m %s\n' "$*" >&2; } +die() { printf '\033[1;31mError:\033[0m %s\n' "$*" >&2; exit 1; } +need() { command -v "$1" >/dev/null 2>&1 || die "required tool not found: $1"; } +``` + +- [ ] **Step 6: Write the test runner** + +Create `tests/run-tests.sh`: + +```bash +#!/usr/bin/env bash +# Runs every tests/test-*.sh and reports a combined result. +set -uo pipefail +cd "$(dirname "${BASH_SOURCE[0]}")" + +failed=0 +for t in test-*.sh; do + printf '\n\033[1m%s\033[0m\n' "$t" + bash "$t" || failed=$((failed + 1)) +done + +printf '\n' +if [ "$failed" -ne 0 ]; then + printf '\033[1;31m%d test file(s) failed\033[0m\n' "$failed" + exit 1 +fi +printf '\033[1;32mAll test files passed\033[0m\n' +``` + +- [ ] **Step 7: Write .gitignore** + +Create `.gitignore`: + +```gitignore +# Build outputs — MARS.ASM is the single source of truth +build/ +_site/ +.toolchain/ +MARS.COM +*.jsdos + +# Fetched dependencies +node_modules/ +*.tgz + +# OS turds +.DS_Store +``` + +- [ ] **Step 8: Run the tests and make sure they pass** + +Run: `chmod +x tests/run-tests.sh tests/test-lib.sh && bash tests/run-tests.sh` +Expected: PASS — 7 assertions, 0 failures. + +(Count check: 1 `assert_file_exists`, 5 `assert_eq`, 1 `die` branch = 7.) + +- [ ] **Step 9: Commit** + +```bash +git add versions.env scripts/lib.sh tests/ .gitignore +git commit -m "build: add version pins, shared script library, and test harness" +``` + +--- + +### Task 2: Build script — assemble MARS.ASM into MARS.COM + +**Files:** +- Create: `scripts/build.sh` +- Create: `tests/test-build.sh` + +**Interfaces:** +- Consumes: `scripts/lib.sh` (`REPO_ROOT`, `BUILD_DIR`, `TOOLCHAIN_DIR`, `JWASM_TAG`, `JWASM_REPO`, `MARS_COM_MAX_SIZE`, `log`, `die`, `need`). +- Produces: `build/MARS.COM`. Accepts flags `--docker` (run the whole build in a `debian:bookworm-slim` container) and `--check` (after assembling, verify size against `MARS_COM_SIZE` and exit non-zero on mismatch). Exposes no functions to later tasks; later tasks depend only on the output path `build/MARS.COM`. + +- [ ] **Step 1: Write the failing test** + +Create `tests/test-build.sh`: + +```bash +#!/usr/bin/env bash +# Verifies the assembler pipeline produces the expected MARS.COM. +# First run is slow: it builds JWasm from source. +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" +. "$(dirname "${BASH_SOURCE[0]}")/../scripts/lib.sh" + +rm -f "$BUILD_DIR/MARS.COM" +assert_success bash "$REPO_ROOT/scripts/build.sh" +assert_file_exists "$BUILD_DIR/MARS.COM" + +actual_size=$(wc -c < "$BUILD_DIR/MARS.COM" | tr -d ' ') +assert_eq "$actual_size" "$MARS_COM_SIZE" "MARS.COM is the expected size" + +actual_sha=$(shasum -a 256 "$BUILD_DIR/MARS.COM" 2>/dev/null | cut -d' ' -f1 \ + || sha256sum "$BUILD_DIR/MARS.COM" | cut -d' ' -f1) +assert_eq "$actual_sha" "$MARS_COM_SHA256" "MARS.COM is byte-identical to the pinned build" + +# --check must succeed on a good build +assert_success bash "$REPO_ROOT/scripts/build.sh" --check + +finish_tests +``` + +- [ ] **Step 2: Run it to make sure it fails** + +Run: `bash tests/test-build.sh` +Expected: FAIL — `scripts/build.sh` does not exist, so `assert_success` reports a failed command and the file assertions fail. + +- [ ] **Step 3: Write the build script** + +Create `scripts/build.sh`: + +```bash +#!/usr/bin/env bash +# Assembles MARS.ASM into a runnable DOS .COM image. +# +# MARS.ASM is MASM/TASM dialect (.model tiny, org 100h, COMMENT # blocks), +# so it needs a MASM-compatible assembler — NASM cannot build it. We use +# JWasm, pinned in versions.env. Because the model is tiny, the assembler +# emits the .COM memory image directly; there is no link step. +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +usage() { + cat <<'EOF' +Usage: scripts/build.sh [--docker] [--check] + + --docker Run the build inside a container (no host compiler needed). + --check Verify the output matches the pinned size and SHA-256. + +Assembler resolution order: + 1. $JWASM environment variable + 2. jwasm on PATH + 3. build JWasm from source into .toolchain/ (needs gcc, make, git) +EOF +} + +use_docker=0 +do_check=0 +for arg in "$@"; do + case "$arg" in + --docker) use_docker=1 ;; + --check) do_check=1 ;; + -h|--help) usage; exit 0 ;; + *) die "unknown option: $arg (try --help)" ;; + esac +done + +if [ "$use_docker" -eq 1 ]; then + need docker + log "Building inside debian:bookworm-slim" + docker run --rm -v "$REPO_ROOT:/repo" -w /repo debian:bookworm-slim bash -c ' + set -e + apt-get update -qq >/dev/null + apt-get install -y -qq build-essential git ca-certificates >/dev/null 2>&1 + bash scripts/build.sh + ' + exit $? +fi + +# --- resolve an assembler ----------------------------------------------- +resolve_jwasm() { + if [ -n "${JWASM:-}" ]; then + [ -x "$JWASM" ] || die "\$JWASM is set to '$JWASM' but that is not executable" + printf '%s' "$JWASM" + return + fi + + if command -v jwasm >/dev/null 2>&1; then + command -v jwasm + return + fi + + local cached="$TOOLCHAIN_DIR/bin/jwasm" + if [ -x "$cached" ]; then + printf '%s' "$cached" + return + fi + + log "No assembler found; building JWasm $JWASM_TAG from source" >&2 + need git + need make + command -v cc >/dev/null 2>&1 || command -v gcc >/dev/null 2>&1 \ + || die "no C compiler found. Install gcc/clang, set \$JWASM to a jwasm binary, or rerun with --docker" + + local src="$TOOLCHAIN_DIR/JWasm" + rm -rf "$src" + mkdir -p "$TOOLCHAIN_DIR/bin" + git clone -q "$JWASM_REPO" "$src" >&2 + ( cd "$src" && git checkout -q "$JWASM_TAG" && make -f GccUnix.mak >/dev/null 2>&1 ) \ + || die "JWasm build failed. Rerun with --docker, or set \$JWASM to a prebuilt binary." + + local built + built="$(find "$src" -name jwasm -type f -perm -u+x | head -1)" + [ -n "$built" ] || die "JWasm built but no 'jwasm' binary was produced" + cp "$built" "$cached" + printf '%s' "$cached" +} + +JWASM_BIN="$(resolve_jwasm)" +log "Assembler: $JWASM_BIN" + +# --- assemble ------------------------------------------------------------ +mkdir -p "$BUILD_DIR" +out="$BUILD_DIR/MARS.COM" +rm -f "$out" + +# -bin: raw binary output, which for .model tiny is exactly a .COM image. +"$JWASM_BIN" -bin -Fo="$out" "$REPO_ROOT/MARS.ASM" \ + || die "assembly failed" + +[ -s "$out" ] || die "assembler produced an empty $out" + +size=$(wc -c < "$out" | tr -d ' ') +[ "$size" -le "$MARS_COM_MAX_SIZE" ] \ + || die "$out is $size bytes, which exceeds the $MARS_COM_MAX_SIZE byte .COM ceiling" + +if command -v shasum >/dev/null 2>&1; then + sha=$(shasum -a 256 "$out" | cut -d' ' -f1) +else + sha=$(sha256sum "$out" | cut -d' ' -f1) +fi + +log "Built $out — $size bytes, sha256 $sha" + +if [ "$do_check" -eq 1 ]; then + [ "$size" = "$MARS_COM_SIZE" ] \ + || die "size mismatch: expected $MARS_COM_SIZE bytes, got $size. If MARS.ASM changed on purpose, update versions.env." + [ "$sha" = "$MARS_COM_SHA256" ] \ + || die "sha256 mismatch: expected $MARS_COM_SHA256, got $sha. If MARS.ASM changed on purpose, update versions.env." + log "Reproducibility check passed" +fi +``` + +- [ ] **Step 4: Run the tests and make sure they pass** + +Run: `chmod +x scripts/build.sh && bash tests/test-build.sh` +Expected: PASS — 5 assertions, 0 failures. First run takes 1–2 minutes (JWasm compile); subsequent runs are instant because `.toolchain/bin/jwasm` is cached. + +- [ ] **Step 5: Commit** + +```bash +git add scripts/build.sh tests/test-build.sh +git commit -m "build: assemble MARS.ASM into MARS.COM with pinned JWasm" +``` + +--- + +### Task 3: DOSBox config and js-dos bundle + +**Files:** +- Create: `site/dosbox.conf` +- Create: `scripts/bundle.sh` +- Create: `tests/test-bundle.sh` + +**Interfaces:** +- Consumes: `scripts/lib.sh`; `build/MARS.COM` from Task 2. +- Produces: `build/mars.jsdos` — a ZIP with `MARS.COM` at the root and `.jsdos/dosbox.conf` inside. Task 5 copies this file into the site output as `mars.jsdos`. + +- [ ] **Step 1: Write the failing test** + +Create `tests/test-bundle.sh`: + +```bash +#!/usr/bin/env bash +# Verifies the .jsdos bundle has the layout js-dos expects. +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" +. "$(dirname "${BASH_SOURCE[0]}")/../scripts/lib.sh" + +bash "$REPO_ROOT/scripts/build.sh" >/dev/null 2>&1 +assert_success bash "$REPO_ROOT/scripts/bundle.sh" +assert_file_exists "$BUILD_DIR/mars.jsdos" + +listing="$BUILD_DIR/.bundle-listing.txt" +unzip -l "$BUILD_DIR/mars.jsdos" > "$listing" +assert_contains "$listing" "MARS.COM" +assert_contains "$listing" ".jsdos/dosbox.conf" + +conf="$BUILD_DIR/.bundle-conf.txt" +unzip -p "$BUILD_DIR/mars.jsdos" ".jsdos/dosbox.conf" > "$conf" +assert_contains "$conf" "machine=vgaonly" +assert_contains "$conf" "mount c ." +assert_contains "$conf" "MARS.COM" + +rm -f "$listing" "$conf" +finish_tests +``` + +- [ ] **Step 2: Run it to make sure it fails** + +Run: `bash tests/test-bundle.sh` +Expected: FAIL — `scripts/bundle.sh` does not exist; `build/mars.jsdos` missing. + +- [ ] **Step 3: Write the DOSBox config** + +Create `site/dosbox.conf`: + +```ini +# DOSBox configuration for the MARS landscape renderer. +# +# machine=vgaonly is mandatory, not cosmetic. MARS writes directly to VGA +# mode 13h and reprograms the palette DAC. DOSBox's default svga_s3 +# emulation changes DAC behaviour and produces visible artifacts — the +# original author's notes call for "-machine vgaonly" explicitly. + +[sdl] +autolock=true + +[dosbox] +machine=vgaonly + +[cpu] +core=auto +cputype=auto +cycles=auto + +[render] +aspect=true + +[autoexec] +echo off +mount c . +c: +MARS.COM +``` + +- [ ] **Step 4: Write the bundle script** + +Create `scripts/bundle.sh`: + +```bash +#!/usr/bin/env bash +# Packages MARS.COM and its DOSBox config into a js-dos .jsdos bundle. +# +# A .jsdos bundle is a ZIP whose root becomes drive C: and which carries +# its DOSBox config at .jsdos/dosbox.conf. js-dos does NOT auto-mount, so +# the [autoexec] section in site/dosbox.conf does the mount itself. +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +need zip + +com="$BUILD_DIR/MARS.COM" +[ -f "$com" ] || die "$com not found — run scripts/build.sh first" + +staging="$BUILD_DIR/bundle" +out="$BUILD_DIR/mars.jsdos" + +rm -rf "$staging" "$out" +mkdir -p "$staging/.jsdos" + +cp "$com" "$staging/MARS.COM" +cp "$REPO_ROOT/site/dosbox.conf" "$staging/.jsdos/dosbox.conf" + +# -X strips extra file attributes so the bundle is reproducible. +( cd "$staging" && zip -q -r -X "$out" . ) + +log "Bundled $out ($(wc -c < "$out" | tr -d ' ') bytes)" +``` + +- [ ] **Step 5: Run the tests and make sure they pass** + +Run: `chmod +x scripts/bundle.sh && bash tests/test-bundle.sh` +Expected: PASS — 7 assertions, 0 failures. + +(Count check: 1 `assert_success`, 1 `assert_file_exists`, 5 `assert_contains` = 7.) + +- [ ] **Step 6: Commit** + +```bash +git add site/dosbox.conf scripts/bundle.sh tests/test-bundle.sh +git commit -m "build: package MARS.COM into a js-dos bundle with vgaonly config" +``` + +--- + +### Task 4: Fetch pinned js-dos assets + +**Files:** +- Create: `scripts/fetch-jsdos.sh` +- Create: `tests/test-fetch-jsdos.sh` + +**Interfaces:** +- Consumes: `scripts/lib.sh` (`BUILD_DIR`, `JSDOS_VERSION`). +- Produces: `build/js-dos/` containing `js-dos.js`, `js-dos.css`, and `emulators/{emulators.js,wdosbox.js,wdosbox.wasm}`. Task 5 copies this directory into the site output as `js-dos/`. + +- [ ] **Step 1: Write the failing test** + +Create `tests/test-fetch-jsdos.sh`: + +```bash +#!/usr/bin/env bash +# Verifies we fetch exactly the js-dos assets the page needs, and no more. +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" +. "$(dirname "${BASH_SOURCE[0]}")/../scripts/lib.sh" + +assert_success bash "$REPO_ROOT/scripts/fetch-jsdos.sh" + +assert_file_exists "$BUILD_DIR/js-dos/js-dos.js" +assert_file_exists "$BUILD_DIR/js-dos/js-dos.css" +assert_file_exists "$BUILD_DIR/js-dos/emulators/emulators.js" +assert_file_exists "$BUILD_DIR/js-dos/emulators/wdosbox.js" +assert_file_exists "$BUILD_DIR/js-dos/emulators/wdosbox.wasm" + +# The heavy dosbox-x builds must NOT be shipped (~15.5 MB of dead weight). +if [ -f "$BUILD_DIR/js-dos/emulators/wdosbox-x.wasm" ]; then + fail "wdosbox-x.wasm was shipped but is not needed" +else + pass "dosbox-x builds excluded" +fi + +finish_tests +``` + +- [ ] **Step 2: Run it to make sure it fails** + +Run: `bash tests/test-fetch-jsdos.sh` +Expected: FAIL — `scripts/fetch-jsdos.sh` does not exist. + +- [ ] **Step 3: Write the fetch script** + +Create `scripts/fetch-jsdos.sh`: + +```bash +#!/usr/bin/env bash +# Downloads a pinned js-dos release and extracts only the assets the page +# needs. This runs at BUILD time, never at page-view time — the deployed +# site serves js-dos from its own origin, so it does not depend on a CDN. +# +# We ship the plain dosbox backend (wdosbox, ~1.5 MB) and deliberately skip +# dosbox-x (~15.5 MB across two builds). MARS only needs machine=vgaonly, +# which plain DOSBox supports. +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +need npm +need tar + +dest="$BUILD_DIR/js-dos" +work="$BUILD_DIR/.jsdos-download" + +if [ -f "$dest/js-dos.js" ] && [ "${FORCE_FETCH:-0}" != "1" ]; then + log "js-dos $JSDOS_VERSION already present in $dest (set FORCE_FETCH=1 to refetch)" + exit 0 +fi + +rm -rf "$work" "$dest" +mkdir -p "$work" "$dest/emulators" + +log "Fetching js-dos $JSDOS_VERSION from npm" +( cd "$work" && npm pack "js-dos@$JSDOS_VERSION" >/dev/null ) \ + || die "npm pack failed for js-dos@$JSDOS_VERSION" + +tarball="$work/js-dos-$JSDOS_VERSION.tgz" +[ -f "$tarball" ] || die "expected tarball $tarball was not produced" + +tar xzf "$tarball" -C "$work" \ + package/dist/js-dos.js \ + package/dist/js-dos.css \ + package/dist/emulators/emulators.js \ + package/dist/emulators/wdosbox.js \ + package/dist/emulators/wdosbox.wasm \ + || die "js-dos tarball did not contain the expected dist layout" + +cp "$work/package/dist/js-dos.js" "$dest/js-dos.js" +cp "$work/package/dist/js-dos.css" "$dest/js-dos.css" +cp "$work/package/dist/emulators/emulators.js" "$dest/emulators/emulators.js" +cp "$work/package/dist/emulators/wdosbox.js" "$dest/emulators/wdosbox.js" +cp "$work/package/dist/emulators/wdosbox.wasm" "$dest/emulators/wdosbox.wasm" + +rm -rf "$work" + +log "js-dos $JSDOS_VERSION staged in $dest ($(du -sh "$dest" | cut -f1))" +``` + +- [ ] **Step 4: Run the tests and make sure they pass** + +Run: `chmod +x scripts/fetch-jsdos.sh && bash tests/test-fetch-jsdos.sh` +Expected: PASS — 7 assertions, 0 failures. + +(Count check: 1 `assert_success`, 5 `assert_file_exists`, 1 dosbox-x branch = 7.) + +- [ ] **Step 5: Commit** + +```bash +git add scripts/fetch-jsdos.sh tests/test-fetch-jsdos.sh +git commit -m "build: fetch pinned js-dos 8.4.1 assets at build time" +``` + +--- + +### Task 5: The page and the site build + +**Files:** +- Create: `site/index.html` +- Create: `site/style.css` +- Create: `scripts/build-site.sh` +- Create: `tests/test-site.sh` + +**Interfaces:** +- Consumes: `scripts/lib.sh`; `build/MARS.COM` (Task 2), `build/mars.jsdos` (Task 3), `build/js-dos/` (Task 4). +- Produces: `_site/` containing `index.html`, `style.css`, `mars.jsdos`, `MARS.COM`, `mars_4_3.png`, `MARS.ASM`, and `js-dos/`. Task 6 serves it; Task 8 deploys it. + +- [ ] **Step 1: Write the failing test** + +Create `tests/test-site.sh`: + +```bash +#!/usr/bin/env bash +# Verifies the composed site is complete and self-contained. +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" +. "$(dirname "${BASH_SOURCE[0]}")/../scripts/lib.sh" + +assert_success bash "$REPO_ROOT/scripts/build-site.sh" + +assert_file_exists "$SITE_OUT/index.html" +assert_file_exists "$SITE_OUT/style.css" +assert_file_exists "$SITE_OUT/mars.jsdos" +assert_file_exists "$SITE_OUT/MARS.COM" +assert_file_exists "$SITE_OUT/MARS.ASM" +assert_file_exists "$SITE_OUT/js-dos/js-dos.js" +assert_file_exists "$SITE_OUT/js-dos/emulators/wdosbox.wasm" + +# The page must reference its own copies, not a CDN. +assert_contains "$SITE_OUT/index.html" 'js-dos/emulators/' +assert_contains "$SITE_OUT/index.html" 'mars.jsdos' + +# No third-party origins anywhere in the page. +if grep -qE 'https?://(v8\.)?js-dos\.com|cdn\.|unpkg|jsdelivr' "$SITE_OUT/index.html"; then + fail "index.html references a third-party asset origin" +else + pass "no third-party asset origins in index.html" +fi + +# Attribution must survive any future edit of the page. +assert_contains "$SITE_OUT/index.html" "Tim J. Clarke" +assert_contains "$SITE_OUT/index.html" "Bruzda" + +finish_tests +``` + +Note: the third-party check greps for asset origins, so `index.html` must not +contain a scheme-qualified `js-dos.com` URL. The page as written credits js-dos +in plain text and points every anchor at github.com, so it passes. If you later +want a clickable js-dos link on the page, relax this assertion deliberately +rather than working around it. + +- [ ] **Step 2: Run it to make sure it fails** + +Run: `bash tests/test-site.sh` +Expected: FAIL — `scripts/build-site.sh` does not exist. + +- [ ] **Step 3: Write the page** + +Create `site/index.html`: + +```html + + + + + +MARS — a martian landscape in 1550 bytes + + + + + + +
+
+

MARS

+

A martian landscape renderer from 1993, in 1550 bytes.

+
+ +
+
+ + +
+ +

+ Click the canvas to capture your mouse — moving it pans the camera. + Press Esc to release. +

+ +
+

What this is

+

+ In 1993 Tim J. Clarke wrote a real-time martian landscape + renderer that fit in a 5649-byte DOS executable. In 2021 + Wojciech Bruzda disassembled it, rewrote it, and reduced it + to roughly a tenth of that size — the binary running above is + assembled from that annotated source. +

+

+ It draws into VGA mode 13h and reprograms the palette DAC directly, which + is why the emulator here is pinned to machine=vgaonly. +

+
+ + +
+ + + + + + +``` + +- [ ] **Step 4: Write the stylesheet** + +Create `site/style.css`: + +```css +/* Dark, low-chrome framing so the 320x200 canvas is the loudest thing here. */ +:root { + --bg: #0d0b0a; + --fg: #e8ddd4; + --muted: #9a8d83; + --accent: #d97742; + --rule: #2a2320; +} + +* { box-sizing: border-box; } + +body { + margin: 0; + padding: 2rem 1.25rem 4rem; + background: var(--bg); + color: var(--fg); + font: 16px/1.65 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; +} + +main { max-width: 46rem; margin: 0 auto; } + +header { text-align: center; margin-bottom: 1.75rem; } + +h1 { + margin: 0; + font-size: clamp(2.5rem, 9vw, 4rem); + letter-spacing: 0.22em; + text-indent: 0.22em; + color: var(--accent); + font-weight: 700; +} + +.tagline { margin: 0.35rem 0 0; color: var(--muted); } + +/* The emulator canvas. 4:3 keeps mode 13h's aspect honest. */ +#player { + position: relative; + aspect-ratio: 4 / 3; + width: 100%; + background: #000; + border: 1px solid var(--rule); + border-radius: 4px; + overflow: hidden; +} + +#dos { width: 100%; height: 100%; } + +.notice { + position: absolute; + inset: auto 0 0; + margin: 0; + padding: 0.75rem 1rem; + background: #3a1410; + color: #ffb4a2; + font-size: 0.9rem; + text-align: center; +} + +.hint { + margin: 0.9rem 0 2.5rem; + color: var(--muted); + font-size: 0.9rem; + text-align: center; +} + +kbd { + padding: 0.1em 0.4em; + border: 1px solid var(--rule); + border-radius: 3px; + background: #191412; + font: 0.85em ui-monospace, SFMono-Regular, Menlo, monospace; +} + +section { border-top: 1px solid var(--rule); padding-top: 1.5rem; } + +h2 { font-size: 1.05rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); } + +code { + padding: 0.1em 0.35em; + border-radius: 3px; + background: #191412; + color: var(--accent); + font: 0.9em ui-monospace, SFMono-Regular, Menlo, monospace; +} + +footer { margin-top: 2.5rem; border-top: 1px solid var(--rule); padding-top: 1.5rem; } + +.links { list-style: none; margin: 0 0 1.25rem; padding: 0; display: grid; gap: 0.5rem; } + +a { color: var(--accent); text-underline-offset: 3px; } +a:hover { color: #f0a070; } + +.legal { margin: 0; color: var(--muted); font-size: 0.82rem; } + +@media (max-width: 480px) { + body { padding: 1.25rem 0.75rem 3rem; } +} +``` + +- [ ] **Step 5: Write the site build script** + +Create `scripts/build-site.sh`: + +```bash +#!/usr/bin/env bash +# Composes the deployable site from the built binary, the js-dos bundle, +# the fetched emulator assets, and the static page sources. +# +# Everything the browser loads is copied into _site/, so the deployed page +# makes no third-party requests at view time. +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +log "Building MARS.COM" +bash "$REPO_ROOT/scripts/build.sh" + +log "Bundling for js-dos" +bash "$REPO_ROOT/scripts/bundle.sh" + +log "Fetching js-dos $JSDOS_VERSION" +bash "$REPO_ROOT/scripts/fetch-jsdos.sh" + +log "Composing $SITE_OUT" +rm -rf "$SITE_OUT" +mkdir -p "$SITE_OUT" + +cp "$REPO_ROOT/site/index.html" "$SITE_OUT/index.html" +cp "$REPO_ROOT/site/style.css" "$SITE_OUT/style.css" +cp "$BUILD_DIR/mars.jsdos" "$SITE_OUT/mars.jsdos" +cp "$BUILD_DIR/MARS.COM" "$SITE_OUT/MARS.COM" +cp "$REPO_ROOT/MARS.ASM" "$SITE_OUT/MARS.ASM" +cp "$REPO_ROOT/mars_4_3.png" "$SITE_OUT/mars_4_3.png" +cp -R "$BUILD_DIR/js-dos" "$SITE_OUT/js-dos" + +# GitHub Pages runs Jekyll by default, which strips paths beginning with a +# dot and can mangle asset directories. .nojekyll turns that off. +touch "$SITE_OUT/.nojekyll" + +log "Site ready in $SITE_OUT ($(du -sh "$SITE_OUT" | cut -f1))" +``` + +- [ ] **Step 6: Run the tests and make sure they pass** + +Run: `chmod +x scripts/build-site.sh && bash tests/test-site.sh` +Expected: PASS — 13 assertions, 0 failures. + +(Count check: 1 `assert_success`, 7 `assert_file_exists`, 4 `assert_contains`, +1 third-party branch = 13.) + +- [ ] **Step 7: Commit** + +```bash +git add site/index.html site/style.css scripts/build-site.sh tests/test-site.sh +git commit -m "site: add js-dos player page and site composition script" +``` + +--- + +### Task 6: Local preview server and Makefile + +**Files:** +- Create: `scripts/serve.sh` +- Create: `Makefile` + +**Interfaces:** +- Consumes: all scripts from Tasks 2–5. +- Produces: `make build|bundle|site|serve|test|clean` targets. Nothing downstream depends on these. + +- [ ] **Step 1: Write the preview server** + +Create `scripts/serve.sh`: + +```bash +#!/usr/bin/env bash +# Builds the site and serves it over HTTP. +# +# HTTP rather than file:// is required: js-dos loads mars.jsdos with fetch(), +# and browsers block fetch on file:// origins. +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +port="${PORT:-8080}" + +bash "$REPO_ROOT/scripts/build-site.sh" + +need python3 +log "Serving $SITE_OUT at http://localhost:$port (Ctrl-C to stop)" +cd "$SITE_OUT" +exec python3 -m http.server "$port" +``` + +- [ ] **Step 2: Write the Makefile** + +Create `Makefile`: + +```makefile +# Thin wrappers over scripts/. Every target is also runnable directly. +.PHONY: all build bundle jsdos site serve test clean + +all: site + +build: ## Assemble MARS.ASM into build/MARS.COM + @bash scripts/build.sh + +bundle: build ## Package build/mars.jsdos for js-dos + @bash scripts/bundle.sh + +jsdos: ## Fetch the pinned js-dos assets + @bash scripts/fetch-jsdos.sh + +site: ## Compose the deployable site into _site/ + @bash scripts/build-site.sh + +serve: ## Build the site and serve it at http://localhost:8080 + @bash scripts/serve.sh + +test: ## Run the test suite + @bash tests/run-tests.sh + +clean: ## Remove build outputs (keeps the cached toolchain) + @rm -rf build _site + @echo "Removed build/ and _site/ (run 'rm -rf .toolchain' to drop the assembler too)" +``` + +- [ ] **Step 3: Verify the targets work** + +Run: `make clean && make site && bash tests/run-tests.sh` +Expected: `_site/` rebuilt; all test files pass. + +- [ ] **Step 4: Verify the demo actually renders** + +Run: `make serve`, then open in a browser. + +Confirm all four, and do not proceed until they hold: +1. The landscape draws (orange/brown terrain against a graded sky). +2. Colours look correct — a washed-out or wrongly-shaded palette means + `machine=vgaonly` is not being applied; check the conf inside the bundle. +3. Moving the mouse after clicking the canvas pans the camera. +4. The browser network panel shows **no** requests to any origin other than + `localhost`. + +If js-dos reports a missing emulator asset, `pathPrefix` is wrong: it must be +`js-dos/emulators/`, with the trailing slash. + +- [ ] **Step 5: Commit** + +```bash +git add scripts/serve.sh Makefile +git commit -m "build: add local preview server and Makefile targets" +``` + +--- + +### Task 7: CI workflow + +**Files:** +- Create: `.github/workflows/ci.yml` + +**Interfaces:** +- Consumes: `scripts/build.sh --check`, `tests/run-tests.sh`. +- Produces: a `MARS.COM` workflow artifact on every push and pull request. + +- [ ] **Step 1: Write the workflow** + +Create `.github/workflows/ci.yml`: + +```yaml +name: Build MARS.COM + +on: + push: + branches: ["**"] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Cache the JWasm toolchain + uses: actions/cache@v4 + with: + path: .toolchain + key: jwasm-${{ hashFiles('versions.env') }} + + - name: Assemble MARS.COM and verify reproducibility + run: bash scripts/build.sh --check + + - name: Run the test suite + run: bash tests/run-tests.sh + + - name: Report size and checksum + run: | + size=$(wc -c < build/MARS.COM | tr -d ' ') + sha=$(sha256sum build/MARS.COM | cut -d' ' -f1) + { + echo "### MARS.COM" + echo "" + echo "| | |" + echo "|---|---|" + echo "| Size | $size bytes |" + echo "| SHA-256 | \`$sha\` |" + } >> "$GITHUB_STEP_SUMMARY" + + - uses: actions/upload-artifact@v4 + with: + name: MARS.COM + path: build/MARS.COM + if-no-files-found: error +``` + +- [ ] **Step 2: Commit and push, then verify the run is green** + +```bash +git add .github/workflows/ci.yml +git commit -m "ci: verify MARS.COM builds reproducibly on every push" +git push -u origin feat/jsdos-pages-build +``` + +Then: `gh run watch` (or `gh run list --limit 1`). +Expected: the workflow completes successfully and the summary shows +1550 bytes with the pinned SHA-256. + +If the run does not appear at all, Actions is disabled on the fork — enable it +with `gh api repos/dtz-labs/MARS.COM/actions/permissions -X PUT --input - <<< '{"enabled":true,"allowed_actions":"all"}'`. + +--- + +### Task 8: Pages workflow and repository configuration + +**Files:** +- Create: `.github/workflows/pages.yml` + +**Interfaces:** +- Consumes: `scripts/build-site.sh`. +- Produces: a deployed GitHub Pages site at `https://dtz-labs.github.io/MARS.COM/`. + +- [ ] **Step 1: Write the workflow** + +Create `.github/workflows/pages.yml`: + +```yaml +name: Deploy Pages + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +# Let a running deploy finish rather than cancelling it mid-flight. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Cache the JWasm toolchain + uses: actions/cache@v4 + with: + path: .toolchain + key: jwasm-${{ hashFiles('versions.env') }} + + - name: Build the site + run: bash scripts/build-site.sh + + - uses: actions/configure-pages@v5 + + - uses: actions/upload-pages-artifact@v3 + with: + path: _site + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 +``` + +- [ ] **Step 2: Point Pages at GitHub Actions** + +Forks do not inherit Pages settings, so set the source explicitly: + +```bash +gh api repos/dtz-labs/MARS.COM/pages -X POST --input - <<< '{"build_type":"workflow"}' \ + || gh api repos/dtz-labs/MARS.COM/pages -X PUT --input - <<< '{"build_type":"workflow"}' +gh api repos/dtz-labs/MARS.COM/pages --jq '{status,html_url,build_type}' +``` + +Expected: `build_type` is `workflow`. + +- [ ] **Step 3: Commit** + +```bash +git add .github/workflows/pages.yml +git commit -m "ci: build and deploy the js-dos player to GitHub Pages" +``` + +--- + +### Task 9: README + +**Files:** +- Modify: `README.md` + +**Interfaces:** +- Consumes: nothing. Terminal task. +- Produces: nothing. + +- [ ] **Step 1: Rewrite the README** + +Replace `README.md` with the content below. It keeps upstream's text and +credits intact and adds only what this fork contributes. Note the byte-count +paragraph: upstream claims 1517 bytes, our reproducible build is 1550 — the +discrepancy is stated rather than papered over. + +```markdown +# MARS landscape + +A comprehensive study of the outstanding code example from 1993 — the martian +landscape renderer by Tim J. Clarke. + +Original code has been disassembled, rewritten and reduced from **5649** bytes +to about **1.5 kB** by [Wojciech Bruzda](https://github.com/matrix-toolbox). + +**Tim, if you read this, please contact +[upstream](https://github.com/matrix-toolbox/MARS.COM)!** + +![MARS](mars_4_3.png) + +## ▶ Run it in your browser + +**** + +Click the canvas to capture the mouse — moving it pans the camera. + +## What this fork adds + +Upstream ships the annotated assembly only. This fork adds a reproducible +build and a browser player: + +- `scripts/build.sh` — assembles `MARS.ASM` into `MARS.COM` +- `scripts/build-site.sh` — composes the GitHub Pages site +- GitHub Actions workflows that verify the build and deploy the page + +`MARS.ASM` remains the single source of truth; the binary is never committed. + +## Building + +```sh +make build # assemble build/MARS.COM +make serve # build the site and preview at http://localhost:8080 +make test # run the test suite +``` + +`MARS.ASM` is MASM/TASM dialect (`.model tiny`, `org 100h`, `COMMENT #` +blocks), so it needs a MASM-compatible assembler — NASM cannot build it. The +build uses [JWasm](https://github.com/Baron-von-Riedesel/JWasm), pinned in +`versions.env`. If no assembler is on your `PATH`, `build.sh` compiles JWasm +from source into `.toolchain/` automatically. With no C compiler available, +`bash scripts/build.sh --docker` runs the whole build in a container. + +### Build output + +The pinned toolchain produces a **1550-byte** `MARS.COM` +(`sha256:10a1bb6c…`), byte-identical under JWasm v2.20 and v2.21. Upstream's +README reports **1517** bytes; the 33-byte difference comes from the original +author's toolchain and has not been reconciled. `scripts/build.sh --check` +enforces the 1550-byte result so unintended changes to `MARS.ASM` are caught. + +## Running it natively + +It works on DOSBox and on genuine x86 machines (a mouse is needed). Under +DOSBox, always use `-machine vgaonly` — the renderer drives VGA mode 13h and +reprograms the palette DAC, and the default `svga_s3` emulation shows +artifacts. More details are on the +[author's page](https://chaos.if.uj.edu.pl/~wojtek/MARS.COM). + +## Credits and licence + +- Original martian landscape renderer — **Tim J. Clarke**, 1993 +- Disassembly, rewrite and size reduction — **Wojciech Bruzda**, 2021 +- Browser emulation — [js-dos](https://js-dos.com), pinned in `versions.env` + +Released under GPL-3.0, as upstream. +``` + +- [ ] **Step 2: Verify links resolve** + +Run: `grep -oE 'https?://[^)]+' README.md` +Check each host is reachable and that the Pages URL matches the one reported +by `gh api repos/dtz-labs/MARS.COM/pages --jq .html_url`. + +- [ ] **Step 3: Commit** + +```bash +git add README.md +git commit -m "docs: document the build, the player, and the byte-count discrepancy" +``` + +--- + +## Final verification + +After all tasks, before merging to `main`: + +- [ ] `make clean && rm -rf .toolchain && make test` passes from a cold start +- [ ] `make serve` renders the demo correctly (terrain, palette, mouse) +- [ ] `gh run list --limit 3` shows CI green +- [ ] Merge to `main`, then confirm the Pages deploy succeeds and + loads with no third-party requests diff --git a/docs/superpowers/specs/2026-08-16-mars-com-jsdos-pages-design.md b/docs/superpowers/specs/2026-08-16-mars-com-jsdos-pages-design.md index ab52eb3..3e73053 100644 --- a/docs/superpowers/specs/2026-08-16-mars-com-jsdos-pages-design.md +++ b/docs/superpowers/specs/2026-08-16-mars-com-jsdos-pages-design.md @@ -64,6 +64,16 @@ These were established by direct investigation, not assumption: ### Components +**`versions.env`** — the single source of truth for every pin: JWasm tag, js-dos +version, the expected `MARS.COM` size and SHA-256, and the `.COM` size ceiling. +Plain `KEY=value` so both shell scripts and CI can read it. + +**`scripts/lib.sh`** — sourced by every script. Sets `set -euo pipefail`, derives +the repo paths, loads `versions.env`, and provides `log`, `die`, and `need`. + +**`tests/`** — a dependency-free bash test harness (`tests/lib.sh` assertions, +`tests/run-tests.sh` runner) with one test file per script. + **`scripts/build.sh`** — assembles `MARS.ASM` into `MARS.COM`. Resolves a toolchain in order: `$JWASM` environment variable, then `jwasm` on @@ -118,7 +128,8 @@ directory, `backend: "dosbox"`, and `url` pointing at `mars.jsdos`. **`.github/workflows/ci.yml`** — on push and pull request. -Runs `scripts/build.sh`, asserts the output is non-empty and under 64 KB, and +Runs `scripts/build.sh --check`, which asserts the output is non-empty, within +the `.COM` ceiling, and byte-identical to the pinned size and SHA-256. Also records size and SHA-256 in the job summary. Uploads `MARS.COM` as a workflow artifact. @@ -155,8 +166,14 @@ At page view time nothing is fetched from outside the Pages origin. `command not found`. - **Assembly failure.** JWasm's non-zero exit fails the build; its diagnostics are surfaced verbatim. CI does not deploy a stale binary. -- **Size regression.** CI fails if the output is empty or exceeds 64 KB, which - would mean the tiny-model assumption has broken. +- **Size regression.** `build.sh` fails if the output is empty or exceeds + **65280 bytes** (0xFF00 — the true `.COM` ceiling, since the PSP occupies the + first 256 bytes of the segment), which would mean the tiny-model assumption + has broken. +- **Unintended source change.** `build.sh --check`, which CI runs, fails if the + binary no longer matches the pinned size and SHA-256. A deliberate change to + `MARS.ASM` must update `versions.env` in the same commit, making it visible in + review rather than silent. - **js-dos fetch failure.** The Pages build fails rather than deploying a page with missing emulator assets. - **Browser-side load failure.** The page shows an inline error message instead From 9aec67f2171db97e85046d3e6902b2fc0db4faae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 16 Aug 2026 20:39:07 +0200 Subject: [PATCH 03/12] build: add version pins, shared script library, and test harness Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014Cv1JoZyJdqYiCXiVSKDQP --- .gitignore | 13 +++++++++++++ scripts/lib.sh | 15 +++++++++++++++ tests/lib.sh | 43 +++++++++++++++++++++++++++++++++++++++++++ tests/run-tests.sh | 17 +++++++++++++++++ tests/test-lib.sh | 20 ++++++++++++++++++++ versions.env | 16 ++++++++++++++++ 6 files changed, 124 insertions(+) create mode 100644 .gitignore create mode 100644 scripts/lib.sh create mode 100644 tests/lib.sh create mode 100755 tests/run-tests.sh create mode 100755 tests/test-lib.sh create mode 100644 versions.env diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1c90d17 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# Build outputs — MARS.ASM is the single source of truth +build/ +_site/ +.toolchain/ +MARS.COM +*.jsdos + +# Fetched dependencies +node_modules/ +*.tgz + +# OS turds +.DS_Store diff --git a/scripts/lib.sh b/scripts/lib.sh new file mode 100644 index 0000000..57e28d9 --- /dev/null +++ b/scripts/lib.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Shared helpers for every script in this repo. Source it, don't execute it. +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +BUILD_DIR="$REPO_ROOT/build" +SITE_OUT="$REPO_ROOT/_site" +TOOLCHAIN_DIR="$REPO_ROOT/.toolchain" + +# shellcheck source=/dev/null +. "$REPO_ROOT/versions.env" + +log() { printf '\033[1;34m==>\033[0m %s\n' "$*" >&2; } +die() { printf '\033[1;31mError:\033[0m %s\n' "$*" >&2; exit 1; } +need() { command -v "$1" >/dev/null 2>&1 || die "required tool not found: $1"; } diff --git a/tests/lib.sh b/tests/lib.sh new file mode 100644 index 0000000..4485c4c --- /dev/null +++ b/tests/lib.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Minimal assertion helpers. No external test framework dependency. +TESTS_RUN=0 +TESTS_FAILED=0 + +pass() { TESTS_RUN=$((TESTS_RUN + 1)); printf ' \033[0;32mok\033[0m %s\n' "$1"; } + +fail() { + TESTS_RUN=$((TESTS_RUN + 1)) + TESTS_FAILED=$((TESTS_FAILED + 1)) + printf ' \033[0;31mFAIL\033[0m %s\n' "$1" +} + +assert_eq() { + local actual="$1" expected="$2" label="$3" + if [ "$actual" = "$expected" ]; then + pass "$label" + else + fail "$label (expected '$expected', got '$actual')" + fi +} + +assert_file_exists() { + if [ -f "$1" ]; then pass "file exists: $1"; else fail "file missing: $1"; fi +} + +assert_contains() { + local file="$1" needle="$2" + if grep -qF -- "$needle" "$file" 2>/dev/null; then + pass "$file contains '$needle'" + else + fail "$file does not contain '$needle'" + fi +} + +assert_success() { + if "$@" >/dev/null 2>&1; then pass "command succeeded: $*"; else fail "command failed: $*"; fi +} + +finish_tests() { + printf '\n %d assertion(s), %d failure(s)\n' "$TESTS_RUN" "$TESTS_FAILED" + [ "$TESTS_FAILED" -eq 0 ] || exit 1 +} diff --git a/tests/run-tests.sh b/tests/run-tests.sh new file mode 100755 index 0000000..60fb53f --- /dev/null +++ b/tests/run-tests.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Runs every tests/test-*.sh and reports a combined result. +set -uo pipefail +cd "$(dirname "${BASH_SOURCE[0]}")" + +failed=0 +for t in test-*.sh; do + printf '\n\033[1m%s\033[0m\n' "$t" + bash "$t" || failed=$((failed + 1)) +done + +printf '\n' +if [ "$failed" -ne 0 ]; then + printf '\033[1;31m%d test file(s) failed\033[0m\n' "$failed" + exit 1 +fi +printf '\033[1;32mAll test files passed\033[0m\n' diff --git a/tests/test-lib.sh b/tests/test-lib.sh new file mode 100755 index 0000000..095fb71 --- /dev/null +++ b/tests/test-lib.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Verifies scripts/lib.sh exposes the contract the other scripts rely on. +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" +. "$(dirname "${BASH_SOURCE[0]}")/../scripts/lib.sh" + +assert_file_exists "$REPO_ROOT/MARS.ASM" # REPO_ROOT points at the repo +assert_eq "$BUILD_DIR" "$REPO_ROOT/build" "BUILD_DIR derived from REPO_ROOT" +assert_eq "$SITE_OUT" "$REPO_ROOT/_site" "SITE_OUT derived from REPO_ROOT" +assert_eq "$JWASM_TAG" "v2.20" "JWasm pin loaded from versions.env" +assert_eq "$JSDOS_VERSION" "8.4.1" "js-dos pin loaded from versions.env" +assert_eq "$MARS_COM_SIZE" "1550" "expected binary size loaded" + +# die must exit non-zero and print to stderr +if ( die "boom" ) 2>/dev/null; then + fail "die should exit non-zero" +else + pass "die exits non-zero" +fi + +finish_tests diff --git a/versions.env b/versions.env new file mode 100644 index 0000000..aff5fed --- /dev/null +++ b/versions.env @@ -0,0 +1,16 @@ +# Single source of truth for pinned versions. +# Sourced by scripts/lib.sh; plain KEY=value so CI can read it too. + +# JWasm — MASM-compatible assembler used to build MARS.COM +JWASM_TAG=v2.20 +JWASM_REPO=https://github.com/Baron-von-Riedesel/JWasm.git + +# js-dos — browser DOS emulator, fetched at site build time +JSDOS_VERSION=8.4.1 + +# Expected build output (JWasm v2.20 and v2.21 agree byte-for-byte) +MARS_COM_SIZE=1550 +MARS_COM_SHA256=10a1bb6c319296dd8628e8fd2d705b50fca97b82d6d9a811c91c30c06199d773 + +# Hard ceiling for a DOS .COM image (0xFF00) +MARS_COM_MAX_SIZE=65280 From d1c3b47419510c9dda14175a72cc3464e65dafc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 16 Aug 2026 20:43:00 +0200 Subject: [PATCH 04/12] build: assemble MARS.ASM into MARS.COM with pinned JWasm Resolves an assembler from $JWASM, then PATH, then builds JWasm v2.20 from source into .toolchain/. A --docker flag covers hosts with no compiler, and --check enforces the pinned size and SHA-256. JWasm's GccUnix.mak targets Linux/FreeBSD and does not build on macOS, which the plan did not anticipate. Two fixes are applied automatically on Darwin: an include shim so resolves to (injected via inc_dirs rather than editing 58 source files), and removal of the GNU-only -s and -Wl,-Map link flags that Apple's ld rejects. Both are confined to the gitignored .toolchain/ checkout. The macOS/clang/ARM64 build produces a MARS.COM byte-identical to the Linux/gcc/aarch64 build, so the pinned SHA is a real reproducibility gate rather than a description of one machine. Build output now goes to a log file instead of /dev/null, so a failure is diagnosable without re-running the build by hand. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014Cv1JoZyJdqYiCXiVSKDQP --- .../plans/2026-08-16-jsdos-pages-build.md | 1 + scripts/build.sh | 145 ++++++++++++++++++ tests/test-build.sh | 21 +++ 3 files changed, 167 insertions(+) create mode 100755 scripts/build.sh create mode 100644 tests/test-build.sh diff --git a/docs/superpowers/plans/2026-08-16-jsdos-pages-build.md b/docs/superpowers/plans/2026-08-16-jsdos-pages-build.md index 8124502..09d3d04 100644 --- a/docs/superpowers/plans/2026-08-16-jsdos-pages-build.md +++ b/docs/superpowers/plans/2026-08-16-jsdos-pages-build.md @@ -13,6 +13,7 @@ ## Global Constraints - **Assembler:** JWasm, pinned to tag `v2.20`, repo `https://github.com/Baron-von-Riedesel/JWasm.git`. Build with `make -f GccUnix.mak`; binary lands at `build/GccUnixR/jwasm` inside the JWasm checkout. +- **macOS caveat (found during execution, not anticipated by this plan):** `GccUnix.mak` targets Linux/FreeBSD. On Darwin it needs (a) an include shim so `` resolves to ``, injected by overriding `inc_dirs`, and (b) removal of the GNU-only `-s` and `-Wl,-Map` link flags, which are hardcoded in the recipe and must be `sed`-stripped. `scripts/build.sh` applies both automatically when `uname -s` is `Darwin`. Verified: the macOS/clang/ARM64 build produces a `MARS.COM` byte-identical to the Linux/gcc/aarch64 build. - **js-dos:** pinned to version `8.4.1`, fetched via `npm pack js-dos@8.4.1`. - **Emulator backend:** `dosbox` (`wdosbox.js` + `wdosbox.wasm`, ~1.5 MB). Do **not** ship `wdosbox-x*` (~15.5 MB combined). - **Expected build output:** `MARS.COM`, exactly **1550 bytes**, SHA-256 `10a1bb6c319296dd8628e8fd2d705b50fca97b82d6d9a811c91c30c06199d773`. Verified byte-identical under JWasm v2.20 and v2.21. diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..86d3ce5 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash +# Assembles MARS.ASM into a runnable DOS .COM image. +# +# MARS.ASM is MASM/TASM dialect (.model tiny, org 100h, COMMENT # blocks), +# so it needs a MASM-compatible assembler — NASM cannot build it. We use +# JWasm, pinned in versions.env. Because the model is tiny, the assembler +# emits the .COM memory image directly; there is no link step. +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +usage() { + cat <<'EOF' +Usage: scripts/build.sh [--docker] [--check] + + --docker Run the build inside a container (no host compiler needed). + --check Verify the output matches the pinned size and SHA-256. + +Assembler resolution order: + 1. $JWASM environment variable + 2. jwasm on PATH + 3. build JWasm from source into .toolchain/ (needs gcc, make, git) +EOF +} + +use_docker=0 +do_check=0 +for arg in "$@"; do + case "$arg" in + --docker) use_docker=1 ;; + --check) do_check=1 ;; + -h|--help) usage; exit 0 ;; + *) die "unknown option: $arg (try --help)" ;; + esac +done + +if [ "$use_docker" -eq 1 ]; then + need docker + log "Building inside debian:bookworm-slim" + docker run --rm -v "$REPO_ROOT:/repo" -w /repo debian:bookworm-slim bash -c ' + set -e + apt-get update -qq >/dev/null + apt-get install -y -qq build-essential git ca-certificates >/dev/null 2>&1 + bash scripts/build.sh + ' + exit $? +fi + +# --- resolve an assembler ----------------------------------------------- +resolve_jwasm() { + if [ -n "${JWASM:-}" ]; then + [ -x "$JWASM" ] || die "\$JWASM is set to '$JWASM' but that is not executable" + printf '%s' "$JWASM" + return + fi + + if command -v jwasm >/dev/null 2>&1; then + command -v jwasm + return + fi + + local cached="$TOOLCHAIN_DIR/bin/jwasm" + if [ -x "$cached" ]; then + printf '%s' "$cached" + return + fi + + log "No assembler found; building JWasm $JWASM_TAG from source" + need git + need make + command -v cc >/dev/null 2>&1 || command -v gcc >/dev/null 2>&1 \ + || die "no C compiler found. Install gcc/clang, set \$JWASM to a jwasm binary, or rerun with --docker" + + local src="$TOOLCHAIN_DIR/JWasm" + local buildlog="$TOOLCHAIN_DIR/jwasm-build.log" + rm -rf "$src" + mkdir -p "$TOOLCHAIN_DIR/bin" + git clone -q "$JWASM_REPO" "$src" >&2 + ( cd "$src" && git checkout -q "$JWASM_TAG" ) \ + || die "could not check out JWasm $JWASM_TAG" + + # GccUnix.mak targets Linux/FreeBSD, so it needs two fixes on macOS: + # + # 1. It includes , a glibc location. macOS declares malloc in + # . Rather than editing 58 source files, we prepend an + # include directory holding a malloc.h that forwards to stdlib.h. + # 2. It links with -s and -Wl,-Map, both GNU ld flags that Apple's ld + # rejects. These are hardcoded in the recipe, not in a variable, so + # they have to be stripped from our throwaway checkout. + # + # Both are confined to .toolchain/, which is gitignored. Verified to yield + # a binary byte-identical to the Linux build. + local inc_dirs="-Isrc/H" + if [ "$(uname -s)" = "Darwin" ]; then + local shim="$TOOLCHAIN_DIR/shim" + mkdir -p "$shim" + printf '/* Darwin shim: malloc is declared in stdlib.h, not malloc.h */\n#include \n' \ + > "$shim/malloc.h" + sed -i.bak -e 's/ -Wl,-Map,[^ ]*//' -e 's/ -s -o / -o /' "$src/GccUnix.mak" \ + || die "could not patch GccUnix.mak for macOS" + inc_dirs="-Isrc/H -I$shim" + log "Applied macOS portability patches to the JWasm checkout" + fi + + ( cd "$src" && make -f GccUnix.mak inc_dirs="$inc_dirs" ) >"$buildlog" 2>&1 \ + || die "JWasm build failed; see $buildlog. Rerun with --docker, or set \$JWASM to a prebuilt binary." + + local built + built="$(find "$src" -name jwasm -type f -perm -u+x | head -1)" + [ -n "$built" ] || die "JWasm built but no 'jwasm' binary was produced" + cp "$built" "$cached" + printf '%s' "$cached" +} + +JWASM_BIN="$(resolve_jwasm)" +log "Assembler: $JWASM_BIN" + +# --- assemble ------------------------------------------------------------ +mkdir -p "$BUILD_DIR" +out="$BUILD_DIR/MARS.COM" +rm -f "$out" + +# -bin: raw binary output, which for .model tiny is exactly a .COM image. +"$JWASM_BIN" -bin -Fo="$out" "$REPO_ROOT/MARS.ASM" \ + || die "assembly failed" + +[ -s "$out" ] || die "assembler produced an empty $out" + +size=$(wc -c < "$out" | tr -d ' ') +[ "$size" -le "$MARS_COM_MAX_SIZE" ] \ + || die "$out is $size bytes, which exceeds the $MARS_COM_MAX_SIZE byte .COM ceiling" + +if command -v shasum >/dev/null 2>&1; then + sha=$(shasum -a 256 "$out" | cut -d' ' -f1) +else + sha=$(sha256sum "$out" | cut -d' ' -f1) +fi + +log "Built $out — $size bytes, sha256 $sha" + +if [ "$do_check" -eq 1 ]; then + [ "$size" = "$MARS_COM_SIZE" ] \ + || die "size mismatch: expected $MARS_COM_SIZE bytes, got $size. If MARS.ASM changed on purpose, update versions.env." + [ "$sha" = "$MARS_COM_SHA256" ] \ + || die "sha256 mismatch: expected $MARS_COM_SHA256, got $sha. If MARS.ASM changed on purpose, update versions.env." + log "Reproducibility check passed" +fi diff --git a/tests/test-build.sh b/tests/test-build.sh new file mode 100644 index 0000000..f143242 --- /dev/null +++ b/tests/test-build.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Verifies the assembler pipeline produces the expected MARS.COM. +# First run is slow: it builds JWasm from source. +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" +. "$(dirname "${BASH_SOURCE[0]}")/../scripts/lib.sh" + +rm -f "$BUILD_DIR/MARS.COM" +assert_success bash "$REPO_ROOT/scripts/build.sh" +assert_file_exists "$BUILD_DIR/MARS.COM" + +actual_size=$(wc -c < "$BUILD_DIR/MARS.COM" | tr -d ' ') +assert_eq "$actual_size" "$MARS_COM_SIZE" "MARS.COM is the expected size" + +actual_sha=$(shasum -a 256 "$BUILD_DIR/MARS.COM" 2>/dev/null | cut -d' ' -f1 \ + || sha256sum "$BUILD_DIR/MARS.COM" | cut -d' ' -f1) +assert_eq "$actual_sha" "$MARS_COM_SHA256" "MARS.COM is byte-identical to the pinned build" + +# --check must succeed on a good build +assert_success bash "$REPO_ROOT/scripts/build.sh" --check + +finish_tests From 1969a88946822992cec30fe6a17f6c76b8c61bf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 16 Aug 2026 20:43:38 +0200 Subject: [PATCH 05/12] build: package MARS.COM into a js-dos bundle with vgaonly config The bundle is a ZIP whose root becomes drive C:. js-dos does not auto-mount, so [autoexec] performs the mount itself, matching the layout js-dos's own bundle generator emits. machine=vgaonly is pinned because the renderer drives VGA mode 13h and reprograms the palette DAC; DOSBox's default svga_s3 shows artifacts. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014Cv1JoZyJdqYiCXiVSKDQP --- scripts/bundle.sh | 26 ++++++++++++++++++++++++++ site/dosbox.conf | 26 ++++++++++++++++++++++++++ tests/test-bundle.sh | 22 ++++++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100755 scripts/bundle.sh create mode 100644 site/dosbox.conf create mode 100644 tests/test-bundle.sh diff --git a/scripts/bundle.sh b/scripts/bundle.sh new file mode 100755 index 0000000..b38d564 --- /dev/null +++ b/scripts/bundle.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Packages MARS.COM and its DOSBox config into a js-dos .jsdos bundle. +# +# A .jsdos bundle is a ZIP whose root becomes drive C: and which carries +# its DOSBox config at .jsdos/dosbox.conf. js-dos does NOT auto-mount, so +# the [autoexec] section in site/dosbox.conf does the mount itself. +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +need zip + +com="$BUILD_DIR/MARS.COM" +[ -f "$com" ] || die "$com not found — run scripts/build.sh first" + +staging="$BUILD_DIR/bundle" +out="$BUILD_DIR/mars.jsdos" + +rm -rf "$staging" "$out" +mkdir -p "$staging/.jsdos" + +cp "$com" "$staging/MARS.COM" +cp "$REPO_ROOT/site/dosbox.conf" "$staging/.jsdos/dosbox.conf" + +# -X strips extra file attributes so the bundle is reproducible. +( cd "$staging" && zip -q -r -X "$out" . ) + +log "Bundled $out ($(wc -c < "$out" | tr -d ' ') bytes)" diff --git a/site/dosbox.conf b/site/dosbox.conf new file mode 100644 index 0000000..a184edd --- /dev/null +++ b/site/dosbox.conf @@ -0,0 +1,26 @@ +# DOSBox configuration for the MARS landscape renderer. +# +# machine=vgaonly is mandatory, not cosmetic. MARS writes directly to VGA +# mode 13h and reprograms the palette DAC. DOSBox's default svga_s3 +# emulation changes DAC behaviour and produces visible artifacts — the +# original author's notes call for "-machine vgaonly" explicitly. + +[sdl] +autolock=true + +[dosbox] +machine=vgaonly + +[cpu] +core=auto +cputype=auto +cycles=auto + +[render] +aspect=true + +[autoexec] +echo off +mount c . +c: +MARS.COM diff --git a/tests/test-bundle.sh b/tests/test-bundle.sh new file mode 100644 index 0000000..45c9f5a --- /dev/null +++ b/tests/test-bundle.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Verifies the .jsdos bundle has the layout js-dos expects. +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" +. "$(dirname "${BASH_SOURCE[0]}")/../scripts/lib.sh" + +bash "$REPO_ROOT/scripts/build.sh" >/dev/null 2>&1 +assert_success bash "$REPO_ROOT/scripts/bundle.sh" +assert_file_exists "$BUILD_DIR/mars.jsdos" + +listing="$BUILD_DIR/.bundle-listing.txt" +unzip -l "$BUILD_DIR/mars.jsdos" > "$listing" +assert_contains "$listing" "MARS.COM" +assert_contains "$listing" ".jsdos/dosbox.conf" + +conf="$BUILD_DIR/.bundle-conf.txt" +unzip -p "$BUILD_DIR/mars.jsdos" ".jsdos/dosbox.conf" > "$conf" +assert_contains "$conf" "machine=vgaonly" +assert_contains "$conf" "mount c ." +assert_contains "$conf" "MARS.COM" + +rm -f "$listing" "$conf" +finish_tests From 82f7168be76756beaf2ae7a538a7e03246483bc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 16 Aug 2026 20:44:17 +0200 Subject: [PATCH 06/12] build: fetch pinned js-dos 8.4.1 assets at build time Downloads the js-dos npm tarball during the build and extracts only the five files the page needs, so the repo stays lean and the deployed page serves everything from its own origin with no CDN dependency at view time. Ships the plain dosbox backend and skips dosbox-x, cutting the staged payload from roughly 17 MB to 2.0 MB. MARS only needs machine=vgaonly, which plain DOSBox supports. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014Cv1JoZyJdqYiCXiVSKDQP --- scripts/fetch-jsdos.sh | 48 +++++++++++++++++++++++++++++++++++++++ tests/test-fetch-jsdos.sh | 21 +++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100755 scripts/fetch-jsdos.sh create mode 100644 tests/test-fetch-jsdos.sh diff --git a/scripts/fetch-jsdos.sh b/scripts/fetch-jsdos.sh new file mode 100755 index 0000000..9e84167 --- /dev/null +++ b/scripts/fetch-jsdos.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Downloads a pinned js-dos release and extracts only the assets the page +# needs. This runs at BUILD time, never at page-view time — the deployed +# site serves js-dos from its own origin, so it does not depend on a CDN. +# +# We ship the plain dosbox backend (wdosbox, ~1.5 MB) and deliberately skip +# dosbox-x (~15.5 MB across two builds). MARS only needs machine=vgaonly, +# which plain DOSBox supports. +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +need npm +need tar + +dest="$BUILD_DIR/js-dos" +work="$BUILD_DIR/.jsdos-download" + +if [ -f "$dest/js-dos.js" ] && [ "${FORCE_FETCH:-0}" != "1" ]; then + log "js-dos $JSDOS_VERSION already present in $dest (set FORCE_FETCH=1 to refetch)" + exit 0 +fi + +rm -rf "$work" "$dest" +mkdir -p "$work" "$dest/emulators" + +log "Fetching js-dos $JSDOS_VERSION from npm" +( cd "$work" && npm pack "js-dos@$JSDOS_VERSION" >/dev/null ) \ + || die "npm pack failed for js-dos@$JSDOS_VERSION" + +tarball="$work/js-dos-$JSDOS_VERSION.tgz" +[ -f "$tarball" ] || die "expected tarball $tarball was not produced" + +tar xzf "$tarball" -C "$work" \ + package/dist/js-dos.js \ + package/dist/js-dos.css \ + package/dist/emulators/emulators.js \ + package/dist/emulators/wdosbox.js \ + package/dist/emulators/wdosbox.wasm \ + || die "js-dos tarball did not contain the expected dist layout" + +cp "$work/package/dist/js-dos.js" "$dest/js-dos.js" +cp "$work/package/dist/js-dos.css" "$dest/js-dos.css" +cp "$work/package/dist/emulators/emulators.js" "$dest/emulators/emulators.js" +cp "$work/package/dist/emulators/wdosbox.js" "$dest/emulators/wdosbox.js" +cp "$work/package/dist/emulators/wdosbox.wasm" "$dest/emulators/wdosbox.wasm" + +rm -rf "$work" + +log "js-dos $JSDOS_VERSION staged in $dest ($(du -sh "$dest" | cut -f1))" diff --git a/tests/test-fetch-jsdos.sh b/tests/test-fetch-jsdos.sh new file mode 100644 index 0000000..93fdf69 --- /dev/null +++ b/tests/test-fetch-jsdos.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Verifies we fetch exactly the js-dos assets the page needs, and no more. +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" +. "$(dirname "${BASH_SOURCE[0]}")/../scripts/lib.sh" + +assert_success bash "$REPO_ROOT/scripts/fetch-jsdos.sh" + +assert_file_exists "$BUILD_DIR/js-dos/js-dos.js" +assert_file_exists "$BUILD_DIR/js-dos/js-dos.css" +assert_file_exists "$BUILD_DIR/js-dos/emulators/emulators.js" +assert_file_exists "$BUILD_DIR/js-dos/emulators/wdosbox.js" +assert_file_exists "$BUILD_DIR/js-dos/emulators/wdosbox.wasm" + +# The heavy dosbox-x builds must NOT be shipped (~15.5 MB of dead weight). +if [ -f "$BUILD_DIR/js-dos/emulators/wdosbox-x.wasm" ]; then + fail "wdosbox-x.wasm was shipped but is not needed" +else + pass "dosbox-x builds excluded" +fi + +finish_tests From c66ef0e85c1cd6cf5a230c88bbccedadcb8cfb7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 16 Aug 2026 20:45:21 +0200 Subject: [PATCH 07/12] site: add js-dos player page and site composition script Composes _site/ from the built binary, the .jsdos bundle, the fetched emulator assets and the static page sources, so nothing is loaded from a third-party origin at view time. A test asserts that invariant directly. Adds .nojekyll because GitHub Pages otherwise runs Jekyll, which strips dot-prefixed paths and would mangle the asset directories. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014Cv1JoZyJdqYiCXiVSKDQP --- scripts/build-site.sh | 34 ++++++++++++++ site/index.html | 101 ++++++++++++++++++++++++++++++++++++++++++ site/style.css | 97 ++++++++++++++++++++++++++++++++++++++++ tests/test-site.sh | 31 +++++++++++++ 4 files changed, 263 insertions(+) create mode 100755 scripts/build-site.sh create mode 100644 site/index.html create mode 100644 site/style.css create mode 100644 tests/test-site.sh diff --git a/scripts/build-site.sh b/scripts/build-site.sh new file mode 100755 index 0000000..2ee4067 --- /dev/null +++ b/scripts/build-site.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Composes the deployable site from the built binary, the js-dos bundle, +# the fetched emulator assets, and the static page sources. +# +# Everything the browser loads is copied into _site/, so the deployed page +# makes no third-party requests at view time. +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +log "Building MARS.COM" +bash "$REPO_ROOT/scripts/build.sh" + +log "Bundling for js-dos" +bash "$REPO_ROOT/scripts/bundle.sh" + +log "Fetching js-dos $JSDOS_VERSION" +bash "$REPO_ROOT/scripts/fetch-jsdos.sh" + +log "Composing $SITE_OUT" +rm -rf "$SITE_OUT" +mkdir -p "$SITE_OUT" + +cp "$REPO_ROOT/site/index.html" "$SITE_OUT/index.html" +cp "$REPO_ROOT/site/style.css" "$SITE_OUT/style.css" +cp "$BUILD_DIR/mars.jsdos" "$SITE_OUT/mars.jsdos" +cp "$BUILD_DIR/MARS.COM" "$SITE_OUT/MARS.COM" +cp "$REPO_ROOT/MARS.ASM" "$SITE_OUT/MARS.ASM" +cp "$REPO_ROOT/mars_4_3.png" "$SITE_OUT/mars_4_3.png" +cp -R "$BUILD_DIR/js-dos" "$SITE_OUT/js-dos" + +# GitHub Pages runs Jekyll by default, which strips paths beginning with a +# dot and can mangle asset directories. .nojekyll turns that off. +touch "$SITE_OUT/.nojekyll" + +log "Site ready in $SITE_OUT ($(du -sh "$SITE_OUT" | cut -f1))" diff --git a/site/index.html b/site/index.html new file mode 100644 index 0000000..03afae1 --- /dev/null +++ b/site/index.html @@ -0,0 +1,101 @@ + + + + + +MARS — a martian landscape in 1550 bytes + + + + + + +
+
+

MARS

+

A martian landscape renderer from 1993, in 1550 bytes.

+
+ +
+
+ + +
+ +

+ Click the canvas to capture your mouse — moving it pans the camera. + Press Esc to release. +

+ +
+

What this is

+

+ In 1993 Tim J. Clarke wrote a real-time martian landscape + renderer that fit in a 5649-byte DOS executable. In 2021 + Wojciech Bruzda disassembled it, rewrote it, and reduced it + to roughly a tenth of that size — the binary running above is + assembled from that annotated source. +

+

+ It draws into VGA mode 13h and reprograms the palette DAC directly, which + is why the emulator here is pinned to machine=vgaonly. +

+
+ + +
+ + + + + + diff --git a/site/style.css b/site/style.css new file mode 100644 index 0000000..a78f118 --- /dev/null +++ b/site/style.css @@ -0,0 +1,97 @@ +/* Dark, low-chrome framing so the 320x200 canvas is the loudest thing here. */ +:root { + --bg: #0d0b0a; + --fg: #e8ddd4; + --muted: #9a8d83; + --accent: #d97742; + --rule: #2a2320; +} + +* { box-sizing: border-box; } + +body { + margin: 0; + padding: 2rem 1.25rem 4rem; + background: var(--bg); + color: var(--fg); + font: 16px/1.65 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; +} + +main { max-width: 46rem; margin: 0 auto; } + +header { text-align: center; margin-bottom: 1.75rem; } + +h1 { + margin: 0; + font-size: clamp(2.5rem, 9vw, 4rem); + letter-spacing: 0.22em; + text-indent: 0.22em; + color: var(--accent); + font-weight: 700; +} + +.tagline { margin: 0.35rem 0 0; color: var(--muted); } + +/* The emulator canvas. 4:3 keeps mode 13h's aspect honest. */ +#player { + position: relative; + aspect-ratio: 4 / 3; + width: 100%; + background: #000; + border: 1px solid var(--rule); + border-radius: 4px; + overflow: hidden; +} + +#dos { width: 100%; height: 100%; } + +.notice { + position: absolute; + inset: auto 0 0; + margin: 0; + padding: 0.75rem 1rem; + background: #3a1410; + color: #ffb4a2; + font-size: 0.9rem; + text-align: center; +} + +.hint { + margin: 0.9rem 0 2.5rem; + color: var(--muted); + font-size: 0.9rem; + text-align: center; +} + +kbd { + padding: 0.1em 0.4em; + border: 1px solid var(--rule); + border-radius: 3px; + background: #191412; + font: 0.85em ui-monospace, SFMono-Regular, Menlo, monospace; +} + +section { border-top: 1px solid var(--rule); padding-top: 1.5rem; } + +h2 { font-size: 1.05rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); } + +code { + padding: 0.1em 0.35em; + border-radius: 3px; + background: #191412; + color: var(--accent); + font: 0.9em ui-monospace, SFMono-Regular, Menlo, monospace; +} + +footer { margin-top: 2.5rem; border-top: 1px solid var(--rule); padding-top: 1.5rem; } + +.links { list-style: none; margin: 0 0 1.25rem; padding: 0; display: grid; gap: 0.5rem; } + +a { color: var(--accent); text-underline-offset: 3px; } +a:hover { color: #f0a070; } + +.legal { margin: 0; color: var(--muted); font-size: 0.82rem; } + +@media (max-width: 480px) { + body { padding: 1.25rem 0.75rem 3rem; } +} diff --git a/tests/test-site.sh b/tests/test-site.sh new file mode 100644 index 0000000..f7a467b --- /dev/null +++ b/tests/test-site.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Verifies the composed site is complete and self-contained. +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" +. "$(dirname "${BASH_SOURCE[0]}")/../scripts/lib.sh" + +assert_success bash "$REPO_ROOT/scripts/build-site.sh" + +assert_file_exists "$SITE_OUT/index.html" +assert_file_exists "$SITE_OUT/style.css" +assert_file_exists "$SITE_OUT/mars.jsdos" +assert_file_exists "$SITE_OUT/MARS.COM" +assert_file_exists "$SITE_OUT/MARS.ASM" +assert_file_exists "$SITE_OUT/js-dos/js-dos.js" +assert_file_exists "$SITE_OUT/js-dos/emulators/wdosbox.wasm" + +# The page must reference its own copies, not a CDN. +assert_contains "$SITE_OUT/index.html" 'js-dos/emulators/' +assert_contains "$SITE_OUT/index.html" 'mars.jsdos' + +# No third-party origins anywhere in the page. +if grep -qE 'https?://(v8\.)?js-dos\.com|cdn\.|unpkg|jsdelivr' "$SITE_OUT/index.html"; then + fail "index.html references a third-party asset origin" +else + pass "no third-party asset origins in index.html" +fi + +# Attribution must survive any future edit of the page. +assert_contains "$SITE_OUT/index.html" "Tim J. Clarke" +assert_contains "$SITE_OUT/index.html" "Bruzda" + +finish_tests From 7031a5f7c8d35d62e288c294c20305a897b2b0e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 16 Aug 2026 20:52:08 +0200 Subject: [PATCH 08/12] build: add preview server, Makefile, and the missing wlibzip assets Browser testing revealed the asset subset was too aggressive: js-dos unpacks the .jsdos bundle (a ZIP) with wlibzip at runtime, so omitting wlibzip.js and wlibzip.wasm made the emulator 404 before DOS could boot. Both are now fetched and asserted by the test. serve.sh builds and serves over HTTP because js-dos loads the bundle with fetch(), which browsers block on file:// origins. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014Cv1JoZyJdqYiCXiVSKDQP --- Makefile | 31 +++++++++++++++++++++++++++++++ scripts/fetch-jsdos.sh | 6 ++++++ scripts/serve.sh | 15 +++++++++++++++ tests/test-fetch-jsdos.sh | 3 +++ 4 files changed, 55 insertions(+) create mode 100644 Makefile create mode 100755 scripts/serve.sh diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..376512d --- /dev/null +++ b/Makefile @@ -0,0 +1,31 @@ +# Thin wrappers over scripts/. Every target is also runnable directly. +.PHONY: all build bundle jsdos site serve test clean help + +all: site + +help: ## List the available targets + @grep -hE '^[a-z-]+:.*##' $(MAKEFILE_LIST) \ + | sed -e 's/:.*##/\t/' \ + | awk -F'\t' '{ printf " \033[1m%-8s\033[0m %s\n", $$1, $$2 }' + +build: ## Assemble MARS.ASM into build/MARS.COM + @bash scripts/build.sh + +bundle: build ## Package build/mars.jsdos for js-dos + @bash scripts/bundle.sh + +jsdos: ## Fetch the pinned js-dos assets + @bash scripts/fetch-jsdos.sh + +site: ## Compose the deployable site into _site/ + @bash scripts/build-site.sh + +serve: ## Build the site and serve it at http://localhost:8080 + @bash scripts/serve.sh + +test: ## Run the test suite + @bash tests/run-tests.sh + +clean: ## Remove build outputs (keeps the cached toolchain) + @rm -rf build _site + @echo "Removed build/ and _site/ (run 'rm -rf .toolchain' to drop the assembler too)" diff --git a/scripts/fetch-jsdos.sh b/scripts/fetch-jsdos.sh index 9e84167..917f175 100755 --- a/scripts/fetch-jsdos.sh +++ b/scripts/fetch-jsdos.sh @@ -29,12 +29,16 @@ log "Fetching js-dos $JSDOS_VERSION from npm" tarball="$work/js-dos-$JSDOS_VERSION.tgz" [ -f "$tarball" ] || die "expected tarball $tarball was not produced" +# wlibzip is not optional: js-dos unpacks the .jsdos bundle (a ZIP) with it +# at runtime, and without it the emulator 404s before DOS ever boots. tar xzf "$tarball" -C "$work" \ package/dist/js-dos.js \ package/dist/js-dos.css \ package/dist/emulators/emulators.js \ package/dist/emulators/wdosbox.js \ package/dist/emulators/wdosbox.wasm \ + package/dist/emulators/wlibzip.js \ + package/dist/emulators/wlibzip.wasm \ || die "js-dos tarball did not contain the expected dist layout" cp "$work/package/dist/js-dos.js" "$dest/js-dos.js" @@ -42,6 +46,8 @@ cp "$work/package/dist/js-dos.css" "$dest/js-dos.css" cp "$work/package/dist/emulators/emulators.js" "$dest/emulators/emulators.js" cp "$work/package/dist/emulators/wdosbox.js" "$dest/emulators/wdosbox.js" cp "$work/package/dist/emulators/wdosbox.wasm" "$dest/emulators/wdosbox.wasm" +cp "$work/package/dist/emulators/wlibzip.js" "$dest/emulators/wlibzip.js" +cp "$work/package/dist/emulators/wlibzip.wasm" "$dest/emulators/wlibzip.wasm" rm -rf "$work" diff --git a/scripts/serve.sh b/scripts/serve.sh new file mode 100755 index 0000000..d768dee --- /dev/null +++ b/scripts/serve.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +# Builds the site and serves it over HTTP. +# +# HTTP rather than file:// is required: js-dos loads mars.jsdos with fetch(), +# and browsers block fetch on file:// origins. +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +port="${PORT:-8080}" + +bash "$REPO_ROOT/scripts/build-site.sh" + +need python3 +log "Serving $SITE_OUT at http://localhost:$port (Ctrl-C to stop)" +cd "$SITE_OUT" +exec python3 -m http.server "$port" diff --git a/tests/test-fetch-jsdos.sh b/tests/test-fetch-jsdos.sh index 93fdf69..7fb5890 100644 --- a/tests/test-fetch-jsdos.sh +++ b/tests/test-fetch-jsdos.sh @@ -10,6 +10,9 @@ assert_file_exists "$BUILD_DIR/js-dos/js-dos.css" assert_file_exists "$BUILD_DIR/js-dos/emulators/emulators.js" assert_file_exists "$BUILD_DIR/js-dos/emulators/wdosbox.js" assert_file_exists "$BUILD_DIR/js-dos/emulators/wdosbox.wasm" +# wlibzip unpacks the .jsdos bundle at runtime; without it nothing boots. +assert_file_exists "$BUILD_DIR/js-dos/emulators/wlibzip.js" +assert_file_exists "$BUILD_DIR/js-dos/emulators/wlibzip.wasm" # The heavy dosbox-x builds must NOT be shipped (~15.5 MB of dead weight). if [ -f "$BUILD_DIR/js-dos/emulators/wdosbox-x.wasm" ]; then From 0fa952851e7983532a1294a163dec3729788b412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 16 Aug 2026 20:52:47 +0200 Subject: [PATCH 09/12] ci: verify the build on every push and deploy the player to Pages Adds a CI workflow that runs build.sh --check plus the test suite on every push and PR, and a Pages workflow that builds the site and deploys it. Documents the build, the browser player, the macOS caveat, and the 33-byte gap between upstream's reported 1517 bytes and this build's 1550. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014Cv1JoZyJdqYiCXiVSKDQP --- .github/workflows/ci.yml | 47 ++++++++++++++++++++++++ .github/workflows/pages.yml | 51 ++++++++++++++++++++++++++ README.md | 71 ++++++++++++++++++++++++++++++++++--- 3 files changed, 164 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/pages.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..27e718c --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: Build MARS.COM + +on: + push: + branches: ["**"] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Cache the JWasm toolchain + uses: actions/cache@v4 + with: + path: .toolchain + key: jwasm-${{ hashFiles('versions.env') }} + + - name: Assemble MARS.COM and verify reproducibility + run: bash scripts/build.sh --check + + - name: Run the test suite + run: bash tests/run-tests.sh + + - name: Report size and checksum + run: | + size=$(wc -c < build/MARS.COM | tr -d ' ') + sha=$(sha256sum build/MARS.COM | cut -d' ' -f1) + { + echo "### MARS.COM" + echo "" + echo "| | |" + echo "|---|---|" + echo "| Size | $size bytes |" + echo "| SHA-256 | \`$sha\` |" + } >> "$GITHUB_STEP_SUMMARY" + + - uses: actions/upload-artifact@v4 + with: + name: MARS.COM + path: build/MARS.COM + if-no-files-found: error diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..1eaff8e --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,51 @@ +name: Deploy Pages + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +# Let a running deploy finish rather than cancelling it mid-flight. +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Cache the JWasm toolchain + uses: actions/cache@v4 + with: + path: .toolchain + key: jwasm-${{ hashFiles('versions.env') }} + + - name: Build the site + run: bash scripts/build-site.sh + + - uses: actions/configure-pages@v5 + + - uses: actions/upload-pages-artifact@v3 + with: + path: _site + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/README.md b/README.md index 712ee16..1c8a23c 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,72 @@ # MARS landscape -A comprehensive study of the outstanding code example from 1993 - the martian landscape renderer by Tim J. Clarke. +A comprehensive study of the outstanding code example from 1993 — the martian +landscape renderer by Tim J. Clarke. -Original code has been disassembled, rewritten and reduced from **5649** bytes to... **1517 bytes**! :) +Original code has been disassembled, rewritten and reduced from **5649** bytes +to about **1.5 kB** by [Wojciech Bruzda](https://github.com/matrix-toolbox). -**Tim, if you read this, please contact me! I was trying to reach you, but I couldn't...** +**Tim, if you read this, please contact +[upstream](https://github.com/matrix-toolbox/MARS.COM)!** -![MARS](https://raw.githubusercontent.com/matrix-toolbox/MARS.COM/main/mars_4_3.png) +![MARS](mars_4_3.png) -It works on DOSBOX and also on genuine x86 machines (mouse is needed). More details can be found [here](https://chaos.if.uj.edu.pl/~wojtek/MARS.COM). +## ▶ Run it in your browser + +**** + +Click the canvas to capture the mouse — moving it pans the camera. + +## What this fork adds + +Upstream ships the annotated assembly only. This fork adds a reproducible +build and a browser player: + +- `scripts/build.sh` — assembles `MARS.ASM` into `MARS.COM` +- `scripts/build-site.sh` — composes the GitHub Pages site +- GitHub Actions workflows that verify the build and deploy the page + +`MARS.ASM` remains the single source of truth; the binary is never committed. + +## Building + +```sh +make build # assemble build/MARS.COM +make serve # build the site and preview at http://localhost:8080 +make test # run the test suite +``` + +`MARS.ASM` is MASM/TASM dialect (`.model tiny`, `org 100h`, `COMMENT #` +blocks), so it needs a MASM-compatible assembler — NASM cannot build it. The +build uses [JWasm](https://github.com/Baron-von-Riedesel/JWasm), pinned in +`versions.env`. If no assembler is on your `PATH`, `build.sh` compiles JWasm +from source into `.toolchain/` automatically. With no C compiler available, +`bash scripts/build.sh --docker` runs the whole build in a container. + +On macOS the JWasm build needs two portability fixes (its makefile targets +Linux/FreeBSD); `build.sh` applies them automatically. + +### Build output + +The pinned toolchain produces a **1550-byte** `MARS.COM` +(`sha256:10a1bb6c…`), byte-identical under JWasm v2.20 and v2.21 and across +macOS/clang/ARM64 and Linux/gcc. Upstream's README reports **1517** bytes; the +33-byte difference comes from the original author's toolchain and has not been +reconciled. `scripts/build.sh --check` enforces the 1550-byte result so +unintended changes to `MARS.ASM` are caught. + +## Running it natively + +It works on DOSBox and on genuine x86 machines (a mouse is needed). Under +DOSBox, always use `-machine vgaonly` — the renderer drives VGA mode 13h and +reprograms the palette DAC, and the default `svga_s3` emulation shows +artifacts. More details are on the +[author's page](https://chaos.if.uj.edu.pl/~wojtek/MARS.COM). + +## Credits and licence + +- Original martian landscape renderer — **Tim J. Clarke**, 1993 +- Disassembly, rewrite and size reduction — **Wojciech Bruzda**, 2021 +- Browser emulation — [js-dos](https://js-dos.com), pinned in `versions.env` + +Released under GPL-3.0, as upstream. From 7b36d2981e1fe41b9b66f506a58148f5b36c40d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 16 Aug 2026 21:12:38 +0200 Subject: [PATCH 10/12] site: replace js-dos with v86 booting a real FreeDOS floppy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit js-dos crashed repeatedly in Firefox with "Backend crashed, cause: ...", which comes from the Emscripten DOSBox module's exception handler in wdosbox.js — the DOSBox core itself was throwing. Switching to em-dosbox or DOSee would not have helped: they wrap the same DOSBox core. v86 emulates real PC hardware instead of emulating the DOS API, so it has entirely different failure modes, and its Pentium-level instruction set covers the 32-bit code MARS needs (MARS.ASM declares .386 and uses 32-bit registers on 35 lines). The page now boots the 720 KB FreeDOS floppy from v86's own demos, with MARS.COM, CuteMouse and an AUTOEXEC.BAT injected at build time via mtools. The mouse driver is required because MARS calls int 33h, a service DOSBox provided internally but hardware emulation does not; MARS checks for the driver and degrades gracefully, so a missing driver costs camera control rather than the whole demo. Both external downloads are checksum-pinned so a rebuild either reproduces the same image or fails loudly. Also adds a favicon, which was the last remaining 404 on the page. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014Cv1JoZyJdqYiCXiVSKDQP --- .github/workflows/ci.yml | 3 ++ .github/workflows/pages.yml | 3 ++ Makefile | 10 ++-- README.md | 19 +++++++- scripts/build-site.sh | 27 ++++++----- scripts/bundle.sh | 26 ---------- scripts/fetch-jsdos.sh | 54 --------------------- scripts/fetch-v86.sh | 53 +++++++++++++++++++++ scripts/make-image.sh | 87 ++++++++++++++++++++++++++++++++++ site/dosbox.conf | 26 ---------- site/favicon.svg | 5 ++ site/index.html | 94 +++++++++++++++++++++++-------------- site/style.css | 29 ++++++++++-- tests/test-bundle.sh | 22 --------- tests/test-fetch-jsdos.sh | 24 ---------- tests/test-image.sh | 27 +++++++++++ tests/test-lib.sh | 2 +- tests/test-site.sh | 13 ++--- tests/test-v86.sh | 17 +++++++ versions.env | 26 ++++++++-- 20 files changed, 348 insertions(+), 219 deletions(-) delete mode 100755 scripts/bundle.sh delete mode 100755 scripts/fetch-jsdos.sh create mode 100755 scripts/fetch-v86.sh create mode 100755 scripts/make-image.sh delete mode 100644 site/dosbox.conf create mode 100644 site/favicon.svg delete mode 100644 tests/test-bundle.sh delete mode 100644 tests/test-fetch-jsdos.sh create mode 100755 tests/test-image.sh create mode 100755 tests/test-v86.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27e718c..146ace2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,9 @@ jobs: path: .toolchain key: jwasm-${{ hashFiles('versions.env') }} + - name: Install mtools (needed to build the FreeDOS floppy image) + run: sudo apt-get update -qq && sudo apt-get install -y -qq mtools + - name: Assemble MARS.COM and verify reproducibility run: bash scripts/build.sh --check diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 1eaff8e..fb5acb0 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -31,6 +31,9 @@ jobs: path: .toolchain key: jwasm-${{ hashFiles('versions.env') }} + - name: Install mtools (needed to build the FreeDOS floppy image) + run: sudo apt-get update -qq && sudo apt-get install -y -qq mtools + - name: Build the site run: bash scripts/build-site.sh diff --git a/Makefile b/Makefile index 376512d..9b720c3 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ # Thin wrappers over scripts/. Every target is also runnable directly. -.PHONY: all build bundle jsdos site serve test clean help +.PHONY: all build image v86 site serve test clean help all: site @@ -11,11 +11,11 @@ help: ## List the available targets build: ## Assemble MARS.ASM into build/MARS.COM @bash scripts/build.sh -bundle: build ## Package build/mars.jsdos for js-dos - @bash scripts/bundle.sh +image: build ## Build the bootable FreeDOS floppy (build/mars.img) + @bash scripts/make-image.sh -jsdos: ## Fetch the pinned js-dos assets - @bash scripts/fetch-jsdos.sh +v86: ## Fetch the pinned v86 runtime and BIOS blobs + @bash scripts/fetch-v86.sh site: ## Compose the deployable site into _site/ @bash scripts/build-site.sh diff --git a/README.md b/README.md index 1c8a23c..f79b929 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,21 @@ from source into `.toolchain/` automatically. With no C compiler available, On macOS the JWasm build needs two portability fixes (its makefile targets Linux/FreeBSD); `build.sh` applies them automatically. +Building the site also needs `mtools` (`brew install mtools` / +`apt-get install mtools`) to write `MARS.COM` into the floppy image. + +### How the browser player works + +The page boots a real emulated PC rather than emulating DOS. `make image` +takes the 720 KB FreeDOS boot floppy used by v86's own demos and injects +`MARS.COM`, the CuteMouse driver, and an `AUTOEXEC.BAT` that loads the driver +and runs the program. Both downloads are checksum-pinned in `versions.env`. + +The mouse driver is not optional: MARS calls `int 33h`, which DOSBox-based +players implement internally but hardware emulation does not. Without a +resident driver the landscape still renders — MARS checks for the driver and +degrades gracefully — but the camera never moves. + ### Build output The pinned toolchain produces a **1550-byte** `MARS.COM` @@ -67,6 +82,8 @@ artifacts. More details are on the - Original martian landscape renderer — **Tim J. Clarke**, 1993 - Disassembly, rewrite and size reduction — **Wojciech Bruzda**, 2021 -- Browser emulation — [js-dos](https://js-dos.com), pinned in `versions.env` +- Browser emulation — [v86](https://github.com/copy/v86) (BSD), booting + [FreeDOS](https://www.freedos.org/) with the + [CuteMouse](https://cutemouse.sourceforge.net/) driver (GPL) Released under GPL-3.0, as upstream. diff --git a/scripts/build-site.sh b/scripts/build-site.sh index 2ee4067..0c69079 100755 --- a/scripts/build-site.sh +++ b/scripts/build-site.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# Composes the deployable site from the built binary, the js-dos bundle, -# the fetched emulator assets, and the static page sources. +# Composes the deployable site from the bootable image, the v86 runtime and +# the static page sources. # # Everything the browser loads is copied into _site/, so the deployed page # makes no third-party requests at view time. @@ -9,23 +9,24 @@ log "Building MARS.COM" bash "$REPO_ROOT/scripts/build.sh" -log "Bundling for js-dos" -bash "$REPO_ROOT/scripts/bundle.sh" +log "Building the bootable floppy image" +bash "$REPO_ROOT/scripts/make-image.sh" -log "Fetching js-dos $JSDOS_VERSION" -bash "$REPO_ROOT/scripts/fetch-jsdos.sh" +log "Fetching v86 $V86_VERSION" +bash "$REPO_ROOT/scripts/fetch-v86.sh" log "Composing $SITE_OUT" rm -rf "$SITE_OUT" mkdir -p "$SITE_OUT" -cp "$REPO_ROOT/site/index.html" "$SITE_OUT/index.html" -cp "$REPO_ROOT/site/style.css" "$SITE_OUT/style.css" -cp "$BUILD_DIR/mars.jsdos" "$SITE_OUT/mars.jsdos" -cp "$BUILD_DIR/MARS.COM" "$SITE_OUT/MARS.COM" -cp "$REPO_ROOT/MARS.ASM" "$SITE_OUT/MARS.ASM" -cp "$REPO_ROOT/mars_4_3.png" "$SITE_OUT/mars_4_3.png" -cp -R "$BUILD_DIR/js-dos" "$SITE_OUT/js-dos" +cp "$REPO_ROOT/site/index.html" "$SITE_OUT/index.html" +cp "$REPO_ROOT/site/style.css" "$SITE_OUT/style.css" +cp "$REPO_ROOT/site/favicon.svg" "$SITE_OUT/favicon.svg" +cp "$BUILD_DIR/mars.img" "$SITE_OUT/mars.img" +cp "$BUILD_DIR/MARS.COM" "$SITE_OUT/MARS.COM" +cp "$REPO_ROOT/MARS.ASM" "$SITE_OUT/MARS.ASM" +cp "$REPO_ROOT/mars_4_3.png" "$SITE_OUT/mars_4_3.png" +cp -R "$BUILD_DIR/v86" "$SITE_OUT/v86" # GitHub Pages runs Jekyll by default, which strips paths beginning with a # dot and can mangle asset directories. .nojekyll turns that off. diff --git a/scripts/bundle.sh b/scripts/bundle.sh deleted file mode 100755 index b38d564..0000000 --- a/scripts/bundle.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash -# Packages MARS.COM and its DOSBox config into a js-dos .jsdos bundle. -# -# A .jsdos bundle is a ZIP whose root becomes drive C: and which carries -# its DOSBox config at .jsdos/dosbox.conf. js-dos does NOT auto-mount, so -# the [autoexec] section in site/dosbox.conf does the mount itself. -. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" - -need zip - -com="$BUILD_DIR/MARS.COM" -[ -f "$com" ] || die "$com not found — run scripts/build.sh first" - -staging="$BUILD_DIR/bundle" -out="$BUILD_DIR/mars.jsdos" - -rm -rf "$staging" "$out" -mkdir -p "$staging/.jsdos" - -cp "$com" "$staging/MARS.COM" -cp "$REPO_ROOT/site/dosbox.conf" "$staging/.jsdos/dosbox.conf" - -# -X strips extra file attributes so the bundle is reproducible. -( cd "$staging" && zip -q -r -X "$out" . ) - -log "Bundled $out ($(wc -c < "$out" | tr -d ' ') bytes)" diff --git a/scripts/fetch-jsdos.sh b/scripts/fetch-jsdos.sh deleted file mode 100755 index 917f175..0000000 --- a/scripts/fetch-jsdos.sh +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env bash -# Downloads a pinned js-dos release and extracts only the assets the page -# needs. This runs at BUILD time, never at page-view time — the deployed -# site serves js-dos from its own origin, so it does not depend on a CDN. -# -# We ship the plain dosbox backend (wdosbox, ~1.5 MB) and deliberately skip -# dosbox-x (~15.5 MB across two builds). MARS only needs machine=vgaonly, -# which plain DOSBox supports. -. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" - -need npm -need tar - -dest="$BUILD_DIR/js-dos" -work="$BUILD_DIR/.jsdos-download" - -if [ -f "$dest/js-dos.js" ] && [ "${FORCE_FETCH:-0}" != "1" ]; then - log "js-dos $JSDOS_VERSION already present in $dest (set FORCE_FETCH=1 to refetch)" - exit 0 -fi - -rm -rf "$work" "$dest" -mkdir -p "$work" "$dest/emulators" - -log "Fetching js-dos $JSDOS_VERSION from npm" -( cd "$work" && npm pack "js-dos@$JSDOS_VERSION" >/dev/null ) \ - || die "npm pack failed for js-dos@$JSDOS_VERSION" - -tarball="$work/js-dos-$JSDOS_VERSION.tgz" -[ -f "$tarball" ] || die "expected tarball $tarball was not produced" - -# wlibzip is not optional: js-dos unpacks the .jsdos bundle (a ZIP) with it -# at runtime, and without it the emulator 404s before DOS ever boots. -tar xzf "$tarball" -C "$work" \ - package/dist/js-dos.js \ - package/dist/js-dos.css \ - package/dist/emulators/emulators.js \ - package/dist/emulators/wdosbox.js \ - package/dist/emulators/wdosbox.wasm \ - package/dist/emulators/wlibzip.js \ - package/dist/emulators/wlibzip.wasm \ - || die "js-dos tarball did not contain the expected dist layout" - -cp "$work/package/dist/js-dos.js" "$dest/js-dos.js" -cp "$work/package/dist/js-dos.css" "$dest/js-dos.css" -cp "$work/package/dist/emulators/emulators.js" "$dest/emulators/emulators.js" -cp "$work/package/dist/emulators/wdosbox.js" "$dest/emulators/wdosbox.js" -cp "$work/package/dist/emulators/wdosbox.wasm" "$dest/emulators/wdosbox.wasm" -cp "$work/package/dist/emulators/wlibzip.js" "$dest/emulators/wlibzip.js" -cp "$work/package/dist/emulators/wlibzip.wasm" "$dest/emulators/wlibzip.wasm" - -rm -rf "$work" - -log "js-dos $JSDOS_VERSION staged in $dest ($(du -sh "$dest" | cut -f1))" diff --git a/scripts/fetch-v86.sh b/scripts/fetch-v86.sh new file mode 100755 index 0000000..b23d7e6 --- /dev/null +++ b/scripts/fetch-v86.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# Downloads a pinned v86 release plus the BIOS blobs it needs. +# +# This runs at BUILD time, never at page-view time — the deployed site serves +# everything from its own origin, so it depends on no CDN once published. +# +# v86's npm package ships only the runtime (libv86.js + v86.wasm); the BIOS +# images live in the GitHub repo, so they are fetched separately. +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +need npm +need tar +need curl + +dest="$BUILD_DIR/v86" +work="$BUILD_DIR/.v86-download" + +if [ -f "$dest/libv86.js" ] && [ -f "$dest/bios/seabios.bin" ] && [ "${FORCE_FETCH:-0}" != "1" ]; then + log "v86 $V86_VERSION already present in $dest (set FORCE_FETCH=1 to refetch)" + exit 0 +fi + +rm -rf "$work" "$dest" +mkdir -p "$work" "$dest/bios" + +log "Fetching v86 $V86_VERSION from npm" +( cd "$work" && npm pack "v86@$V86_VERSION" >/dev/null ) \ + || die "npm pack failed for v86@$V86_VERSION" + +tarball="$work/v86-$V86_VERSION.tgz" +[ -f "$tarball" ] || die "expected tarball $tarball was not produced" + +tar xzf "$tarball" -C "$work" \ + package/build/libv86.js \ + package/build/v86.wasm \ + || die "v86 tarball did not contain the expected build layout" + +cp "$work/package/build/libv86.js" "$dest/libv86.js" +cp "$work/package/build/v86.wasm" "$dest/v86.wasm" + +# SeaBIOS and the VGA BIOS are not in the npm package. +log "Fetching BIOS blobs from $V86_BIOS_REPO@$V86_BIOS_REF" +for blob in seabios.bin vgabios.bin; do + curl -sSfL --max-time 120 \ + "https://raw.githubusercontent.com/$V86_BIOS_REPO/$V86_BIOS_REF/bios/$blob" \ + -o "$dest/bios/$blob" \ + || die "could not fetch bios/$blob" + [ -s "$dest/bios/$blob" ] || die "bios/$blob came back empty" +done + +rm -rf "$work" + +log "v86 $V86_VERSION staged in $dest ($(du -sh "$dest" | cut -f1))" diff --git a/scripts/make-image.sh b/scripts/make-image.sh new file mode 100755 index 0000000..982c485 --- /dev/null +++ b/scripts/make-image.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# Builds the bootable floppy image that v86 boots. +# +# Starts from the FreeDOS 720 KB boot floppy used by v86's own demos, then +# injects three things with mtools: +# +# MARS.COM the program itself +# CTMOUSE.EXE a resident int 33h mouse driver — DOSBox provided this +# service internally, real hardware emulation does not +# AUTOEXEC.BAT loads the driver, then runs MARS +# +# Both downloads are checksum-pinned, so a rebuild years from now either +# produces the same image or fails loudly rather than drifting. +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +need curl +need unzip +command -v mcopy >/dev/null 2>&1 \ + || die "mtools not found — install it with 'brew install mtools' or 'apt-get install mtools'" + +# mtools is strict about geometry on these vintage images; this is expected. +export MTOOLS_SKIP_CHECK=1 + +work="$BUILD_DIR/.image-work" +out="$BUILD_DIR/mars.img" + +com="$BUILD_DIR/MARS.COM" +[ -f "$com" ] || die "$com not found — run scripts/build.sh first" + +rm -rf "$work" +mkdir -p "$work" + +verify_sha() { + local file="$1" expected="$2" label="$3" actual + if command -v shasum >/dev/null 2>&1; then + actual=$(shasum -a 256 "$file" | cut -d' ' -f1) + else + actual=$(sha256sum "$file" | cut -d' ' -f1) + fi + [ "$actual" = "$expected" ] \ + || die "$label checksum mismatch: expected $expected, got $actual" +} + +# --- FreeDOS boot floppy ------------------------------------------------- +log "Fetching the FreeDOS boot floppy" +curl -sSfL --max-time 180 "$FREEDOS_IMG_URL" -o "$work/freedos.img" \ + || die "could not download $FREEDOS_IMG_URL" +verify_sha "$work/freedos.img" "$FREEDOS_IMG_SHA256" "FreeDOS image" + +# --- CuteMouse ----------------------------------------------------------- +log "Fetching the CuteMouse driver" +curl -sSfL --max-time 180 "$CTMOUSE_URL" -o "$work/ctmouse.zip" \ + || die "could not download $CTMOUSE_URL" +verify_sha "$work/ctmouse.zip" "$CTMOUSE_ZIP_SHA256" "CuteMouse archive" + +unzip -o -q "$work/ctmouse.zip" -d "$work/ctmouse" \ + || die "could not unpack the CuteMouse archive" +ctmouse="$work/ctmouse/CTMOUSE.EXE" +[ -f "$ctmouse" ] || die "CTMOUSE.EXE not found in the CuteMouse archive" + +# --- compose the image --------------------------------------------------- +cp "$work/freedos.img" "$out" + +# The stock image ships demo programs that crowd a 720 KB floppy. Drop the +# large ones so there is comfortable room, and so the boot is not cluttered. +for junk in ::/VIM.EXE ::/NASM.EXE ::/DEBUG.COM ::/PRIMES.EXE ::/X86TEST.ASM \ + ::/HELLO.ASM ::/CLOCK.COM ::/CAL.COM ::/FOO ::/README; do + mdel -i "$out" "$junk" >/dev/null 2>&1 || true +done + +cat > "$work/AUTOEXEC.BAT" <<'EOF' +@ECHO OFF +CTMOUSE.EXE +MARS.COM +EOF +# DOS expects CRLF line endings in batch files. +awk 'BEGIN{RS="\n";ORS="\r\n"} {print}' "$work/AUTOEXEC.BAT" > "$work/AUTOEXEC.CRLF" +mv "$work/AUTOEXEC.CRLF" "$work/AUTOEXEC.BAT" + +mcopy -i "$out" -o "$com" ::/MARS.COM || die "could not copy MARS.COM into the image" +mcopy -i "$out" -o "$ctmouse" ::/CTMOUSE.EXE || die "could not copy CTMOUSE.EXE into the image" +mcopy -i "$out" -o "$work/AUTOEXEC.BAT" ::/AUTOEXEC.BAT || die "could not write AUTOEXEC.BAT into the image" + +rm -rf "$work" + +log "Built $out ($(wc -c < "$out" | tr -d ' ') bytes)" +mdir -i "$out" ::/ 2>/dev/null | grep -iE "MARS|CTMOUSE|AUTOEXEC|bytes free" >&2 || true diff --git a/site/dosbox.conf b/site/dosbox.conf deleted file mode 100644 index a184edd..0000000 --- a/site/dosbox.conf +++ /dev/null @@ -1,26 +0,0 @@ -# DOSBox configuration for the MARS landscape renderer. -# -# machine=vgaonly is mandatory, not cosmetic. MARS writes directly to VGA -# mode 13h and reprograms the palette DAC. DOSBox's default svga_s3 -# emulation changes DAC behaviour and produces visible artifacts — the -# original author's notes call for "-machine vgaonly" explicitly. - -[sdl] -autolock=true - -[dosbox] -machine=vgaonly - -[cpu] -core=auto -cputype=auto -cycles=auto - -[render] -aspect=true - -[autoexec] -echo off -mount c . -c: -MARS.COM diff --git a/site/favicon.svg b/site/favicon.svg new file mode 100644 index 0000000..24e44eb --- /dev/null +++ b/site/favicon.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/site/index.html b/site/index.html index 03afae1..b15e06a 100644 --- a/site/index.html +++ b/site/index.html @@ -5,7 +5,7 @@ MARS — a martian landscape in 1550 bytes - + @@ -17,13 +17,16 @@

MARS

-
- - +
+
+ +
+

Booting FreeDOS…

+

- Click the canvas to capture your mouse — moving it pans the camera. + Click the screen to capture your mouse — moving it pans the camera. Press Esc to release.

@@ -37,8 +40,9 @@

What this is

assembled from that annotated source.

- It draws into VGA mode 13h and reprograms the palette DAC directly, which - is why the emulator here is pinned to machine=vgaonly. + It draws into VGA mode 13h, reprograms the palette DAC directly, and needs + 32-bit registers, so it requires a 386 or better. Rather than emulate DOS, + this page boots an actual FreeDOS floppy on an emulated PC.

@@ -51,49 +55,71 @@

What this is

- + diff --git a/site/style.css b/site/style.css index a78f118..29f05fe 100644 --- a/site/style.css +++ b/site/style.css @@ -43,15 +43,36 @@ h1 { overflow: hidden; } -#dos { width: 100%; height: 100%; } +#screen_container { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; +} + +/* v86 renders text modes into a
and graphics into the . */ +#screen_container .v86-text { + white-space: pre; + font: 13px/13px ui-monospace, SFMono-Regular, Menlo, monospace; + color: #b8b0a8; +} + +#screen_container canvas { + max-width: 100%; + max-height: 100%; + image-rendering: pixelated; +} + +#screen_container.is-locked { cursor: none; } .notice { position: absolute; inset: auto 0 0; margin: 0; padding: 0.75rem 1rem; - background: #3a1410; - color: #ffb4a2; + background: #1a1512; + color: #b8b0a8; font-size: 0.9rem; text-align: center; } @@ -95,3 +116,5 @@ a:hover { color: #f0a070; } @media (max-width: 480px) { body { padding: 1.25rem 0.75rem 3rem; } } + +.notice.is-error { background: #3a1410; color: #ffb4a2; } diff --git a/tests/test-bundle.sh b/tests/test-bundle.sh deleted file mode 100644 index 45c9f5a..0000000 --- a/tests/test-bundle.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env bash -# Verifies the .jsdos bundle has the layout js-dos expects. -. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" -. "$(dirname "${BASH_SOURCE[0]}")/../scripts/lib.sh" - -bash "$REPO_ROOT/scripts/build.sh" >/dev/null 2>&1 -assert_success bash "$REPO_ROOT/scripts/bundle.sh" -assert_file_exists "$BUILD_DIR/mars.jsdos" - -listing="$BUILD_DIR/.bundle-listing.txt" -unzip -l "$BUILD_DIR/mars.jsdos" > "$listing" -assert_contains "$listing" "MARS.COM" -assert_contains "$listing" ".jsdos/dosbox.conf" - -conf="$BUILD_DIR/.bundle-conf.txt" -unzip -p "$BUILD_DIR/mars.jsdos" ".jsdos/dosbox.conf" > "$conf" -assert_contains "$conf" "machine=vgaonly" -assert_contains "$conf" "mount c ." -assert_contains "$conf" "MARS.COM" - -rm -f "$listing" "$conf" -finish_tests diff --git a/tests/test-fetch-jsdos.sh b/tests/test-fetch-jsdos.sh deleted file mode 100644 index 7fb5890..0000000 --- a/tests/test-fetch-jsdos.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash -# Verifies we fetch exactly the js-dos assets the page needs, and no more. -. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" -. "$(dirname "${BASH_SOURCE[0]}")/../scripts/lib.sh" - -assert_success bash "$REPO_ROOT/scripts/fetch-jsdos.sh" - -assert_file_exists "$BUILD_DIR/js-dos/js-dos.js" -assert_file_exists "$BUILD_DIR/js-dos/js-dos.css" -assert_file_exists "$BUILD_DIR/js-dos/emulators/emulators.js" -assert_file_exists "$BUILD_DIR/js-dos/emulators/wdosbox.js" -assert_file_exists "$BUILD_DIR/js-dos/emulators/wdosbox.wasm" -# wlibzip unpacks the .jsdos bundle at runtime; without it nothing boots. -assert_file_exists "$BUILD_DIR/js-dos/emulators/wlibzip.js" -assert_file_exists "$BUILD_DIR/js-dos/emulators/wlibzip.wasm" - -# The heavy dosbox-x builds must NOT be shipped (~15.5 MB of dead weight). -if [ -f "$BUILD_DIR/js-dos/emulators/wdosbox-x.wasm" ]; then - fail "wdosbox-x.wasm was shipped but is not needed" -else - pass "dosbox-x builds excluded" -fi - -finish_tests diff --git a/tests/test-image.sh b/tests/test-image.sh new file mode 100755 index 0000000..ef05275 --- /dev/null +++ b/tests/test-image.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Verifies the bootable floppy image contains everything DOS needs. +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" +. "$(dirname "${BASH_SOURCE[0]}")/../scripts/lib.sh" +export MTOOLS_SKIP_CHECK=1 + +bash "$REPO_ROOT/scripts/build.sh" >/dev/null 2>&1 +assert_success bash "$REPO_ROOT/scripts/make-image.sh" +assert_file_exists "$BUILD_DIR/mars.img" + +size=$(wc -c < "$BUILD_DIR/mars.img" | tr -d ' ') +assert_eq "$size" "$FREEDOS_IMG_SIZE" "image keeps the 720 KB floppy geometry" + +listing="$BUILD_DIR/.img-listing.txt" +mdir -i "$BUILD_DIR/mars.img" ::/ > "$listing" 2>/dev/null +assert_contains "$listing" "MARS COM" +assert_contains "$listing" "CTMOUSE EXE" +assert_contains "$listing" "AUTOEXEC BAT" +assert_contains "$listing" "COMMAND COM" + +conf="$BUILD_DIR/.img-autoexec.txt" +mtype -i "$BUILD_DIR/mars.img" ::/AUTOEXEC.BAT > "$conf" 2>/dev/null +assert_contains "$conf" "CTMOUSE.EXE" +assert_contains "$conf" "MARS.COM" + +rm -f "$listing" "$conf" +finish_tests diff --git a/tests/test-lib.sh b/tests/test-lib.sh index 095fb71..3ea6e24 100755 --- a/tests/test-lib.sh +++ b/tests/test-lib.sh @@ -7,7 +7,7 @@ assert_file_exists "$REPO_ROOT/MARS.ASM" # REPO_ROOT points at the repo assert_eq "$BUILD_DIR" "$REPO_ROOT/build" "BUILD_DIR derived from REPO_ROOT" assert_eq "$SITE_OUT" "$REPO_ROOT/_site" "SITE_OUT derived from REPO_ROOT" assert_eq "$JWASM_TAG" "v2.20" "JWasm pin loaded from versions.env" -assert_eq "$JSDOS_VERSION" "8.4.1" "js-dos pin loaded from versions.env" +assert_eq "$V86_VERSION" "0.5.432" "v86 pin loaded from versions.env" assert_eq "$MARS_COM_SIZE" "1550" "expected binary size loaded" # die must exit non-zero and print to stderr diff --git a/tests/test-site.sh b/tests/test-site.sh index f7a467b..5f889db 100644 --- a/tests/test-site.sh +++ b/tests/test-site.sh @@ -7,18 +7,19 @@ assert_success bash "$REPO_ROOT/scripts/build-site.sh" assert_file_exists "$SITE_OUT/index.html" assert_file_exists "$SITE_OUT/style.css" -assert_file_exists "$SITE_OUT/mars.jsdos" +assert_file_exists "$SITE_OUT/mars.img" assert_file_exists "$SITE_OUT/MARS.COM" assert_file_exists "$SITE_OUT/MARS.ASM" -assert_file_exists "$SITE_OUT/js-dos/js-dos.js" -assert_file_exists "$SITE_OUT/js-dos/emulators/wdosbox.wasm" +assert_file_exists "$SITE_OUT/v86/libv86.js" +assert_file_exists "$SITE_OUT/v86/v86.wasm" +assert_file_exists "$SITE_OUT/v86/bios/seabios.bin" # The page must reference its own copies, not a CDN. -assert_contains "$SITE_OUT/index.html" 'js-dos/emulators/' -assert_contains "$SITE_OUT/index.html" 'mars.jsdos' +assert_contains "$SITE_OUT/index.html" 'v86/v86.wasm' +assert_contains "$SITE_OUT/index.html" 'mars.img' # No third-party origins anywhere in the page. -if grep -qE 'https?://(v8\.)?js-dos\.com|cdn\.|unpkg|jsdelivr' "$SITE_OUT/index.html"; then +if grep -qE 'https?://(v8\.)?js-dos\.com|copy\.sh|cdn\.|unpkg|jsdelivr' "$SITE_OUT/index.html"; then fail "index.html references a third-party asset origin" else pass "no third-party asset origins in index.html" diff --git a/tests/test-v86.sh b/tests/test-v86.sh new file mode 100755 index 0000000..4bf7ebc --- /dev/null +++ b/tests/test-v86.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Verifies we fetch exactly the v86 assets the page needs. +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" +. "$(dirname "${BASH_SOURCE[0]}")/../scripts/lib.sh" + +assert_success bash "$REPO_ROOT/scripts/fetch-v86.sh" + +assert_file_exists "$BUILD_DIR/v86/libv86.js" +assert_file_exists "$BUILD_DIR/v86/v86.wasm" +assert_file_exists "$BUILD_DIR/v86/bios/seabios.bin" +assert_file_exists "$BUILD_DIR/v86/bios/vgabios.bin" + +# The BIOS blobs are binaries, not an HTML error page. +sea=$(wc -c < "$BUILD_DIR/v86/bios/seabios.bin" | tr -d ' ') +if [ "$sea" -gt 60000 ]; then pass "seabios.bin looks like a real BIOS ($sea bytes)"; else fail "seabios.bin is only $sea bytes"; fi + +finish_tests diff --git a/versions.env b/versions.env index aff5fed..d564bcb 100644 --- a/versions.env +++ b/versions.env @@ -5,12 +5,30 @@ JWASM_TAG=v2.20 JWASM_REPO=https://github.com/Baron-von-Riedesel/JWasm.git -# js-dos — browser DOS emulator, fetched at site build time -JSDOS_VERSION=8.4.1 - -# Expected build output (JWasm v2.20 and v2.21 agree byte-for-byte) +# Expected build output (JWasm v2.20 and v2.21 agree byte-for-byte, +# as do macOS/clang/ARM64 and Linux/gcc) MARS_COM_SIZE=1550 MARS_COM_SHA256=10a1bb6c319296dd8628e8fd2d705b50fca97b82d6d9a811c91c30c06199d773 # Hard ceiling for a DOS .COM image (0xFF00) MARS_COM_MAX_SIZE=65280 + +# v86 — x86 PC emulator (BSD), fetched from npm at site build time. +# Chosen over DOSBox-based players (js-dos, em-dosbox, DOSee) because those +# all wrap the same DOSBox core, which crashed on this program in Firefox. +# v86 emulates real hardware instead, so it has entirely different failure +# modes — and MARS needs a 386, which v86 provides comfortably. +V86_VERSION=0.5.432 +V86_BIOS_REPO=copy/v86 +V86_BIOS_REF=master + +# FreeDOS boot floppy used by the v86 project's own demos (720 KB). +FREEDOS_IMG_URL=https://i.copy.sh/freedos722.img +FREEDOS_IMG_SHA256=8ecc7604d4c17c16e136d219a92e64747196d9ae044690e90be9ca0468b1ff12 +FREEDOS_IMG_SIZE=737280 + +# CuteMouse — DOS mouse driver (GPL). MARS calls int 33h, which DOSBox +# implemented internally but real hardware emulation does not: without a +# resident driver the landscape still renders, the camera just never moves. +CTMOUSE_URL=https://downloads.sourceforge.net/project/cutemouse/cutemouse%20-%20stable/1.9/ctm19bin.zip +CTMOUSE_ZIP_SHA256=4867c162bafaf25d09322832a0e5e9c0e508812a3deaa969376a7e6628c94bd0 From 032c1c98c6365bea4164a46f49df63a09441ab2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 16 Aug 2026 21:17:43 +0200 Subject: [PATCH 11/12] security: pin v86 BIOS blobs by commit and checksum every download A push-time security review flagged fetch-v86.sh for supply-chain integrity, correctly: it pulled seabios.bin and vgabios.bin from the "master" branch of copy/v86 with no verification at all, while make-image.sh already checksum-pinned both of its downloads. The BIOS blobs are executed by the emulator, and a mutable branch ref can change under us at any time. Pins V86_BIOS_REF to an immutable commit and adds SHA-256 verification for both BIOS blobs and the v86 npm tarball, so every artifact that is fetched and then executed is pinned by content rather than by name. Hoists verify_sha into scripts/lib.sh so the two fetchers share one implementation instead of make-image.sh carrying a private copy. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_014Cv1JoZyJdqYiCXiVSKDQP --- scripts/fetch-v86.sh | 10 ++++++++-- scripts/lib.sh | 18 ++++++++++++++++++ scripts/make-image.sh | 11 ----------- versions.env | 8 +++++++- 4 files changed, 33 insertions(+), 14 deletions(-) diff --git a/scripts/fetch-v86.sh b/scripts/fetch-v86.sh index b23d7e6..6ce1c75 100755 --- a/scripts/fetch-v86.sh +++ b/scripts/fetch-v86.sh @@ -29,6 +29,7 @@ log "Fetching v86 $V86_VERSION from npm" tarball="$work/v86-$V86_VERSION.tgz" [ -f "$tarball" ] || die "expected tarball $tarball was not produced" +verify_sha "$tarball" "$V86_TARBALL_SHA256" "v86 npm tarball" tar xzf "$tarball" -C "$work" \ package/build/libv86.js \ @@ -40,13 +41,18 @@ cp "$work/package/build/v86.wasm" "$dest/v86.wasm" # SeaBIOS and the VGA BIOS are not in the npm package. log "Fetching BIOS blobs from $V86_BIOS_REPO@$V86_BIOS_REF" -for blob in seabios.bin vgabios.bin; do +fetch_bios() { + local blob="$1" expected="$2" curl -sSfL --max-time 120 \ "https://raw.githubusercontent.com/$V86_BIOS_REPO/$V86_BIOS_REF/bios/$blob" \ -o "$dest/bios/$blob" \ || die "could not fetch bios/$blob" [ -s "$dest/bios/$blob" ] || die "bios/$blob came back empty" -done + verify_sha "$dest/bios/$blob" "$expected" "bios/$blob" +} + +fetch_bios seabios.bin "$V86_SEABIOS_SHA256" +fetch_bios vgabios.bin "$V86_VGABIOS_SHA256" rm -rf "$work" diff --git a/scripts/lib.sh b/scripts/lib.sh index 57e28d9..bd82358 100644 --- a/scripts/lib.sh +++ b/scripts/lib.sh @@ -13,3 +13,21 @@ TOOLCHAIN_DIR="$REPO_ROOT/.toolchain" log() { printf '\033[1;34m==>\033[0m %s\n' "$*" >&2; } die() { printf '\033[1;31mError:\033[0m %s\n' "$*" >&2; exit 1; } need() { command -v "$1" >/dev/null 2>&1 || die "required tool not found: $1"; } + +sha256_of() { + if command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | cut -d' ' -f1 + else + sha256sum "$1" | cut -d' ' -f1 + fi +} + +# Every third-party download must pass through this. Anything fetched over +# the network and then executed — a BIOS blob, a DOS driver, a wasm runtime — +# is pinned by content, not just by name or version. +verify_sha() { + local file="$1" expected="$2" label="$3" actual + actual="$(sha256_of "$file")" + [ "$actual" = "$expected" ] \ + || die "$label checksum mismatch: expected $expected, got $actual" +} diff --git a/scripts/make-image.sh b/scripts/make-image.sh index 982c485..e54d0d1 100755 --- a/scripts/make-image.sh +++ b/scripts/make-image.sh @@ -30,17 +30,6 @@ com="$BUILD_DIR/MARS.COM" rm -rf "$work" mkdir -p "$work" -verify_sha() { - local file="$1" expected="$2" label="$3" actual - if command -v shasum >/dev/null 2>&1; then - actual=$(shasum -a 256 "$file" | cut -d' ' -f1) - else - actual=$(sha256sum "$file" | cut -d' ' -f1) - fi - [ "$actual" = "$expected" ] \ - || die "$label checksum mismatch: expected $expected, got $actual" -} - # --- FreeDOS boot floppy ------------------------------------------------- log "Fetching the FreeDOS boot floppy" curl -sSfL --max-time 180 "$FREEDOS_IMG_URL" -o "$work/freedos.img" \ diff --git a/versions.env b/versions.env index d564bcb..99366b2 100644 --- a/versions.env +++ b/versions.env @@ -19,8 +19,14 @@ MARS_COM_MAX_SIZE=65280 # v86 emulates real hardware instead, so it has entirely different failure # modes — and MARS needs a 386, which v86 provides comfortably. V86_VERSION=0.5.432 +V86_TARBALL_SHA256=de9379ee1ccc118903558faed9ff577a66d486c5551b9e5ef359f0d388c40ebb + +# BIOS blobs are pinned to an immutable commit, not a branch: they are +# executed by the emulator, and "master" could change under us at any time. V86_BIOS_REPO=copy/v86 -V86_BIOS_REF=master +V86_BIOS_REF=f3d4472a9c934b9ad78a311f5849ba711a296d23 +V86_SEABIOS_SHA256=73e3f359102e3a9982c35fce98eb7cd08f18303ac7f1ba6ebfbe6cdc1c244d98 +V86_VGABIOS_SHA256=a4bc0d80cc3ca028c73dafa8fee396b8d054ce87ebd8abfbd31b06b437607880 # FreeDOS boot floppy used by the v86 project's own demos (720 KB). FREEDOS_IMG_URL=https://i.copy.sh/freedos722.img From ca932ba0e19925a03dac67c732a30f6cf6c07bfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 16 Aug 2026 21:18:40 +0200 Subject: [PATCH 12/12] Add a reproducible build and a browser-playable page Adds a build that assembles MARS.ASM into MARS.COM, and an optional GitHub Pages site that runs the result in a browser. The build uses JWasm, pinned by tag, because MARS.ASM is MASM/TASM dialect and NASM cannot assemble it. Output is a 1550-byte MARS.COM, byte-identical under JWasm v2.20 and v2.21 and across macOS/clang/ARM64 and Linux/gcc. MARS.ASM stays the single source of truth; the binary is never committed. The page boots a real emulated PC via v86 rather than emulating DOS: the 720 KB FreeDOS floppy from v86's own demos, with MARS.COM, the CuteMouse driver and an AUTOEXEC.BAT injected at build time. The mouse driver is needed because MARS calls int 33h, which hardware emulation does not provide on its own. Every third-party download is checksum-pinned. --- README.md | 14 +- .../plans/2026-08-16-jsdos-pages-build.md | 1358 ----------------- .../2026-08-16-mars-com-jsdos-pages-design.md | 198 --- site/index.html | 4 +- 4 files changed, 9 insertions(+), 1565 deletions(-) delete mode 100644 docs/superpowers/plans/2026-08-16-jsdos-pages-build.md delete mode 100644 docs/superpowers/specs/2026-08-16-mars-com-jsdos-pages-design.md diff --git a/README.md b/README.md index f79b929..05606b4 100644 --- a/README.md +++ b/README.md @@ -4,23 +4,23 @@ A comprehensive study of the outstanding code example from 1993 — the martian landscape renderer by Tim J. Clarke. Original code has been disassembled, rewritten and reduced from **5649** bytes -to about **1.5 kB** by [Wojciech Bruzda](https://github.com/matrix-toolbox). +to about **1.5 kB**! :) -**Tim, if you read this, please contact -[upstream](https://github.com/matrix-toolbox/MARS.COM)!** +**Tim, if you read this, please contact me! I was trying to reach you, but I +couldn't...** ![MARS](mars_4_3.png) ## ▶ Run it in your browser -**** +**** Click the canvas to capture the mouse — moving it pans the camera. -## What this fork adds +## Building and running it here -Upstream ships the annotated assembly only. This fork adds a reproducible -build and a browser player: +Alongside the annotated assembly, this repository can build the binary and +publish a browser-playable page: - `scripts/build.sh` — assembles `MARS.ASM` into `MARS.COM` - `scripts/build-site.sh` — composes the GitHub Pages site diff --git a/docs/superpowers/plans/2026-08-16-jsdos-pages-build.md b/docs/superpowers/plans/2026-08-16-jsdos-pages-build.md deleted file mode 100644 index 09d3d04..0000000 --- a/docs/superpowers/plans/2026-08-16-jsdos-pages-build.md +++ /dev/null @@ -1,1358 +0,0 @@ -# MARS.COM Build Scripts and js-dos Pages Player — Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Assemble `MARS.ASM` into a runnable `MARS.COM` with a reproducible script, and publish a GitHub Pages site that runs that binary in the browser via a self-hosted js-dos. - -**Architecture:** Plain POSIX-ish bash scripts under `scripts/`, each with one responsibility, sharing `scripts/lib.sh`. Version pins live in a single `versions.env`. A bash test harness under `tests/` exercises each script. Two GitHub Actions workflows: one verifies the build on every push, one builds and deploys the site to Pages. Nothing is fetched from a third-party origin at page-view time. - -**Tech Stack:** JWasm v2.20 (MASM-compatible assembler, built from source), js-dos 8.4.1 (fetched from npm at build time), GitHub Actions, GitHub Pages. No package manager, no framework, no runtime dependencies. - -**Spec:** `docs/superpowers/specs/2026-08-16-mars-com-jsdos-pages-design.md` - -## Global Constraints - -- **Assembler:** JWasm, pinned to tag `v2.20`, repo `https://github.com/Baron-von-Riedesel/JWasm.git`. Build with `make -f GccUnix.mak`; binary lands at `build/GccUnixR/jwasm` inside the JWasm checkout. -- **macOS caveat (found during execution, not anticipated by this plan):** `GccUnix.mak` targets Linux/FreeBSD. On Darwin it needs (a) an include shim so `` resolves to ``, injected by overriding `inc_dirs`, and (b) removal of the GNU-only `-s` and `-Wl,-Map` link flags, which are hardcoded in the recipe and must be `sed`-stripped. `scripts/build.sh` applies both automatically when `uname -s` is `Darwin`. Verified: the macOS/clang/ARM64 build produces a `MARS.COM` byte-identical to the Linux/gcc/aarch64 build. -- **js-dos:** pinned to version `8.4.1`, fetched via `npm pack js-dos@8.4.1`. -- **Emulator backend:** `dosbox` (`wdosbox.js` + `wdosbox.wasm`, ~1.5 MB). Do **not** ship `wdosbox-x*` (~15.5 MB combined). -- **Expected build output:** `MARS.COM`, exactly **1550 bytes**, SHA-256 `10a1bb6c319296dd8628e8fd2d705b50fca97b82d6d9a811c91c30c06199d773`. Verified byte-identical under JWasm v2.20 and v2.21. -- **`.COM` size ceiling:** 65280 bytes (0xFF00). CI fails above this. -- **DOSBox machine:** `machine=vgaonly` is mandatory — the renderer uses VGA mode 13h and reprograms the palette DAC; the default `svga_s3` produces artifacts. -- **Bundle layout:** a ZIP named `mars.jsdos` containing `MARS.COM` at the root and `.jsdos/dosbox.conf`. js-dos does **not** auto-mount — the `[autoexec]` section must contain `mount c .` then `c:` then the command. (Confirmed against js-dos's own bundle generator in `emulators.js`.) -- **js-dos asset path:** `pathPrefix` must point at the *emulators* directory and end with a slash (the built-in default is `https://v8.js-dos.com/latest/emulators/`). -- **No third-party runtime requests.** All assets served from the Pages origin. -- **Never commit** `build/`, `_site/`, `.toolchain/`, or `MARS.COM`. -- **Attribution:** original by Tim J. Clarke (1993); disassembly and reduction by Wojciech Bruzda (2021). GPL-3.0 preserved. -- **Shell style:** every script starts `#!/usr/bin/env bash` and sources `scripts/lib.sh`, which sets `set -euo pipefail`. - ---- - -### Task 1: Scaffolding — version pins, shared library, test harness - -**Files:** -- Create: `versions.env` -- Create: `scripts/lib.sh` -- Create: `tests/lib.sh` -- Create: `tests/run-tests.sh` -- Create: `tests/test-lib.sh` -- Create: `.gitignore` - -**Interfaces:** -- Consumes: nothing. -- Produces: `scripts/lib.sh` exporting `REPO_ROOT`, `BUILD_DIR`, `SITE_OUT`, `TOOLCHAIN_DIR` (all absolute paths) and functions `log(msg)`, `die(msg)` (exit 1), `need(tool)` (die if absent); plus every variable from `versions.env`. `tests/lib.sh` exports `assert_eq(actual, expected, label)`, `assert_file_exists(path)`, `assert_contains(haystack_file, needle)`, `assert_success(cmd...)`, and `finish_tests()`. - -- [ ] **Step 1: Write the failing test** - -Create `tests/test-lib.sh`: - -```bash -#!/usr/bin/env bash -# Verifies scripts/lib.sh exposes the contract the other scripts rely on. -. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" -. "$(dirname "${BASH_SOURCE[0]}")/../scripts/lib.sh" - -assert_file_exists "$REPO_ROOT/MARS.ASM" # REPO_ROOT points at the repo -assert_eq "$BUILD_DIR" "$REPO_ROOT/build" "BUILD_DIR derived from REPO_ROOT" -assert_eq "$SITE_OUT" "$REPO_ROOT/_site" "SITE_OUT derived from REPO_ROOT" -assert_eq "$JWASM_TAG" "v2.20" "JWasm pin loaded from versions.env" -assert_eq "$JSDOS_VERSION" "8.4.1" "js-dos pin loaded from versions.env" -assert_eq "$MARS_COM_SIZE" "1550" "expected binary size loaded" - -# die must exit non-zero and print to stderr -if ( die "boom" ) 2>/dev/null; then - fail "die should exit non-zero" -else - pass "die exits non-zero" -fi - -finish_tests -``` - -- [ ] **Step 2: Run it to make sure it fails** - -Run: `bash tests/test-lib.sh` -Expected: FAIL — `tests/lib.sh: No such file or directory`. - -- [ ] **Step 3: Write the test harness library** - -Create `tests/lib.sh`: - -```bash -#!/usr/bin/env bash -# Minimal assertion helpers. No external test framework dependency. -TESTS_RUN=0 -TESTS_FAILED=0 - -pass() { TESTS_RUN=$((TESTS_RUN + 1)); printf ' \033[0;32mok\033[0m %s\n' "$1"; } - -fail() { - TESTS_RUN=$((TESTS_RUN + 1)) - TESTS_FAILED=$((TESTS_FAILED + 1)) - printf ' \033[0;31mFAIL\033[0m %s\n' "$1" -} - -assert_eq() { - local actual="$1" expected="$2" label="$3" - if [ "$actual" = "$expected" ]; then - pass "$label" - else - fail "$label (expected '$expected', got '$actual')" - fi -} - -assert_file_exists() { - if [ -f "$1" ]; then pass "file exists: $1"; else fail "file missing: $1"; fi -} - -assert_contains() { - local file="$1" needle="$2" - if grep -qF -- "$needle" "$file" 2>/dev/null; then - pass "$file contains '$needle'" - else - fail "$file does not contain '$needle'" - fi -} - -assert_success() { - if "$@" >/dev/null 2>&1; then pass "command succeeded: $*"; else fail "command failed: $*"; fi -} - -finish_tests() { - printf '\n %d assertion(s), %d failure(s)\n' "$TESTS_RUN" "$TESTS_FAILED" - [ "$TESTS_FAILED" -eq 0 ] || exit 1 -} -``` - -- [ ] **Step 4: Write the version pins** - -Create `versions.env`: - -```sh -# Single source of truth for pinned versions. -# Sourced by scripts/lib.sh; plain KEY=value so CI can read it too. - -# JWasm — MASM-compatible assembler used to build MARS.COM -JWASM_TAG=v2.20 -JWASM_REPO=https://github.com/Baron-von-Riedesel/JWasm.git - -# js-dos — browser DOS emulator, fetched at site build time -JSDOS_VERSION=8.4.1 - -# Expected build output (JWasm v2.20 and v2.21 agree byte-for-byte) -MARS_COM_SIZE=1550 -MARS_COM_SHA256=10a1bb6c319296dd8628e8fd2d705b50fca97b82d6d9a811c91c30c06199d773 - -# Hard ceiling for a DOS .COM image (0xFF00) -MARS_COM_MAX_SIZE=65280 -``` - -- [ ] **Step 5: Write the shared script library** - -Create `scripts/lib.sh`: - -```bash -#!/usr/bin/env bash -# Shared helpers for every script in this repo. Source it, don't execute it. -set -euo pipefail - -REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -BUILD_DIR="$REPO_ROOT/build" -SITE_OUT="$REPO_ROOT/_site" -TOOLCHAIN_DIR="$REPO_ROOT/.toolchain" - -# shellcheck source=/dev/null -. "$REPO_ROOT/versions.env" - -log() { printf '\033[1;34m==>\033[0m %s\n' "$*" >&2; } -die() { printf '\033[1;31mError:\033[0m %s\n' "$*" >&2; exit 1; } -need() { command -v "$1" >/dev/null 2>&1 || die "required tool not found: $1"; } -``` - -- [ ] **Step 6: Write the test runner** - -Create `tests/run-tests.sh`: - -```bash -#!/usr/bin/env bash -# Runs every tests/test-*.sh and reports a combined result. -set -uo pipefail -cd "$(dirname "${BASH_SOURCE[0]}")" - -failed=0 -for t in test-*.sh; do - printf '\n\033[1m%s\033[0m\n' "$t" - bash "$t" || failed=$((failed + 1)) -done - -printf '\n' -if [ "$failed" -ne 0 ]; then - printf '\033[1;31m%d test file(s) failed\033[0m\n' "$failed" - exit 1 -fi -printf '\033[1;32mAll test files passed\033[0m\n' -``` - -- [ ] **Step 7: Write .gitignore** - -Create `.gitignore`: - -```gitignore -# Build outputs — MARS.ASM is the single source of truth -build/ -_site/ -.toolchain/ -MARS.COM -*.jsdos - -# Fetched dependencies -node_modules/ -*.tgz - -# OS turds -.DS_Store -``` - -- [ ] **Step 8: Run the tests and make sure they pass** - -Run: `chmod +x tests/run-tests.sh tests/test-lib.sh && bash tests/run-tests.sh` -Expected: PASS — 7 assertions, 0 failures. - -(Count check: 1 `assert_file_exists`, 5 `assert_eq`, 1 `die` branch = 7.) - -- [ ] **Step 9: Commit** - -```bash -git add versions.env scripts/lib.sh tests/ .gitignore -git commit -m "build: add version pins, shared script library, and test harness" -``` - ---- - -### Task 2: Build script — assemble MARS.ASM into MARS.COM - -**Files:** -- Create: `scripts/build.sh` -- Create: `tests/test-build.sh` - -**Interfaces:** -- Consumes: `scripts/lib.sh` (`REPO_ROOT`, `BUILD_DIR`, `TOOLCHAIN_DIR`, `JWASM_TAG`, `JWASM_REPO`, `MARS_COM_MAX_SIZE`, `log`, `die`, `need`). -- Produces: `build/MARS.COM`. Accepts flags `--docker` (run the whole build in a `debian:bookworm-slim` container) and `--check` (after assembling, verify size against `MARS_COM_SIZE` and exit non-zero on mismatch). Exposes no functions to later tasks; later tasks depend only on the output path `build/MARS.COM`. - -- [ ] **Step 1: Write the failing test** - -Create `tests/test-build.sh`: - -```bash -#!/usr/bin/env bash -# Verifies the assembler pipeline produces the expected MARS.COM. -# First run is slow: it builds JWasm from source. -. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" -. "$(dirname "${BASH_SOURCE[0]}")/../scripts/lib.sh" - -rm -f "$BUILD_DIR/MARS.COM" -assert_success bash "$REPO_ROOT/scripts/build.sh" -assert_file_exists "$BUILD_DIR/MARS.COM" - -actual_size=$(wc -c < "$BUILD_DIR/MARS.COM" | tr -d ' ') -assert_eq "$actual_size" "$MARS_COM_SIZE" "MARS.COM is the expected size" - -actual_sha=$(shasum -a 256 "$BUILD_DIR/MARS.COM" 2>/dev/null | cut -d' ' -f1 \ - || sha256sum "$BUILD_DIR/MARS.COM" | cut -d' ' -f1) -assert_eq "$actual_sha" "$MARS_COM_SHA256" "MARS.COM is byte-identical to the pinned build" - -# --check must succeed on a good build -assert_success bash "$REPO_ROOT/scripts/build.sh" --check - -finish_tests -``` - -- [ ] **Step 2: Run it to make sure it fails** - -Run: `bash tests/test-build.sh` -Expected: FAIL — `scripts/build.sh` does not exist, so `assert_success` reports a failed command and the file assertions fail. - -- [ ] **Step 3: Write the build script** - -Create `scripts/build.sh`: - -```bash -#!/usr/bin/env bash -# Assembles MARS.ASM into a runnable DOS .COM image. -# -# MARS.ASM is MASM/TASM dialect (.model tiny, org 100h, COMMENT # blocks), -# so it needs a MASM-compatible assembler — NASM cannot build it. We use -# JWasm, pinned in versions.env. Because the model is tiny, the assembler -# emits the .COM memory image directly; there is no link step. -. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" - -usage() { - cat <<'EOF' -Usage: scripts/build.sh [--docker] [--check] - - --docker Run the build inside a container (no host compiler needed). - --check Verify the output matches the pinned size and SHA-256. - -Assembler resolution order: - 1. $JWASM environment variable - 2. jwasm on PATH - 3. build JWasm from source into .toolchain/ (needs gcc, make, git) -EOF -} - -use_docker=0 -do_check=0 -for arg in "$@"; do - case "$arg" in - --docker) use_docker=1 ;; - --check) do_check=1 ;; - -h|--help) usage; exit 0 ;; - *) die "unknown option: $arg (try --help)" ;; - esac -done - -if [ "$use_docker" -eq 1 ]; then - need docker - log "Building inside debian:bookworm-slim" - docker run --rm -v "$REPO_ROOT:/repo" -w /repo debian:bookworm-slim bash -c ' - set -e - apt-get update -qq >/dev/null - apt-get install -y -qq build-essential git ca-certificates >/dev/null 2>&1 - bash scripts/build.sh - ' - exit $? -fi - -# --- resolve an assembler ----------------------------------------------- -resolve_jwasm() { - if [ -n "${JWASM:-}" ]; then - [ -x "$JWASM" ] || die "\$JWASM is set to '$JWASM' but that is not executable" - printf '%s' "$JWASM" - return - fi - - if command -v jwasm >/dev/null 2>&1; then - command -v jwasm - return - fi - - local cached="$TOOLCHAIN_DIR/bin/jwasm" - if [ -x "$cached" ]; then - printf '%s' "$cached" - return - fi - - log "No assembler found; building JWasm $JWASM_TAG from source" >&2 - need git - need make - command -v cc >/dev/null 2>&1 || command -v gcc >/dev/null 2>&1 \ - || die "no C compiler found. Install gcc/clang, set \$JWASM to a jwasm binary, or rerun with --docker" - - local src="$TOOLCHAIN_DIR/JWasm" - rm -rf "$src" - mkdir -p "$TOOLCHAIN_DIR/bin" - git clone -q "$JWASM_REPO" "$src" >&2 - ( cd "$src" && git checkout -q "$JWASM_TAG" && make -f GccUnix.mak >/dev/null 2>&1 ) \ - || die "JWasm build failed. Rerun with --docker, or set \$JWASM to a prebuilt binary." - - local built - built="$(find "$src" -name jwasm -type f -perm -u+x | head -1)" - [ -n "$built" ] || die "JWasm built but no 'jwasm' binary was produced" - cp "$built" "$cached" - printf '%s' "$cached" -} - -JWASM_BIN="$(resolve_jwasm)" -log "Assembler: $JWASM_BIN" - -# --- assemble ------------------------------------------------------------ -mkdir -p "$BUILD_DIR" -out="$BUILD_DIR/MARS.COM" -rm -f "$out" - -# -bin: raw binary output, which for .model tiny is exactly a .COM image. -"$JWASM_BIN" -bin -Fo="$out" "$REPO_ROOT/MARS.ASM" \ - || die "assembly failed" - -[ -s "$out" ] || die "assembler produced an empty $out" - -size=$(wc -c < "$out" | tr -d ' ') -[ "$size" -le "$MARS_COM_MAX_SIZE" ] \ - || die "$out is $size bytes, which exceeds the $MARS_COM_MAX_SIZE byte .COM ceiling" - -if command -v shasum >/dev/null 2>&1; then - sha=$(shasum -a 256 "$out" | cut -d' ' -f1) -else - sha=$(sha256sum "$out" | cut -d' ' -f1) -fi - -log "Built $out — $size bytes, sha256 $sha" - -if [ "$do_check" -eq 1 ]; then - [ "$size" = "$MARS_COM_SIZE" ] \ - || die "size mismatch: expected $MARS_COM_SIZE bytes, got $size. If MARS.ASM changed on purpose, update versions.env." - [ "$sha" = "$MARS_COM_SHA256" ] \ - || die "sha256 mismatch: expected $MARS_COM_SHA256, got $sha. If MARS.ASM changed on purpose, update versions.env." - log "Reproducibility check passed" -fi -``` - -- [ ] **Step 4: Run the tests and make sure they pass** - -Run: `chmod +x scripts/build.sh && bash tests/test-build.sh` -Expected: PASS — 5 assertions, 0 failures. First run takes 1–2 minutes (JWasm compile); subsequent runs are instant because `.toolchain/bin/jwasm` is cached. - -- [ ] **Step 5: Commit** - -```bash -git add scripts/build.sh tests/test-build.sh -git commit -m "build: assemble MARS.ASM into MARS.COM with pinned JWasm" -``` - ---- - -### Task 3: DOSBox config and js-dos bundle - -**Files:** -- Create: `site/dosbox.conf` -- Create: `scripts/bundle.sh` -- Create: `tests/test-bundle.sh` - -**Interfaces:** -- Consumes: `scripts/lib.sh`; `build/MARS.COM` from Task 2. -- Produces: `build/mars.jsdos` — a ZIP with `MARS.COM` at the root and `.jsdos/dosbox.conf` inside. Task 5 copies this file into the site output as `mars.jsdos`. - -- [ ] **Step 1: Write the failing test** - -Create `tests/test-bundle.sh`: - -```bash -#!/usr/bin/env bash -# Verifies the .jsdos bundle has the layout js-dos expects. -. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" -. "$(dirname "${BASH_SOURCE[0]}")/../scripts/lib.sh" - -bash "$REPO_ROOT/scripts/build.sh" >/dev/null 2>&1 -assert_success bash "$REPO_ROOT/scripts/bundle.sh" -assert_file_exists "$BUILD_DIR/mars.jsdos" - -listing="$BUILD_DIR/.bundle-listing.txt" -unzip -l "$BUILD_DIR/mars.jsdos" > "$listing" -assert_contains "$listing" "MARS.COM" -assert_contains "$listing" ".jsdos/dosbox.conf" - -conf="$BUILD_DIR/.bundle-conf.txt" -unzip -p "$BUILD_DIR/mars.jsdos" ".jsdos/dosbox.conf" > "$conf" -assert_contains "$conf" "machine=vgaonly" -assert_contains "$conf" "mount c ." -assert_contains "$conf" "MARS.COM" - -rm -f "$listing" "$conf" -finish_tests -``` - -- [ ] **Step 2: Run it to make sure it fails** - -Run: `bash tests/test-bundle.sh` -Expected: FAIL — `scripts/bundle.sh` does not exist; `build/mars.jsdos` missing. - -- [ ] **Step 3: Write the DOSBox config** - -Create `site/dosbox.conf`: - -```ini -# DOSBox configuration for the MARS landscape renderer. -# -# machine=vgaonly is mandatory, not cosmetic. MARS writes directly to VGA -# mode 13h and reprograms the palette DAC. DOSBox's default svga_s3 -# emulation changes DAC behaviour and produces visible artifacts — the -# original author's notes call for "-machine vgaonly" explicitly. - -[sdl] -autolock=true - -[dosbox] -machine=vgaonly - -[cpu] -core=auto -cputype=auto -cycles=auto - -[render] -aspect=true - -[autoexec] -echo off -mount c . -c: -MARS.COM -``` - -- [ ] **Step 4: Write the bundle script** - -Create `scripts/bundle.sh`: - -```bash -#!/usr/bin/env bash -# Packages MARS.COM and its DOSBox config into a js-dos .jsdos bundle. -# -# A .jsdos bundle is a ZIP whose root becomes drive C: and which carries -# its DOSBox config at .jsdos/dosbox.conf. js-dos does NOT auto-mount, so -# the [autoexec] section in site/dosbox.conf does the mount itself. -. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" - -need zip - -com="$BUILD_DIR/MARS.COM" -[ -f "$com" ] || die "$com not found — run scripts/build.sh first" - -staging="$BUILD_DIR/bundle" -out="$BUILD_DIR/mars.jsdos" - -rm -rf "$staging" "$out" -mkdir -p "$staging/.jsdos" - -cp "$com" "$staging/MARS.COM" -cp "$REPO_ROOT/site/dosbox.conf" "$staging/.jsdos/dosbox.conf" - -# -X strips extra file attributes so the bundle is reproducible. -( cd "$staging" && zip -q -r -X "$out" . ) - -log "Bundled $out ($(wc -c < "$out" | tr -d ' ') bytes)" -``` - -- [ ] **Step 5: Run the tests and make sure they pass** - -Run: `chmod +x scripts/bundle.sh && bash tests/test-bundle.sh` -Expected: PASS — 7 assertions, 0 failures. - -(Count check: 1 `assert_success`, 1 `assert_file_exists`, 5 `assert_contains` = 7.) - -- [ ] **Step 6: Commit** - -```bash -git add site/dosbox.conf scripts/bundle.sh tests/test-bundle.sh -git commit -m "build: package MARS.COM into a js-dos bundle with vgaonly config" -``` - ---- - -### Task 4: Fetch pinned js-dos assets - -**Files:** -- Create: `scripts/fetch-jsdos.sh` -- Create: `tests/test-fetch-jsdos.sh` - -**Interfaces:** -- Consumes: `scripts/lib.sh` (`BUILD_DIR`, `JSDOS_VERSION`). -- Produces: `build/js-dos/` containing `js-dos.js`, `js-dos.css`, and `emulators/{emulators.js,wdosbox.js,wdosbox.wasm}`. Task 5 copies this directory into the site output as `js-dos/`. - -- [ ] **Step 1: Write the failing test** - -Create `tests/test-fetch-jsdos.sh`: - -```bash -#!/usr/bin/env bash -# Verifies we fetch exactly the js-dos assets the page needs, and no more. -. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" -. "$(dirname "${BASH_SOURCE[0]}")/../scripts/lib.sh" - -assert_success bash "$REPO_ROOT/scripts/fetch-jsdos.sh" - -assert_file_exists "$BUILD_DIR/js-dos/js-dos.js" -assert_file_exists "$BUILD_DIR/js-dos/js-dos.css" -assert_file_exists "$BUILD_DIR/js-dos/emulators/emulators.js" -assert_file_exists "$BUILD_DIR/js-dos/emulators/wdosbox.js" -assert_file_exists "$BUILD_DIR/js-dos/emulators/wdosbox.wasm" - -# The heavy dosbox-x builds must NOT be shipped (~15.5 MB of dead weight). -if [ -f "$BUILD_DIR/js-dos/emulators/wdosbox-x.wasm" ]; then - fail "wdosbox-x.wasm was shipped but is not needed" -else - pass "dosbox-x builds excluded" -fi - -finish_tests -``` - -- [ ] **Step 2: Run it to make sure it fails** - -Run: `bash tests/test-fetch-jsdos.sh` -Expected: FAIL — `scripts/fetch-jsdos.sh` does not exist. - -- [ ] **Step 3: Write the fetch script** - -Create `scripts/fetch-jsdos.sh`: - -```bash -#!/usr/bin/env bash -# Downloads a pinned js-dos release and extracts only the assets the page -# needs. This runs at BUILD time, never at page-view time — the deployed -# site serves js-dos from its own origin, so it does not depend on a CDN. -# -# We ship the plain dosbox backend (wdosbox, ~1.5 MB) and deliberately skip -# dosbox-x (~15.5 MB across two builds). MARS only needs machine=vgaonly, -# which plain DOSBox supports. -. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" - -need npm -need tar - -dest="$BUILD_DIR/js-dos" -work="$BUILD_DIR/.jsdos-download" - -if [ -f "$dest/js-dos.js" ] && [ "${FORCE_FETCH:-0}" != "1" ]; then - log "js-dos $JSDOS_VERSION already present in $dest (set FORCE_FETCH=1 to refetch)" - exit 0 -fi - -rm -rf "$work" "$dest" -mkdir -p "$work" "$dest/emulators" - -log "Fetching js-dos $JSDOS_VERSION from npm" -( cd "$work" && npm pack "js-dos@$JSDOS_VERSION" >/dev/null ) \ - || die "npm pack failed for js-dos@$JSDOS_VERSION" - -tarball="$work/js-dos-$JSDOS_VERSION.tgz" -[ -f "$tarball" ] || die "expected tarball $tarball was not produced" - -tar xzf "$tarball" -C "$work" \ - package/dist/js-dos.js \ - package/dist/js-dos.css \ - package/dist/emulators/emulators.js \ - package/dist/emulators/wdosbox.js \ - package/dist/emulators/wdosbox.wasm \ - || die "js-dos tarball did not contain the expected dist layout" - -cp "$work/package/dist/js-dos.js" "$dest/js-dos.js" -cp "$work/package/dist/js-dos.css" "$dest/js-dos.css" -cp "$work/package/dist/emulators/emulators.js" "$dest/emulators/emulators.js" -cp "$work/package/dist/emulators/wdosbox.js" "$dest/emulators/wdosbox.js" -cp "$work/package/dist/emulators/wdosbox.wasm" "$dest/emulators/wdosbox.wasm" - -rm -rf "$work" - -log "js-dos $JSDOS_VERSION staged in $dest ($(du -sh "$dest" | cut -f1))" -``` - -- [ ] **Step 4: Run the tests and make sure they pass** - -Run: `chmod +x scripts/fetch-jsdos.sh && bash tests/test-fetch-jsdos.sh` -Expected: PASS — 7 assertions, 0 failures. - -(Count check: 1 `assert_success`, 5 `assert_file_exists`, 1 dosbox-x branch = 7.) - -- [ ] **Step 5: Commit** - -```bash -git add scripts/fetch-jsdos.sh tests/test-fetch-jsdos.sh -git commit -m "build: fetch pinned js-dos 8.4.1 assets at build time" -``` - ---- - -### Task 5: The page and the site build - -**Files:** -- Create: `site/index.html` -- Create: `site/style.css` -- Create: `scripts/build-site.sh` -- Create: `tests/test-site.sh` - -**Interfaces:** -- Consumes: `scripts/lib.sh`; `build/MARS.COM` (Task 2), `build/mars.jsdos` (Task 3), `build/js-dos/` (Task 4). -- Produces: `_site/` containing `index.html`, `style.css`, `mars.jsdos`, `MARS.COM`, `mars_4_3.png`, `MARS.ASM`, and `js-dos/`. Task 6 serves it; Task 8 deploys it. - -- [ ] **Step 1: Write the failing test** - -Create `tests/test-site.sh`: - -```bash -#!/usr/bin/env bash -# Verifies the composed site is complete and self-contained. -. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" -. "$(dirname "${BASH_SOURCE[0]}")/../scripts/lib.sh" - -assert_success bash "$REPO_ROOT/scripts/build-site.sh" - -assert_file_exists "$SITE_OUT/index.html" -assert_file_exists "$SITE_OUT/style.css" -assert_file_exists "$SITE_OUT/mars.jsdos" -assert_file_exists "$SITE_OUT/MARS.COM" -assert_file_exists "$SITE_OUT/MARS.ASM" -assert_file_exists "$SITE_OUT/js-dos/js-dos.js" -assert_file_exists "$SITE_OUT/js-dos/emulators/wdosbox.wasm" - -# The page must reference its own copies, not a CDN. -assert_contains "$SITE_OUT/index.html" 'js-dos/emulators/' -assert_contains "$SITE_OUT/index.html" 'mars.jsdos' - -# No third-party origins anywhere in the page. -if grep -qE 'https?://(v8\.)?js-dos\.com|cdn\.|unpkg|jsdelivr' "$SITE_OUT/index.html"; then - fail "index.html references a third-party asset origin" -else - pass "no third-party asset origins in index.html" -fi - -# Attribution must survive any future edit of the page. -assert_contains "$SITE_OUT/index.html" "Tim J. Clarke" -assert_contains "$SITE_OUT/index.html" "Bruzda" - -finish_tests -``` - -Note: the third-party check greps for asset origins, so `index.html` must not -contain a scheme-qualified `js-dos.com` URL. The page as written credits js-dos -in plain text and points every anchor at github.com, so it passes. If you later -want a clickable js-dos link on the page, relax this assertion deliberately -rather than working around it. - -- [ ] **Step 2: Run it to make sure it fails** - -Run: `bash tests/test-site.sh` -Expected: FAIL — `scripts/build-site.sh` does not exist. - -- [ ] **Step 3: Write the page** - -Create `site/index.html`: - -```html - - - - - -MARS — a martian landscape in 1550 bytes - - - - - - -
-
-

MARS

-

A martian landscape renderer from 1993, in 1550 bytes.

-
- -
-
- - -
- -

- Click the canvas to capture your mouse — moving it pans the camera. - Press Esc to release. -

- -
-

What this is

-

- In 1993 Tim J. Clarke wrote a real-time martian landscape - renderer that fit in a 5649-byte DOS executable. In 2021 - Wojciech Bruzda disassembled it, rewrote it, and reduced it - to roughly a tenth of that size — the binary running above is - assembled from that annotated source. -

-

- It draws into VGA mode 13h and reprograms the palette DAC directly, which - is why the emulator here is pinned to machine=vgaonly. -

-
- - -
- - - - - - -``` - -- [ ] **Step 4: Write the stylesheet** - -Create `site/style.css`: - -```css -/* Dark, low-chrome framing so the 320x200 canvas is the loudest thing here. */ -:root { - --bg: #0d0b0a; - --fg: #e8ddd4; - --muted: #9a8d83; - --accent: #d97742; - --rule: #2a2320; -} - -* { box-sizing: border-box; } - -body { - margin: 0; - padding: 2rem 1.25rem 4rem; - background: var(--bg); - color: var(--fg); - font: 16px/1.65 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; -} - -main { max-width: 46rem; margin: 0 auto; } - -header { text-align: center; margin-bottom: 1.75rem; } - -h1 { - margin: 0; - font-size: clamp(2.5rem, 9vw, 4rem); - letter-spacing: 0.22em; - text-indent: 0.22em; - color: var(--accent); - font-weight: 700; -} - -.tagline { margin: 0.35rem 0 0; color: var(--muted); } - -/* The emulator canvas. 4:3 keeps mode 13h's aspect honest. */ -#player { - position: relative; - aspect-ratio: 4 / 3; - width: 100%; - background: #000; - border: 1px solid var(--rule); - border-radius: 4px; - overflow: hidden; -} - -#dos { width: 100%; height: 100%; } - -.notice { - position: absolute; - inset: auto 0 0; - margin: 0; - padding: 0.75rem 1rem; - background: #3a1410; - color: #ffb4a2; - font-size: 0.9rem; - text-align: center; -} - -.hint { - margin: 0.9rem 0 2.5rem; - color: var(--muted); - font-size: 0.9rem; - text-align: center; -} - -kbd { - padding: 0.1em 0.4em; - border: 1px solid var(--rule); - border-radius: 3px; - background: #191412; - font: 0.85em ui-monospace, SFMono-Regular, Menlo, monospace; -} - -section { border-top: 1px solid var(--rule); padding-top: 1.5rem; } - -h2 { font-size: 1.05rem; letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); } - -code { - padding: 0.1em 0.35em; - border-radius: 3px; - background: #191412; - color: var(--accent); - font: 0.9em ui-monospace, SFMono-Regular, Menlo, monospace; -} - -footer { margin-top: 2.5rem; border-top: 1px solid var(--rule); padding-top: 1.5rem; } - -.links { list-style: none; margin: 0 0 1.25rem; padding: 0; display: grid; gap: 0.5rem; } - -a { color: var(--accent); text-underline-offset: 3px; } -a:hover { color: #f0a070; } - -.legal { margin: 0; color: var(--muted); font-size: 0.82rem; } - -@media (max-width: 480px) { - body { padding: 1.25rem 0.75rem 3rem; } -} -``` - -- [ ] **Step 5: Write the site build script** - -Create `scripts/build-site.sh`: - -```bash -#!/usr/bin/env bash -# Composes the deployable site from the built binary, the js-dos bundle, -# the fetched emulator assets, and the static page sources. -# -# Everything the browser loads is copied into _site/, so the deployed page -# makes no third-party requests at view time. -. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" - -log "Building MARS.COM" -bash "$REPO_ROOT/scripts/build.sh" - -log "Bundling for js-dos" -bash "$REPO_ROOT/scripts/bundle.sh" - -log "Fetching js-dos $JSDOS_VERSION" -bash "$REPO_ROOT/scripts/fetch-jsdos.sh" - -log "Composing $SITE_OUT" -rm -rf "$SITE_OUT" -mkdir -p "$SITE_OUT" - -cp "$REPO_ROOT/site/index.html" "$SITE_OUT/index.html" -cp "$REPO_ROOT/site/style.css" "$SITE_OUT/style.css" -cp "$BUILD_DIR/mars.jsdos" "$SITE_OUT/mars.jsdos" -cp "$BUILD_DIR/MARS.COM" "$SITE_OUT/MARS.COM" -cp "$REPO_ROOT/MARS.ASM" "$SITE_OUT/MARS.ASM" -cp "$REPO_ROOT/mars_4_3.png" "$SITE_OUT/mars_4_3.png" -cp -R "$BUILD_DIR/js-dos" "$SITE_OUT/js-dos" - -# GitHub Pages runs Jekyll by default, which strips paths beginning with a -# dot and can mangle asset directories. .nojekyll turns that off. -touch "$SITE_OUT/.nojekyll" - -log "Site ready in $SITE_OUT ($(du -sh "$SITE_OUT" | cut -f1))" -``` - -- [ ] **Step 6: Run the tests and make sure they pass** - -Run: `chmod +x scripts/build-site.sh && bash tests/test-site.sh` -Expected: PASS — 13 assertions, 0 failures. - -(Count check: 1 `assert_success`, 7 `assert_file_exists`, 4 `assert_contains`, -1 third-party branch = 13.) - -- [ ] **Step 7: Commit** - -```bash -git add site/index.html site/style.css scripts/build-site.sh tests/test-site.sh -git commit -m "site: add js-dos player page and site composition script" -``` - ---- - -### Task 6: Local preview server and Makefile - -**Files:** -- Create: `scripts/serve.sh` -- Create: `Makefile` - -**Interfaces:** -- Consumes: all scripts from Tasks 2–5. -- Produces: `make build|bundle|site|serve|test|clean` targets. Nothing downstream depends on these. - -- [ ] **Step 1: Write the preview server** - -Create `scripts/serve.sh`: - -```bash -#!/usr/bin/env bash -# Builds the site and serves it over HTTP. -# -# HTTP rather than file:// is required: js-dos loads mars.jsdos with fetch(), -# and browsers block fetch on file:// origins. -. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" - -port="${PORT:-8080}" - -bash "$REPO_ROOT/scripts/build-site.sh" - -need python3 -log "Serving $SITE_OUT at http://localhost:$port (Ctrl-C to stop)" -cd "$SITE_OUT" -exec python3 -m http.server "$port" -``` - -- [ ] **Step 2: Write the Makefile** - -Create `Makefile`: - -```makefile -# Thin wrappers over scripts/. Every target is also runnable directly. -.PHONY: all build bundle jsdos site serve test clean - -all: site - -build: ## Assemble MARS.ASM into build/MARS.COM - @bash scripts/build.sh - -bundle: build ## Package build/mars.jsdos for js-dos - @bash scripts/bundle.sh - -jsdos: ## Fetch the pinned js-dos assets - @bash scripts/fetch-jsdos.sh - -site: ## Compose the deployable site into _site/ - @bash scripts/build-site.sh - -serve: ## Build the site and serve it at http://localhost:8080 - @bash scripts/serve.sh - -test: ## Run the test suite - @bash tests/run-tests.sh - -clean: ## Remove build outputs (keeps the cached toolchain) - @rm -rf build _site - @echo "Removed build/ and _site/ (run 'rm -rf .toolchain' to drop the assembler too)" -``` - -- [ ] **Step 3: Verify the targets work** - -Run: `make clean && make site && bash tests/run-tests.sh` -Expected: `_site/` rebuilt; all test files pass. - -- [ ] **Step 4: Verify the demo actually renders** - -Run: `make serve`, then open in a browser. - -Confirm all four, and do not proceed until they hold: -1. The landscape draws (orange/brown terrain against a graded sky). -2. Colours look correct — a washed-out or wrongly-shaded palette means - `machine=vgaonly` is not being applied; check the conf inside the bundle. -3. Moving the mouse after clicking the canvas pans the camera. -4. The browser network panel shows **no** requests to any origin other than - `localhost`. - -If js-dos reports a missing emulator asset, `pathPrefix` is wrong: it must be -`js-dos/emulators/`, with the trailing slash. - -- [ ] **Step 5: Commit** - -```bash -git add scripts/serve.sh Makefile -git commit -m "build: add local preview server and Makefile targets" -``` - ---- - -### Task 7: CI workflow - -**Files:** -- Create: `.github/workflows/ci.yml` - -**Interfaces:** -- Consumes: `scripts/build.sh --check`, `tests/run-tests.sh`. -- Produces: a `MARS.COM` workflow artifact on every push and pull request. - -- [ ] **Step 1: Write the workflow** - -Create `.github/workflows/ci.yml`: - -```yaml -name: Build MARS.COM - -on: - push: - branches: ["**"] - pull_request: - workflow_dispatch: - -permissions: - contents: read - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Cache the JWasm toolchain - uses: actions/cache@v4 - with: - path: .toolchain - key: jwasm-${{ hashFiles('versions.env') }} - - - name: Assemble MARS.COM and verify reproducibility - run: bash scripts/build.sh --check - - - name: Run the test suite - run: bash tests/run-tests.sh - - - name: Report size and checksum - run: | - size=$(wc -c < build/MARS.COM | tr -d ' ') - sha=$(sha256sum build/MARS.COM | cut -d' ' -f1) - { - echo "### MARS.COM" - echo "" - echo "| | |" - echo "|---|---|" - echo "| Size | $size bytes |" - echo "| SHA-256 | \`$sha\` |" - } >> "$GITHUB_STEP_SUMMARY" - - - uses: actions/upload-artifact@v4 - with: - name: MARS.COM - path: build/MARS.COM - if-no-files-found: error -``` - -- [ ] **Step 2: Commit and push, then verify the run is green** - -```bash -git add .github/workflows/ci.yml -git commit -m "ci: verify MARS.COM builds reproducibly on every push" -git push -u origin feat/jsdos-pages-build -``` - -Then: `gh run watch` (or `gh run list --limit 1`). -Expected: the workflow completes successfully and the summary shows -1550 bytes with the pinned SHA-256. - -If the run does not appear at all, Actions is disabled on the fork — enable it -with `gh api repos/dtz-labs/MARS.COM/actions/permissions -X PUT --input - <<< '{"enabled":true,"allowed_actions":"all"}'`. - ---- - -### Task 8: Pages workflow and repository configuration - -**Files:** -- Create: `.github/workflows/pages.yml` - -**Interfaces:** -- Consumes: `scripts/build-site.sh`. -- Produces: a deployed GitHub Pages site at `https://dtz-labs.github.io/MARS.COM/`. - -- [ ] **Step 1: Write the workflow** - -Create `.github/workflows/pages.yml`: - -```yaml -name: Deploy Pages - -on: - push: - branches: [main] - workflow_dispatch: - -permissions: - contents: read - pages: write - id-token: write - -# Let a running deploy finish rather than cancelling it mid-flight. -concurrency: - group: pages - cancel-in-progress: false - -jobs: - build: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-node@v4 - with: - node-version: "20" - - - name: Cache the JWasm toolchain - uses: actions/cache@v4 - with: - path: .toolchain - key: jwasm-${{ hashFiles('versions.env') }} - - - name: Build the site - run: bash scripts/build-site.sh - - - uses: actions/configure-pages@v5 - - - uses: actions/upload-pages-artifact@v3 - with: - path: _site - - deploy: - needs: build - runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - steps: - - id: deployment - uses: actions/deploy-pages@v4 -``` - -- [ ] **Step 2: Point Pages at GitHub Actions** - -Forks do not inherit Pages settings, so set the source explicitly: - -```bash -gh api repos/dtz-labs/MARS.COM/pages -X POST --input - <<< '{"build_type":"workflow"}' \ - || gh api repos/dtz-labs/MARS.COM/pages -X PUT --input - <<< '{"build_type":"workflow"}' -gh api repos/dtz-labs/MARS.COM/pages --jq '{status,html_url,build_type}' -``` - -Expected: `build_type` is `workflow`. - -- [ ] **Step 3: Commit** - -```bash -git add .github/workflows/pages.yml -git commit -m "ci: build and deploy the js-dos player to GitHub Pages" -``` - ---- - -### Task 9: README - -**Files:** -- Modify: `README.md` - -**Interfaces:** -- Consumes: nothing. Terminal task. -- Produces: nothing. - -- [ ] **Step 1: Rewrite the README** - -Replace `README.md` with the content below. It keeps upstream's text and -credits intact and adds only what this fork contributes. Note the byte-count -paragraph: upstream claims 1517 bytes, our reproducible build is 1550 — the -discrepancy is stated rather than papered over. - -```markdown -# MARS landscape - -A comprehensive study of the outstanding code example from 1993 — the martian -landscape renderer by Tim J. Clarke. - -Original code has been disassembled, rewritten and reduced from **5649** bytes -to about **1.5 kB** by [Wojciech Bruzda](https://github.com/matrix-toolbox). - -**Tim, if you read this, please contact -[upstream](https://github.com/matrix-toolbox/MARS.COM)!** - -![MARS](mars_4_3.png) - -## ▶ Run it in your browser - -**** - -Click the canvas to capture the mouse — moving it pans the camera. - -## What this fork adds - -Upstream ships the annotated assembly only. This fork adds a reproducible -build and a browser player: - -- `scripts/build.sh` — assembles `MARS.ASM` into `MARS.COM` -- `scripts/build-site.sh` — composes the GitHub Pages site -- GitHub Actions workflows that verify the build and deploy the page - -`MARS.ASM` remains the single source of truth; the binary is never committed. - -## Building - -```sh -make build # assemble build/MARS.COM -make serve # build the site and preview at http://localhost:8080 -make test # run the test suite -``` - -`MARS.ASM` is MASM/TASM dialect (`.model tiny`, `org 100h`, `COMMENT #` -blocks), so it needs a MASM-compatible assembler — NASM cannot build it. The -build uses [JWasm](https://github.com/Baron-von-Riedesel/JWasm), pinned in -`versions.env`. If no assembler is on your `PATH`, `build.sh` compiles JWasm -from source into `.toolchain/` automatically. With no C compiler available, -`bash scripts/build.sh --docker` runs the whole build in a container. - -### Build output - -The pinned toolchain produces a **1550-byte** `MARS.COM` -(`sha256:10a1bb6c…`), byte-identical under JWasm v2.20 and v2.21. Upstream's -README reports **1517** bytes; the 33-byte difference comes from the original -author's toolchain and has not been reconciled. `scripts/build.sh --check` -enforces the 1550-byte result so unintended changes to `MARS.ASM` are caught. - -## Running it natively - -It works on DOSBox and on genuine x86 machines (a mouse is needed). Under -DOSBox, always use `-machine vgaonly` — the renderer drives VGA mode 13h and -reprograms the palette DAC, and the default `svga_s3` emulation shows -artifacts. More details are on the -[author's page](https://chaos.if.uj.edu.pl/~wojtek/MARS.COM). - -## Credits and licence - -- Original martian landscape renderer — **Tim J. Clarke**, 1993 -- Disassembly, rewrite and size reduction — **Wojciech Bruzda**, 2021 -- Browser emulation — [js-dos](https://js-dos.com), pinned in `versions.env` - -Released under GPL-3.0, as upstream. -``` - -- [ ] **Step 2: Verify links resolve** - -Run: `grep -oE 'https?://[^)]+' README.md` -Check each host is reachable and that the Pages URL matches the one reported -by `gh api repos/dtz-labs/MARS.COM/pages --jq .html_url`. - -- [ ] **Step 3: Commit** - -```bash -git add README.md -git commit -m "docs: document the build, the player, and the byte-count discrepancy" -``` - ---- - -## Final verification - -After all tasks, before merging to `main`: - -- [ ] `make clean && rm -rf .toolchain && make test` passes from a cold start -- [ ] `make serve` renders the demo correctly (terrain, palette, mouse) -- [ ] `gh run list --limit 3` shows CI green -- [ ] Merge to `main`, then confirm the Pages deploy succeeds and - loads with no third-party requests diff --git a/docs/superpowers/specs/2026-08-16-mars-com-jsdos-pages-design.md b/docs/superpowers/specs/2026-08-16-mars-com-jsdos-pages-design.md deleted file mode 100644 index 3e73053..0000000 --- a/docs/superpowers/specs/2026-08-16-mars-com-jsdos-pages-design.md +++ /dev/null @@ -1,198 +0,0 @@ -# MARS.COM — build scripts and js-dos GitHub Pages player - -**Date:** 2026-08-16 -**Repo:** `dtz-labs/MARS.COM` (fork of `matrix-toolbox/MARS.COM`, GPL-3.0) -**Status:** approved design - -## Purpose - -The upstream repository ships the MARS landscape renderer as assembly source -only. There is no binary, no build instructions, and no way to see the demo -without a DOS machine or a local DOSBox install. - -This project adds two things: - -1. A reproducible build that turns `MARS.ASM` into a runnable `MARS.COM`. -2. A GitHub Pages site that runs that binary in the browser via js-dos, so the - demo can be seen by clicking a link. - -## Background - -`MARS.ASM` is a 778-line MASM/TASM-dialect source: `.model tiny`, `.code`, -`.386`, `org 100h`, `end start`. Two lines that look like corruption (bare `#` -at lines 615 and 777) are MASM `COMMENT #` block delimiters around a disabled -jump-table dispatch. The dialect is not NASM-compatible and cannot be ported -without a rewrite. - -Because the model is tiny, the assembler emits a `.COM` memory image directly. -No linker or `exe2bin` step is required. - -The source comments record two runtime requirements: DOSBox must run with -`-machine vgaonly`, and the demo needs a mouse (it drives the camera). The -`vgaonly` requirement is about rendering correctness, not preference — the -renderer writes to VGA mode 13h and reprograms the palette DAC, and DOSBox's -default `svga_s3` emulation produces visual artifacts. - -## Decisions - -| Decision | Choice | Rationale | -|---|---|---| -| Repo relationship | True GitHub fork | Preserves upstream attribution link; allows upstream PRs | -| Assembler | JWasm, pinned version | MASM-compatible, open source, builds from source with gcc | -| Built binary | CI-only, never committed | `MARS.ASM` stays the single source of truth | -| js-dos delivery | Downloaded at Pages build time, pinned version | Self-hosted at runtime, lean repo, reproducible | -| js-dos version | 8.4.1 | Current release; verified self-hostable | -| Emulator backend | `dosbox` (`wdosbox`) | 1.5 MB vs 7.9 MB for dosbox-x; `vgaonly` is supported | -| Page scope | Framed player plus context | Credits, usage hint, size story, source and download links | - -## Verified findings - -These were established by direct investigation, not assumption: - -- JWasm 2.21 assembles `MARS.ASM` cleanly: 778 lines, 3 passes, 0 warnings, - 0 errors, producing a **1550-byte** `MARS.COM`. The upstream README claims - 1517 bytes; the discrepancy is documented rather than hidden. -- js-dos 8.4.1's `dist/` contains **no** references to `SharedArrayBuffer` or - `crossOriginIsolated`. It therefore does not require COOP/COEP headers, which - GitHub Pages cannot set. Self-hosting on Pages is viable. -- `js-dos.js` contains no hardcoded absolute base path. The CDN URL appears only - as a default for the `pathPrefix` option, which we override. -- The js-dos npm tarball (14 MB) contains `dist/js-dos.js`, `dist/js-dos.css`, - and `dist/emulators/*`. Only a subset is needed for this deployment. - -## Architecture - -### Components - -**`versions.env`** — the single source of truth for every pin: JWasm tag, js-dos -version, the expected `MARS.COM` size and SHA-256, and the `.COM` size ceiling. -Plain `KEY=value` so both shell scripts and CI can read it. - -**`scripts/lib.sh`** — sourced by every script. Sets `set -euo pipefail`, derives -the repo paths, loads `versions.env`, and provides `log`, `die`, and `need`. - -**`tests/`** — a dependency-free bash test harness (`tests/lib.sh` assertions, -`tests/run-tests.sh` runner) with one test file per script. - -**`scripts/build.sh`** — assembles `MARS.ASM` into `MARS.COM`. - -Resolves a toolchain in order: `$JWASM` environment variable, then `jwasm` on -`PATH`, then builds JWasm from a pinned upstream tag into `.toolchain/`. A -`--docker` flag runs the whole build in a container for hosts with no compiler. -The same script runs locally and in CI, so the two cannot drift. - -Output: `build/MARS.COM`. - -**`scripts/bundle.sh`** — produces the js-dos bundle. - -Creates `build/mars.jsdos`, a ZIP containing `MARS.COM` and `.jsdos/dosbox.conf`. -The config pins `machine=vgaonly`, enables mouse autolock, and autoexecs the -program. - -**`scripts/fetch-jsdos.sh`** — downloads the pinned js-dos release. - -Fetches the js-dos npm tarball at the pinned version, extracts only the files -the page needs (`js-dos.js`, `js-dos.css`, `emulators/emulators.js`, -`emulators/wdosbox.js`, `emulators/wdosbox.wasm`), and places them under the -site output. Never runs at page-view time. - -**`scripts/build-site.sh`** — assembles the deployable site. - -Composes `site/` sources, the built `MARS.COM`, the `.jsdos` bundle, and the -fetched js-dos assets into `_site/`. - -**`scripts/serve.sh`** — local preview. - -Builds the site and serves `_site/` over HTTP. HTTP rather than `file://` is -required because bundle loading uses `fetch`. - -**`Makefile`** — thin targets over the scripts: `build`, `bundle`, `site`, -`serve`, `clean`. - -### Site - -Static HTML and CSS, no framework, no build step beyond file composition. - -`site/index.html` frames the js-dos canvas with: - -- Credit to Tim J. Clarke (original, 1993) and Wojciech Bruzda (disassembly and - reduction, 2021) -- A usage hint: click to capture the mouse; the mouse drives the camera -- The size story: 5649 bytes reduced to roughly 1.5 KB -- Links to `MARS.ASM`, the `MARS.COM` download, and the upstream repository - -js-dos is initialised with `pathPrefix` pointing at the self-hosted emulator -directory, `backend: "dosbox"`, and `url` pointing at `mars.jsdos`. - -### Workflows - -**`.github/workflows/ci.yml`** — on push and pull request. - -Runs `scripts/build.sh --check`, which asserts the output is non-empty, within -the `.COM` ceiling, and byte-identical to the pinned size and SHA-256. Also -records size and SHA-256 in the job summary. Uploads `MARS.COM` as a workflow -artifact. - -**`.github/workflows/pages.yml`** — on push to `main` and manual dispatch. - -Builds the binary, fetches pinned js-dos, assembles the site, uploads a Pages -artifact, and deploys. Requires `pages: write` and `id-token: write`. - -### Repository configuration - -Two settings are applied via the GitHub API, since forks do not inherit them: - -- GitHub Actions enabled on the fork -- Pages source set to `github-actions` - -## Data flow - -``` -MARS.ASM ──[JWasm -bin]──> build/MARS.COM ──┐ - ├──[zip]──> build/mars.jsdos ──┐ -.jsdos/dosbox.conf ─────────────────────────┘ │ - ├──> _site/ ──> Pages -site/index.html, site/style.css ───────────────────────────────────────────┤ - │ -js-dos 8.4.1 npm tarball ──[fetch, extract subset]──> _site/js-dos/ ───────┘ -``` - -At page view time nothing is fetched from outside the Pages origin. - -## Error handling - -- **Assembler unavailable.** `build.sh` falls back through its toolchain chain - and fails with an explicit message naming the options, rather than a bare - `command not found`. -- **Assembly failure.** JWasm's non-zero exit fails the build; its diagnostics - are surfaced verbatim. CI does not deploy a stale binary. -- **Size regression.** `build.sh` fails if the output is empty or exceeds - **65280 bytes** (0xFF00 — the true `.COM` ceiling, since the PSP occupies the - first 256 bytes of the segment), which would mean the tiny-model assumption - has broken. -- **Unintended source change.** `build.sh --check`, which CI runs, fails if the - binary no longer matches the pinned size and SHA-256. A deliberate change to - `MARS.ASM` must update `versions.env` in the same commit, making it visible in - review rather than silent. -- **js-dos fetch failure.** The Pages build fails rather than deploying a page - with missing emulator assets. -- **Browser-side load failure.** The page shows an inline error message instead - of an indefinitely blank canvas. - -## Testing - -- `scripts/build.sh` produces a `MARS.COM` of the expected size, from a clean - checkout, on a machine with no assembler pre-installed. -- The same script succeeds in CI on `ubuntu-latest`. -- `scripts/serve.sh` renders the demo locally: the landscape draws, the palette - is correct, and mouse movement pans the camera. -- The deployed Pages URL loads with no requests to third-party origins - (verifiable in the browser network panel). - -## Out of scope - -- Modifying `MARS.ASM` itself -- Mobile or touch controls -- Reconciling the 1517-byte figure by reproducing the author's original - toolchain; the discrepancy is documented, not resolved -- Upstreaming the build to `matrix-toolbox` diff --git a/site/index.html b/site/index.html index b15e06a..a225d01 100644 --- a/site/index.html +++ b/site/index.html @@ -50,8 +50,8 @@

What this is