| kaboom | kernel | fs | syscalls | userland | build |
this page is the actual pipeline: mk.conf's shared config, then four real steps run in order -- mk/b.sh (kernel), mk/bu.sh (userspace), mk/bd.sh (disk image), mk/rn.sh or mk/r.sh (run) -- and, alongside all of that, the entirely separate pipeline that builds the site you're reading right now. read kernel and userland for what actually gets built; this page only covers how, and in what order.
every script under mk/ starts with . ./mk.conf, which means every one of them has to be run from the repo root -- a relative source path, on purpose, so nothing here assumes an install location. it sets src/out (plain src and out, always), the three toolchain binaries (nscc, as, ld, each overridable via the NSCC/AS/LD environment variables rather than hardcoded), and the flag sets every compile/link actually uses.
asflgs is --64 --fatal-warnings -moperand-check=error -msse-check=error: fatal-warnings in the same spirit as tape-kernel's -Werror, -moperand-check=error upgrades gas's default warning for ambiguous mixed-width operand encodings to a hard failure (real hand-written 32/64-bit-mixed asm -- boot.s's mode transition, every isr stub -- is exactly where a silent wrong-width encoding is both easy to write and nasty to debug on real hardware), and -msse-check=error because nothing here ever saves or restores fpu/sse state, so a stray sse instruction from a compiler bug or a typo fails the build instead of quietly corrupting whatever ran before it.
ldflgs is --fatal-warnings --error-execstack -z noexecstack --build-id=none: fatal-warnings again catches --warn-rwx-segments (a regression back to one merged rwx load segment fails outright, not a line to scroll past) and --warn-common (harmless here, .comm is deliberate); --error-execstack/-z noexecstack make an executable stack a hard build failure even though nothing here actually reads that marking during a direct qemu -kernel/pvh boot the way a real os loader would; --build-id=none skips the .note.gnu.build-id section entirely, pure overhead for a from-scratch image that nothing ever consumes. ldscript points at linker.ld, and qemu/qemuflgs (qemu-system-x86_64, -m 128) are shared by both run scripts below.
wipes and recreates $out, then runs every .nsc source under src/kernel, src/fs, and src/drivers through $nscc into an intermediate .s in $out, assembles every hand-written .s (boot.s, io.s, and each subsystem's own _asm.s counterpart) straight to .o, assembles each nscc-produced .s to .o the same way, and links the whole pile with $ld $ldflgs -T linker.ld into $out/kaboom.elf. boot.o goes first on the link line by convention, not requirement -- the linker script's ENTRY(_start) is what actually decides where execution begins, object order on the ld command line doesn't matter for that, mk.conf's own comment says as much.
compiles every program under src/user/ into a trimmed, ready-to-place-in-/bin elf at $out/user/<name>. the userspace libc (lib, mem, string, ctype, malloc, stdio, plus the hand-written crt.s) is compiled once up front, not inside the per-program loop -- nsc has no #include-and-compile-together model, but nothing stops sharing already-compiled objects across multiple final links. lib.o is linked into every program unconditionally, always has been; the rest are opt-in per program, decided by grepping each program's own source for "mod.nsh", plus one level of dependency resolution on top (string/stdio both call into mem internally, so mem.o goes on the link line even for a program that never includes mem.nsh directly). see userland for why: linking everything unconditionally was tried first and measured to add roughly 19kb of dead code to every binary, which pushed ed over disk.pl's own binary size cap for zero benefit to programs that never call any of it.
sh links against user_shell.ld (fixed at 0x400000); everything else links against user_prog.ld (0x600000) -- see kernel's paging coverage for why sh specifically needs a load address nothing else uses. the actual link uses plain flags, not $ldflgs (the kernel's strict set), with -z max-page-size=16 -z common-page-size=16 -- set far below the real 4096 specifically because the two real PT_LOAD segments (r+x, r+w) user_prog.ld/user_shell.ld now define would otherwise cost up to roughly 4095 padding bytes per boundary satisfying page alignment this loader (elf.nsc) has no actual use for. those two segments replaced a single -n-flagged combined one, which is what was triggering ld's "rwx permissions" warning on every userspace binary -- a real warning, not cosmetic, given an actual paging-enforced os is exactly what it's about, this kernel just doesn't enforce it yet. after linking, strip -s drops symbols, and mk/trim-elf.pl truncates the result right after the last byte any PT_LOAD segment's file data actually uses, discarding the section header table and shstrtab entirely -- section headers plus a small string table routinely add two to four hundred bytes to an otherwise-tiny binary, often enough on its own to push something over the size cap.
disk-image building used to happen at the end of this same script. it doesn't any more -- see below.
this is genuinely recent: disk-image building lived at the tail end of mk/bu.sh until it was recently split out into its own script. two real reasons, both in bd.sh's own header comment: a freshly rebuilt binary can now sit in $out/user and be inspected or tested standalone before it's baked onto anything, and disk-building reads the whole $out/user tree regardless of which binary bu.sh most recently touched -- that's a genuinely different concern from "did this one program compile and link," and it earns its own place in the pipeline instead of riding along at the bottom of someone else's script. the pipeline is now four steps, not three: mk . b, mk . bu, mk . bd, mk . rn (or r).
the script itself is one line past sourcing mk.conf: perl mk/disk.pl "$out/disk.img". disk.pl is a from-scratch, host-side reimplementation of kfs's on-disk format -- not a port of any part of kfs.nsc, just written to match its byte layout exactly (superblock fields, one inode per block, direct plus one indirect block pointer, the dirent format's single-low-byte inode number and sentinel). it's kept in sync with kfs.nsc by hand; there's no shared source of truth between them, the same way a bootloader and the kernel it loads usually don't have one either. it globs $out/user/*.elf rather than naming programs, so dropping a new .nsc file in src/user/ and running mk/bu.sh puts it on the disk with zero changes to disk.pl itself -- the one exception is sh, which kmain.nsc's boot sequence execs by name as init, so it's the one binary the disk layout has to know about specifically.
both refuse to run before their inputs exist -- $out/kaboom.elf (mk/b.sh) and $out/disk.img (mk/bd.sh), checked explicitly with a message naming the right script to run first, not just qemu failing on a missing file with no context. that message used to say "run mk/bu.sh first" for the missing-disk-image case, a leftover from before bd.sh existed -- fixed once the split actually landed, since pointing someone at the wrong script for a two-command fix is worse than no message at all.
mk/rn.sh runs headless: -nographic merges the vga console and qemu's own monitor onto the current terminal (ctrl-a c toggles between them, ctrl-a x quits), no separate window, works over a plain ssh session the same as any other headless qemu invocation. mk/r.sh runs the same kernel against the same disk with -display curses instead -- needs a real tty, ctrl+alt+2 switches to the qemu monitor and ctrl+alt+1 back. same disk, same kernel, genuinely just a different console; neither script's own comment claims otherwise.
worth being direct about, since it's genuinely a little funny: this paragraph describes the exact pipeline that turns this file into the page rendering it, and running that same pipeline against this file is exactly how it was confirmed to work. mk/d.sh is two sequential calls, not a real dependency graph -- same reasoning as mp/nscc's own d step not chaining into anything else: perl mk/gensummary.pl first, then sh mk/dh.sh.
gensummary.pl regenerates docs/index.btft from docs/index.btft.in (the hand-edited template, tracked in git) by substituting the literal {{SUMMARY}} marker with docs/summary.txt's own one-line content -- mirrors mp's own mk/genconfdoc.pl pattern exactly, one source of truth instead of the banner text and the generated page slowly drifting apart by hand-editing both. docs/index.btft itself is never hand-edited; edit the .in or summary.txt and regenerate.
dh.sh renders every docs/*.btft (recursively, any subfolder included) into docs/html/, mirroring each source file's own relative path under docs/. it resolves btf2html off $PATH or the BTF2HTML environment variable, the same pattern mk.conf uses for nscc/as/ld -- never a hardcoded install location -- and fails loudly with a pointer to git://git.druid.rocks/druid520/btf.git if it isn't found at all. after rendering it copies docs/keyframes.css into docs/html/ alongside everything else, since btf2html itself has no notion of a stylesheet living outside the tree it's told to render.
