SchOS/tools/mkschfs.c
Nexoniarz edc4e0537d schfs: commit every 5 s, survive a full disk, mount up to 64 TiB
Transaction groups commit from a schfsd thread every 5 seconds, on sync
and fsync, on unmount and shutdown, and early when freed-but-pending
blocks crowd the free ones -- the contract ext4 and ZFS have. A crash
still lands on the last group whole, and loses at most those seconds.
Committing before every write(2) returned cost two cache flushes and an
uberblock a call: 4 KB writes go from 2.8 to 6.6 MB/s on AHCI, and from
66 KB/s to 3.9 MB/s on QEMU's IDE disk.

Filling the disk made the root volume unmountable, on the last commit
as well: a tree update that ran out of blocks halfway was published by
the next commit. 1% of the volume (1 to 64 MiB) is now a metadata
reserve, as btrfs and ZFS keep one; data stops at it with ENOSPC as a
short write, new names at half of it, and deletes may use the rest. A
write that runs out part way records what it wrote in the inode rather
than leaving blocks past an end of file nothing moved.

Deleting a large file goes in 4 MiB steps from the end with a commit
allowed between them, as ext4 restarts its transaction through a long
truncate. Deleting 80 MB on a full disk ran out of room halfway and
leaked the rest.

The VFS's dirunlink called the object layer's whole unlink, which freed
the object with its last name, so a file removed while open lost its
data under the reader. It removes the name only now, and the object is
freed on the last iput. An object still open when the machine went down
is found by the mount's space scan and freed, like ext4's orphan list.

The allocation bitmap has a second level of pointer pages, so the mount
ceiling is 64 TiB instead of 64 GiB, and the freed-this-group bitmap
has pages only where frees happened: a 1 TiB volume takes 32 MiB.

A freed block written after the newest snapshot is reused at once
(ZFS's birth-time rule, so bfree carries the block's generation); only
older ones are kept while a snapshot exists. Deleting one snapshot no
longer stops retaining for the others, which let their blocks be
handed out again.

Checked under QEMU: crash after sync, after 8 s and within 5 s; fill to
ENOSPC, rm, remount; unlink while open; an orphan freed at the next
mount; a 1 TiB volume. schfs-check passes, with a new test for the
snapshot rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
2026-10-23 12:20:05 +02:00

753 lines
27 KiB
C

// SPDX-License-Identifier: GPL-2.0-or-later
//
// mkschfs — build a SchFS image from a host directory tree.
//
// This is the tool that makes the on-disk format real: everything the kernel
// mounts in phase 2 is produced here. It deliberately shares the actual
// filesystem code (btree.c, vol.c, obj.c, objw.c) rather than reimplementing
// the layout, so a disagreement between writer and reader is impossible by
// construction — there is only one implementation, exercised on the host where
// a mistake costs a second instead of a boot.
//
// mkschfs out.img --sysroot DIR [--size BLOCKS] [--label NAME]
//
// Two behaviours worth knowing about:
//
// * Identical file contents are stored once and hard-linked. sysroot/bin
// holds a full copy of the toybox binary per applet, so this is the
// difference between a 190 MB image and a 6 MB one.
// * Output is reproducible. Directory entries are sorted, the UUID is
// derived rather than random, and timestamps come from the source files,
// so building the same tree twice gives byte-identical images and a diff
// means something actually changed.
#define _FILE_OFFSET_BITS 64
#define SCHFS_HOSTED 1
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <dirent.h>
#include <errno.h>
#include <sys/stat.h>
#include <sys/types.h>
/* major()/minor() for device nodes. glibc puts them here; mingw has no such
header and no device numbers at all, because Windows has no mknod. On that
host the macros are stubbed: a device node cannot exist in the source tree
being imaged, so the values are never anything but zero, and stubbing is
honest where pretending to read st_rdev would not be. */
#ifdef __MINGW32__
#define major(x) 0
#define minor(x) 0
/* Symlinks, likewise. Windows has no lstat/readlink and mingw exposes neither,
so on this host a symlink cannot be encountered while walking the tree --
lstat degrades to stat, nothing is ever S_ISLNK, and readlink can only be
reached through a branch that is now unreachable. Verified rather than
assumed: `find sysroot -type l` returns nothing.
If symlinks are ever staged (procfs wants /proc/<pid>/exe to be one), this
host stops being able to build a faithful image and the stub must become a
real Win32 reparse-point read. Failing loudly then is the point of the
abort() below. */
#define lstat(p, b) stat((p), (b))
#define S_ISLNK(m) 0
static long readlink(const char *p, char *b, unsigned long n)
{ (void)p; (void)b; (void)n; return -1; }
#elif defined(__linux__)
/* glibc and musl both put major()/minor() here, and glibc warns and then
errors if you take them from <sys/types.h> instead. Linux only: macOS and
the BSDs have no <sys/sysmacros.h> at all and define both in <sys/types.h>,
which is already included above -- including it unconditionally is a build
failure on every non-Linux Unix. */
#include <sys/sysmacros.h>
#endif
#include "../kernel/fs/schfs/btree.c"
#include "../kernel/fs/schfs/space.c"
#include "../kernel/fs/schfs/vol.c"
#include "../kernel/fs/schfs/obj.c"
#include "../kernel/fs/schfs/objw.c"
#include "../kernel/fs/schfs/attr.c"
/* ------------------------------------------------------------------ */
/* File-backed block device */
/* ------------------------------------------------------------------ */
struct imgdev {
FILE *f;
struct schfs_vol *vol;
uint64_t nblocks;
long reads, writes;
};
static struct imgdev IMG;
static int
img_read(void *ctx, uint64_t b, void *buf)
{
struct imgdev *d = ctx;
if (b >= d->nblocks)
return -1;
if (fseeko(d->f, (off_t)b * SCHFS_BLKSIZE, SEEK_SET) != 0)
return -1;
if (fread(buf, 1, SCHFS_BLKSIZE, d->f) != SCHFS_BLKSIZE)
return -1;
d->reads++;
return 0;
}
static int
img_write(void *ctx, uint64_t b, const void *buf)
{
struct imgdev *d = ctx;
if (b >= d->nblocks)
return -1;
if (fseeko(d->f, (off_t)b * SCHFS_BLKSIZE, SEEK_SET) != 0)
return -1;
if (fwrite(buf, 1, SCHFS_BLKSIZE, d->f) != SCHFS_BLKSIZE)
return -1;
d->writes++;
return 0;
}
// Make what has been written actually reach the file.
//
// The kernel's version of this hook drains the buffer cache and then tells the
// drive to commit its own cache; here the equivalent stages are stdio's buffer
// and the operating system's page cache. The image mkschfs is building is
// normally consumed by something else the moment it exits, so this is mostly
// belt and braces -- but the commit path calls it, and a hook that lies about
// having flushed is exactly the bug this whole change set is about.
static int
img_flush(void *ctx)
{
struct imgdev *d = ctx;
if (fflush(d->f) != 0)
return -1;
return 0;
}
/* ------------------------------------------------------------------ */
/* Allocation */
/* */
/* Straight through to the volume's space manager. An earlier version */
/* kept a build-time free list of its own, because the bump allocator it */
/* replaced could only ever take back the block it handed out last and */
/* a build otherwise leaked one root-to-leaf path per insert. Two things */
/* made that unnecessary: the space manager reuses any freed block, and */
/* the tree now rewrites its own nodes in place within a transaction */
/* group, so a build barely frees anything in the first place. */
/* ------------------------------------------------------------------ */
static uint64_t
img_alloc(void *ctx)
{
return schfs_vol_alloc(((struct imgdev *)ctx)->vol);
}
static uint64_t
img_alloc_run(void *ctx, uint64_t n)
{
return schfs_vol_alloc_run(((struct imgdev *)ctx)->vol, n);
}
static void
img_free(void *ctx, uint64_t b, uint64_t gen)
{
schfs_vol_free(((struct imgdev *)ctx)->vol, b, gen);
}
// Pages for the allocation bitmap. calloc rather than malloc so a bug that
// forgets to initialise one shows up as "everything is free" consistently
// rather than as whatever the heap happened to contain.
static void *
img_palloc(void)
{
return calloc(1, SCHFS_BLKSIZE);
}
static void
img_pfree(void *p)
{
free(p);
}
/* ------------------------------------------------------------------ */
/* Content dedup */
/* */
/* sysroot/bin contains one independent ~800 KB copy of toybox per */
/* applet name. Storing each separately is the difference between an */
/* image that fits in RAM and one that does not, so identical content */
/* becomes one object with several names -- a plain hard link, which */
/* the format already supports through nlink. */
/* ------------------------------------------------------------------ */
struct dedup_ent {
char *path;
dev_t hdev; /* host device+inode: when these match, */
ino_t hino; /* it is literally the same file */
off_t size;
uint32_t crc;
uint64_t ino;
};
static struct dedup_ent *dedup_tab;
static int dedup_n, dedup_cap;
/* A file the host already told us is the same file.
*
* ports/toybox/Makefile hard-links its 236 applet names to one binary, so this
* settles all but the first of them without reading anything: the content path
* below reads the candidate to CRC it and then re-reads both files to confirm,
* which is half a gigabyte of I/O for an answer fstat already has. */
static uint64_t
dedup_lookup_ino(dev_t hdev, ino_t hino)
{
if (hino == 0)
return 0;
for (int i = 0; i < dedup_n; i++)
if (dedup_tab[i].hdev == hdev && dedup_tab[i].hino == hino)
return dedup_tab[i].ino;
return 0;
}
static int
files_identical(const char *a, const char *b)
{
FILE *fa = fopen(a, "rb"), *fb = fopen(b, "rb");
if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return 0; }
int same = 1;
for (;;) {
char ba[8192], bb[8192];
size_t na = fread(ba, 1, sizeof(ba), fa);
size_t nb = fread(bb, 1, sizeof(bb), fb);
if (na != nb || memcmp(ba, bb, na) != 0) { same = 0; break; }
if (na == 0) break;
}
fclose(fa); fclose(fb);
return same;
}
// Size and CRC agreeing is not proof, so a candidate is confirmed byte by byte.
// Silently hard-linking two files that merely collide would corrupt the image
// in a way nothing downstream could detect.
static uint64_t
dedup_lookup(const char *path, off_t size, uint32_t crc)
{
for (int i = 0; i < dedup_n; i++)
if (dedup_tab[i].size == size && dedup_tab[i].crc == crc &&
files_identical(dedup_tab[i].path, path))
return dedup_tab[i].ino;
return 0;
}
static void
dedup_remember(const char *path, const struct stat *st,
off_t size, uint32_t crc, uint64_t ino)
{
dev_t dedup_hdev = st->st_nlink > 1 ? st->st_dev : 0;
ino_t dedup_hino = st->st_nlink > 1 ? st->st_ino : 0;
if (dedup_n == dedup_cap) {
dedup_cap = dedup_cap ? dedup_cap * 2 : 256;
dedup_tab = realloc(dedup_tab, (size_t)dedup_cap * sizeof(*dedup_tab));
if (!dedup_tab) { perror("realloc"); exit(1); }
}
dedup_tab[dedup_n].path = strdup(path);
dedup_tab[dedup_n].hdev = dedup_hdev;
dedup_tab[dedup_n].hino = dedup_hino;
dedup_tab[dedup_n].size = size;
dedup_tab[dedup_n].crc = crc;
dedup_tab[dedup_n].ino = ino;
dedup_n++;
}
/* ------------------------------------------------------------------ */
static struct schfs_vol VOL;
static char DATABLK[SCHFS_BLKSIZE];
static char CURBLK[SCHFS_BLKSIZE];
// The volume's three scratch blocks. Statics rather than locals because the
// same code runs in the kernel, where a 4096-byte local is the whole stack —
// keeping the shapes identical is what makes the host the place bugs are found.
static char CTX_A[SCHFS_BLKSIZE];
static char CTX_B[SCHFS_BLKSIZE];
static char CTX_C[SCHFS_BLKSIZE];
static struct schfs_ctx CTX = { CTX_A, CTX_B, CTX_C };
static long nfiles, ndirs, nlinks, nsymlinks, ndevs;
static long long nbytes, ndeduped;
static void
die(const char *what, int r)
{
fprintf(stderr, "mkschfs: %s failed (%d)%s\n", what, r,
r == -3 ? " -- image is full, try a larger --size" : "");
exit(1);
}
static void
inode_init(struct schfs_inode *in, const struct stat *st, uint16_t type,
uint64_t parent)
{
memset(in, 0, sizeof(*in));
in->type = type;
in->parent = parent;
in->nlink = 1;
in->mode = st ? (uint32_t)(st->st_mode & 07777)
: (type == SCHFS_T_DIR ? 0755u : 0644u);
in->uid = 0;
in->gid = 0;
if (st) {
// All three come from mtime, deliberately.
//
// Copying the host's atime would make the output depend on when the
// source files were last *read* -- and mkschfs reads every one of them,
// so building the same tree twice produced two different images. ctime
// is similar: it tracks host inode changes that mean nothing inside the
// image. mtime is the only one that describes the content, which is the
// only thing being copied.
in->atime = (uint64_t)st->st_mtime;
in->mtime = (uint64_t)st->st_mtime;
in->ctime = (uint64_t)st->st_mtime;
}
}
static void
bump_nlink(uint64_t ino)
{
struct schfs_cursor c;
schfs_cursor_init(&c, &VOL.tree, CURBLK);
struct schfs_inode in;
int r = schfs_inode_get(&VOL, &c, ino, &in);
if (r != 0)
die("re-reading an inode for a hard link", r);
in.nlink++;
// Under v1 this was the bug that shipped broken images: nlink was bumped
// but the inode's checksum was not recomputed, so every deduped file failed
// its own integrity check on first open. Here the checksum belongs to the
// tree node and is recomputed by the insert, so it cannot get out of step.
r = schfs_inode_put(&VOL, ino, &in);
if (r != 0)
die("writing back a hard link count", r);
}
static uint64_t
add_file(uint64_t parent, const char *host, const char *name,
const struct stat *st)
{
// A hard link is the same inode by definition, so ask the host before
// reading anything.
if (st->st_nlink > 1) {
uint64_t hit = dedup_lookup_ino(st->st_dev, st->st_ino);
if (hit) {
int r = schfs_dir_add(&VOL, parent, name, (unsigned)strlen(name),
hit, SCHFS_T_FILE);
if (r != 0)
die("linking a deduplicated file", r);
bump_nlink(hit);
nlinks++;
ndeduped += st->st_size;
return hit;
}
}
// CRC the content: it is both the dedup key and a cheap way to avoid
// reading candidates that cannot possibly match.
FILE *f = fopen(host, "rb");
if (!f) { perror(host); exit(1); }
uint32_t crc = 0;
for (;;) {
char buf[8192];
size_t n = fread(buf, 1, sizeof(buf), f);
if (n == 0) break;
crc = schfs_crc32c(crc, buf, (unsigned)n);
}
uint64_t existing = dedup_lookup(host, st->st_size, crc);
if (existing) {
fclose(f);
int r = schfs_dir_add(&VOL, parent, name, (unsigned)strlen(name),
existing, SCHFS_T_FILE);
if (r != 0)
die("linking a deduplicated file", r);
bump_nlink(existing);
nlinks++;
ndeduped += st->st_size;
return existing;
}
// The final size is known before a byte is written, so the block count goes
// into the inode on its first and only write, and the whole run of data
// blocks is claimed up front. That is what collapses a multi-megabyte file
// into a single extent record instead of one per 4 KB block.
uint64_t need = ((uint64_t)st->st_size + SCHFS_BLKSIZE - 1) / SCHFS_BLKSIZE;
uint64_t ino = schfs_alloc_objectid(&VOL);
struct schfs_inode in;
inode_init(&in, st, SCHFS_T_FILE, parent);
in.blocks = need;
int r = schfs_inode_put(&VOL, ino, &in);
if (r != 0) die("writing a file inode", r);
r = schfs_file_reserve(&VOL, ino, need);
if (r != 0) die("reserving data blocks", r);
rewind(f);
for (;;) {
// Large chunks on purpose: each append call re-reads and rewrites the
// inode, and every tree write copies its path, so appending a big file
// 8 KB at a time spends more space on metadata churn than on data.
static char buf[256 * 1024];
size_t n = fread(buf, 1, sizeof(buf), f);
if (n == 0) break;
r = schfs_file_append(&VOL, ino, buf, (unsigned)n, DATABLK);
if (r != 0) die("writing file data", r);
nbytes += (long long)n;
}
fclose(f);
r = schfs_dir_add(&VOL, parent, name, (unsigned)strlen(name),
ino, SCHFS_T_FILE);
if (r != 0) die("adding a directory entry", r);
dedup_remember(host, st, st->st_size, crc, ino);
nfiles++;
return ino;
}
static void
add_symlink(uint64_t parent, const char *host, const char *name,
const struct stat *st)
{
char target[4096];
ssize_t n = readlink(host, target, sizeof(target) - 1);
if (n < 0) { perror(host); exit(1); }
target[n] = '\0';
uint64_t need = ((uint64_t)n + SCHFS_BLKSIZE - 1) / SCHFS_BLKSIZE;
uint64_t ino = schfs_alloc_objectid(&VOL);
struct schfs_inode in;
inode_init(&in, st, SCHFS_T_SYMLINK, parent);
in.blocks = need;
int r = schfs_inode_put(&VOL, ino, &in);
if (r != 0) die("writing a symlink inode", r);
// The target is stored as the object's data, exactly like a file's
// contents, so nothing special is needed to read it back.
r = schfs_file_reserve(&VOL, ino, need);
if (r != 0) die("reserving a symlink block", r);
r = schfs_file_append(&VOL, ino, target, (unsigned)n, DATABLK);
if (r != 0) die("writing a symlink target", r);
r = schfs_dir_add(&VOL, parent, name, (unsigned)strlen(name),
ino, SCHFS_T_SYMLINK);
if (r != 0) die("adding a symlink entry", r);
nsymlinks++;
}
static void
add_dev(uint64_t parent, const char *name, const struct stat *st)
{
uint64_t ino = schfs_alloc_objectid(&VOL);
struct schfs_inode in;
inode_init(&in, st, SCHFS_T_DEV, parent);
in.rdev_major = (uint16_t)major(st->st_rdev);
in.rdev_minor = (uint16_t)minor(st->st_rdev);
int r = schfs_inode_put(&VOL, ino, &in);
if (r != 0) die("writing a device inode", r);
r = schfs_dir_add(&VOL, parent, name, (unsigned)strlen(name),
ino, SCHFS_T_DEV);
if (r != 0) die("adding a device entry", r);
ndevs++;
}
/* Sorted directory listing, so images are reproducible. */
struct namelist { char **v; int n, cap; };
static void
nl_add(struct namelist *l, const char *s)
{
if (l->n == l->cap) {
l->cap = l->cap ? l->cap * 2 : 64;
l->v = realloc(l->v, (size_t)l->cap * sizeof(*l->v));
if (!l->v) { perror("realloc"); exit(1); }
}
l->v[l->n++] = strdup(s);
}
static int
nl_cmp(const void *a, const void *b)
{
return strcmp(*(char *const *)a, *(char *const *)b);
}
static void add_tree(uint64_t dirino, const char *hostdir);
// Paths kept out of the image, sysroot-relative.
//
// Same rule and same spelling as mkfs.c, because the two tools build the same
// tree and a path excluded from one must be excluded from the other. This is a
// licensing mechanism before it is a size one: the ramdisk image is linked into
// the kernel binary, so anything staged in sysroot/ when it is built becomes
// part of a distributable artifact.
#define MAXEXCLUDE 32
static const char *excludes[MAXEXCLUDE];
static int nexcludes;
// strlen(sysroot) + 1, so a host path can be turned back into the
// sysroot-relative one the exclude patterns are written against.
static size_t sysroot_prefix;
// Matches exactly, or as a directory prefix so that "usr/share/misc" also drops
// "usr/share/misc/big.dat".
static int
is_excluded(const char *relpath)
{
for (int i = 0; i < nexcludes; i++) {
size_t n = strlen(excludes[i]);
if (strncmp(relpath, excludes[i], n) == 0 &&
(relpath[n] == '\0' || relpath[n] == '/'))
return 1;
}
return 0;
}
// Returns 1 if the entry was a directory, so the caller can total up the link
// count without walking the directory a second time.
static int
add_entry(uint64_t dirino, const char *hostdir, const char *name)
{
char path[8192];
snprintf(path, sizeof(path), "%s/%s", hostdir, name);
// Checked before the entry is dispatched, so an excluded directory is never
// opened and never gets an inode -- an empty directory left behind would
// still describe what was removed.
if (nexcludes && strlen(path) > sysroot_prefix &&
is_excluded(path + sysroot_prefix)) {
fprintf(stderr, "mkschfs: excluding %s\n", path + sysroot_prefix);
return 0;
}
struct stat st;
// lstat, not stat: a symlink must be stored as a symlink rather than
// silently turned into a second copy of whatever it points at.
if (lstat(path, &st) != 0) { perror(path); exit(1); }
if (strlen(name) > SCHFS_NAME_MAX) {
fprintf(stderr, "mkschfs: name too long (max %d): %s\n",
SCHFS_NAME_MAX, path);
exit(1);
}
if (S_ISDIR(st.st_mode)) {
uint64_t ino = schfs_alloc_objectid(&VOL);
struct schfs_inode in;
inode_init(&in, &st, SCHFS_T_DIR, dirino);
in.nlink = 2; /* itself and its own "." */
int r = schfs_inode_put(&VOL, ino, &in);
if (r != 0) die("writing a directory inode", r);
r = schfs_dir_add(&VOL, dirino, name, (unsigned)strlen(name),
ino, SCHFS_T_DIR);
if (r != 0) die("adding a subdirectory entry", r);
ndirs++;
add_tree(ino, path);
return 1;
}
if (S_ISLNK(st.st_mode))
add_symlink(dirino, path, name, &st);
else if (S_ISCHR(st.st_mode) || S_ISBLK(st.st_mode))
add_dev(dirino, name, &st);
else if (S_ISREG(st.st_mode))
add_file(dirino, path, name, &st);
else
fprintf(stderr, "mkschfs: skipping %s (unsupported file type)\n", path);
return 0;
}
static void
add_tree(uint64_t dirino, const char *hostdir)
{
DIR *d = opendir(hostdir);
if (!d) { perror(hostdir); exit(1); }
struct namelist l = { 0, 0, 0 };
struct dirent *e;
while ((e = readdir(d)) != NULL) {
if (strcmp(e->d_name, ".") == 0 || strcmp(e->d_name, "..") == 0)
continue;
nl_add(&l, e->d_name);
}
closedir(d);
// qsort declares its first argument non-null, and an empty directory
// leaves l.v at NULL. Every implementation ignores the pointer when the
// count is zero; the standard does not say they have to.
if (l.n > 0)
qsort(l.v, l.n, sizeof(*l.v), nl_cmp);
long subdirs = 0;
for (int i = 0; i < l.n; i++) {
subdirs += add_entry(dirino, hostdir, l.v[i]);
free(l.v[i]);
}
free(l.v);
// A directory's link count is 2 (itself, and its own ".") plus one per
// subdirectory, because each subdirectory's ".." points back here. ".." is
// synthetic in SchFS, but the count still has to match what POSIX tools
// expect, or "find -type d" prunes real directories and du walks the tree
// wrongly. Written last because the count is not known until the children
// have been added.
struct schfs_cursor c;
schfs_cursor_init(&c, &VOL.tree, CURBLK);
struct schfs_inode in;
if (schfs_inode_get(&VOL, &c, dirino, &in) != 0)
die("re-reading a directory inode", -2);
in.nlink = (uint32_t)(2 + subdirs);
if (schfs_inode_put(&VOL, dirino, &in) != 0)
die("writing a directory link count", -2);
}
/* ------------------------------------------------------------------ */
int
main(int argc, char **argv)
{
const char *out = NULL, *sysroot = NULL, *label = "schos";
// Take a snapshot of the finished tree. Mostly for testing the snapshot
// paths end to end -- an image that ships with one is a perfectly ordinary
// thing to want, and it is one call.
const char *snapname = NULL;
uint64_t nblocks = 6000;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--sysroot") == 0 && i + 1 < argc)
sysroot = argv[++i];
else if (strcmp(argv[i], "--size") == 0 && i + 1 < argc)
nblocks = strtoull(argv[++i], NULL, 0);
else if (strcmp(argv[i], "--label") == 0 && i + 1 < argc)
label = argv[++i];
else if (strcmp(argv[i], "--snapshot") == 0 && i + 1 < argc)
snapname = argv[++i];
else if (strcmp(argv[i], "--exclude") == 0 && i + 1 < argc) {
if (nexcludes >= MAXEXCLUDE) {
fprintf(stderr, "mkschfs: too many --exclude paths (max %d)\n",
MAXEXCLUDE);
return 1;
}
excludes[nexcludes++] = argv[++i];
}
else if (strcmp(argv[i], "--hash-bits") == 0 && i + 1 < argc) {
// Testing only: shrink the name hash so collisions are the norm
// rather than a once-in-a-lifetime event. schfs_fsck takes the
// same flag and must be given the same value to read the image.
int bits = atoi(argv[++i]);
schfs_hash_mask = bits >= 64 ? ~0ULL : ((1ULL << bits) - 1);
}
else if (argv[i][0] == '-') {
fprintf(stderr, "mkschfs: unknown option %s\n", argv[i]);
return 1;
} else if (!out)
out = argv[i];
else {
fprintf(stderr, "mkschfs: too many arguments\n");
return 1;
}
}
if (!out || !sysroot) {
fprintf(stderr,
"usage: mkschfs out.img --sysroot DIR [--size BLOCKS] [--label NAME]\n"
" [--exclude PATH]... [--snapshot NAME]\n"
" BLOCKS are %d-byte SchFS blocks (default 6000 = %d MB)\n"
" --exclude omits a sysroot-relative path and its subtree\n",
SCHFS_BLKSIZE, (int)(6000 * SCHFS_BLKSIZE / (1024 * 1024)));
return 1;
}
if (nblocks <= SCHFS_FIRST_BLOCK) {
fprintf(stderr, "mkschfs: --size must exceed %d blocks (boot block "
"plus the uberblock ring)\n", SCHFS_FIRST_BLOCK);
return 1;
}
FILE *f = fopen(out, "w+b");
if (!f) { perror(out); return 1; }
// Pre-extend so every block is readable. The bump allocator hands out
// blocks in order, but the tree reads a block back the moment it is
// written, and a short file would fail that read rather than the write.
if (ftruncate(fileno(f), (off_t)nblocks * SCHFS_BLKSIZE) != 0) {
perror("ftruncate");
return 1;
}
IMG.f = f;
IMG.vol = &VOL;
IMG.nblocks = nblocks;
struct schfs_bdev bd;
memset(&bd, 0, sizeof(bd)); /* optional hooks must read as absent */
bd.ctx = &IMG;
bd.bread = img_read;
bd.bwrite = img_write;
bd.balloc = img_alloc;
bd.bfree = img_free;
bd.balloc_run = img_alloc_run;
bd.bflush = img_flush;
int r = schfs_vol_format(&VOL, &bd, nblocks, label, &CTX, img_palloc, img_pfree);
if (r != 0) die("formatting", r);
// The root directory is an object like any other; nothing points at it
// except the uberblock's root_objectid.
struct schfs_inode root;
struct stat rst;
int have_rst = (lstat(sysroot, &rst) == 0);
inode_init(&root, have_rst ? &rst : NULL, SCHFS_T_DIR, 0);
root.mode = 0755;
root.nlink = 2;
r = schfs_inode_put(&VOL, VOL.root_objectid, &root);
if (r != 0) die("writing the root inode", r);
ndirs++;
// add_entry() builds host paths as "<sysroot>/<name>", so this is where the
// sysroot-relative part of such a path begins.
sysroot_prefix = strlen(sysroot) + 1;
add_tree(VOL.root_objectid, sysroot);
// Publish. Everything above this line is invisible to a reader; if the tool
// died half way through, the image would simply have no valid uberblock and
// be rejected as "not a SchFS volume" rather than mounted half-built.
r = schfs_vol_commit(&VOL);
if (r != 0) die("committing the uberblock", r);
// A snapshot of the finished tree, if one was asked for. After the commit
// above, because a snapshot names a *committed* root -- taking it before
// would record a root that only exists in memory.
if (snapname) {
unsigned n = (unsigned)strlen(snapname);
r = schfs_snap_create(&VOL, 1, snapname, n);
if (r != 0) die("creating the snapshot", r);
r = schfs_vol_commit(&VOL);
if (r != 0) die("committing the snapshot", r);
}
if (fflush(f) != 0 || fclose(f) != 0) { perror(out); return 1; }
uint64_t used = nblocks - VOL.free_blocks - SCHFS_FIRST_BLOCK;
printf("mkschfs: %s\n", out);
printf(" %ld dirs, %ld files, %ld hard links, %ld symlinks, %ld devices\n",
ndirs, nfiles, nlinks, nsymlinks, ndevs);
printf(" %lld bytes of data written, %lld bytes saved by dedup\n",
nbytes, ndeduped);
printf(" %llu of %llu blocks used (%llu KB of %llu KB), generation %llu\n",
(unsigned long long)used, (unsigned long long)nblocks,
(unsigned long long)used * SCHFS_BLKSIZE / 1024,
(unsigned long long)nblocks * SCHFS_BLKSIZE / 1024,
(unsigned long long)VOL.generation);
return 0;
}