MiNix/reference.md
2026-03-28 17:36:43 +07:00

24 KiB

MiNix: Architecture & Reference

Vision

A minimal, non-systemd Linux system with Nix as the operating system layer. dinit for PID 1 and process supervision. NixOS module system for declarative configuration. Bubblewrap for service sandboxing. No Nix daemon.

Two core principles drive the architecture:

  1. dinit over runit — proper dependency DAG, parallel boot, native service types that map cleanly to systemd, readiness notification, cgroup placement.
  2. Daemon-less Nix — the system store is root-owned (single-user Nix), user stores are per-user overlay stores. No nix-daemon.

Flake Architecture

Two independent flakes. The distro flake is build tooling — it never leaks into the end machine. The machine flake lives on the device and drives both system rebuilds and user configuration.

DISTRO FLAKE (git.axiomania.org/lebowski/MiNix)
  Build tooling only — produces images, exports lib + modules.
  Never baked into end machines.

  inputs:
    nixpkgs-stable    github:NixOS/nixpkgs/nixos-25.11
    nixpkgs-unstable   github:NixOS/nixpkgs/nixos-unstable
    home-manager       github:nix-community/home-manager/release-25.11

  outputs:
    lib.minixSystem         eval function for machine flakes
    homeModules.default     hm-dinit.nix (dinit user service module)
    packages.*.vm           disk image (dev only)
    devShells.*             qemu, debug tools

MACHINE FLAKE (/etc/minix/ on end device)
  Per-machine config. Mutable — installed on first boot, user edits persist.
  References distro via git+https:// (resolved by narHash, no path: fetcher).

  inputs:
    minix              git+https://git.axiomania.org/lebowski/MiNix.git
    nixpkgs-stable     follows minix/nixpkgs-stable
    nixpkgs-unstable   follows minix/nixpkgs-unstable

  outputs:
    packages.*.system   systemProfile (minix.lib.minixSystem + ./configuration.nix)

USER HM FLAKE (~/.config/home-manager/ per user)
  Fully independent from machine flake. Auto-scaffolded on first boot.
  User owns and edits these files.

  inputs:
    minix              git+https://git.axiomania.org/lebowski/MiNix.git
    nixpkgs            follows minix/nixpkgs-stable
    home-manager       follows minix/home-manager

  outputs:
    homeConfigurations.<user>   HM config (./home.nix)

Input flow (follows chain)

git+https://git.axiomania.org/lebowski/MiNix.git
                    |
             distro flake
            /      |       \
   nixpkgs-stable  nixpkgs-unstable  home-manager
         |               |                |
    "follows"        "follows"        "follows"
         |               |                |
  machine flake (/etc/minix/)     user flake (~/.config/home-manager/)

One fetch (distro flake) pins everything. Machine and user flakes
have zero independent inputs — all shared via follows.

Why git+https:// (not path:)

The path: flake fetcher triggers builtins.path { filter; } which copies store paths. In a local-overlay-store, this creates a SYMLINK in the upper layer instead of a real directory. Nix then rejects it: "path is a symlink".

git+https:// inputs are locked by narHash. The overlay store resolves narHashes by reading the lower store's DB — if the system build already fetched the input, it's found without any copy. No symlink, no bug.

On-device layout

/etc/minix/                          mutable (first-boot copy, edits persist)
  flake.nix                          machine flake (git+https://...MiNix)
  flake.lock                         locked pins
  configuration.nix                  system config
  hardware-configuration.nix         hardware-specific

~/.config/home-manager/              per-user, auto-scaffolded on first boot
  flake.nix                          user flake (git+https://...MiNix)
  flake.lock                         locked pins
  home.nix                           HM config

Who evaluates what

ROOT (minix-rebuild build):
  nix build /etc/minix#packages.<arch>.system
    -> fetches minix from Forgejo (by narHash)
    -> minix.lib.minixSystem { modules = [ ./configuration.nix ] }
    -> systemProfile -> nix-env --set -> reboot

USER (home-manager switch):
  home-manager switch --flake ~/.config/home-manager
    -> fetches minix from Forgejo (narHash hit — already in lower store)
    -> homeConfigurations.<user> -> builds in overlay store -> activates

Architecture Overview

Layer 2: User Environment (Home Manager, per-user overlay store)
  Per-user, declarative. Owns $HOME only.
  Packages, dotfiles, user dinit services.
  Overlay store: ~/.local/share/nix/ upper on /nix/store lower.
  Updated: home-manager switch (live, no reboot)

Layer 1: System Profile (root-owned Nix store, single-user Nix)
  /nix/var/nix/profiles/system
    init          — dinit init wrapper (generated)
    kernel/       — vmlinuz, modules
    etc/          — /etc files (built by NixOS etc.nix)
    dinit.d/      — dinit service files
    sw/           — system PATH (buildEnv)
    firmware/     — hardware firmware
    sysusers-credentials/ — hashed passwords for systemd-sysusers
  Updated: minix-rebuild (root, single-user nix build) + reboot

Layer 0: Boot (NixOS initrd)
  NixOS scripted initrd -> switch_root -> $profile/init -> dinit
  Kernel cmdline: init=$profile/init

Two-layer model

System profile:  "What does the machine need to run?"  -> kernel, /etc, services
User environment: "What does the user see and use?"     -> $HOME only

No separate "base rootfs" layer. The init wrapper and all boot logic are generated as Nix derivations and live inside the system profile. The system profile is self-contained — switch profiles by pointing /nix/var/nix/profiles/system at a new store path and rebooting.

Per-user overlay stores

/nix/store (system)           — root-owned, single-user Nix mode
                                minix-rebuild runs nix build as root
                                read-only bind mount at runtime (minix.hardenStore)
                                = overlay "lower" layer

~/.local/share/nix/ (user)    — per-user, daemon-less
  upper/                        overlayfs upper layer (new packages go here)
  work/                         overlayfs work directory
  merged/nix/store/             overlayfs mount point
  var/nix/                      overlay store state (db, profiles, gcroots)

Mount namespace setup (user-init at boot):

  1. unshare --mount creates a new mount namespace
  2. Bind-mount ns handle to /run/user/$UID/ns/mnt
  3. nsenter sets up overlayfs: lower=/nix/store, upper=~/.local/share/nix/upper
  4. Bind-mount merged store over /nix/store within the namespace
  5. pam_minix_ns.so enters the namespace at login via setns()

Store DB permissions: /nix/var/nix/db/ group-writable by users so overlay store can read the lower store's sqlite DB (narHash lookups).

No nix-daemon. No nix-daemon.socket. No nixbld build users.

dinit service supervision

PID 1: dinit --system
  +-- early-* services (scripted: pseudofs, eudev, modules, hostname, etc.)
  +-- system services (translated from systemd.services.*)
  +-- user dinit instances (spawned by turnstile at login)
       +-- user services (from Home Manager / user store)

dinit provides dependency-ordered parallel startup, readiness notification, native service types (process, bgprocess, scripted, internal, triggered), and template services via @ suffix with $1 substitution.


Module Evaluation

system/eval.nix imports the full NixOS module-list.nix and filters via a blacklist, then adds MiNix modules:

Blacklisted patterns (hasInfix match):
  "system/boot/systemd"                   -> systemd-compat.nix (optionsOnly)
  "system/boot/stage-2.nix"              -> systemd-compat.nix (optionsOnly)
  "system/activation/"                    -> systemd-compat.nix (optionsOnly)
  "tasks/network-interfaces-systemd.nix"  -> networkd backend, needs systemd
  "system/boot/networkd.nix"              -> same
  "system/boot/shutdown.nix"              -> unconditionally refs systemd
  "services/system/nix-daemon"            -> systemd-compat.nix (optionsOnly; no daemon)

Cherry-picked back from blanket-excluded dirs:
  system/activation/activation-script.nix  -> activation script infrastructure
  system/boot/systemd/tmpfiles.nix         -> tmpfiles.d generation

Uses evalModulesMinimal (not evalModules) for a lighter evaluation path.

The optionsOnly pattern

systemd-compat.nix uses optionsOnly to import stock NixOS modules keeping only their options and imports — dropping config so no systemd wiring fires. This gives us 1:1 API match with stock NixOS and zero maintenance when upstream adds or changes options:

optionsOnly = path:
  let mod = (import path) args;
  in { options = mod.options or {}; imports = mod.imports or []; };

Applied to ~25 systemd sub-modules (journald, logind, user, coredump, oomd, nspawn, sysupdate, sysusers, homed, userdbd, initrd, stage-2, networkd, top-level, bootspec, activatable-system, switchable-system, nixos-init, etc.) plus nix-daemon.nix (suppresses daemon service + nixbld users while keeping nix.enable, nix.package etc. that config/nix.nix depends on).


Boot Flow

NixOS initrd -> switch_root -> $PROFILE/init (minix-init)
  -> mount proc, sys, dev, run (fallbacks)
  -> remount / rw
  -> busybox --install -s /bin
  -> exec dinit --system --services-dir $PROFILE/dinit.d

3-layer boot DAG

boot (internal)
  +-- depends-on: system
  +-- waits-for: serial-getty@ttyS0, autovt@tty1  (login last)

system (internal)
  +-- depends-on: early
  +-- waits-for: sshd, dhcpcd, firewall, ...  (all auto-start services)

early (internal)
  +-- depends-on: early-sysinit, early-hostname, early-loopback

early-sysinit (scripted)
  +-- depends-on: early-etc, early-eudev, early-modules
      tmpfiles --create --remove
      sysusers (create/merge users, groups, passwords)
      machine-id, sysctl, fstab mount
      seedrng (entropy credit)
      nix store registration
      store hardening (optional)
      activation scripts

early-etc (scripted) — populate /etc from profile
  +-- depends-on: early-pseudofs

early-eudev (scripted) — udevd + coldplug + settle
  +-- depends-on: early-pseudofs

early-modules (scripted) — link kernel modules, load modules-load.d
  +-- depends-on: early-pseudofs, early-etc

early-hostname (scripted)
  +-- depends-on: early-etc

early-loopback (scripted) — bring up lo
  +-- depends-on: early-pseudofs

early-pseudofs (scripted) — mount tmp, devpts, shm, cgroup2, /run/wrappers

Getty templates have dependsMs = [ "system" ] so login prompts appear after all service status lines.

Session management (turnstile)

turnstile (Chimera Linux project) provides session tracking and per-user dinit instance spawning:

Login flow:
  user logs in via getty/sshd
    -> PAM calls pam_turnstile.so (session rule)
    -> pam_minix_ns.so enters user's mount namespace (overlay store)
    -> turnstiled spawns dinit --user for that UID
    -> XDG_RUNTIME_DIR created at /run/user/$UID
    -> user dinit reads ~/.config/dinit.d/ + /etc/dinit.d/user/
    -> PAM returns -> shell prompt

Logout flow:
  last session for UID closes
    -> turnstiled stops user dinit instance
    -> XDG_RUNTIME_DIR cleaned up

/etc population (early-etc)

  • Symlinks for immutable files (store paths)
  • Copies for mutable files (those with .mode sidecar)
  • Mutable files preserved across reboots (only written on first boot)
  • Stale symlinks from previous profiles cleaned up
  • passwd/shadow/group/gshadow NOT in /etc profile — managed by sysusers at runtime

The systemd->dinit Translator

systemd-compat.nix is the core translation layer. It reads systemd.services.* set by stock NixOS modules and generates dinit service files. The optionsOnly pattern (importing NixOS modules with only their option declarations) remains unchanged.

systemd -> dinit mapping

systemd directive dinit equivalent
ExecStart command =
ExecStop stop-command =
Type=simple type = process
Type=oneshot type = scripted
Type=forking type = bgprocess + pid-file =
Type=notify type = process (readiness not translated)
User, Group run-as =
Environment env-file = (generated)
EnvironmentFile env-file +=
WorkingDirectory working-dir =
Restart=always restart = yes
Restart=on-failure restart = on-failure
Restart=no restart = no
After= + Requires= depends-on: (hard dep)
After= + Wants= waits-for: (soft dep)
After= (ordering only) after:
LimitNOFILE rlimit-nofile =
LimitCORE rlimit-core =
LimitDATA rlimit-data =
LimitAS rlimit-addrspace =
AmbientCapabilities capabilities = (IAB ^CAP_* format)
NoNewPrivileges options: no-new-privs
PIDFile pid-file = (for bgprocess)
StateDirectory etc. pre-start scripted setup
Template services (@) native $1 substitution
WantedBy= (empty) service not depended on by system (dormant)
Unix socket activation socket-listen = + permissions/uid/gid

Sandboxing (bubblewrap)

systemd directive bwrap equivalent
ProtectSystem=strict --ro-bind / / + writable /var, /run, /tmp + declared dirs
ProtectSystem=full --ro-bind /usr /usr --ro-bind /boot /boot --ro-bind /etc /etc
ProtectSystem=true --ro-bind /usr /usr --ro-bind /boot /boot
ProtectHome=true --tmpfs /home --tmpfs /root --tmpfs /run/user
ProtectHome=read-only --ro-bind /home /home --ro-bind /root /root
PrivateTmp=true --tmpfs /tmp --tmpfs /var/tmp
PrivateDevices=true --dev /dev
PrivateNetwork=true --unshare-net
ProtectKernelTunables --ro-bind /proc/sys /proc/sys
ProtectControlGroups --ro-bind /sys/fs/cgroup /sys/fs/cgroup
ProtectHostname --unshare-uts
NoNewPrivileges --new-session (+ native no-new-privs option)
ReadOnlyPaths --ro-bind per path
ReadWritePaths --bind per path

Services with AmbientCapabilities skip bwrap and setpriv — bwrap can't restore caps after uid change. These use dinit native capabilities instead.

Timer -> snooze translation

systemd timerConfig snooze flag
OnCalendar -d, -m, -w, -H, -M, -S (parsed from calendar spec)
RandomizedDelaySec -R <seconds>
Persistent=true -t /var/lib/snooze/<name>.timefile
AccuracySec -s <seconds> (slack)
OnBootSec builtins.throw (not supported)
OnUnitActiveSec builtins.throw (not supported)

What can't be translated

Feature Status with dinit
Socket activation (full) dinit has socket-listen for Unix sockets; TCP/UDP not supported
DynamicUser Needs ephemeral UID allocator
sd_notify protocol dinit has ready-notification (pipefd/pipevar) — different protocol
Cgroup resource limits (CPUQuota, MemoryMax) dinit has run-in-cgroup; resource limits need cgroup controller config
Monotonic timers (OnBootSec, etc.) Hard error — calendar scheduling only

User/Group Management

users.nix provides minix.users and minix.groups options.

systemd-sysusers (standalone)

Users and groups are created at boot by systemd-sysusers (standalone build from upstream systemd). The sysusers config is generated at build time:

/etc/sysusers.d/00-minix.conf:
  u root 0:0 "System administrator" /root /bin/sh
  u sshd 806:803 "SSH privilege separation user" /var/empty /bin/sh
  g root 0
  g wheel 1
  m root wheel

Hashed passwords are passed via $CREDENTIALS_DIRECTORY. On first boot, sysusers creates all entries. On subsequent boots, existing entries are preserved (merge semantics) — password changes via passwd persist.

user-init service

The user-init dinit service runs at boot (after early) and:

  1. Creates home directories + sets ownership
  2. Fixes store DB permissions (group-writable for overlay store access)
  3. Creates build log directory permissions
  4. Per interactive user (uid >= 1000):
    • Creates overlay store directories (~/.local/share/nix/)
    • Writes ~/.config/nix/nix.conf (local-overlay-store config, first-boot only)
    • Scaffolds ~/.config/home-manager/{flake.nix,home.nix} (first-boot only)
    • Sets up mount namespace with overlay store (non-fatal)

Bridge from stock NixOS API

Translates users.users -> minix.users and users.groups -> minix.groups with resolved UIDs/GIDs, including extraGroups membership bridging.


Standalone Tools

Tool Source Purpose
systemd-tmpfiles systemd v260.1 standalone build tmpfiles.d directory/symlink creation
systemd-sysusers systemd v260.1 standalone build User/group creation with merge semantics
seedrng busybox built-in RNG seeding with BLAKE2s mixing + entropy credit
virt-what nixpkgs (48 KiB) Virtualization detection (CPUID-level)
turnstile chimera-linux/turnstile v0.1.11 Session tracking + per-user dinit spawning
hwdb.bin systemd v260.1 hwdb.d + eudev udevadm Hardware identification database
pam_minix_ns.so system/packages/pam_minix_ns.c PAM module — enters user mount namespace at login

System Commands

Command User Purpose
minix-rebuild build [FLAKE] root Build system profile from /etc/minix/ and switch to it
minix-rebuild switch PATH root Switch to a pre-built system profile
minix-generate-config root Probe hardware, generate configuration.nix
home-manager switch --flake ~/.config/home-manager user Build and activate HM generation

NixOS Compatibility

What works (standard NixOS options)

  • boot.kernelPackages, boot.kernelModules, boot.kernelParams
  • boot.initrd.availableKernelModules, boot.initrd.kernelModules
  • fileSystems.*, swapDevices
  • hardware.firmware, hardware.enableRedistributableFirmware
  • services.udev.extraRules, services.udev.packages
  • environment.etc.*, environment.systemPackages
  • nixpkgs.overlays, nixpkgs.config, nixpkgs.hostPlatform
  • security.pam.services.* (stock NixOS PAM, with SHA-512 enforced)
  • networking.firewall.* (via translated iptables service)
  • systemd.services.* (auto-translated to dinit)
  • systemd.timers.*, startAt (translated to snooze)
  • systemd.tmpfiles.* (standalone tmpfiles binary)
  • system.stateVersion

A standard NixOS hardware-configuration.nix works as-is in MiNix.

MiNix-specific options

Option Purpose
minix.users.* / minix.groups.* sysusers-based user/group management
minix.hostname Hostname (decoupled from networkd)
minix.hardenStore Read-only bind mount for /nix/store
minix.turnstile.* Session tracking config
minix.flakeRef Default flake ref for minix-rebuild (default: /etc/minix)
minix.machineFlake Path to machine flake dir (installed to /etc/minix/)
dinit.services.* Native dinit service definitions

users.users and users.groups from stock NixOS modules are bridged automatically to minix.users/minix.groups — both APIs work.


File Structure

flake.nix                         Distro flake: minixSystem, homeModules, packages
reference.md                      This document
system/
  eval.nix                        Module evaluator (blacklist + evalModulesMinimal)
  overlay.nix                     nixpkgs overlay (strip systemd deps, enable dinit caps)
  lib/
    translator.nix                systemd->dinit service translator
  modules/
    systemd-compat.nix            optionsOnly imports + translator wiring
    dinit-services.nix            dinit.services -> environment.etc."dinit.d/..."
    dinit-init.nix                Init wrapper + 3-layer boot DAG + shutdown hook
    users.nix                     minix.users/groups + sysusers + overlay store + HM scaffold
    eudev-compat.nix              eudev-as-systemd wrapper + standalone tools + initrd fixes
    toplevel.nix                  Assembles system profile derivation
    generate-config.nix           minix-generate-config (virt-what + stock perl script)
    base-etc.nix                  minix.hostname, LOCALE_ARCHIVE override
    defaults.nix                  Feature flags, distro identity, overlay, syslogd
    rebuild.nix                   minix-rebuild (build + switch)
    turnstile.nix                 turnstile session tracking + PAM integration
    hm-dinit.nix                  Home Manager module for dinit user services
    system-path.nix               Busybox-based core packages
  packages/
    systemd-tmpfiles-standalone.nix  Standalone tmpfiles from systemd v260.1
    systemd-sysusers-standalone.nix  Standalone sysusers from systemd v260.1
    turnstile.nix                    turnstile v0.1.11 (session/login tracker)
    pam-minix-ns.nix                 PAM module for mount namespace entry
    pam_minix_ns.c                   PAM module C source
machines/
  qemu-vm/
    flake.nix                     Machine flake (git+https://...MiNix, system only)
    configuration.nix             QEMU VM config (getty, sshd, dhcpcd, etc.)
    hardware-configuration.nix    Detected hardware (kernel modules, filesystems)
image/
  disk.nix                        Persistent qcow2 disk image + QEMU runner
Refs/
  nixpkgs/                        Shallow clone for module inspection
  dinit/                          dinit source + man pages
  dinit-chimera/                  Chimera Linux's dinit integration
  NixNG/modules/dinit/            NixNG's Nix-native dinit module
  turnstile/                      turnstile source (Chimera Linux)

Known Optimization Opportunities

nixpkgs source duplication (~444 MB)

The nixpkgs source tree is not part of the system closure (only package outputs are). When a user evaluates their HM flake, nix fetches and unpacks the full nixpkgs source into the overlay upper layer. Fix: add nixpkgs source to system closure via system.extraDependencies so it's in the lower store.

glibc-locales duplication (~222 MB)

The systemd-stripping overlay changes the derivation graph enough to produce a different glibc-locales hash than "clean" nixpkgs. Users get a second copy. Fix: investigate overlay impact on locale derivation chain.

.drv file accumulation (~1280 files)

Nix evaluation instantiates all referenced derivations in the store. In the overlay store, these accumulate in the upper layer. Normal nix behavior — visible because overlay makes the user's additions explicit.


Development Status

Done

  • System profile via evalModulesMinimal + full NixOS module set
  • systemd->dinit translator with bwrap sandboxing
  • Timer->snooze translation (calendar timers, RandomizedDelaySec, Persistent)
  • AmbientCapabilities -> dinit native IAB capabilities
  • Socket activation for Unix sockets
  • Template services (getty@, serial-getty@, autovt@) with native dinit $1
  • User/group management with sysusers + dynamic allocation + NixOS bridge
  • eudev compatibility layer + standalone tools (tmpfiles, sysusers)
  • Persistent qcow2 disk image builder
  • minix-generate-config and minix-rebuild tools
  • 3-layer boot DAG (early->system->boot) + shutdown hook
  • Serial console detection via kernel params
  • Mutable /etc with first-boot-only writes + sysusers merge semantics
  • nix-daemon suppressed (no daemon, no nixbld users)
  • turnstile session tracking + PAM integration
  • hwdb.bin + systemd-cat stub
  • Per-user overlay stores (local-overlay-store, mount namespace, pam_minix_ns.so)
  • Flake architecture: distro flake (build tooling) + machine flake (/etc/minix/)
  • Home Manager integration (user flake auto-scaffolded, follows distro pins)
  • VM-tested: full pipeline — boot, overlay store, nix run, home-manager switch

Future

  • Optimize: preload nixpkgs/HM source into system closure
  • Optimize: fix glibc-locales derivation divergence from overlay
  • System-provided user services (D-Bus session bus, PipeWire)
  • Board support (Rockchip SBCs, SD card images)
  • ISO for generic x86 install
  • Live system switching without reboot