Files
SplitBit-Emulator/makefile
T
AnachronautandClaude Opus 5 f351ee8844 Every score on the disk, not a named one
The makefile compiled splash.score by name, so intro.score sat beside it
turning into nothing - which is exactly the failure the mirror three
lines above exists to prevent, and its comment says so: a list in a
makefile goes stale the moment somebody adds a file, and what they forgot
is invisible until they go looking for it on the machine.

Found by the user going looking for it on the machine.

Every .score in Programs/Tunes is compiled and put on the disk now, and
every patch in Programs/Sounds is converted first so a score can name any
of them. The source is mirrored to /Source/Tunes with everything else
that was written; the tune goes to the root, where a program looking for
one expects it - which is worth writing down, because the two being in
different places is the thing that sent somebody hunting.

two.score moved to Tests. It is a fixture, it names patches that only
exist in the test build, and it was in the music tree only because that
is where scores were when it was written - which the game disk build
found immediately by failing to compile it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-06 11:15:47 -04:00

765 lines
36 KiB
Makefile

# SplitBit Makefile
# Anachronaut
# 10/16/2024
#
# ---- One makefile, three groups of target ----
#
# make The tools, CosmOS, and a disk to boot it from.
# make run-voyager ... and start the machine that has a screen.
# make test The whole suite.
# make clean Everything any of it built.
#
# THE PLATFORM AND THE SYSTEM ARE SEPARATE TARGETS, NOT SEPARATE MAKEFILES. SplitBit is a
# machine and CosmOS is a program for it, and somebody who wants to write their own system
# should be able to build the tools and ignore the rest - which is what 'make SplitBit
# Assembler' is for. That separation lives in the target graph, where make enforces it.
#
# It used to live in a second makefile under Programs/, and that did not work: nothing at
# this level ever ran it, so it rotted. It named two source files that had been renamed
# months earlier and simply failed; the disk did not depend on the tree it mirrors, so new
# files silently were not on it; and the disk was not in any default target, so 'make clean'
# threw it away and 'make' did not bring it back. Three bugs, one cause - a makefile nothing
# exercises is a makefile nobody notices is broken.
# Compiler and flags
CC ?= gcc
CFLAGS ?= -Wall -Os
PREFIX ?= /usr/local
# Have the compiler write out which headers each object depends on, so that
# editing a header rebuilds everything that includes it.
DEPFLAGS = -MMD -MP
# The emulator and the assembler use POSIX interfaces that ISO C does not have:
# realpath, clock_gettime,
# strdup, dirname and getopt. Asking for POSIX.1-2008 by name means the build does
# not rely on the compiler happening to default to a mode where those are visible,
# and it survives someone overriding CFLAGS, which is why it is kept separate.
#
# _XOPEN_SOURCE=700 IS POSIX.1-2008, plus the XSI extensions. The plain
# _POSIX_C_SOURCE=200809L was here and is not quite enough: realpath is an XSI interface,
# so under -std=c11 -pedantic it went undeclared and the assembler would not compile. The
# ordinary build never noticed, because without -std=c11 the compiler's own default
# already declares it. The README makes a claim about the strict build, so 'make strict'
# below settles it rather than leaving it to be discovered.
POSIXFLAGS = -D_XOPEN_SOURCE=700
# Directories
SRC_DIR_EMU = Source/Emulator
SRC_DIR_ASM = Source/Assembler
SRC_DIR_DSK = Source/DiskTool
SRC_DIR_LINT = Source/Linter
SRC_DIR_PATCH = Source/Patch
SRC_DIR_TUNE = Source/Tune
OBJ_DIR = Object
# Source files
#
# MACHINE_SRCS is the machine itself, and both front ends link all of it. What separates
# SplitBit from Voyager is one file each: a terminal or a window. Anything that drifts out
# of the shared list and into one of those is behaviour the other does not have, which is
# the thing this split exists to prevent.
MACHINE_SRCS = machine.c io.c controller.c video.c pad.c font.c sound.c synth.c utility.c cpu.c bootstrap.c assembly.c rom.c
EMU_SRCS = emulator.c $(MACHINE_SRCS)
VOY_SRCS = voyager.c $(MACHINE_SRCS)
ASM_SRCS = Assembler.c assembly.c firstPass.c Assm-util.c secondPass.c
DSK_SRCS = SplitDisk.c
LINT_SRCS = Linter.c
PATCH_SRCS = SoundPatch.c
TUNE_SRCS = TuneC.c
EMU_OBJS = $(EMU_SRCS:%.c=$(OBJ_DIR)/%.o)
VOY_OBJS = $(VOY_SRCS:%.c=$(OBJ_DIR)/%.o)
ASM_OBJS = $(ASM_SRCS:%.c=$(OBJ_DIR)/%.o)
DSK_OBJS = $(DSK_SRCS:%.c=$(OBJ_DIR)/%.o)
LINT_OBJS = $(LINT_SRCS:%.c=$(OBJ_DIR)/%.o) $(OBJ_DIR)/assembly.o
PATCH_OBJS = $(PATCH_SRCS:%.c=$(OBJ_DIR)/%.o)
TUNE_OBJS = $(TUNE_SRCS:%.c=$(OBJ_DIR)/%.o)
# Output binary names
EMU_TARGET = SplitBit
VOY_TARGET = Voyager
ASM_TARGET = Assembler
DSK_TARGET = SplitDisk
LINT_TARGET = SplitLint
PATCH_TARGET = SoundPatch
TUNE_TARGET = TuneC
# ---- Whether this machine can build Voyager ----
#
# PROBED BY BUILDING SOMETHING, not by looking for a file. A header that is present with no
# library behind it, or a library that needs flags this does not pass, would both pass a
# file check and then fail at link time, which is a much worse way to find out. If this
# compiles and links, so will Voyager.
#
# pkg-config first because that is what a packaged Raylib provides, and a bare -lraylib
# after it because a Raylib built from source usually does not install one.
RAYLIB_CFLAGS := $(shell pkg-config --cflags raylib 2>/dev/null)
RAYLIB_PKG := $(shell pkg-config --libs raylib 2>/dev/null)
# ---- The flags are FOUND rather than assumed ----
#
# What a static Raylib needs beside itself changes with the version. Six's GetClipboardImage
# calls X11 directly, so -lX11 became necessary on a Linux desktop where five did not want it
# - and because it is a static archive, one function nobody calls drags the whole object in
# and every X symbol with it.
#
# THE OLD SHAPE FAILED IN THE WORST WAY. One hardcoded guess was test-linked, and when the
# guess went short the probe said Raylib was not installed - so make quietly stopped building
# Voyager and said so in the words it uses for a machine with no graphics library at all. The
# answer looked like an absence and was a missing flag.
#
# So the candidates are tried in order and the first that links is the answer. The probe and
# the flags are now the same thing rather than two facts that can disagree.
#
# GetTime is enough to catch it because a static link pulls objects, not functions: it lives
# in the same object as the clipboard code, so a program that calls it needs everything that
# code needs. -lm is not spare either - Raylib does not link without it, so dropping it here
# does not tidy a duplicate, it makes the probe say Raylib is missing.
RAYLIB_LIBS := $(shell for libs in "$(RAYLIB_PKG)" "-lraylib -lm" "-lraylib -lm -lX11"; do \
[ -z "$$libs" ] && continue; \
printf '#include <raylib.h>\nint main(void){return (int)GetTime();}\n' \
| $(CC) -x c - -o /dev/null $(RAYLIB_CFLAGS) $$libs 2>/dev/null \
&& { printf '%s' "$$libs"; break; }; \
done)
HAVE_RAYLIB := $(if $(strip $(RAYLIB_LIBS)),yes,)
# Default target: the machine, its three host-side tools, and Voyager where it can be built.
#
# =====================================================================================
# Where the SplitBit programs are, and what they build into.
# =====================================================================================
#
# ---- These are up here because 'all' below needs them ----
#
# A prerequisite list is expanded WHEN MAKE READS THE LINE, so a variable defined further
# down expands to nothing and takes its target with it, silently. That is not a hypothetical:
# these sat at the bottom with the rules that use them, 'all' asked for $(COSMOS_DISK), and
# 'make' on a clean tree built every tool, said "Nothing to be done", and left no disk.
#
# Every path is built from the first two, so moving the tree is a change to those lines
# rather than a search through this file.
# ---- Where things are, said once ----
#
# Every path below is built from these two, so moving the tree is a change to these lines
# rather than a search through the file.
PROG_DIR = Programs
PROG_BUILD = $(PROG_DIR)/build
# The tools, as commands. Built by the rules above, and named here so the assembly rules
# read the way the shell would.
ASM_RUN = ./$(ASM_TARGET)
PATCH_RUN = ./$(PATCH_TARGET)
TUNE_RUN = ./$(TUNE_TARGET)
EMU_RUN = ./$(EMU_TARGET)
VOY_RUN = ./$(VOY_TARGET)
DISKTOOL = ./$(DSK_TARGET)
# Libraries are included by bare name, so the assembler is told where to find them. CosmOS
# owns the filesystem library and the service names, so it is a place to look too.
INCLUDES = -I $(PROG_DIR)/Sounds -I $(PROG_DIR)/Libraries -I $(PROG_DIR)/CosmOS/Source
# The programs worth building. Every one lives in a directory that says what kind it is:
# Examples/ is what you read to learn, Loader/ is the standalone loader CosmOS grew out of,
# CosmOS/ is the system. Files in Libraries/ are left out because they have no entry point
# of their own, and the ones in testPrograms/ are covered by 'make test'.
PROGRAM_SOURCES = \
CosmOS/Source/cosmos.asm \
Examples/hello.asm \
Examples/printHello.asm \
Examples/inputTest.asm \
Examples/replCalculator.asm \
Examples/Fibonacci/8bitFibonacci.asm \
Examples/Fibonacci/16bitFibonacci.asm \
Examples/Fibonacci/32bitFibonacci.asm \
Examples/primeSieve/8bitSieve.asm \
Examples/primeSieve/16bitSieve.asm \
Examples/primeSieve/16bitSieveModern.asm \
Examples/gameOfLife/16x16Life.asm \
Examples/gameOfLife/16x16LifeModern.asm \
Loader/loader.asm \
Loader/loadable.asm
BINARIES = $(PROGRAM_SOURCES:%.asm=$(PROG_BUILD)/%.bin)
PROG_DEPS = $(BINARIES:.bin=.d)
COSMOS = $(PROG_BUILD)/CosmOS/Source/cosmos.bin
APPS = $(patsubst $(PROG_DIR)/CosmOS/Apps/%.asm,$(PROG_BUILD)/CosmOS/Apps/%.sbx,\
$(wildcard $(PROG_DIR)/CosmOS/Apps/*.asm))
COSMOS_DISK = $(PROG_BUILD)/cosmos.img
# ---- A disk that is yours ----
#
# Drive 1 whenever the system is run, and the only thing in this repository that is never
# rebuilt, never cleaned and never committed. Everything else here is made from source and
# can be thrown away without losing anything; this is the one place where something MADE ON
# THE MACHINE can live, which matters more the moment there are tools on it that make things.
#
# NOT UNDER build/, which is the whole point. 'make clean' empties that, and a disk of your
# own that a clean deletes is not a disk of your own. It is a rule with no prerequisites, so
# once it exists make never looks at it again.
PERSONAL_DISK = Disks/personal.img
# How many starts back the personal disk is kept. See personalBackup for why it is not one.
PERSONAL_BACKUPS = 3
# ---- And a drive made of memory, as drive 2 ----
#
# 2,048 blocks, half a megabyte, gone when the machine stops. The system keeps there whatever
# it wants back later and cannot hold - the screen a program took, and whatever comes after
# that. A machine without one does without: osTakeScreen answers no and a program told no
# carries on.
#
# AFTER the personal disk, so drive 1 stays the one that is yours. A scratch drive that took
# drive 1 would renumber somebody's disk the day it was added.
SCRATCH_BLOCKS = 2048
# VOYAGER IS NOT IN THE HARD LIST. Everything below it - the assembler, the disk tool, the
# linter, the whole test suite - has to build on a machine with no graphics library at all,
# because a project about a small understandable CPU should not need OpenGL to run its
# tests. Where Raylib is missing, 'make' says so once and builds everything else.
TOOLS = $(EMU_TARGET) $(ASM_TARGET) $(DSK_TARGET) $(LINT_TARGET) $(PATCH_TARGET) $(TUNE_TARGET)
# ---- What 'make' builds ----
#
# The tools AND a bootable disk. A machine with nothing in the drive does not do anything,
# and the first thing anybody wants after building this is to see CosmOS come up - so the
# default build hands them one. It costs half a second: the C assembler goes through the
# whole system, twenty six apps and the native assembler in less time than the link.
#
# This is also the answer to 'make clean' leaving no disk behind, which is what a disk that
# no default target built did every time.
ifeq ($(HAVE_RAYLIB),yes)
all: $(TOOLS) $(VOY_TARGET) $(COSMOS_DISK)
else
all: $(TOOLS) $(COSMOS_DISK)
@echo "Raylib was not found, so Voyager was not built. Everything else is here."
endif
# ---- The ROM the machine wakes up in ----
#
# Generated from the assembly rather than committed, because a copy of a program kept
# beside the program is a copy that goes stale. It needs the assembler, which is built
# first; that is a real dependency and saying so is better than hiding it.
#
# od and awk rather than xxd, which is not everywhere, and rather than python, which the
# README does not ask anybody to install to build this.
$(SRC_DIR_EMU)/rom.c: Programs/Boot/stage1.asm $(ASM_TARGET)
@mkdir -p $(OBJ_DIR)
@./$(ASM_TARGET) Programs/Boot/stage1.asm -o $(OBJ_DIR)/stage1.bin > /dev/null
@{ \
echo '// rom.c'; \
echo '// GENERATED from Programs/Boot/stage1.asm by the makefile. Do not edit.'; \
echo '//'; \
echo '// The first thing the machine runs, and the only part of it that is not on'; \
echo '// the disk. On hardware this is a chip; here it is an array, placed into'; \
echo '// Program Memory at reset the way a shadowed ROM is.'; \
echo ''; \
echo '#include "rom.h"'; \
echo ''; \
echo 'const unsigned char bootROM[] = {'; \
od -v -An -tu1 $(OBJ_DIR)/stage1.bin | awk '{ printf " "; for (i = 1; i <= NF; i++) printf " %s,", $$i; print "" }'; \
echo '};'; \
echo ''; \
echo 'const unsigned long bootROMBytes = sizeof(bootROM);'; \
} > $@
$(OBJ_DIR)/rom.o: $(SRC_DIR_EMU)/rom.c $(SRC_DIR_EMU)/rom.h
# The maths library, which the machine wants now that it has a synthesizer in it: the voice
# engine works in hertz and seconds and reaches for powf and tanf to do it.
MATHLIB = -lm
# Emulator binary
$(EMU_TARGET): $(EMU_OBJS)
$(CC) $(CFLAGS) -o $(EMU_TARGET) $(EMU_OBJS) $(MATHLIB)
# Voyager: the same machine with a screen and a speaker. Its own object for the front end,
# and the shared ones for everything that is actually the machine.
$(VOY_TARGET): $(VOY_OBJS)
$(CC) $(CFLAGS) -o $(VOY_TARGET) $(VOY_OBJS) $(RAYLIB_LIBS) $(MATHLIB)
$(OBJ_DIR)/voyager.o: $(SRC_DIR_EMU)/voyager.c
mkdir -p $(OBJ_DIR)
$(CC) $(CFLAGS) $(POSIXFLAGS) $(RAYLIB_CFLAGS) $(DEPFLAGS) -c $< -o $@
# Assembler binary
$(ASM_TARGET): $(ASM_OBJS)
$(CC) $(CFLAGS) -o $(ASM_TARGET) $(ASM_OBJS)
# Compile emulator source files to object files
$(OBJ_DIR)/%.o: $(SRC_DIR_EMU)/%.c
mkdir -p $(OBJ_DIR)
$(CC) $(CFLAGS) $(POSIXFLAGS) $(DEPFLAGS) -c $< -o $@
# Disk tool binary
$(DSK_TARGET): $(DSK_OBJS)
$(CC) $(CFLAGS) -o $(DSK_TARGET) $(DSK_OBJS)
# Assembly source linter. The instruction table is shared with the assembler and
# emulator so that adding an opcode cannot leave the linter with a private copy.
$(LINT_TARGET): $(LINT_OBJS)
$(CC) $(CFLAGS) -o $(LINT_TARGET) $(LINT_OBJS)
# ---- SoundPatch, which needs the maths library and nothing else ----
#
# It converts a soundThing patch into a table of bytes for the sound device. Nothing in the
# build depends on it, and that is deliberate: soundThing lives in another repository and the
# tables it produces are CHECKED IN, so this tree builds on a machine that has never heard of
# it. The tool is here for when a sound is being changed.
$(PATCH_TARGET): $(PATCH_OBJS)
$(CC) $(CFLAGS) -o $(PATCH_TARGET) $(PATCH_OBJS) -lm
# Compile patch tool source files to object files
# ---- TuneC, which turns a written tune into the bytes the player reads ----
#
# It needs nothing: patches reach it as bytes from SoundPatch, which stays the only thing that
# understands what a soundThing patch means.
$(TUNE_TARGET): $(TUNE_OBJS)
$(CC) $(CFLAGS) -o $(TUNE_TARGET) $(TUNE_OBJS)
$(OBJ_DIR)/%.o: $(SRC_DIR_TUNE)/%.c
@mkdir -p $(OBJ_DIR)
$(CC) $(CFLAGS) -MMD -MP -c $< -o $@
$(OBJ_DIR)/%.o: $(SRC_DIR_PATCH)/%.c
@mkdir -p $(OBJ_DIR)
$(CC) $(CFLAGS) $(POSIXFLAGS) $(DEPFLAGS) -c $< -o $@
$(OBJ_DIR)/Linter.o: $(SRC_DIR_LINT)/Linter.c
@mkdir -p $(OBJ_DIR)
$(CC) $(CFLAGS) $(POSIXFLAGS) $(DEPFLAGS) -c $< -o $@
# Compile disk tool source files to object files
$(OBJ_DIR)/%.o: $(SRC_DIR_DSK)/%.c
@mkdir -p $(OBJ_DIR)
$(CC) $(CFLAGS) $(POSIXFLAGS) $(DEPFLAGS) -c $< -o $@
# Compile assembler source files to object files
$(OBJ_DIR)/%.o: $(SRC_DIR_ASM)/%.c
mkdir -p $(OBJ_DIR)
$(CC) $(CFLAGS) $(POSIXFLAGS) $(DEPFLAGS) -c $< -o $@
# Pull in the header dependencies written out by the compiler above.
# VOY_OBJS IS IN THIS LIST FOR A REASON. It was not, and voyager.o therefore never rebuilt
# when a header changed - so when EmulatorOptions grew a field, Voyager kept an object that
# disagreed with everything else about how big the struct was, and smashed its stack on every
# run. A clean build hid it, which is why 'make sanitize' would never have found it either.
# Tests/voyager.sh did, by failing all 115 tests that start the machine.
-include $(EMU_OBJS:.o=.d) $(VOY_OBJS:.o=.d) $(ASM_OBJS:.o=.d) $(DSK_OBJS:.o=.d) $(LINT_OBJS:.o=.d) $(PATCH_OBJS:.o=.d) $(TUNE_OBJS:.o=.d)
# ---- The strict build the README promises ----
#
# "The sources are ISO C and build clean under -std=c11 -pedantic with -Wall -Wextra."
# That is a claim somebody may check by typing it, so the suite checks it first. It was
# false when this target was written: realpath went undeclared under a feature test macro
# that did not reach far enough, which the ordinary -Os build never saw.
STRICT = -std=c11 -pedantic -Wall -Wextra -Werror $(POSIXFLAGS)
#
# VOYAGER IS CHECKED SEPARATELY, and only where Raylib is. It includes raylib.h, so putting
# it in the loop below would make 'make test' fail on exactly the machines the two-binary
# split exists to support - and it would not be noticed here, where Raylib is installed.
strict:
@for source in $(SRC_DIR_EMU)/*.c $(SRC_DIR_ASM)/*.c $(SRC_DIR_DSK)/*.c $(SRC_DIR_LINT)/*.c; do \
case "$$source" in *voyager.c) continue ;; esac; \
$(CC) $(STRICT) -c $$source -o /dev/null || exit 1; \
done
ifeq ($(HAVE_RAYLIB),yes)
@$(CC) $(STRICT) $(RAYLIB_CFLAGS) -c $(SRC_DIR_EMU)/voyager.c -o /dev/null
endif
@echo "The sources build clean under -std=c11 -pedantic."
# Run the test suite against the programs in Programs/
test: all strict
@./Tests/lint.sh
@echo
@echo
@./Tests/run.sh
@echo
@./Tests/voyager.sh
@echo
@./Tests/disk.sh
@echo
@./Tests/cycles.sh
@echo
@./Tests/video.sh
@echo
@./Tests/sound.sh
@echo
@./Tests/terminal.sh
@echo
@./Tests/native.sh
@echo
@./Tests/agree.sh
@echo
@./Tests/replay.sh
@echo
@./Tests/docs.sh
# Rebuild all three tools with the address and undefined behaviour sanitizers and run
# the test suite under them. Slower than 'make test', and worth running before a
# release or after anything that touches memory handling.
#
# THE WHOLE SUITE, which it did not used to be: it built all three tools sanitized and
# then ran only run.sh and terminal.sh, so SplitDisk was compiled with the sanitizers and
# never exercised, and native.sh - which drives the assembler and the emulator harder than
# anything else here - was skipped. Those are the parts where block arithmetic on disk
# images and buffer indexing in two assemblers live, which is exactly what the sanitizers
# are for. Adding the three of them costs about six seconds.
#
# The sanitizers catch reads and writes off the end of an array, use after free,
# leaks, and undefined arithmetic. What they do NOT usefully catch here is uninitialised
# memory: AddressSanitizer's junk fill is a toolchain default this build does not
# configure, there are six heap allocations in the repository and the largest is a
# deliberate calloc, and the machine's own memories are static arrays it never touches.
#
# If the suite fails, the sanitizer binaries are deliberately left in place so
# that the failing case can be run again by hand. 'make' puts the normal ones back.
SANITIZE_FLAGS = -Wall -Wextra -g -O1 -fsanitize=address,undefined -fno-omit-frame-pointer
sanitize:
@$(MAKE) --no-print-directory clean
@$(MAKE) --no-print-directory CFLAGS="$(SANITIZE_FLAGS)"
@echo "Running the test suite under AddressSanitizer and UndefinedBehaviorSanitizer."
@./Tests/lint.sh
@echo
@./Tests/run.sh
@echo
@./Tests/voyager.sh
@echo
@./Tests/disk.sh
@echo
@./Tests/cycles.sh
@echo
@./Tests/video.sh
@echo
@./Tests/sound.sh
@echo
@./Tests/terminal.sh
@echo
@./Tests/native.sh
@echo
@./Tests/agree.sh
@echo
@./Tests/replay.sh
@echo
@./Tests/docs.sh
@$(MAKE) --no-print-directory clean
@$(MAKE) --no-print-directory
@echo "Sanitizer run finished cleanly. Normal binaries rebuilt."
# Record the current output of every test as the expected result.
# Only do this when the current output is known to be correct.
bless: $(EMU_TARGET) $(ASM_TARGET)
@./Tests/run.sh --bless
# Clean up object and binary files
clean:
rm -rf $(OBJ_DIR)
rm -rf Tests/build
rm -rf $(PROG_BUILD)
rm -f $(SRC_DIR_EMU)/rom.c
rm -f $(EMU_TARGET) $(ASM_TARGET) $(DSK_TARGET) $(LINT_TARGET) $(VOY_TARGET) $(PATCH_TARGET) $(TUNE_TARGET)
# Install compiled binaries
install: $(EMU_TARGET) $(ASM_TARGET) $(DSK_TARGET) $(LINT_TARGET)
mkdir -p "$(PREFIX)/bin"
install -m 755 $^ "$(PREFIX)/bin/"
# =====================================================================================
# The SplitBit programs: everything written in assembly rather than in C.
# =====================================================================================
#
programs: $(BINARIES)
# ---- Assembling needs the assembler ----
#
# An ORDER ONLY prerequisite, which is the bar after the pipe. It makes sure the assembler
# exists before anything is assembled with it - which the two makefile arrangement got for
# free, because you had already built the tools before you went into Programs. It is not an
# ordinary prerequisite because that would reassemble every program on every relink of the
# assembler, which is a rebuild of the whole system whenever a C file changes.
#
# -M writes out which source files went into the binary, in the form of a make rule.
$(PROG_BUILD)/%.bin: $(PROG_DIR)/%.asm | $(ASM_TARGET)
@mkdir -p $(@D)
$(ASM_RUN) $(INCLUDES) -M $(@:.bin=.d) -o $@ $<
# Assemble and run a single program, as in 'make run-hello'.
#
# THE NAME IS THE PROGRAM'S, NOT ITS PATH. This used to be a pattern rule against the build
# directory, which worked while every program sat at the top of Programs/ and stopped working
# the moment they were filed into Examples/ - 'make run-hello' had nothing to match. Somebody
# trying the one command the header advertises should not be the way that is discovered, so
# the name is looked up among the programs instead.
run-%:
@target=`echo $(BINARIES) | tr ' ' '\n' | grep -E "(^|/)$*\.bin$$" | head -1`; \
if [ -z "$$target" ]; then \
echo "There is no program called '$*'. What there is:"; \
echo $(PROGRAM_SOURCES) | tr ' ' '\n' | sed 's|.*/||;s|\.asm$$||;s|^| |'; \
exit 1; \
fi; \
$(MAKE) --no-print-directory "$$target" && $(EMU_RUN) "$$target"
# ---- CosmOS ----
#
# make cosmos Assemble the system and everything it can load.
# make disk ... and put the loadable programs on a disk image.
# make run-cosmos ... and boot the machine with that disk in the drive.
# make run-voyager ... and boot the machine that has a screen instead of a terminal.
#
# Programs in Apps/ say where they live with #Base, so the assembler writes them out as
# loadable programs rather than as boot images. They are named .sbx to keep that difference
# visible: a .bin is something the machine boots, a .sbx is something a running system loads.
# ---- What starts the machine ----
#
# Stage two goes into a boot slot as RAW BYTES: stage one reads blocks into Program Memory
# and jumps to the first one, so a sixteen byte header would be sixteen bytes of nonsense
# executed first. Its Data Segment travels with it and it copies that down itself.
#
# Stage one is not here. It is the ROM, built into the emulator by the rule further up from
# the same source, which is what makes it the one part of this that a disk cannot replace.
# That the two now sit in one file is an improvement on its own: the ROM rule and the boot
# slot rule are the same boot chain and used to be a directory apart.
STAGE2 = $(PROG_BUILD)/Boot/stage2.raw
PROG_DEPS += $(PROG_BUILD)/Boot/stage2.d
$(STAGE2): $(PROG_DIR)/Boot/stage2.asm | $(ASM_TARGET)
@mkdir -p $(@D)
$(ASM_RUN) $(INCLUDES) -M $(PROG_BUILD)/Boot/stage2.d \
-o $(PROG_BUILD)/Boot/stage2.sbx $<
tail -c +17 $(PROG_BUILD)/Boot/stage2.sbx > $@
PROG_DEPS += $(APPS:.sbx=.d)
$(PROG_BUILD)/CosmOS/Apps/%.sbx: $(PROG_DIR)/CosmOS/Apps/%.asm | $(ASM_TARGET)
@mkdir -p $(@D)
$(ASM_RUN) $(INCLUDES) -M $(@:.sbx=.d) -o $@ $<
# The assembler that runs on the machine. It is not in Apps/ because it is not one file: it
# has a directory of its own, the way the C assembler does. Its own pieces are found beside
# it without being told, since an include is looked for next to the file that asked for it
# before anywhere else; only services.asm needs the include path.
NATIVE_ASM = $(PROG_BUILD)/CosmOS/Assembler/Asm.sbx
PROG_DEPS += $(NATIVE_ASM:.sbx=.d)
$(NATIVE_ASM): $(PROG_DIR)/CosmOS/Assembler/Asm.asm | $(ASM_TARGET)
@mkdir -p $(@D)
$(ASM_RUN) $(INCLUDES) -M $(@:.sbx=.d) -o $@ $<
cosmos: $(COSMOS) $(APPS) $(NATIVE_ASM)
# Made from scratch every time, so that what is on it is what is in Apps/ now and not also
# whatever used to be.
#
# TWENTY FOUR DIRECTORY BLOCKS, WHICH IS ONE HUNDRED AND NINETY TWO NAMES. It was eight, and
# that is sixty four, of which thirty nine were already spoken for. The two ceilings a disk
# has were nowhere near each other: at the average file on here, twenty six blocks, sixty
# four names run out with the disk forty one per cent full. Names were going to be gone long
# before space was.
#
# A directory block is 256 bytes and holds eight entries, so the difference costs sixteen
# blocks of four thousand and ninety six - three tenths of one per cent - to buy a hundred
# and twenty eight more names. The superblock has carried this number per disk since the
# format was written, so nothing but this line knows what it is.
#
# ---- And on the recipe that lays it out ----
#
# Changing HOW the disk is built has to rebuild the disk. It did not, so adding the libraries
# to /Lib left an image that had been made without them, and the next run said the same thing
# was still missing - which sends you looking at the change you just made rather than at the
# stale thing in front of you.
$(COSMOS_DISK): makefile
# ---- And on everything it mirrors ----
#
# The mirror exists so that adding a file is the whole of putting it on the disk, and that
# only works if adding a file also REBUILDS the disk - but the prerequisites were as
# hand-maintained as the list the mirror replaced. tune.asm went into Examples, the image was
# not remade, and it was simply not there to assemble on the machine. Nothing said so, which
# is the failure a mirror is for.
#
# Found rather than wildcarded, because the tree the mirror walks is a tree and $(wildcard)
# does not recurse. build is left out for the reason the mirror leaves it out.
MIRRORED := $(shell find $(PROG_DIR) \( -name '*.asm' -o -name '*.score' \) -not -path '$(PROG_BUILD)/*' 2>/dev/null)
$(COSMOS_DISK): $(MIRRORED)
$(COSMOS_DISK): $(APPS) $(NATIVE_ASM) $(COSMOS) $(STAGE2) \
$(PROG_DIR)/testPrograms/stringKeyword.asm \
$(wildcard $(PROG_DIR)/CosmOS/Apps/*.asm) \
$(wildcard $(PROG_DIR)/CosmOS/Source/*.asm) \
$(wildcard $(PROG_DIR)/CosmOS/Assembler/*.asm) \
| $(DSK_TARGET) $(PATCH_TARGET) $(TUNE_TARGET)
@mkdir -p $(@D)
rm -f $@
@# A BOOT AREA, so that this is a disk the machine can start itself from rather than
@# one it has to be handed. Forty blocks a slot and two slots: stage two is about
@# eight thousand bytes, and the second slot is what makes replacing it survivable,
@# since raw blocks have no name and so nothing to rename.
@# ---- Room for the whole source tree ----
@#
@# Sixteen thousand blocks is four megabytes, which is absurd for a machine with 128K
@# of memory and exactly right for a disk: the sources alone are 2,850 blocks and the
@# point of mirroring them is that nobody has to think about it again when they add
@# one. A hundred and twenty-eight directory blocks is 1,024 entries against the 149
@# the tree has now, for the same reason - it was 24, which is 192, and the mirror
@# filled it on its first run.
$(DISKTOOL) format $@ 16384 128 40
$(DISKTOOL) boot $@ $(STAGE2) 0
@# THREE DIRECTORIES, WHICH IS WHAT A CLEAN INSTALL LOOKS LIKE: what you run, what you
@# assemble, and what those include. It was thirty nine files in one list with
@# cosmos.asm sitting between fileStream.asm and sbfs.asm.
@#
@# The split is by ROLE rather than by which directory the host keeps them in. /Source
@# holds the things you name to the assembler and /Lib the things they pull in, which is
@# a distinction the host makes with -I and the machine now makes with a search path of
@# its own: an include is looked for beside you and then in /Lib. Without that, every
@# source that calls a service would have to sit in the same directory as services.asm
@# and there would be nothing to organise.
$(DISKTOOL) mkdir $@ /Apps
$(DISKTOOL) mkdir $@ /Source
$(DISKTOOL) mkdir $@ /Lib
$(DISKTOOL) mkdir $@ /System
$(DISKTOOL) mkdir $@ /System/Boot
@# The system itself, as a file, which is the whole of what boot.cfg chooses between.
@# No boot.cfg is written: stage two falls back to this name when there is none, and a
@# clean install having nothing to configure is the right default.
$(DISKTOOL) put $@ $(COSMOS) /System/Boot/cosmos.bin
@# What you run. /Apps is the second place the shell looks when a word it does not know
@# turns out to be a program, so anything in here starts by name from anywhere.
@for app in $(APPS); do \
$(DISKTOOL) put $@ $$app /Apps/`basename $$app` >/dev/null || exit 1; done
$(DISKTOOL) put $@ $(NATIVE_ASM) /Apps/Asm.sbx
@# ---- And the music, compiled ----
@#
@# A .score is written and a .tune is what the machine reads, the same way a .asm is
@# written and a .sbx is what it loads. The patches come from SoundPatch, which stays the
@# only thing that knows what a soundThing patch means; TuneC embeds the bytes and knows
@# nothing about them.
@# EVERY SCORE, NOT A NAMED ONE. A list here goes stale the moment somebody writes a
@# second piece of music, and what they forgot is invisible until they go looking for
@# it on the machine - which is the same argument the mirror above is made of, and the
@# same way it went wrong the first time: splash.score was named and intro.score sat
@# beside it compiling into nothing.
@#
@# The patches every score can name are converted first. They come from SoundPatch,
@# which stays the only thing that knows what a soundThing patch means.
@mkdir -p $(PROG_BUILD)/tunes
@for json in $(PROG_DIR)/Sounds/*.json; do \
$(PATCH_RUN) --blob $$json `basename $$json .json` \
$(PROG_BUILD)/tunes/`basename $$json .json | tr 'A-Z' 'a-z'`.patch \
>/dev/null || exit 1; done
@for score in $(PROG_DIR)/Tunes/*.score; do \
name=`basename $$score .score`; \
$(TUNE_RUN) -I $(PROG_BUILD)/tunes $$score $(PROG_BUILD)/tunes/$$name.tune \
>/dev/null || exit 1; \
$(DISKTOOL) put $@ $(PROG_BUILD)/tunes/$$name.tune $$name.tune >/dev/null \
|| exit 1; done
@# ---- What you assemble: all of it ----
@#
@# MIRRORED RATHER THAN LISTED. A list in a makefile goes stale the moment somebody
@# adds a program and forgets to name it here, and what they forgot is invisible until
@# they go looking for it on the machine. Now putting a file where the others live is
@# the whole of putting it on the disk.
@#
@# THE DIRECTORY IT WALKS IS NAMED, and that is load bearing now that this file sits at
@# the top of the tree. It used to be "." and mean Programs/, because the makefile was
@# in there. Left as "." it would mean the whole repository - the C sources, the tests,
@# the manuals - mirrored onto a disk for an 8-bit machine.
@#
@# build is left behind, because what a project builds is not what it wrote. Anything
@# with a name longer than a directory entry holds is refused rather than skipped: a
@# disk quietly missing a file is the failure a mirror exists to prevent.
$(DISKTOOL) mirror $@ $(PROG_DIR) /Source build
@# ---- And the libraries proper ----
@#
@# Programs/Libraries is what an #Include means when it is not a CosmOS source: print,
@# the integer helpers, the maths. They were never here, because /Lib was a hand-written
@# list and nobody thought of them - so Sieve-16.asm, Life.asm and Fib-16.asm could be
@# read on the machine and not assembled on it, and the assembler said only "nothing was
@# written". Mirrored, so that the next one nobody thinks of is here anyway.
$(DISKTOOL) mirror $@ $(PROG_DIR)/Libraries /Lib
@for f in console fileStream sbfs services text config script; do \
$(DISKTOOL) put $@ $(PROG_DIR)/CosmOS/Source/$$f.asm /Lib/$$f.asm >/dev/null \
|| exit 1; done
@for f in classify labels numbers scratch source table token vectors; do \
$(DISKTOOL) put $@ $(PROG_DIR)/CosmOS/Assembler/$$f.asm /Lib/$$f.asm >/dev/null \
|| exit 1; done
# The system as well as the disk. Building only the image leaves whatever cosmos.bin was
# there before, or none at all, and then the disk is booted with a system that does not match
# the programs on it.
disk: $(COSMOS) $(COSMOS_DISK)
# THE MACHINE STARTS ITSELF. No image is named, so the emulator shadows its ROM into Program
# Memory and that reads the disk for everything else - a boot slot, then a loader, then
# whatever /System/Boot/boot.cfg names, or cosmos.bin when it names nothing.
$(PERSONAL_DISK):
@mkdir -p $(@D)
$(DISKTOOL) format $@ 2048 8
@echo " That disk is yours. It is drive 1, and nothing in this makefile will write to"
@echo " it again - not clean, not a rebuild. Delete it by hand if you want a new one."
@echo " Starting the machine copies it to $(PERSONAL_DISK).1, keeping $(PERSONAL_BACKUPS) starts back."
# ---- And kept a few starts back ----
#
# Copied before every start, and KEPT SEVERAL DEEP rather than copied over one file.
#
# ONE BACKUP TAKEN AT EVERY START IS WORSE THAN NONE. The way a disk is lost is that
# something goes wrong - and the very next thing anybody does is start the machine again to
# see how bad it is, which is exactly when a single backup is overwritten by the wreckage.
# Three deep means the damage has to happen and then be started past three times before the
# copy that would have helped is gone.
#
# AND IT IS NOT GUARDED BY "does the disk still look all right", because that check cannot
# be written. The disk that went missing here was a perfectly valid and perfectly empty
# filesystem: the format had succeeded. Nothing about a disk says it has lost anything,
# which is the whole reason to keep the old ones rather than to judge the new one.
#
# Outside clean's reach, like the disk itself. A backup a rebuild deletes is not one.
personalBackup:
@if [ -f $(PERSONAL_DISK) ]; then \
n=$(PERSONAL_BACKUPS); \
while [ $$n -gt 1 ]; do \
p=$$((n - 1)); \
if [ -f $(PERSONAL_DISK).$$p ]; then cp -f $(PERSONAL_DISK).$$p $(PERSONAL_DISK).$$n; fi; \
n=$$p; \
done; \
cp -f $(PERSONAL_DISK) $(PERSONAL_DISK).1; \
fi
run-cosmos: $(COSMOS_DISK) $(PERSONAL_DISK) personalBackup
$(EMU_RUN) --disk $(COSMOS_DISK) --disk $(PERSONAL_DISK) --ram-disk $(SCRATCH_BLOCKS)
# The same disk with the system handed over directly instead, which is what a debugger does:
# memory is placed from outside and nothing on the disk is consulted about it. Useful when the
# thing being debugged is the boot chain itself, since it skips the boot chain.
run-cosmos-direct: $(COSMOS) $(COSMOS_DISK) $(PERSONAL_DISK) personalBackup
$(EMU_RUN) --disk $(COSMOS_DISK) --disk $(PERSONAL_DISK) --ram-disk $(SCRATCH_BLOCKS) $(COSMOS)
# ---- The same disk, on the machine with a screen ----
#
# Voyager rather than SplitBit, which is the only difference: same disk, same system, same
# programs, presented through a window instead of a terminal.
#
# IT DEPENDS ON THE DISK, and that matters more than it looks. What is on a disk is whatever
# was built when the disk was made, so a machine whose console has changed will happily boot
# an image full of programs written for the old one - and they will draw whatever the old way
# now means. Making the disk a dependency of running it is what stops that being a puzzle.
run-voyager: $(COSMOS_DISK) $(PERSONAL_DISK) personalBackup
$(VOY_RUN) --disk $(COSMOS_DISK) --disk $(PERSONAL_DISK) --ram-disk $(SCRATCH_BLOCKS)
run-voyager-direct: $(COSMOS) $(COSMOS_DISK) $(PERSONAL_DISK) personalBackup
$(VOY_RUN) --disk $(COSMOS_DISK) --disk $(PERSONAL_DISK) --ram-disk $(SCRATCH_BLOCKS) $(COSMOS)
# Pull in the dependency rules the assembler wrote with -M, so that touching a library
# reassembles every program that includes it.
-include $(PROG_DEPS)
# Phony targets
.PHONY: all clean install test bless sanitize strict personalBackup \
programs cosmos disk run-cosmos run-cosmos-direct run-voyager run-voyager-direct