One byte a frame, in exactly the format the player takes, so a recording needs no conversion and there is no second format to keep in step. That symmetry is the feature, and it makes the strongest form of the claim testable: a recording is made OF a playback, and the bytes coming out have to be the bytes that went in. It exists because some inputs cannot sensibly be written by hand. Flying a lander from one base to another is a few hundred frames of steering that has to arrive somewhere eight cells wide, and several attempts at authoring one got within two columns and no closer. That is a piloting exercise rather than a test. Playing it once and keeping what happened is the answer. A BYTE FOR EVERY FRAME, written inside the loop that advances the recordings rather than after it, so a machine that jumped several frames at once still writes one for each. A recording is a timeline: one that skipped the frames nobody looked at would play back faster than it was flown. What is recorded is what the DEVICE WOULD REPORT, not the live state - a recording of a playback that wrote the live state would be a file of noughts. And it is flushed as it goes, because a recording is usually stopped by whoever is playing rather than by the program ending, and a demo lost to a buffer is a demo flown twice. Tests/replay.sh is where this and whatever follows it are checked. Twelve scripts now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
704 lines
33 KiB
Makefile
704 lines
33 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
|
|
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
|
|
|
|
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
|
|
|
|
# Output binary names
|
|
EMU_TARGET = SplitBit
|
|
VOY_TARGET = Voyager
|
|
ASM_TARGET = Assembler
|
|
DSK_TARGET = SplitDisk
|
|
LINT_TARGET = SplitLint
|
|
|
|
# ---- 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)
|
|
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)/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)
|
|
|
|
# ---- 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)
|
|
|
|
$(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)
|
|
|
|
# ---- 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)
|
|
|
|
# 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' -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)
|
|
@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
|
|
@# ---- 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
|