# 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_LIBS   := $(shell pkg-config --libs raylib 2>/dev/null)
ifeq ($(strip $(RAYLIB_LIBS)),)
# The -lm is not spare, even though MATHLIB names it again on the link line below. This
# variable is what the probe underneath test-links with, and Raylib does not link without it,
# so taking it out here does not tidy a duplicate - it makes the probe say Raylib is missing.
RAYLIB_LIBS := -lraylib -lm
endif
HAVE_RAYLIB := $(shell printf '#include <raylib.h>\nint main(void){return (int)GetTime();}\n' \
                 | $(CC) -x c - -o /dev/null $(RAYLIB_CFLAGS) $(RAYLIB_LIBS) 2>/dev/null \
                 && echo 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/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/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
