Fixes for home-manager to work, now multi-user daemon-less works

end-to-end, although not optimal
This commit is contained in:
GammaKinematics 2026-03-28 17:36:43 +07:00
parent e287082dba
commit bfead1719d
5 changed files with 312 additions and 372 deletions

View file

@ -94,10 +94,10 @@ let
export EXT2FS_NO_MTAB_OK=yes
fsck.ext4 -n -f rootfs.img
# Shrink to actual content size, then add 2 GiB headroom for in-VM builds
# Shrink to actual content size, then add 4 GiB headroom for in-VM builds
resize2fs -M rootfs.img
new_size=$(dumpe2fs -h rootfs.img 2>/dev/null | awk -F: \
'/Block count/{count=$2} /Block size/{size=$2} END{print int((count*size + 2 * 2^30) / size)}')
'/Block count/{count=$2} /Block size/{size=$2} END{print int((count*size + 4 * 2^30) / size)}')
resize2fs rootfs.img "$new_size"
echo "Final ext4 size: $(( $(stat -c %s rootfs.img) / 1024 / 1024 )) MiB"

View file

@ -4,27 +4,129 @@
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 by default.
The key insight: you don't need NixOS to get most of its benefits — just Nix +
the NixOS module evaluator + cherry-picked modules + a translation shim for
services.
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 (daemon-less Home Manager). No nix-daemon by default.
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 Nix store)
Layer 2: User Environment (Home Manager, per-user overlay store)
Per-user, declarative. Owns $HOME only.
Packages, dotfiles, user dinit services.
Own Nix store (~/.local/share/nix/store) — no daemon needed.
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)
@ -55,34 +157,41 @@ 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.
### Daemon-less Nix stores
### Per-user overlay stores
```
/nix/store (system) — owned by root, single-user Nix mode
minix-rebuild runs nix build directly as root
read-only bind mount at runtime (minix.hardenStore)
/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) — owned by each user, daemon-less
Home Manager operates against this store
binary cache for substitutes (no local rebuilds needed)
user dinit services reference paths in this store
~/.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)
```
No nix-daemon service. No nix-daemon.socket. No nixbld build users. The daemon
is available as an opt-in for multi-user shared-store setups, not a default.
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()`
The store boundary and dinit instance boundary align: system dinit (PID 1)
launches system services from `/nix/store`; user dinit instances (`dinit --user`)
launch user services from the user's store. No cross-store references.
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 at login)
└── user services (from Home Manager / user store)
+-- 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,
@ -146,46 +255,42 @@ NixOS initrd -> switch_root -> $PROFILE/init (minix-init)
### 3-layer boot DAG
dinit starts the `boot` service by default. The boot DAG is 3 layers deep:
```
boot (internal)
├── depends-on: system
└── waits-for: serial-getty@ttyS0, autovt@tty1 (login last)
+-- 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)
+-- depends-on: early
+-- waits-for: sshd, dhcpcd, firewall, ... (all auto-start services)
early (internal)
└── depends-on: early-sysinit, early-hostname, early-loopback
+-- 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
+-- 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
+-- depends-on: early-pseudofs
early-eudev (scripted) — udevd + coldplug + settle
└── depends-on: early-pseudofs
+-- depends-on: early-pseudofs
early-modules (scripted) — link kernel modules, load modules-load.d
└── depends-on: early-pseudofs, early-etc
+-- depends-on: early-pseudofs, early-etc
early-hostname (scripted)
└── depends-on: early-etc
+-- depends-on: early-etc
early-loopback (scripted) — bring up lo
└── depends-on: early-pseudofs
+-- depends-on: early-pseudofs
early-pseudofs (scripted) — mount tmp, devpts, shm, cgroup2, /run/wrappers
```
@ -202,6 +307,7 @@ 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/
@ -211,21 +317,8 @@ Logout flow:
last session for UID closes
-> turnstiled stops user dinit instance
-> XDG_RUNTIME_DIR cleaned up
(linger=maybe checks /var/lib/turnstiled/linger/<username>)
```
Boot placement: `turnstiled` is a process service with `dependsMs = [ "early" ]`,
auto-starts via system milestone, ready before gettys accept login.
Configuration:
- `/etc/turnstile/turnstiled.conf` — backend selection, rundir, linger policy
- `/etc/turnstile/backend/dinit.conf` — user dinit service directories
- `/etc/dinit.d/user/` — system-provided user services (D-Bus, PipeWire, etc.)
- `/etc/dinit.d/user/boot.d/` — system-provided user boot services
PAM integration: `pam_turnstile.so` added to login, sshd, su, sudo session
rules (order 11900). Separate `turnstiled` PAM service for backend spawning.
### /etc population (early-etc)
- Symlinks for immutable files (store paths)
@ -234,15 +327,6 @@ rules (order 11900). Separate `turnstiled` PAM service for backend spawning.
- Stale symlinks from previous profiles cleaned up
- passwd/shadow/group/gshadow NOT in /etc profile — managed by sysusers at runtime
### Shutdown
```
dinit ordered service shutdown via dependency graph
-> shutdown-hook: seedrng (save entropy seed)
-> exit 1 (tells dinit to handle unmounting)
-> dinit unmounts filesystems, remounts / ro, sync, halt/reboot
```
---
## The systemd->dinit Translator
@ -254,9 +338,6 @@ option declarations) remains unchanged.
### systemd -> dinit mapping
dinit's native primitives map directly to systemd.
Most translations become 1:1 config directives rather than shell script hacks:
| systemd directive | dinit equivalent |
|---|---|
| `ExecStart` | `command =` |
@ -287,18 +368,8 @@ Most translations become 1:1 config directives rather than shell script hacks:
| `WantedBy=` (empty) | service not depended on by `system` (dormant) |
| Unix socket activation | `socket-listen =` + permissions/uid/gid |
Key features:
- **Dependencies are real**`depends-on`/`waits-for`/`after` give proper
startup ordering and failure propagation.
- **Service types map natively** — oneshot->scripted, forking->bgprocess, etc.
- **Restart policy is a directive** — no generated finish scripts.
- **Socket pre-opening**`socket-listen =` provides basic socket activation.
- **Capabilities** — native `capabilities =` with IAB syntax.
### Sandboxing (bubblewrap)
bwrap wraps the `command` in the dinit service file:
| systemd directive | bwrap equivalent |
|---|---|
| `ProtectSystem=strict` | `--ro-bind / /` + writable /var, /run, /tmp + declared dirs |
@ -321,9 +392,6 @@ restore caps after uid change. These use dinit native `capabilities` instead.
### Timer -> snooze translation
`systemd.timers` and `startAt` are translated to per-timer dinit services
using snooze (a lightweight cron alternative):
| systemd timerConfig | snooze flag |
|---|---|
| `OnCalendar` | `-d`, `-m`, `-w`, `-H`, `-M`, `-S` (parsed from calendar spec) |
@ -333,10 +401,6 @@ using snooze (a lightweight cron alternative):
| `OnBootSec` | `builtins.throw` (not supported) |
| `OnUnitActiveSec` | `builtins.throw` (not supported) |
Each timer becomes a supervised `dinit.services."timer-<name>"` (process type,
restart=yes). The triggered service has `autoStart = false` (started by snooze
via `dinitctl start`).
### What can't be translated
| Feature | Status with dinit |
@ -356,8 +420,7 @@ via `dinitctl start`).
### systemd-sysusers (standalone)
Users and groups are created at boot by `systemd-sysusers` (standalone build
from upstream systemd, statically links all systemd internals, only libc is
dynamic). The sysusers config is generated at build time:
from upstream systemd). The sysusers config is generated at build time:
```
/etc/sysusers.d/00-minix.conf:
@ -368,54 +431,51 @@ dynamic). The sysusers config is generated at build time:
m root wheel
```
Hashed passwords are passed via `$CREDENTIALS_DIRECTORY` (a store path linked
from the system profile). On first boot, sysusers creates all entries. On
subsequent boots, existing entries are preserved (merge semantics) — password
changes via `passwd` persist across reboots.
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.
### Dynamic UID/GID allocation
### user-init service
Groups and users without well-known IDs get deterministic allocation:
- Groups sorted by name, sequential GIDs from base 800
- Users sorted by name, sequential UIDs from 800 + num_groups
- Fallback chain: explicit UID/GID -> ids.nix lookup -> dynamic -> throw
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. Stock
NixOS activation scripts (Perl-based user/group management) are force-emptied.
with resolved UIDs/GIDs, including `extraGroups` membership bridging.
---
## Standalone Tools
Built from upstream sources, replacing hand-rolled implementations:
| 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, replaces shell script) |
| `turnstile` | chimera-linux/turnstile v0.1.11 | Session tracking + per-user dinit instance spawning |
| `hwdb.bin` | systemd v260.1 hwdb.d data + eudev udevadm | Hardware identification database (PCI/USB/Bluetooth/input) |
| `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 |
---
## eudev Compatibility
## System Commands
`eudev-compat.nix` presents eudev as `systemd.package` for stock NixOS modules
that expect systemd paths:
- Symlinks eudev binaries and lib/udev into a wrapper derivation
- Stubs: `systemd-sysctl` (exit 0), `systemd-run` (exec directly), `systemd-cat` (strip flags, exec)
- Empty stubs for systemd-specific udev rules
- `99-default.link` in `lib/systemd/network/`
- `kmod` attribute passthrough (stage-1.nix reads `udev.kmod`)
- tmpfiles.d configs from standalone systemd-tmpfiles build
- Initrd fixes: dereference symlinks, provide `systemd-udevd` name
- `hwdb.bin` compiled from systemd v260.1 hwdb.d data files using eudev's `udevadm hwdb --update`
| 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 |
---
@ -423,250 +483,134 @@ that expect systemd paths:
### What works (standard NixOS options)
These work identically to NixOS because we use the stock modules:
- `boot.kernelPackages`, `boot.kernelModules`, `boot.kernelParams`
- `boot.initrd.availableKernelModules`, `boot.initrd.kernelModules`
- `boot.blacklistedKernelModules`, `boot.extraModprobeConfig`
- `fileSystems.*`, `swapDevices`
- `hardware.firmware`, `hardware.enableRedistributableFirmware`
- `hardware.deviceTree.*`
- `hardware.cpu.amd.updateMicrocode`, `hardware.cpu.intel.updateMicrocode`
- `services.udev.extraRules`, `services.udev.packages`
- `environment.etc.*`, `environment.systemPackages`
- `nixpkgs.overlays`, `nixpkgs.config`, `nixpkgs.hostPlatform`
- `boot.kernel.sysctl.*`
- `time.timeZone`, `i18n.defaultLocale`
- `security.pki.certificateFiles`
- `security.pam.services.*` (stock NixOS PAM, with SHA-512 enforced)
- `networking.iproute2.*`
- `networking.firewall.*` (via translated iptables service)
- `system.stateVersion`
- `systemd.services.*` (auto-translated to dinit)
- `systemd.timers.*`, `startAt` (translated to snooze)
- `systemd.tmpfiles.*` (standalone tmpfiles binary)
- `systemd.targets.*` (wants/requires used for template instantiation)
- `system.stateVersion`
A standard NixOS `hardware-configuration.nix` works as-is in MiNix.
### MiNix-specific options
| NixOS option | MiNix equivalent | Why |
|---|---|---|
| `users.users.*` | `minix.users.*` | sysusers standalone replaces Perl activation |
| `users.groups.*` | `minix.groups.*` | Same |
| `networking.hostName` | `minix.hostname` | Simpler, decoupled from networkd |
| `dinit.services.*` | (native) | Direct dinit service definitions |
| `minix.hardenStore` | (native) | Read-only bind mount for /nix/store |
| `minix.turnstile.*` | (native) | Session tracking, XDG_RUNTIME_DIR, user dinit instances |
| `minix.flakeRef` | (native) | Default flake reference for minix-rebuild build |
| 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 |
Note: `users.users` and `users.groups` from stock NixOS modules are bridged
`users.users` and `users.groups` from stock NixOS modules are bridged
automatically to `minix.users`/`minix.groups` — both APIs work.
### What doesn't work
- `networking.interfaces.*` (scripted backend) — deeply systemd-coupled
- `virtualisation.*` — systemd-coupled
- `services.*` modules using `DynamicUser` or full socket activation
- `systemd.network.*` (networkd) — blacklisted, no backend
- `powerManagement.*` — disabled by default
---
## File Structure
```
/data/Distro/
flake.nix Main flake: minixSystem, packages, devShell
reference.md This document
system/
eval.nix Module evaluator (blacklist + evalModulesMinimal)
overlay.nix nixpkgs overlay (strip systemd deps, enable dinit caps)
modules/
systemd-compat.nix optionsOnly imports + systemd->dinit translator
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.d generation + bridge
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 subcommands)
turnstile.nix turnstile session tracking + PAM integration
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)
machines/
qemu-vm/
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
systemd/ systemd v260.1 source (for standalone builds)
seedrng/ seedrng source (reference only, busybox ships it)
turnstile/ turnstile source (session tracker, Chimera Linux)
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)
```
---
## nixpkgs Overlay
## Known Optimization Opportunities
```nix
final: prev: {
procps = prev.procps.override { withSystemd = false; };
util-linux = prev.util-linux.override { systemdSupport = false; };
openssh = prev.openssh.override { withFIDO = false; };
dinit = prev.dinit.overrideAttrs (old: {
buildInputs = (old.buildInputs or []) ++ [ prev.libcap ];
configureFlags = (old.configureFlags or []) ++ [ "--enable-capabilities" ];
});
}
```
### 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.
Applied via `nixpkgs.overlays` in defaults.nix. Strips systemd linkage from
key packages. dinit built with `--enable-capabilities` for native IAB support.
Uses a separate `hostPkgs` (without overlay) for build tools to avoid cache misses.
### 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.
---
## Flake Outputs
```nix
{
lib.minixSystem = minixSystem; # For external consumers
packages.x86_64-linux = {
system = systemProfile; # Just the system profile
vm = diskImage; # qcow2 disk + QEMU runner
default = diskImage;
};
devShells.x86_64-linux.default = ...; # qemu, cpio, gzip, etc.
}
```
### Build & test
```bash
nix build .#vm # Build disk image + runner
./result/bin/run-minix-vm # Boot in QEMU
ssh -p 2222 root@localhost # SSH into VM (empty password)
dinitctl list # Check services inside VM
```
---
## Key Design Rules
1. **Reuse NixOS modules wherever possible.** Use stock modules as-is via
optionsOnly or direct import. Only write from scratch where NixOS is
structurally incompatible.
2. **Translate, don't rewrite.** The systemd->dinit translator gives access
to the entire NixOS service ecosystem automatically. dinit's native
primitives make most translations 1:1 config directives.
3. **Mutable files with merge semantics.** /etc files with `.mode` sidecar
are preserved across reboots (early-etc only writes on first boot).
passwd/shadow/group/gshadow managed by sysusers with entry-level merge —
runtime changes via `passwd` persist.
4. **Sandboxing matters.** Bubblewrap provides real namespace isolation
equivalent to systemd's security directives. dinit's native capabilities
and no-new-privs complement bwrap.
5. **Keep it minimal.** dinit + busybox + eudev base. No systemd runtime.
No Nix daemon by default.
6. **Use standalone tools.** Prefer standalone builds (systemd-tmpfiles,
systemd-sysusers) and busybox builtins (seedrng) over hand-rolled code.
---
## Key Tradeoffs
### What we lose vs NixOS
- Live system switching (we require reboot for system profile changes)
- DynamicUser (needs ephemeral UID allocator)
- Full sd_listen_fds socket activation (dinit covers Unix sockets)
- networkd, resolved, journald (use scripted networking, dhcpcd, syslogd)
- Monotonic timers (OnBootSec, etc. — calendar only via snooze)
- Shared binary cache between users (each user has own store)
### What we gain
- No systemd runtime
- No Nix daemon (simpler, fewer moving parts, smaller attack surface)
- Minimal base (dinit + busybox + eudev)
- Parallel dependency-ordered boot (3-layer dinit DAG)
- Readiness notification for services
- Native capabilities, cgroup placement, rlimits in the init
- Full control over every layer
- Access to ~80-85% of NixOS service modules via the translator
- Real sandboxing via bubblewrap + dinit native security
- Standard hardware-configuration.nix compatibility
- Per-user isolation (separate stores, separate dinit instances)
- Proper user merge semantics (sysusers — password changes persist)
---
## Development Roadmap
## Development Status
### Done
- [x] System profile via evalModulesMinimal + full NixOS module set
- [x] systemd->dinit translator with bwrap sandboxing
- [x] Timer->snooze translation (calendar timers, RandomizedDelaySec, Persistent)
- [x] AmbientCapabilities -> dinit native IAB capabilities
- [x] Socket activation for Unix sockets (socket-listen)
- [x] PIDFile, wants translation, rlimits (nofile, core, data, addrspace)
- [x] Template services (getty@, serial-getty@, autovt@) with native dinit `$1`
- [x] Socket activation for Unix sockets
- [x] Template services (getty@, serial-getty@, autovt@) with native dinit $1
- [x] User/group management with sysusers + dynamic allocation + NixOS bridge
- [x] eudev compatibility layer + standalone tools (tmpfiles, sysusers)
- [x] Persistent qcow2 disk image builder
- [x] minix-generate-config (virt-what) and minix-rebuild tools
- [x] dinit-init.nix — 3-layer boot DAG (early->system->boot) + shutdown hook
- [x] seedrng (busybox built-in) for entropy management
- [x] Serial console detection via kernel params (no udev deadlock)
- [x] minix-generate-config and minix-rebuild tools
- [x] 3-layer boot DAG (early->system->boot) + shutdown hook
- [x] Serial console detection via kernel params
- [x] Mutable /etc with first-boot-only writes + sysusers merge semantics
- [x] VM-tested: boot, reboot, SSH, password persistence, service ordering
- [x] nix-daemon suppressed (optionsOnly — no daemon service, no nixbld users)
- [x] turnstile session tracking (per-user dinit instances, XDG_RUNTIME_DIR)
- [x] PAM integration (pam_turnstile.so in login/sshd/su/sudo)
- [x] minix-rebuild enhanced (build + switch subcommands, configurable flake ref)
- [x] VM-tested: turnstiled running, user dinit instance, XDG_RUNTIME_DIR=/run/user/$UID
- [x] hwdb.bin compiled from systemd hwdb.d data (11 MB, eudev device identification working)
- [x] systemd-cat stub (strips journal flags, execs command — for X11 display manager modules)
### Next: per-user Nix stores + Home Manager
- [ ] User store: per-user Nix store paths (~/.local/share/nix/)
- [ ] Home Manager integration with per-user stores
- [ ] System-provided user services (D-Bus session bus, PipeWire via /etc/dinit.d/user/)
- [x] nix-daemon suppressed (no daemon, no nixbld users)
- [x] turnstile session tracking + PAM integration
- [x] hwdb.bin + systemd-cat stub
- [x] Per-user overlay stores (local-overlay-store, mount namespace, pam_minix_ns.so)
- [x] Flake architecture: distro flake (build tooling) + machine flake (/etc/minix/)
- [x] Home Manager integration (user flake auto-scaffolded, follows distro pins)
- [x] 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)
- [ ] GRUB bootloader (currently extlinux only)
- [ ] ISO for generic x86 install
- [ ] Seccomp filter generation (SystemCallFilter -> BPF)
- [ ] Live system switching without reboot (stretch goal)
- [ ] Rewrite minix-generate-config in shell (drop Perl dependency)
---
## References
```
Refs/
nixpkgs/ Shallow clone of nixpkgs for module inspection
dinit/ dinit source + man pages (service file format spec)
dinit-chimera/ Chimera Linux's dinit system integration (production reference)
NixNG/ NixNG — has a Nix-native dinit module (modules/dinit/)
systemd/ systemd v260.1 source (standalone tmpfiles + sysusers builds)
seedrng/ seedrng source by Jason Donenfeld (reference; busybox ships it)
turnstile/ turnstile source (session/login tracker, Chimera Linux)
```
- [ ] Live system switching without reboot

View file

@ -30,6 +30,9 @@
# as a distro default so all MiNix systems support overlay stores.
nix.settings.experimental-features = lib.mkDefault [ "nix-command" "flakes" "local-overlay-store" ];
# git is needed by nix's git+https:// fetcher (used by machine and user flakes).
environment.systemPackages = [ pkgs.gitMinimal ];
# vconsole-setup is handled by systemd-vconsole-setup.service which
# we don't have. The reload service calls systemctl — disable it.
systemd.services.reload-systemd-vconsole-setup.enable = lib.mkDefault false;

View file

@ -1,5 +1,4 @@
# minix-rebuild — build and/or switch the system profile.
# minix-home — scaffold per-user Home Manager config.
# Installs the machine flake to /etc/minix/ as mutable files (first-boot-only).
{ config, pkgs, lib, ... }:
@ -72,56 +71,6 @@ in {
;;
esac
'')
# minix-home — scaffold user Home Manager config
(pkgs.writeShellScriptBin "minix-home" ''
set -e
HM_DIR="''${HOME}/.config/home-manager"
case "''${1:-}" in
init)
if [ -f "$HM_DIR/flake.nix" ]; then
echo "''${HM_DIR}/flake.nix already exists. Remove it first to re-init."
exit 1
fi
USERNAME="$(id -un)"
HOMEDIR="$HOME"
ARCH="${pkgs.stdenv.hostPlatform.system}"
mkdir -p "$HM_DIR"
cat > "$HM_DIR/flake.nix" << 'FLAKE'
{
inputs = {
minix.url = "git+https://git.axiomania.org/lebowski/MiNix.git";
nixpkgs.follows = "minix/nixpkgs-stable";
home-manager.follows = "minix/home-manager";
};
outputs = { minix, nixpkgs, home-manager, ... }: {
homeConfigurations."MINIX_USERNAME" = home-manager.lib.homeManagerConfiguration {
pkgs = nixpkgs.legacyPackages.MINIX_ARCH;
modules = [ minix.homeModules.default ./home.nix ];
};
};
}
FLAKE
${pkgs.gnused}/bin/sed -i "s|MINIX_USERNAME|$USERNAME|;s|MINIX_ARCH|$ARCH|" "$HM_DIR/flake.nix"
cat > "$HM_DIR/home.nix" << HOMENIX
{ pkgs, ... }: {
home.username = "$USERNAME";
home.homeDirectory = "$HOMEDIR";
home.stateVersion = "25.11";
programs.home-manager.enable = true;
}
HOMENIX
echo "Created $HM_DIR/flake.nix and $HM_DIR/home.nix"
echo "Edit home.nix, then run: home-manager switch --flake $HM_DIR"
;;
*)
echo "Usage: minix-home init"
echo ""
echo "Scaffolds ~/.config/home-manager/ with a starter flake + home.nix."
exit 1
;;
esac
'')
];
}

View file

@ -167,6 +167,33 @@ let
experimental-features = nix-command flakes local-overlay-store
'';
# Per-user HM flake (mutable — only created on first boot)
mkUserFlake = u: pkgs.writeText "hm-flake-${u.name}" ''
{
inputs = {
minix.url = "git+https://git.axiomania.org/lebowski/MiNix.git";
nixpkgs.follows = "minix/nixpkgs-stable";
home-manager.follows = "minix/home-manager";
};
outputs = { minix, nixpkgs, home-manager, ... }: {
homeConfigurations."${u.name}" = home-manager.lib.homeManagerConfiguration {
pkgs = nixpkgs.legacyPackages.x86_64-linux;
modules = [ minix.homeModules.default ./home.nix ];
};
};
}
'';
# Per-user starter home.nix (mutable — only created on first boot)
mkUserHomeNix = u: pkgs.writeText "home-${u.name}" ''
{ pkgs, ... }: {
home.username = "${u.name}";
home.homeDirectory = "${u.home}";
home.stateVersion = "25.11";
programs.home-manager.enable = true;
}
'';
in {
options = {
minix.users = mkOption {
@ -313,6 +340,11 @@ in {
chmod 0660 /nix/var/nix/db/big-lock
chmod 0664 /nix/var/nix/db/db.sqlite-shm /nix/var/nix/db/db.sqlite-wal
# Build log directory — nix writes build logs here during derivation builds.
mkdir -p /nix/var/log/nix/drvs
chgrp -R users /nix/var/log/nix
chmod -R 0775 /nix/var/log/nix
${concatLines (mapAttrsToList (name: u: let
nixDir = "${u.home}/.local/share/nix";
configDir = "${u.home}/.config";
@ -330,7 +362,19 @@ in {
chmod 644 "${configDir}/nix/nix.conf"
fi
chown -R ${toString u.uid}:${toString u.gid} "${configDir}/nix"
# Per-user HM config (mutable — only create if missing)
mkdir -p "${configDir}/home-manager"
if [ ! -f "${configDir}/home-manager/flake.nix" ]; then
cp ${mkUserFlake u} "${configDir}/home-manager/flake.nix"
chmod 644 "${configDir}/home-manager/flake.nix"
fi
if [ ! -f "${configDir}/home-manager/home.nix" ]; then
cp ${mkUserHomeNix u} "${configDir}/home-manager/home.nix"
chmod 644 "${configDir}/home-manager/home.nix"
fi
chown ${toString u.uid}:${toString u.gid} "${configDir}"
chown -R ${toString u.uid}:${toString u.gid} "${configDir}/nix" "${configDir}/home-manager"
# --- Mount namespace with overlay store ---
# Strategy: start a helper in a new mount ns, bind-mount the ns