--snapshots is a range scan over SCHFS_SNAP_OBJECTID, printing each retained root's id, name, block and the generation it was taken at. The name is printed with an explicit precision: it is bounded but a damaged image need not have terminated it. mkschfs grows --snapshot NAME, which takes one of the finished tree. It runs after the commit, because a snapshot names a *committed* root -- taking it before would record a root that exists only in memory. Verified end to end: an image built with --snapshot golden lists the snapshot and still passes --check. An image without one prints "no snapshots" rather than nothing at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
723 lines
26 KiB
C
723 lines
26 KiB
C
// SPDX-License-Identifier: Apache-2.0
|
|
//
|
|
// 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"
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* 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;
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* 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)
|
|
{
|
|
schfs_vol_free(((struct imgdev *)ctx)->vol, b);
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
/* ------------------------------------------------------------------ */
|
|
/* 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(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;
|
|
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;
|
|
|
|
int r = schfs_vol_format(&VOL, &bd, nblocks, label, &CTX, img_palloc);
|
|
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;
|
|
}
|