McRogueFace/Makefile
John McCardle 50d6f1e5d2 build(docs): chain the release documentation pipeline into make release-docs
Every piece of this pipeline already existed -- generate_all_docs.sh,
generate_api_manifest.py, api_delta.py (with its --site-dir page resolver and its
--format gitea checklist), and the site's build_reference.py / build_library.py,
which already knew how to read this repo. Nothing chained them. So the site's
generated reference sat 26 engine commits behind, stamped verified_commit b6720f6,
and nothing noticed or could have noticed.

  make docs             regenerate man page, API reference, stubs, manifest
  make stamp-snippets   re-run the docs snippets; stamp what actually happened
  make check-snippets   CI gate: fail on a broken snippet or a stale stamp
  make api-delta        what changed, and which site pages that obligates
  make release-docs     all of it, for cutting a tag

release-docs deliberately does not commit and does not touch the hand-written
pages. It refreshes what is derived, proves the published samples still run,
rebuilds the site's generated half against this engine, and prints the checklist of
curated pages the API change obliges you to revisit. The judgment stays with a
human; only the mechanical parts are mechanical.

Two ref-shaped traps, both hit while building this:

  * BASE_REF and RELEASE_REF are different refs and I had conflated them. The delta
    is measured FROM the previous release; the site's source links are pinned TO the
    tag being cut. One variable could not be both.

  * api_delta now distinguishes "this ref predates the manifest" from "this ref is
    broken". 0.2.8 is older than the manifest infra (54624b3), so it has no baseline
    -- every object would read as "added", which is noise, not a delta. It says so
    and exits clean. An unknown ref is still an error.

stamp_snippets --check compares only the CLAIMS a header makes (status, objects),
never `verified`. That field records the engine a snippet was last confirmed
against -- provenance, not a freshness assertion. Had --check demanded it equal
HEAD, every commit would have invalidated all 130 stamps and the gate would have
cried wolf until people stopped listening.

Running it regenerated all 52 reference pages against the current engine, which
cleared every stale UIGrid / compute_astar / gridstate mention the docs audit found
in the generated tree -- the engine's docstrings had been right for weeks; the site
had simply never been rebuilt.
2026-07-14 18:33:31 -04:00

576 lines
22 KiB
Makefile

# McRogueFace Build Makefile
# Usage:
# make - Build for Linux (default)
# make windows - Cross-compile for Windows using MinGW (release)
# make windows-debug - Cross-compile for Windows with console & debug symbols
# make clean - Clean Linux build
# make clean-windows - Clean Windows build
# make run - Run the Linux build
#
# Profiling (see docs/profiling.md):
# make profile - RelWithDebInfo + frame pointers build in build-profile/
# make callgrind SCRIPT=tests/benchmarks/foo.py - Callgrind a headless benchmark
#
# WebAssembly / Emscripten:
# make wasm - Build full game for web (requires emsdk activated)
# make wasm-game - Build game for web with fullscreen canvas (no REPL)
# make playground - Build minimal playground for web REPL
# make serve - Serve wasm build locally on port 8080
# make serve-game - Serve wasm-game build locally on port 8080
# make clean-wasm - Clean Emscripten builds
#
# Packaging:
# make package-windows-light - Windows with minimal stdlib (~5 MB)
# make package-windows-full - Windows with full stdlib (~15 MB)
# make package-linux-light - Linux with minimal stdlib
# make package-linux-full - Linux with full stdlib
# make package-all - All platform/preset combinations
#
# Release:
# make version-bump NEXT_VERSION=x.y.z-suffix
# Tags HEAD with current version, builds all packages, bumps to NEXT_VERSION
.PHONY: all linux windows windows-debug clean clean-windows clean-dist run
.PHONY: wasm wasm-game wasm-debug playground playground-debug serve serve-game serve-playground clean-wasm
.PHONY: package-windows-light package-windows-full package-linux-light package-linux-full package-all
.PHONY: version-bump
.PHONY: debug debug-test asan asan-test tsan tsan-test valgrind-test massif-test analyze clean-debug
.PHONY: profile callgrind clean-profile
.PHONY: install-hooks
# Number of parallel jobs for compilation
JOBS := $(shell nproc 2>/dev/null || echo 4)
all: linux
linux:
@echo "Building McRogueFace for Linux..."
@mkdir -p build
@cd build && cmake .. -DCMAKE_BUILD_TYPE=Release && make -j$(JOBS)
@echo "Build complete! Run with: ./build/mcrogueface"
windows:
@echo "Cross-compiling McRogueFace for Windows..."
@mkdir -p build-windows
@cd build-windows && cmake .. \
-DCMAKE_TOOLCHAIN_FILE=../cmake/toolchains/mingw-w64-x86_64.cmake \
-DCMAKE_BUILD_TYPE=Release && make -j$(JOBS)
@echo "Windows build complete! Output: build-windows/mcrogueface.exe"
windows-debug:
@echo "Cross-compiling McRogueFace for Windows (debug with console)..."
@mkdir -p build-windows-debug
@cd build-windows-debug && cmake .. \
-DCMAKE_TOOLCHAIN_FILE=../cmake/toolchains/mingw-w64-x86_64.cmake \
-DCMAKE_BUILD_TYPE=Debug \
-DMCRF_WINDOWS_CONSOLE=ON && make -j$(JOBS)
@echo "Windows debug build complete! Output: build-windows-debug/mcrogueface.exe"
@echo "Run from cmd.exe to see console output"
clean:
@echo "Cleaning Linux build..."
@rm -rf build
clean-windows:
@echo "Cleaning Windows builds..."
@rm -rf build-windows build-windows-debug
clean-dist:
@echo "Cleaning distribution packages..."
@rm -rf dist
clean-all: clean clean-windows clean-wasm clean-debug clean-profile clean-dist
@echo "All builds and packages cleaned."
run: linux
@cd build && ./mcrogueface
# Install git hooks by symlinking tools/hooks/* into .git/hooks/ (idempotent).
install-hooks:
@hooks_dir="$$(git rev-parse --git-path hooks)"; \
mkdir -p "$$hooks_dir"; \
for hook in tools/hooks/*; do \
[ -e "$$hook" ] || continue; \
name="$$(basename "$$hook")"; \
ln -sf "$$(pwd)/$$hook" "$$hooks_dir/$$name"; \
echo "Linked $$hooks_dir/$$name -> $$hook"; \
done
# ---------------------------------------------------------------------------
# Documentation / release
#
# Every piece of the docs pipeline already existed; nothing chained them, so the
# site drifted 26 engine commits behind without anything noticing. These targets
# are the chain.
#
# SITE_DIR is the mcrogueface.github.io checkout. Override if yours lives elsewhere:
# make release-docs SITE_DIR=/path/to/mcrogueface.github.io
# ---------------------------------------------------------------------------
SITE_DIR ?= $(CURDIR)/../mcrogueface.github.io
# Two DIFFERENT refs, easily conflated:
#
# BASE_REF the PREVIOUS release. The API delta is measured against it, to answer
# "what changed, and which pages does that oblige us to revisit?"
# RELEASE_REF the tag being CUT. The site pins its source links to it, which is what
# freezes this version into the site's history.
#
# When cutting a release, set both:
# make release-docs BASE_REF=0.2.8 RELEASE_REF=0.2.9
#
# Note: BASE_REF must carry api/manifest.json. The manifest infrastructure landed in
# 54624b3, so tags older than that (0.2.8 included) have no baseline to diff against --
# api_delta will say so rather than inventing one.
BASE_REF ?= $(shell git describe --tags --abbrev=0 2>/dev/null)
RELEASE_REF ?= $(shell git describe --tags --always --abbrev=0 2>/dev/null)
# Regenerate everything derived from the compiled module: man page, API reference
# (HTML + Markdown), type stubs, and the tracked api/manifest.json.
docs: linux
@./tools/generate_all_docs.sh
@./build/mcrogueface --headless --exec ../tools/generate_api_manifest.py
# Re-run every docs snippet and stamp its `# mcrf:` header with what actually
# happened (status from the run, verified from the engine, objects from the source).
stamp-snippets: linux
@python3 tools/stamp_snippets.py
# CI gate: fail if any snippet is broken or its stamp is stale. Writes nothing.
check-snippets: linux
@python3 tools/stamp_snippets.py --check
# What changed in the API since the last release, and which site pages that
# obligates you to update (resolved via each page's `mcrf.objects` frontmatter).
api-delta:
@python3 tools/api_delta.py $(BASE_REF) . --site-dir $(SITE_DIR) --format md
# The full release documentation pass. Run this when cutting a tag: it refreshes
# everything the engine derives, proves the published samples still run, rebuilds
# the site's generated reference + snippet library against this engine, and prints
# the checklist of hand-written pages the API change obligates you to revisit.
#
# Deliberately NOT automatic: it does not commit, and it does not touch the
# hand-written pages. It tells you what it found and leaves the judgment to you.
release-docs: docs stamp-snippets
@echo ""
@echo "=== Regenerating the site against this engine (SITE_DIR=$(SITE_DIR)) ==="
@test -d "$(SITE_DIR)" || { echo "SITE_DIR not found: $(SITE_DIR)"; exit 1; }
@cd "$(SITE_DIR)" && python3 tools/build_reference.py
@cd "$(SITE_DIR)" && python3 tools/build_library.py --ref $(RELEASE_REF)
@echo ""
@echo "=== API delta $(BASE_REF) -> now, and the pages it obligates ==="
@python3 tools/api_delta.py $(BASE_REF) . --site-dir $(SITE_DIR) --format md
@echo ""
@echo "Site pinned to RELEASE_REF=$(RELEASE_REF); delta measured from BASE_REF=$(BASE_REF)."
@echo "Next: review the delta above, update the hand-written pages it names,"
@echo " then commit both repos. For a Gitea checklist issue instead, run:"
@echo " python3 tools/api_delta.py $(BASE_REF) . --site-dir $(SITE_DIR) --format gitea"
.PHONY: docs stamp-snippets check-snippets api-delta release-docs
# Debug and sanitizer targets
debug:
@echo "Building McRogueFace with debug Python (pydebug assertions)..."
@mkdir -p build-debug
@cd build-debug && cmake .. \
-DCMAKE_BUILD_TYPE=Debug \
-DMCRF_DEBUG_PYTHON=ON && make -j$(JOBS)
@echo "Debug build complete! Output: build-debug/mcrogueface"
debug-test: debug
@echo "Running test suite with debug Python..."
cd tests && MCRF_BUILD_DIR=../build-debug \
MCRF_LIB_DIR=../__lib_debug \
python3 run_tests.py -v
# Profiling build (#345): RelWithDebInfo (-O2 -g) + frame pointers, self-contained
# in build-profile/ (lib/assets/scripts copied like the normal build). Suited to
# both Callgrind (deterministic, no perms) and perf (--call-graph fp).
profile:
@echo "Building McRogueFace for profiling (RelWithDebInfo + frame pointers)..."
@mkdir -p build-profile
@cd build-profile && cmake .. \
-DCMAKE_BUILD_TYPE=RelWithDebInfo \
-DMCRF_PROFILE=ON && make -j$(JOBS)
@echo "Profile build complete! Binary: build-profile/mcrogueface"
# Callgrind a headless benchmark script. Deterministic, exact instruction counts,
# no special permissions. Usage: make callgrind SCRIPT=tests/benchmarks/foo.py
# Output: callgrind.out (feed to callgrind_annotate).
SCRIPT ?= tests/benchmarks/issue_331_property_read_bench.py
callgrind: profile
@echo "Running Callgrind on $(SCRIPT)..."
@cd build-profile && valgrind --tool=callgrind \
--callgrind-out-file=callgrind.out \
./mcrogueface --headless --exec ../$(SCRIPT)
@echo "Done. Annotate with: callgrind_annotate build-profile/callgrind.out | head -60"
clean-profile:
@echo "Cleaning profile build..."
@rm -rf build-profile
asan:
@echo "Building McRogueFace with ASan + UBSan..."
@mkdir -p build-asan
@cd build-asan && cmake .. \
-DCMAKE_BUILD_TYPE=Debug \
-DMCRF_DEBUG_PYTHON=ON \
-DMCRF_SANITIZE_ADDRESS=ON \
-DMCRF_SANITIZE_UNDEFINED=ON && make -j$(JOBS)
@echo "ASan build complete! Output: build-asan/mcrogueface"
asan-test: asan
@echo "Running test suite under ASan + UBSan..."
cd tests && MCRF_BUILD_DIR=../build-asan \
MCRF_LIB_DIR=../__lib_debug \
PYTHONMALLOC=malloc \
ASAN_OPTIONS="detect_leaks=1:halt_on_error=1:print_summary=1" \
LSAN_OPTIONS="suppressions=$(CURDIR)/sanitizers/asan.supp" \
UBSAN_OPTIONS="print_stacktrace=1:halt_on_error=1" \
python3 run_tests.py -v --sanitizer
# Fuzzing targets (clang-18 + libFuzzer + ASan + UBSan).
# Design: ONE instrumented executable `mcrfpy_fuzz` that embeds CPython,
# registers the mcrfpy module, and dispatches each libFuzzer iteration to
# a Python `fuzz_one_input(data)` function loaded from the script named by
# the MCRF_FUZZ_TARGET env var. libFuzzer instruments the C++ engine code
# where all the #258-#278 bugs live. No atheris dependency.
FUZZ_TARGETS := grid_entity property_types anim_timer_scene maps_procgen fov pathfinding_behavior audio_dsp import_parsers texture_factory shader_bindings
FUZZ_SECONDS ?= 30
# Shared env for running the fuzz binary. PYTHONHOME points at the build-fuzz
# copy of the bundled stdlib (post-build copied into build-fuzz/lib/).
# ASAN_OPTIONS: leak detection disabled because libFuzzer intentionally holds
# inputs for its corpus; abort_on_error ensures crashes are loud and repro-able.
define FUZZ_ENV
MCRF_LIB_DIR=../__lib_debug \
PYTHONMALLOC=malloc \
PYTHONHOME=../__lib/Python \
ASAN_OPTIONS="detect_leaks=0:halt_on_error=1:abort_on_error=1:print_stacktrace=1" \
UBSAN_OPTIONS="print_stacktrace=1:halt_on_error=1"
endef
fuzz-build:
@echo "Building mcrfpy_fuzz with libFuzzer + ASan (clang-18)..."
@mkdir -p build-fuzz
@cd build-fuzz && CC=clang-18 CXX=clang++-18 cmake .. \
-DCMAKE_BUILD_TYPE=Debug \
-DMCRF_DEBUG_PYTHON=ON \
-DMCRF_SANITIZE_ADDRESS=ON \
-DMCRF_SANITIZE_UNDEFINED=ON \
-DMCRF_FUZZER=ON \
-DCMAKE_EXE_LINKER_FLAGS=-fuse-ld=lld && make -j$(JOBS) mcrfpy_fuzz
@echo "Fuzz build complete! Output: build-fuzz/mcrfpy_fuzz"
fuzz: fuzz-build
@for t in $(FUZZ_TARGETS); do \
if [ ! -f tests/fuzz/fuzz_$$t.py ]; then \
echo "SKIP: tests/fuzz/fuzz_$$t.py does not exist yet"; \
continue; \
fi; \
echo "=== fuzzing $$t for $(FUZZ_SECONDS)s ==="; \
mkdir -p tests/fuzz/corpora/$$t tests/fuzz/crashes; \
( cd build-fuzz && $(FUZZ_ENV) MCRF_FUZZ_TARGET=$$t \
./mcrfpy_fuzz \
-max_total_time=$(FUZZ_SECONDS) \
-artifact_prefix=../tests/fuzz/crashes/$$t- \
../tests/fuzz/corpora/$$t ../tests/fuzz/seeds/$$t ) || exit 1; \
done
fuzz-long: fuzz-build
@test -n "$(TARGET)" || (echo "Usage: make fuzz-long TARGET=<name> SECONDS=<n>"; exit 1)
@test -f tests/fuzz/fuzz_$(TARGET).py || (echo "No target: tests/fuzz/fuzz_$(TARGET).py"; exit 1)
@mkdir -p tests/fuzz/corpora/$(TARGET) tests/fuzz/crashes
@( cd build-fuzz && $(FUZZ_ENV) MCRF_FUZZ_TARGET=$(TARGET) \
./mcrfpy_fuzz \
-max_total_time=$(or $(SECONDS),3600) \
-artifact_prefix=../tests/fuzz/crashes/$(TARGET)- \
../tests/fuzz/corpora/$(TARGET) ../tests/fuzz/seeds/$(TARGET) )
fuzz-repro:
@test -n "$(TARGET)" || (echo "Usage: make fuzz-repro TARGET=<name> CRASH=<path>"; exit 1)
@test -n "$(CRASH)" || (echo "Usage: make fuzz-repro TARGET=<name> CRASH=<path>"; exit 1)
@( cd build-fuzz && $(FUZZ_ENV) MCRF_FUZZ_TARGET=$(TARGET) \
./mcrfpy_fuzz ../$(CRASH) )
clean-fuzz:
@echo "Cleaning fuzz build and corpora..."
@rm -rf build-fuzz tests/fuzz/corpora tests/fuzz/crashes
tsan:
@echo "Building McRogueFace with TSan + free-threaded Python..."
@echo "NOTE: Requires free-threaded debug Python built with:"
@echo " tools/build_debug_python.sh --tsan"
@mkdir -p build-tsan
@cd build-tsan && cmake .. \
-DCMAKE_BUILD_TYPE=Debug \
-DMCRF_FREE_THREADED_PYTHON=ON \
-DMCRF_SANITIZE_THREAD=ON && make -j$(JOBS)
@echo "TSan build complete! Output: build-tsan/mcrogueface"
tsan-test: tsan
@echo "Running test suite under TSan..."
cd tests && MCRF_BUILD_DIR=../build-tsan \
MCRF_LIB_DIR=../__lib_debug \
TSAN_OPTIONS="halt_on_error=1:second_deadlock_stack=1" \
python3 run_tests.py -v --sanitizer
valgrind-test: debug
@echo "Running test suite under Valgrind memcheck..."
cd tests && MCRF_BUILD_DIR=../build-debug \
MCRF_LIB_DIR=../__lib_debug \
MCRF_TIMEOUT_MULTIPLIER=50 \
PYTHONMALLOC=malloc \
python3 run_tests.py -v --valgrind
massif-test: debug
@echo "Running heap profiling under Valgrind Massif..."
@mkdir -p build-debug
cd build-debug && valgrind --tool=massif \
--massif-out-file=massif.out \
--pages-as-heap=no \
--detailed-freq=10 \
--max-snapshots=100 \
./mcrogueface --headless --exec ../tests/benchmarks/stress_test_suite.py
@echo "Massif output: build-debug/massif.out"
@echo "View with: ms_print build-debug/massif.out"
analyze:
@echo "Running cppcheck static analysis..."
cppcheck --enable=warning,performance,portability \
--suppress=missingIncludeSystem \
--suppress=unusedFunction \
--suppress=noExplicitConstructor \
--suppress=missingOverride \
--inline-suppr \
-I src/ -I deps/ -I deps/cpython -I deps/Python \
-I src/platform -I src/3d -I src/tiled -I src/ldtk -I src/audio \
--std=c++20 \
--quiet \
src/ 2>&1
@echo "Static analysis complete."
clean-debug:
@echo "Cleaning debug/sanitizer builds..."
@rm -rf build-debug build-asan build-tsan build-fuzz
# Packaging targets using tools/package.sh
package-windows-light: windows
@./tools/package.sh windows light
package-windows-full: windows
@./tools/package.sh windows full
package-linux-light: linux
@./tools/package.sh linux light
package-linux-full: linux
@./tools/package.sh linux full
package-all: windows linux
@./tools/package.sh all
# Legacy target for backwards compatibility
package-windows: package-windows-full
# Emscripten / WebAssembly targets
# Requires: source ~/emsdk/emsdk_env.sh (or wherever your emsdk is installed)
#
# For iterative development, configure once then rebuild:
# source ~/emsdk/emsdk_env.sh && emmake make -C build-emscripten
#
wasm:
@if ! command -v emcmake >/dev/null 2>&1; then \
echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
exit 1; \
fi
@if [ ! -f build-emscripten/Makefile ]; then \
echo "Configuring WebAssembly build (full game)..."; \
mkdir -p build-emscripten; \
cd build-emscripten && emcmake cmake .. \
-DCMAKE_BUILD_TYPE=Release \
-DMCRF_SDL2=ON; \
fi
@echo "Building McRogueFace for WebAssembly..."
@emmake make -C build-emscripten -j$(JOBS)
@echo "WebAssembly build complete! Files in build-emscripten/"
@echo "Run 'make serve' to test locally"
playground:
@if ! command -v emcmake >/dev/null 2>&1; then \
echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
exit 1; \
fi
@if [ ! -f build-playground/Makefile ]; then \
echo "Configuring Playground build..."; \
mkdir -p build-playground; \
cd build-playground && emcmake cmake .. \
-DCMAKE_BUILD_TYPE=Release \
-DMCRF_SDL2=ON \
-DMCRF_PLAYGROUND=ON; \
fi
@echo "Building McRogueFace Playground for WebAssembly..."
@emmake make -C build-playground -j$(JOBS)
@echo "Playground build complete! Files in build-playground/"
@echo "Run 'make serve-playground' to test locally"
serve:
@echo "Serving WebAssembly build at http://localhost:8080"
@echo "Press Ctrl+C to stop"
@cd build-emscripten && python3 -m http.server 8080
serve-playground:
@echo "Serving Playground build at http://localhost:8080"
@echo "Press Ctrl+C to stop"
@cd build-playground && python3 -m http.server 8080
wasm-game:
@if ! command -v emcmake >/dev/null 2>&1; then \
echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
exit 1; \
fi
@if [ ! -f build-wasm-game/Makefile ]; then \
echo "Configuring WebAssembly game build (fullscreen, no REPL)..."; \
mkdir -p build-wasm-game; \
cd build-wasm-game && emcmake cmake .. \
-DCMAKE_BUILD_TYPE=Release \
-DMCRF_SDL2=ON \
-DMCRF_GAME_SHELL=ON; \
fi
@echo "Building McRogueFace game for WebAssembly..."
@emmake make -C build-wasm-game -j$(JOBS)
@echo "Game build complete! Files in build-wasm-game/"
@echo "Run 'make serve-game' to test locally"
serve-game:
@echo "Serving game build at http://localhost:8080"
@echo "Press Ctrl+C to stop"
@cd build-wasm-game && python3 -m http.server 8080
wasm-demo:
@if ! command -v emcmake >/dev/null 2>&1; then \
echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
exit 1; \
fi
@if [ ! -f build-wasm-demo/Makefile ]; then \
echo "Configuring WebAssembly demo build..."; \
mkdir -p build-wasm-demo; \
cd build-wasm-demo && emcmake cmake .. \
-DCMAKE_BUILD_TYPE=Release \
-DMCRF_SDL2=ON \
-DMCRF_DEMO=ON \
-DMCRF_GAME_SHELL=ON; \
fi
@echo "Building McRogueFace demo for WebAssembly..."
@emmake make -C build-wasm-demo -j$(JOBS)
@cp web/index.html build-wasm-demo/index.html
@echo "Demo build complete! Files in build-wasm-demo/"
@echo "Run 'make serve-demo' to test locally"
serve-demo:
@echo "Serving demo build at http://localhost:8080"
@echo "Press Ctrl+C to stop"
@cd build-wasm-demo && python3 -m http.server 8080
clean-wasm:
@echo "Cleaning Emscripten builds..."
@rm -rf build-emscripten build-playground build-wasm-game build-wasm-demo build-wasm-debug build-playground-debug
wasm-debug:
@if ! command -v emcmake >/dev/null 2>&1; then \
echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
exit 1; \
fi
@if [ ! -f build-wasm-debug/Makefile ]; then \
echo "Configuring WebAssembly debug build (DWARF + source maps)..."; \
mkdir -p build-wasm-debug; \
cd build-wasm-debug && emcmake cmake .. \
-DCMAKE_BUILD_TYPE=Debug \
-DMCRF_SDL2=ON \
-DMCRF_WASM_DEBUG=ON; \
fi
@echo "Building McRogueFace for WebAssembly (debug)..."
@emmake make -C build-wasm-debug -j$(JOBS)
@echo "Debug WASM build complete! Files in build-wasm-debug/"
@echo "Debug artifacts: .wasm.map (source map), .symbols (symbol map)"
@echo "Run 'make serve-wasm-debug' to test locally"
serve-wasm-debug:
@echo "Serving debug WASM build at http://localhost:8080"
@echo "Press Ctrl+C to stop"
@cd build-wasm-debug && python3 -m http.server 8080
playground-debug:
@if ! command -v emcmake >/dev/null 2>&1; then \
echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
exit 1; \
fi
@if [ ! -f build-playground-debug/Makefile ]; then \
echo "Configuring Playground debug build (DWARF + source maps)..."; \
mkdir -p build-playground-debug; \
cd build-playground-debug && emcmake cmake .. \
-DCMAKE_BUILD_TYPE=Debug \
-DMCRF_SDL2=ON \
-DMCRF_PLAYGROUND=ON \
-DMCRF_WASM_DEBUG=ON; \
fi
@echo "Building McRogueFace Playground for WebAssembly (debug)..."
@emmake make -C build-playground-debug -j$(JOBS)
@echo "Playground debug build complete! Files in build-playground-debug/"
@echo "Run 'make serve-playground-debug' to test locally"
serve-playground-debug:
@echo "Serving debug Playground build at http://localhost:8080"
@echo "Press Ctrl+C to stop"
@cd build-playground-debug && python3 -m http.server 8080
# Current version extracted from source
CURRENT_VERSION := $(shell grep 'MCRFPY_VERSION' src/McRogueFaceVersion.h | sed 's/.*"\(.*\)"/\1/')
# Release workflow: tag current version, build all packages, bump to next version
# Usage: make version-bump NEXT_VERSION=0.2.6-prerelease-7drl2026
version-bump:
ifndef NEXT_VERSION
$(error Usage: make version-bump NEXT_VERSION=x.y.z-suffix)
endif
@if ! command -v emcmake >/dev/null 2>&1; then \
echo "Error: emcmake not found. Run 'source ~/emsdk/emsdk_env.sh' first."; \
exit 1; \
fi
# git status (clean working dir check), but ignore modules/, because building submodules dirties their subdirs
@if [ -n "$$(git status --porcelain | grep -v modules)" ]; then \
echo "Error: Working tree is not clean. Commit or stash changes first."; \
exit 1; \
fi
@echo "=== Releasing $(CURRENT_VERSION) ==="
@# Idempotent tag: ok if it already points at HEAD (resuming partial run)
@if git rev-parse "$(CURRENT_VERSION)" >/dev/null 2>&1; then \
TAG_COMMIT=$$(git rev-parse "$(CURRENT_VERSION)^{}"); \
HEAD_COMMIT=$$(git rev-parse HEAD); \
if [ "$$TAG_COMMIT" != "$$HEAD_COMMIT" ]; then \
echo "Error: Tag $(CURRENT_VERSION) already exists but points to a different commit."; \
exit 1; \
fi; \
echo "Tag $(CURRENT_VERSION) already exists at HEAD (resuming)."; \
else \
git tag "$(CURRENT_VERSION)"; \
fi
$(MAKE) package-linux-full
$(MAKE) package-windows-full
$(MAKE) wasm
@echo "Packaging WASM build..."
@mkdir -p dist
cd build-emscripten && zip -r ../dist/McRogueFace-$(CURRENT_VERSION)-WASM.zip \
mcrogueface.html mcrogueface.js mcrogueface.wasm mcrogueface.data
@echo ""
@echo "Bumping version: $(CURRENT_VERSION) -> $(NEXT_VERSION)"
@sed -i 's|MCRFPY_VERSION "$(CURRENT_VERSION)"|MCRFPY_VERSION "$(NEXT_VERSION)"|' src/McRogueFaceVersion.h
@TAGGED_HASH=$$(git rev-parse --short HEAD); \
git add src/McRogueFaceVersion.h && \
git commit -m "Version bump: $(CURRENT_VERSION) ($$TAGGED_HASH) -> $(NEXT_VERSION)"
@echo ""
@echo "=== Release $(CURRENT_VERSION) complete ==="
@echo "Tag: $(CURRENT_VERSION)"
@echo "Next: $(NEXT_VERSION)"
@echo "Packages:"
@ls -lh dist/*$(CURRENT_VERSION)* 2>/dev/null