# 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 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 \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 # ---- 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 touch it" @echo " again - not clean, not a rebuild. Delete it by hand if you want a new one." run-cosmos: $(COSMOS_DISK) $(PERSONAL_DISK) $(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) $(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) $(VOY_RUN) --disk $(COSMOS_DISK) --disk $(PERSONAL_DISK) --ram-disk $(SCRATCH_BLOCKS) run-voyager-direct: $(COSMOS) $(COSMOS_DISK) $(PERSONAL_DISK) $(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 \ programs cosmos disk run-cosmos run-cosmos-direct run-voyager run-voyager-direct