Mirror the source tree onto the system disk

A list of files in a makefile 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. So
SplitDisk gained a mirror command and the disk rule is one line: putting a file where the
others live is now the whole of putting it on the disk.

EVERY FILE GOES THROUGH put AND EVERY DIRECTORY THROUGH mkdir. That is the point of it -
mirror adds a walk and no filesystem code at all, so anything the format refuses here it
refuses everywhere, in the same words. What is new is the walk, and the walk is what the
six checks in Tests/disk.sh are about: that it goes all the way down, that it leaves dotfiles
and named directories behind, and that a name too long stops it.

REFUSED RATHER THAN SKIPPED, because a disk quietly missing a file is the exact failure a
mirror exists to prevent. Which meant four sources had to be renamed - a directory entry
holds 22 characters and they were 23, 23, 24 and 29:

  16bitSegmentedSieve.asm        -> 16bitSieve.asm
  16bitSegmentedSieveModern.asm  -> 16bitSieveModern.asm
  consoleInterruptTest.asm       -> consoleInterrupt.asm
  controllerWriteTest.asm        -> controllerWrite.asm

The test names in the manifest are unchanged, since those are identifiers and every recorded
result is filed under them. Only where the source lives has moved.

The entries are sorted before anything is written. 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 could compare against another.

The disk grew from one megabyte to four and from 192 directory entries to 1,024. The sources
are 2,850 blocks and the mirror filled the old directory on its first run, which is a thing
that should not need thinking about again.

The Tests fixture disk is deliberately NOT mirrored. It is a controlled fixture with known
contents, and the shipped disk is the one meant to be useful; they want different things.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-08-29 08:45:46 -04:00
co-authored by Claude Opus 5
parent ff4b025058
commit 0852666e73
13 changed files with 247 additions and 37 deletions
+147
View File
@@ -12,6 +12,10 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 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 <dirent.h>
#include <sys/stat.h>
// 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 <image> <path> Remove a file.\n");
printf(" mkdir <image> <path> Make a directory.\n");
printf(" rmdir <image> <path> Remove an empty one.\n");
printf(" mirror <image> <dir> <path> [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");