Makar — coding style, conventions & practices
The house rules for working in this repo. They override generic defaults.
This is the human-facing companion to the makar-conventions Claude skill
(.claude/skills/makar-conventions/SKILL.md); keep the two in sync — the skill
is the terse checklist, this is the explanation.
Makar is a hobby i386 (i686) bare-metal OS: C + AT&T assembly, booted via
GRUB Multiboot 2 (live ISO) and Limine (installed disk), 32-bit protected mode,
higher-half kernel, single CPU, no SMP / PAE / long mode. The whole
build/test/run toolchain is wrapped by ./run.sh over Docker — no host
cross-compiler required.
1. Golden rule — default to the Linux convention
When a design or hardware detail is ambiguous, default to the Linux
convention: the syscall ABI (int 0x80, i386 numbers), driver shape, ACPI / HID
/ AHCI layout, single-user / rescue mode, ANSI/VT100 terminals, USER_HZ, the
block layer, /proc + /dev + /usr paths, dotfile names. Prefer documented
references — the Linux source, the OSDev wiki, public-domain libraries
(stb_image, miniz/zlib, doomgeneric) — over hand-rolling. When you follow a
Linux convention in a non-obvious spot, just say so plainly in a comment.
2. Repo layout (where things live)
src/kernel/arch/i386/{boot,core,mm,drivers,fs,display,proc,shell,debug}/
src/kernel/kernel/kernel.c kernel_main + boot sequence
src/kernel/include/kernel/ kernel-internal public headers
src/kernel/include/makar_*.h the shared kernel⇄userspace ABI (see §6)
src/libc/ freestanding libc → libk.a (kernel-side)
src/userspace/ ring-3 ELF apps + the hosted libc.a + gui_* libs
toolchain/ host musl cross toolchain (separate; its own README)
tests/ GDB boot-test suite (groups/*.py)
docs/ this tree; docs/kernel/*.md per subsystem
run.sh iso.sh build.sh generate-hdd.sh getfreedoom.sh copy.sh build/run scripts
CLAUDE.md is the index/router — start there, follow its pointers to
docs/, the companions (CLAUDE.history.md, CLAUDE.roadmap.md, SURVEY.md),
and the source. Never paste detail into CLAUDE.md.
3. Build & run — always through ./run.sh
Everything goes through ./run.sh (it wraps the Docker toolchain; falls through
to a host i686-elf-gcc or a CI /.dockerenv when present).
./run.sh iso boot # debug ISO → interactive QEMU
./run.sh iso test # full regression gate (see §4)
./run.sh iso build # kernel + makar.iso (+ makar-test.iso when TEST_ISO=1)
./run.sh iso release # optimised build
./run.sh hdd boot|test|build|release
./run.sh ktest # fast, headless, kernel-only ktest
./run.sh kbtest [gui] # in-guest key-injection test
./run.sh gdb iso|hdd # GDB boot-checkpoint test
./run.sh clean
- One kernel, two ISOs.
makar.kernelis built once;iso.shpackagesmakar.iso(interactive) andmakar-test.iso(test_modecmdline). There is no compile-time test flag — the difference is purelygrub.cfg. - Debug is
-O0 -g3, release-O2 -g; override viaCFLAGS.-j$(nproc)+ ccache (CCACHE=0to disable). - Env knobs:
MACHINE=<PMMX|P2|P3|MODERN|…>picks a QEMU-cpu;RES=<720p| 1080p|900p|480p|WxH>bakesvmode=onto the booted entry;MAKAR_USE_KVM=0|1forces the accelerator. KVM is off for the correctness gates. - IDE/clang “missing kernel header” squiggles are expected — the real build uses the right include paths.
- Hand a built artifact to the user with
./copy.sh(stagesmakar.isoto a Windows-reachable path); it is gitignored.
4. Testing — in-guest and headless only
No host-driven keyboard, no monitor
sendkey, no framebuffer scraping. Ever.
Every test asserts on COM1 serial markers. Two mechanisms:
- In-guest
.shscript drivers —src/userspace/{shell-smoke,incore, libc-tcc}.sh, run by./run.sh iso testafterktest_run_all(). Each gates per-test PASS/FAIL on$?; markers likeSHELL-SMOKE: ALL PASS. Drive the ring-3 shell withsh.elf -c '<payload>';alloctest.cis the canonical ELF shape. - Key injection —
./run.sh kbtest.keyboard_inject_text/_key()feed the live decode→ring→shell pipeline;keyboard_test_driver()(cmdlinekbtest) emitsKBTEST:markers, for paths where the keystroke itself is under test.
Add coverage in those .sh drivers, in keyboard_test_driver(), or as a
ktest in proc/ktest.c (macros in kernel/ktest.h) — never a host
harness. Full detail: docs/testing.md.
The gates (run the relevant ones before every commit; state the real
numbers, never assume): ./run.sh ktest, iso test, guitest, kbtest,
gdb iso|hdd. KVM is forced off for ktest/gdb/nettest/CI (correctness
— KVM broke GDB breakpoints and masked a TCG-only fault). The gdb-hdd content
check is known-flaky (inferior-call timing) — re-run to clear.
5. Code style
- Match the surrounding code — comment density, naming, idiom, brace style. Read the neighbours before adding. Kernel C + AT&T asm; tabs for indentation in kernel C.
- Comments explain why, not what. Reference the concrete bug/quirk a
workaround addresses (e.g. “the 1080p page-fault-at-0xFD400000 bug”,
“VMware reports the CPU ‘disabled’ on
cli;hlt”). Function banners describe intent + lifecycle. - Naming. File-scope globals are
g_-prefixed (g_boot_gui,g_sched_ quantum). Kernel public symbols aresnake_case, grouped by subsystem prefix (vesa_,vfs_,task_,pmm_). Match the module you’re in. - Higher-half kernel linked at
0xC0000000(loaded at phys 1 MiB viaAT()); a low identity window keeps0x0–0x0FFFFFFFmapped for phys access. Hand any kernel pointer to DMA/hardware throughkvirt_to_phys()(kernel/paging.h). User space lives below0xC0000000. - Respect layering — follow the seam, don’t punch through it. The timer IRQ
never reaches into the display layer; modules register tick hooks
(
timer_register_tick_hook). Display presents through the video vtable (kernel/video.h), not by poking a driver directly. - Fallback-gate every hypervisor / hardware / boot-mode-specific change
(
vm_kind(), PCI-id, capability bits, cmdline flags) so the tested QEMU path (BIOS + IDE + PS/2 +-vga std) is never regressed. New accelerated paths degrade cleanly to the dumb LFB / VGA-text path. - Bound every hardware wait. Poll loops on device status (8042, IDE DRQ, UART, the SVGA FIFO) spin with an iteration cap, never forever — an unbounded spin shows up as a “CPU disabled” hang, not a panic.
- Fail safe, and say what failed. Every CPU exception is installed (IDT
0–31); ring-3 faults raise
SIGSEGV, ring-0 faults panic. Panics render in VGA text mode and name the failing context (program/driver + EIP foraddr2line). Prefer a panic with a message over a silent halt. - Paths follow Linux:
/usr/apps/root/proc/dev/tmp/mnt/<name>/mnt/cdrom; apps at/apps/*.elf, libc at/usr/lib/libc.a. Per-user dotfiles:~/.makshrc(sh login),~/.vixrc(vix),~/.sbrc(text statusbar layout).
6. The syscall ABI is one source of truth
The ABI is shared verbatim between kernel and userspace (Linux-uapi style),
in src/kernel/include/:
makar_syscalls.h— theSYS_*numbers.makar_abi.h— clockids,struct timeval/timespec/stat/dirent,tty_cell_t, open/fcntl flags,S_IF*+DT_*bits.makar_keys.h— keycodes/sentinels.makar_signals.h— signal numbers.
Both kernel/syscall.h (kernel-internal dispatch) and src/userspace/syscall.h
(the ring-3 wrappers) include those headers — they never re-declare the
numbers. Never duplicate or let the two sides drift. Allocate a new syscall
past the current max, wire the wrapper, and document it in
docs/syscalls.md in the same change. int 0x80, i386 register convention.
7. Commits
- One commit per discrete work item. Imperative subject; the body explains why.
- NEVER add
Co-Authored-By, “Generated with Claude Code”, or any AI-attribution footer — of any kind. (This overrides the global default.) - Commit/push only when the user asks. Always branch off
main; never commit tomaindirectly. - End the body with the gate results you actually ran (e.g.
ktest PASS; guitest GUI: READY; kbtest ALL PASS).
8. Docs — cover everything, update as you go
- Update
docs/andprogress.mdin the same commit as the code they describe — not at the end of a PR. Any behaviour change or new feature touches the docs; mark the matchingprogress.mdT-item too. - Every app, library, framework, kernel subsystem, driver, and syscall gets
documentation: each
mx*client, thegui_gfx/gui_ui/makx/gui_browserlibs, everydocs/kernel/*.mdmodule, the syscall table, the glossary. Add an element → add its doc. CLAUDE.mdstays a one-line-per-item router. Detail goes indocs/, the companions, or the source.- New abbreviation →
docs/glossary.md. New third-party code/asset → keep its upstream licence in-tree and record it inTHIRD-PARTY-NOTICES.md/LICENSES/. Shipped PRs →CLAUDE.history.md; future work →CLAUDE.roadmap.md.
9. When unsure
Check CLAUDE.md → the relevant docs/ page → the source, in that order. Ask
the user only for decisions you genuinely cannot resolve from those (and that
change what you build); otherwise pick the obvious option and say so.