diff --git a/Programs/CosmOS/Apps/Sieve-16.asm b/Programs/CosmOS/Apps/Sieve-16.asm index 24e3fef..c804caa 100644 --- a/Programs/CosmOS/Apps/Sieve-16.asm +++ b/Programs/CosmOS/Apps/Sieve-16.asm @@ -1,7 +1,7 @@ ; The 16-bit segmented sieve rewritten for SplitBit's four-Data-Pointer ISA. ; ; This deliberately implements the same algorithm and emits the same text as -; 16bitSegmentedSieve.asm, making the two versions useful as a direct comparison. +; 16bitSieve.asm, making the two versions useful as a direct comparison. ; DP0 walks PrimeStates, DP1 holds Page, DP2 walks Segment, and volatile DP3 ; marks multiples. CALL preserves the first three pointers automatically. diff --git a/Programs/CosmOS/README.md b/Programs/CosmOS/README.md index 86e5043..d522900 100644 --- a/Programs/CosmOS/README.md +++ b/Programs/CosmOS/README.md @@ -93,6 +93,31 @@ The generated files are kept under `Programs/build/`: - `CosmOS/Apps/*.sbx` are loadable application images. - `cosmos.img` is the SBFS disk containing those applications. +**The disk carries every source in `Programs/`, mirrored.** Not a list kept in the makefile - +a list goes stale the moment somebody adds a program and forgets to name it, and what they +forgot is invisible until they go looking for it on the machine. Putting a file where the +others live is the whole of putting it on the disk. + +That matters most for the things nobody thought worth shipping a binary of. A demo that is +not interesting enough to build by default is still worth having the source of, because the +machine can build it: + +``` +> cd /Source/Examples +/Source/Examples> Asm colours.asm +wrote colours.bin: program 114, data 86, labels 8 +``` + +Two things are left behind. `build`, because what a project builds is not what it wrote. And +anything whose name is longer than a directory entry holds, which is **refused rather than +skipped**: a disk quietly missing a file is exactly the failure a mirror exists to prevent, +so the build stops and says which name to shorten. + +`/Lib` still holds the library sources separately, because that is where an `#Include` looks +after looking beside the file that asked. The same files therefore appear twice - once as +what a program includes, once as part of the source tree - and that is the difference between +an installed library and a copy of the source. + The disk is rebuilt from scratch when its applications change, so its contents describe the current source tree rather than accumulating files left by older builds. diff --git a/Programs/Examples/primeSieve/16bitSegmentedSieve.asm b/Programs/Examples/primeSieve/16bitSieve.asm similarity index 100% rename from Programs/Examples/primeSieve/16bitSegmentedSieve.asm rename to Programs/Examples/primeSieve/16bitSieve.asm diff --git a/Programs/Examples/primeSieve/16bitSegmentedSieveModern.asm b/Programs/Examples/primeSieve/16bitSieveModern.asm similarity index 98% rename from Programs/Examples/primeSieve/16bitSegmentedSieveModern.asm rename to Programs/Examples/primeSieve/16bitSieveModern.asm index b1c0ae1..951becd 100644 --- a/Programs/Examples/primeSieve/16bitSegmentedSieveModern.asm +++ b/Programs/Examples/primeSieve/16bitSieveModern.asm @@ -1,7 +1,7 @@ ; The 16-bit segmented sieve rewritten for SplitBit's four-Data-Pointer ISA. ; ; This deliberately implements the same algorithm and emits the same text as -; 16bitSegmentedSieve.asm, making the two versions useful as a direct comparison. +; 16bitSieve.asm, making the two versions useful as a direct comparison. ; DP0 walks PrimeStates, DP1 holds Page, DP2 walks Segment, and volatile DP3 ; marks multiples. CALL preserves the first three pointers automatically. diff --git a/Programs/makefile b/Programs/makefile index 570d7f9..200510a 100644 --- a/Programs/makefile +++ b/Programs/makefile @@ -138,7 +138,15 @@ $(COSMOS_DISK): $(APPS) $(NATIVE_ASM) $(COSMOS) $(STAGE2) testPrograms/stringKey @# 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. - $(DISKTOOL) format $@ 4096 24 40 + @# ---- 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 @@ -164,37 +172,24 @@ $(COSMOS_DISK): $(APPS) $(NATIVE_ASM) $(COSMOS) $(STAGE2) testPrograms/stringKey @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, because an assembler with nothing to assemble is a - @# demonstration of nothing: + @# ---- What you assemble: all of it ---- @# - @# > cd /Source - @# /Source> Asm cosmos.asm the system it is running on - @# /Source> Asm Asm.asm and the thing that built 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. @# - @# Both come out byte for byte what the host tool makes from the same source. + @# That matters most for the things nobody thought worth building an .sbx of. A demo + @# that is not interesting enough to ship as a binary is still worth having the source + @# of, because somebody curious can assemble it on the machine itself: @# - @# Keys.asm brings a vector of its own, so assembling it exercises the version two - @# header and the Vector Segment: the loader installs its handler, the console - @# interrupts into it, and the shell takes the vector back at exit. + @# > cd /Source/Examples + @# /Source/Examples> Asm colours.asm @# - @# strings.asm is the odd one out on purpose. It has no #Include and no #Base, so it - @# comes out as a boot image rather than a loadable program, and the difference between - @# the two is visible on one disk. - $(DISKTOOL) put $@ CosmOS/Source/cosmos.asm /Source/cosmos.asm - $(DISKTOOL) put $@ CosmOS/Assembler/Asm.asm /Source/Asm.asm - $(DISKTOOL) put $@ CosmOS/Assembler/readTest.asm /Source/readTest.asm - $(DISKTOOL) put $@ CosmOS/Assembler/tokenTest.asm /Source/tokenTest.asm - $(DISKTOOL) put $@ CosmOS/Apps/hello.asm /Source/hello.asm - $(DISKTOOL) put $@ CosmOS/Apps/Say.asm /Source/Say.asm - $(DISKTOOL) put $@ CosmOS/Apps/Keys.asm /Source/Keys.asm - $(DISKTOOL) put $@ testPrograms/stringKeyword.asm /Source/strings.asm - @# And the loader, so the machine can rebuild what starts it. Assembling stage2.asm on - @# the machine and writing the result into the other boot slot is the whole of a - @# self-hosted boot chain, and everything it includes is already in /Lib. - $(DISKTOOL) put $@ Boot/stage1.asm /Source/stage1.asm - $(DISKTOOL) put $@ Boot/stage2.asm /Source/stage2.asm - @# And what those include. Everything here is named by an #Include somewhere and by - @# nothing else, which is exactly what makes it a library rather than a source. + @# 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 $@ . /Source build @for f in CosmOS/Source/console.asm CosmOS/Source/fileStream.asm \ CosmOS/Source/sbfs.asm CosmOS/Source/services.asm CosmOS/Source/text.asm \ CosmOS/Source/config.asm \ diff --git a/Programs/testPrograms/consoleInterruptTest.asm b/Programs/testPrograms/consoleInterrupt.asm similarity index 100% rename from Programs/testPrograms/consoleInterruptTest.asm rename to Programs/testPrograms/consoleInterrupt.asm diff --git a/Programs/testPrograms/consoleModeTest.asm b/Programs/testPrograms/consoleModeTest.asm index ced4d32..417cfa8 100644 --- a/Programs/testPrograms/consoleModeTest.asm +++ b/Programs/testPrograms/consoleModeTest.asm @@ -12,7 +12,7 @@ ; ; There is a fourth bit, for whether the console interrupts on input, and nothing here ; sets it. Polling and interrupting are the two ways to get a byte and this is the one -; about polling; consoleInterruptTest.asm is the other. +; about polling; consoleInterrupt.asm is the other. ; ; This runs with input from a file rather than a terminal, so key mode has no terminal to ; put into another state and the mode bit is the only thing that changes. That is on diff --git a/Programs/testPrograms/controllerWriteTest.asm b/Programs/testPrograms/controllerWrite.asm similarity index 100% rename from Programs/testPrograms/controllerWriteTest.asm rename to Programs/testPrograms/controllerWrite.asm diff --git a/README.md b/README.md index 5c92ff3..4faf70f 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,9 @@ make run-cosmos Then `dir` to see what is there, `load Snake.sbx` and `run` to play something, or `load Asm.sbx` and `run cosmos.asm` to watch the machine build itself. +Every source in `Programs/` is on that disk, under `/Source`, so anything not shipped as a +binary can still be assembled on the machine: `cd /Source/Examples` and `Asm colours.asm`. + `make run-voyager` boots the same disk on the machine with a screen instead of a terminal. Both targets depend on the disk, so a disk built before a change to the machine is rebuilt rather than booted as it stands: **what is on a disk is whatever was built when the disk was diff --git a/Source/DiskTool/SplitDisk.c b/Source/DiskTool/SplitDisk.c index a773a52..c05efe4 100644 --- a/Source/DiskTool/SplitDisk.c +++ b/Source/DiskTool/SplitDisk.c @@ -12,6 +12,10 @@ #include #include #include +// For mirroring a host directory onto a disk: walking one, and telling a directory from a +// file. POSIX rather than C, which is why the build asks for POSIX.1-2008 by name. +#include +#include // Numbers on a SplitBit disk are most significant byte first, the same as everywhere // else on the machine. @@ -1108,6 +1112,137 @@ done: // disk - so the children of a deleted directory would reappear inside whatever took its // place. Emptying it first is the only safe order, and making the caller do that is the // smallest way to guarantee it. +// ---- Mirroring a host directory onto a disk ---- +// +// So that putting a new program where the others live is all it takes to have it on the +// machine. A list of files in a makefile is a list that goes stale the moment somebody adds +// something and forgets, and the thing they forgot is invisible until they look for it. +// +// EVERY FILE GOES THROUGH put AND EVERY DIRECTORY THROUGH mkdir, which is the point: this +// adds a walk and no filesystem code at all, so anything the format refuses here it refuses +// everywhere, in exactly the same words. + +static int compareEntries(const void *left, const void *right) { + return strcmp(*(const char *const *)left, *(const char *const *)right); +} + +static int mirrorDirectory(const char *path, const char *hostDir, const char *diskDir, + int skipCount, char *const skips[]) { + DIR *open = opendir(hostDir); + if (open == NULL) { + fprintf(stderr, "Error: Couldn't read the directory \"%s\".\n", hostDir); + return 1; + } + + // ---- Read the names first, and sort them ---- + // + // readdir hands them back in whatever order the host filesystem feels like, and a disk + // image that comes out different from one run to the next is an image no test can + // compare against another. Sorted, the same tree always makes the same disk. + char **names = NULL; + size_t count = 0, room = 0; + const struct dirent *entry; + while ((entry = readdir(open)) != NULL) { + // Nothing beginning with a dot: that is . and .. and every editor's leavings, and + // none of it is source anybody wants on the machine. + if (entry->d_name[0] == '.') { + continue; + } + int skipped = 0; + for (int i = 0; i < skipCount; i++) { + if (strcmp(entry->d_name, skips[i]) == 0) { + skipped = 1; + } + } + if (skipped) { + continue; + } + if (count == room) { + room = room ? room * 2 : 32; + char **grown = realloc(names, room * sizeof(*names)); + if (grown == NULL) { + fprintf(stderr, "Error: Out of memory reading \"%s\".\n", hostDir); + closedir(open); + for (size_t i = 0; i < count; i++) free(names[i]); + free(names); + return 1; + } + names = grown; + } + names[count] = strdup(entry->d_name); + if (names[count] == NULL) { + fprintf(stderr, "Error: Out of memory reading \"%s\".\n", hostDir); + closedir(open); + for (size_t i = 0; i < count; i++) free(names[i]); + free(names); + return 1; + } + count++; + } + closedir(open); + qsort(names, count, sizeof(*names), compareEntries); + + int failed = 0; + for (size_t i = 0; i < count; i++) { + char hostChild[1024]; + char diskChild[1024]; + if (snprintf(hostChild, sizeof(hostChild), "%s/%s", hostDir, names[i]) + >= (int)sizeof(hostChild) + || snprintf(diskChild, sizeof(diskChild), "%s/%s", diskDir, names[i]) + >= (int)sizeof(diskChild)) { + fprintf(stderr, "Error: \"%s/%s\" makes a path too long to follow.\n", + hostDir, names[i]); + failed = 1; + continue; + } + + // A NAME TOO LONG IS AN ERROR RATHER THAN A SKIP. Leaving it off would mean a build + // that looks like it worked and a disk quietly missing a program, which is the exact + // failure a mirror exists to prevent. Twenty-two bytes is what a directory entry + // holds, and the fix is to call the file something shorter. + if (strlen(names[i]) > SBFS_NAME_BYTES) { + fprintf(stderr, "Error: \"%s\" is %zu characters, and a name holds %d.\n", + names[i], strlen(names[i]), SBFS_NAME_BYTES); + failed = 1; + continue; + } + + struct stat about; + if (stat(hostChild, &about) != 0) { + fprintf(stderr, "Error: Couldn't look at \"%s\".\n", hostChild); + failed = 1; + continue; + } + if (S_ISDIR(about.st_mode)) { + if (commandMakeDirectory(path, diskChild) + || mirrorDirectory(path, hostChild, diskChild, skipCount, skips)) { + failed = 1; + } + } else if (S_ISREG(about.st_mode)) { + if (commandPut(path, hostChild, diskChild)) { + failed = 1; + } + } + // Anything else - a socket, a device, whatever a host has - is not a thing this + // filesystem has a way to be, so it is passed over without comment. + } + for (size_t i = 0; i < count; i++) { + free(names[i]); + } + free(names); + return failed; +} + +static int commandMirror(const char *path, const char *hostDir, const char *diskDir, + int skipCount, char *const skips[]) { + struct stat about; + if (stat(hostDir, &about) != 0 || !S_ISDIR(about.st_mode)) { + fprintf(stderr, "Error: \"%s\" is not a directory to mirror.\n", hostDir); + return 1; + } + return mirrorDirectory(path, hostDir, diskDir, skipCount, skips); +} + static int commandRemoveDirectory(const char *path, const char *name) { FILE *image = openImage(path, "r+b"); if (image == NULL) { @@ -1162,6 +1297,8 @@ static void printUsage(const char *program) { printf(" delete Remove a file.\n"); printf(" mkdir Make a directory.\n"); printf(" rmdir Remove an empty one.\n"); + printf(" mirror [skip...] Copy a whole host directory onto it,\n"); + printf(" leaving behind anything named in skip.\n"); printf("\n"); printf("Blocks are %d bytes. A name may be %d characters, and a path is names with\n", SBFS_BLOCK_BYTES, SBFS_NAME_BYTES); @@ -1256,6 +1393,16 @@ int main(int argc, char *argv[]) { } return commandGet(path, argv[3], (argc > 4) ? argv[4] : argv[3]); } + if (strcmp(command, "mirror") == 0) { + if (argc < 5) { + fprintf(stderr, "Error: mirror needs a directory to copy and somewhere to put" + " it.\n"); + return 1; + } + // Anything after those is a name to leave behind, which is how a project keeps what + // it builds out of what it wrote. + return commandMirror(path, argv[3], argv[4], argc - 5, &argv[5]); + } if (strcmp(command, "delete") == 0) { if (argc < 4) { fprintf(stderr, "Error: delete needs the name of a file on the disk.\n"); diff --git a/Tests/disk.sh b/Tests/disk.sh index 901fa9d..0976324 100755 --- a/Tests/disk.sh +++ b/Tests/disk.sh @@ -76,7 +76,7 @@ for f in empty.bin one.bin exact.bin part.bin whole.bin; do check "$f comes back byte for byte" roundTrip "$f" done -refuses "refuse a name of 29 characters" "$TOOL" put work.img part.bin 16bitSegmentedSieveModern.asm +refuses "refuse a name of 29 characters" "$TOOL" put work.img part.bin twentyNineCharactersLong.asm refuses "refuse a duplicate name" "$TOOL" put work.img one.bin refuses "refuse a file that is not there" "$TOOL" get work.img nosuch.bin out.bin check "delete" "$TOOL" delete work.img one.bin @@ -228,6 +228,46 @@ check "a fresh disk is settled" python3 -c " import sys sys.exit(0 if open('plain.img','rb').read()[17] == 0 else 1)" +# ---- Mirroring a host directory ---- +# +# What the system disk is built with. Every file goes through put and every directory +# through mkdir, so this is a walk over machinery already checked above - what wants +# checking is the walk: that it goes all the way down, that it leaves behind what it was +# told to, and that it REFUSES a name the format cannot hold rather than skipping it, since +# a disk quietly missing a file is the failure a mirror exists to prevent. +mkdir -p tree/inner/deeper tree/leave +printf 'top' > tree/top.txt +printf 'inner' > tree/inner/middle.txt +printf 'deep' > tree/inner/deeper/bottom.txt +printf 'not this' > tree/leave/ignored.txt +: > tree/.hidden + +"$TOOL" format mirror.img 256 8 >/dev/null +check "mirror a directory tree" "$TOOL" mirror mirror.img tree / + +"$TOOL" list mirror.img > mirrored.txt 2>&1 +grep -q '/inner/deeper/bottom.txt' mirrored.txt \ + && { PASS=$((PASS + 1)); printf " [%sok %s] %s\n" "$GREEN" "$RESET" "it goes all the way down"; } \ + || { FAIL=$((FAIL + 1)); FAILED_NAMES+=("depth"); printf " [%sFAIL%s] %s\n" "$RED" "$RESET" "it goes all the way down"; } +grep -q 'hidden' mirrored.txt \ + && { FAIL=$((FAIL + 1)); FAILED_NAMES+=("hidden"); printf " [%sFAIL%s] %s\n" "$RED" "$RESET" "and leaves dotfiles behind"; } \ + || { PASS=$((PASS + 1)); printf " [%sok %s] %s\n" "$GREEN" "$RESET" "and leaves dotfiles behind"; } + +# Named on the command line, which is how a project keeps what it builds out of what it +# wrote. +"$TOOL" format skipped.img 256 8 >/dev/null +check "mirror with something left out" "$TOOL" mirror skipped.img tree / leave +"$TOOL" list skipped.img > skipped.txt 2>&1 +grep -q 'ignored.txt' skipped.txt \ + && { FAIL=$((FAIL + 1)); FAILED_NAMES+=("skip"); printf " [%sFAIL%s] %s\n" "$RED" "$RESET" "and the skipped one is not there"; } \ + || { PASS=$((PASS + 1)); printf " [%sok %s] %s\n" "$GREEN" "$RESET" "and the skipped one is not there"; } + +# Twenty-three characters, one more than a directory entry holds. +printf 'too long' > tree/aNameOfTwentyThreeChars +"$TOOL" format refused.img 256 8 >/dev/null +refuses "a name too long stops the mirror" "$TOOL" mirror refused.img tree / +rm -f tree/aNameOfTwentyThreeChars + echo if [ "$FAIL" -eq 0 ]; then echo "All $PASS disk tool checks passed." diff --git a/Tests/lint-baseline.txt b/Tests/lint-baseline.txt index fe93cee..7554eb3 100644 --- a/Tests/lint-baseline.txt +++ b/Tests/lint-baseline.txt @@ -24,7 +24,7 @@ Programs/CosmOS/Source/text.asm redundant-setd 2 Programs/Loader/loader.asm redundant-assignment 1 Programs/testPrograms/branchTest.asm redundant-assignment 1 Programs/testPrograms/branchTest.asm redundant-ccf 1 -Programs/testPrograms/controllerWriteTest.asm branch-to-next 1 +Programs/testPrograms/controllerWrite.asm branch-to-next 1 Programs/testPrograms/diagnostics/duplicateLabel.asm branch-to-next 1 Programs/testPrograms/diskTest.asm redundant-assignment 1 Programs/testPrograms/dispatchTest.asm branch-to-next 1 diff --git a/Tests/manifest b/Tests/manifest index 4cc7e14..3a90401 100644 --- a/Tests/manifest +++ b/Tests/manifest @@ -46,10 +46,10 @@ printHello | Examples/printHello.asm | run | - 32bitFibonacci | Examples/Fibonacci/32bitFibonacci.asm | run | - | - colours | Examples/colours.asm | run | - | - 8bitSieve | Examples/primeSieve/8bitSieve.asm | run | - | - -16bitSegmentedSieve | Examples/primeSieve/16bitSegmentedSieve.asm | run | - | - +16bitSegmentedSieve | Examples/primeSieve/16bitSieve.asm | run | - | - # The four pointer rewrite. It emits exactly the same primes as the line above, which # is the whole point of keeping both: the pair is a direct before and after. -16bitSegmentedSieveModern | Examples/primeSieve/16bitSegmentedSieveModern.asm | run | - | - +16bitSegmentedSieveModern | Examples/primeSieve/16bitSieveModern.asm | run | - | - mathTest | testPrograms/mathTest.asm | run | - | - printTest | testPrograms/printTest.asm | run | - | - int16print | Libraries/int16print.asm | run | - | - @@ -73,7 +73,7 @@ moveQTest | testPrograms/moveQTest.asm | run | - # Reading and writing Program Memory, which the instruction set deliberately cannot do, # and the bank table that says what is reachable. controllerReadTest | testPrograms/controllerReadTest.asm | run | - | - -controllerWriteTest | testPrograms/controllerWriteTest.asm | run | - | - +controllerWriteTest | testPrograms/controllerWrite.asm | run | - | - # Block transfers: between banks, within one, overlapping, and one that is refused. blitTest | testPrograms/blitTest.asm | run | - | - # Reading a filesystem that the host tool wrote. The two are separate implementations of @@ -196,7 +196,7 @@ consoleModeTest | testPrograms/consoleModeTest.asm | run | consoleMo # down the two things that make the feature usable rather than merely present: the end of # input raises the line once, so an interrupt-driven program is told when to stop, and a # handler that does not read the byte is not called again, so nothing storms. -consoleInterruptTest | testPrograms/consoleInterruptTest.asm | run | consoleInterruptTest.in | - +consoleInterruptTest | testPrograms/consoleInterrupt.asm | run | consoleInterruptTest.in | - # Picking a typed line apart, which is how the shell understands anything. Includes a # string beginning with a zero: the assembler strips the quotes before deciding what a # token is, so such a string looked like a malformed literal and was refused.