MiNix: initial commit
Minimal dinit + Nix Linux system with per-user overlay stores. Renamed from DiNix, restructured flake architecture: - Distro flake = build tooling (lib.minixSystem, homeModules, disk images) - Machine flake = per-device config at /etc/minix/ (mutable, first-boot-only) - User HM config = independent, scaffolded via minix-home init Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
commit
e287082dba
29 changed files with 4048 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
result
|
||||
result-*
|
||||
.direnv/
|
||||
Refs/
|
||||
*.qcow2
|
||||
66
flake.lock
generated
Normal file
66
flake.lock
generated
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
{
|
||||
"nodes": {
|
||||
"home-manager": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"nixpkgs-stable"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1774559029,
|
||||
"narHash": "sha256-deix7yg3j6AhjMPnFDCmWB3f83LsajaaULP5HH2j34k=",
|
||||
"owner": "nix-community",
|
||||
"repo": "home-manager",
|
||||
"rev": "a0bb0d11514f92b639514220114ac8063c72d0a3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-community",
|
||||
"ref": "release-25.11",
|
||||
"repo": "home-manager",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-stable": {
|
||||
"locked": {
|
||||
"lastModified": 1774388614,
|
||||
"narHash": "sha256-tFwzTI0DdDzovdE9+Ras6CUss0yn8P9XV4Ja6RjA+nU=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "1073dad219cb244572b74da2b20c7fe39cb3fa9e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-25.11",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-unstable": {
|
||||
"locked": {
|
||||
"lastModified": 1774386573,
|
||||
"narHash": "sha256-4hAV26quOxdC6iyG7kYaZcM3VOskcPUrdCQd/nx8obc=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "46db2e09e1d3f113a13c0d7b81e2f221c63b8ce9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"home-manager": "home-manager",
|
||||
"nixpkgs-stable": "nixpkgs-stable",
|
||||
"nixpkgs-unstable": "nixpkgs-unstable"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
77
flake.nix
Normal file
77
flake.nix
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
{
|
||||
description = "MiNix — minimal dinit + Nix Linux system";
|
||||
|
||||
inputs = {
|
||||
nixpkgs-stable.url = "github:NixOS/nixpkgs/nixos-25.11";
|
||||
nixpkgs-unstable.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
home-manager = {
|
||||
url = "github:nix-community/home-manager/release-25.11";
|
||||
inputs.nixpkgs.follows = "nixpkgs-stable";
|
||||
};
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs-stable, nixpkgs-unstable, home-manager }:
|
||||
let
|
||||
system = "x86_64-linux";
|
||||
|
||||
# Clean pkgs without overlay — for build tools that don't go into the system
|
||||
# (avoids cache misses from the systemd-stripping overlay on transitive deps)
|
||||
hostPkgs = import nixpkgs-stable { system = "x86_64-linux"; };
|
||||
|
||||
minixSystem = import ./system/eval.nix { nixpkgs = nixpkgs-stable; };
|
||||
|
||||
# Evaluate the system configuration
|
||||
systemConfig = minixSystem {
|
||||
modules = [
|
||||
./machines/qemu-vm/configuration.nix
|
||||
# Install the machine flake to /etc/minix/ (mutable, first-boot-only).
|
||||
# This is the flake that both root (minix-rebuild) and user (home-manager)
|
||||
# evaluate on the end machine.
|
||||
{ minix.machineFlake = ./machines/qemu-vm; }
|
||||
];
|
||||
};
|
||||
|
||||
pkgs = systemConfig.pkgs;
|
||||
systemProfile = systemConfig.config.system.build.systemProfile;
|
||||
diskImage = import ./image/disk.nix {
|
||||
pkgs = hostPkgs;
|
||||
inherit systemConfig systemProfile;
|
||||
};
|
||||
|
||||
in {
|
||||
# The minixSystem function — for external consumers
|
||||
lib.minixSystem = minixSystem;
|
||||
|
||||
# Home Manager module for dinit user service integration.
|
||||
# Machine flakes: modules = [ minix.homeModules.default ./home/user/home.nix ];
|
||||
homeModules.default = ./system/modules/hm-dinit.nix;
|
||||
|
||||
packages.${system} = {
|
||||
system = systemProfile;
|
||||
vm = diskImage;
|
||||
default = diskImage;
|
||||
};
|
||||
|
||||
devShells.${system}.default = hostPkgs.mkShell {
|
||||
packages = with hostPkgs; [
|
||||
# VM
|
||||
qemu_kvm
|
||||
|
||||
# Debugging initrd / disk images
|
||||
cpio
|
||||
gzip
|
||||
file
|
||||
binutils # readelf, objdump
|
||||
strace
|
||||
|
||||
# General
|
||||
coreutils
|
||||
findutils
|
||||
gnugrep
|
||||
gawk
|
||||
tree
|
||||
jq
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
183
image/disk.nix
Normal file
183
image/disk.nix
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
# Persistent disk image — boots MiNix from a qcow2 with ext4 root.
|
||||
# Sandbox-safe: no loop mounts, no sudo, no /dev access.
|
||||
#
|
||||
# Boot flow:
|
||||
# QEMU loads kernel + initrd (from NixOS module system), disk attached as virtio
|
||||
# → NixOS stage-1 initrd mounts ext4 root partition (from fileSystems config)
|
||||
# → switch_root to ${systemProfile}/init (the init wrapper)
|
||||
# → init wrapper mounts pseudo-fs, execs dinit --system
|
||||
# → dinit starts boot service, early services populate /etc etc.
|
||||
#
|
||||
# Build pipeline:
|
||||
# closureInfo → staging dir → mke2fs -d → sfdisk GPT → dd → qemu-img qcow2
|
||||
|
||||
{ pkgs, systemConfig, systemProfile }:
|
||||
|
||||
let
|
||||
inherit (pkgs) lib;
|
||||
|
||||
kernel = systemConfig.config.boot.kernelPackages.kernel;
|
||||
kernelFile = systemConfig.config.system.boot.loader.kernelFile;
|
||||
initrd = "${systemConfig.config.system.build.initialRamdisk}/initrd";
|
||||
kernelParams = lib.concatStringsSep " " systemConfig.config.boot.kernelParams;
|
||||
|
||||
# Full Nix store closure (system profile + all transitive deps)
|
||||
closureInfo = pkgs.closureInfo {
|
||||
rootPaths = [ systemProfile ];
|
||||
};
|
||||
|
||||
# The disk image derivation
|
||||
diskImage = pkgs.stdenv.mkDerivation {
|
||||
name = "minix-disk-image";
|
||||
|
||||
nativeBuildInputs = with pkgs; [
|
||||
e2fsprogs
|
||||
libfaketime
|
||||
fakeroot
|
||||
util-linux # sfdisk
|
||||
qemu-utils # qemu-img
|
||||
coreutils
|
||||
];
|
||||
|
||||
buildCommand = ''
|
||||
# === Stage 1: Build staging directory ===
|
||||
echo "Building staging directory..."
|
||||
mkdir -p staging/nix/store
|
||||
|
||||
# Copy full Nix store closure
|
||||
echo "Copying store paths..."
|
||||
xargs -I % cp -a --reflink=auto % -t staging/nix/store/ < ${closureInfo}/store-paths
|
||||
chmod -R u+w staging/nix/store
|
||||
|
||||
# Nix store registration (loaded by stage-1 on first boot)
|
||||
cp ${closureInfo}/registration staging/nix-path-registration
|
||||
|
||||
# System profile symlink
|
||||
mkdir -p staging/nix/var/nix/profiles
|
||||
ln -s ${systemProfile} staging/nix/var/nix/profiles/system
|
||||
|
||||
# Nix database directory structure (single-user Nix)
|
||||
mkdir -p staging/nix/var/nix/db
|
||||
mkdir -p staging/nix/var/nix/gcroots
|
||||
mkdir -p staging/nix/var/nix/temproots
|
||||
|
||||
# Directory skeleton (init wrapper creates some of these too, but
|
||||
# having them pre-existing avoids transient errors during early boot)
|
||||
mkdir -p staging/{bin,sbin,etc,proc,sys,dev,run,tmp,home,root,var,lib}
|
||||
mkdir -p staging/var/{log,empty,run,lib/dhcpcd}
|
||||
chmod 1777 staging/tmp
|
||||
|
||||
# === Stage 2: Create ext4 filesystem image ===
|
||||
# Size calculation (following nixpkgs make-ext4-fs.nix)
|
||||
echo "Calculating image size..."
|
||||
numInodes=$(find staging | wc -l)
|
||||
numDataBlocks=$(du -s -c -B 4096 --apparent-size staging | tail -1 | awk '{ print int($1 * 1.20) }')
|
||||
bytes=$((2 * 4096 * numInodes + 4096 * numDataBlocks))
|
||||
|
||||
echo "Base size: $((bytes / 1024 / 1024)) MiB (numInodes=$numInodes, numDataBlocks=$numDataBlocks)"
|
||||
|
||||
# Round up to nearest MiB
|
||||
mebibyte=$((1024 * 1024))
|
||||
if (( bytes % mebibyte )); then
|
||||
bytes=$(( (bytes / mebibyte + 1) * mebibyte ))
|
||||
fi
|
||||
|
||||
truncate -s $bytes rootfs.img
|
||||
echo "Creating ext4 image: $((bytes / 1024 / 1024)) MiB"
|
||||
faketime -f "1970-01-01 00:00:01" fakeroot mkfs.ext4 \
|
||||
-L minix \
|
||||
-U 44444444-4444-4444-8888-888888888888 \
|
||||
-d staging \
|
||||
rootfs.img
|
||||
|
||||
# Verify integrity
|
||||
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
|
||||
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)}')
|
||||
resize2fs rootfs.img "$new_size"
|
||||
echo "Final ext4 size: $(( $(stat -c %s rootfs.img) / 1024 / 1024 )) MiB"
|
||||
|
||||
# Free staging directory — no longer needed, ext4 image has the data
|
||||
rm -rf staging
|
||||
|
||||
# === Stage 3: Create GPT disk image ===
|
||||
rootSize=$(stat -c %s rootfs.img)
|
||||
partOffsetBytes=$((1 * mebibyte)) # partition starts at 1 MiB
|
||||
gptBackupBytes=$((1 * mebibyte)) # GPT backup at end
|
||||
diskSize=$((partOffsetBytes + rootSize + gptBackupBytes))
|
||||
|
||||
# Round to MiB
|
||||
if (( diskSize % mebibyte )); then
|
||||
diskSize=$(( (diskSize / mebibyte + 1) * mebibyte ))
|
||||
fi
|
||||
|
||||
echo "Creating GPT disk image: $((diskSize / 1024 / 1024)) MiB"
|
||||
truncate -s "$diskSize" disk.raw
|
||||
|
||||
# Write GPT partition table — single Linux root partition starting at sector 2048
|
||||
sfdisk disk.raw <<EOF
|
||||
label: gpt
|
||||
first-lba: 2048
|
||||
type=linux, start=2048
|
||||
EOF
|
||||
|
||||
# Write ext4 image into the partition slot (sector 2048 = 1 MiB offset)
|
||||
dd if=rootfs.img of=disk.raw bs=1M seek=1 conv=notrunc status=none
|
||||
rm rootfs.img
|
||||
|
||||
# === Stage 4: Convert to qcow2 ===
|
||||
mkdir -p $out
|
||||
qemu-img convert -f raw -O qcow2 disk.raw $out/minix.qcow2
|
||||
echo "Disk image: $out/minix.qcow2"
|
||||
echo "qcow2 size: $(du -sh $out/minix.qcow2 | cut -f1)"
|
||||
'';
|
||||
};
|
||||
|
||||
# QEMU runner script — creates a copy-on-write overlay for persistence
|
||||
startVM = pkgs.writeShellScript "run-minix-vm" ''
|
||||
if ! command -v qemu-system-x86_64 >/dev/null 2>&1; then
|
||||
echo "qemu-system-x86_64 not found, fetching via nix shell..." >&2
|
||||
exec nix shell nixpkgs#qemu_kvm -c "$0" "$@"
|
||||
fi
|
||||
|
||||
DISK="''${MINIX_DISK:-minix-disk.qcow2}"
|
||||
|
||||
if [ ! -f "$DISK" ]; then
|
||||
echo "Creating working disk at $DISK (backed by Nix store image)..."
|
||||
qemu-img create -f qcow2 -b ${diskImage}/minix.qcow2 -F qcow2 "$DISK"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "=== MiNix QEMU VM (persistent disk) ==="
|
||||
echo "Disk: $DISK"
|
||||
echo "Login as root (no password)"
|
||||
echo "Press Ctrl-A X to quit QEMU"
|
||||
echo "Delete $DISK to start fresh"
|
||||
echo ""
|
||||
|
||||
exec qemu-system-x86_64 \
|
||||
-name minix \
|
||||
-m 4096 \
|
||||
-smp 2 \
|
||||
-enable-kvm \
|
||||
-device virtio-rng-pci \
|
||||
-drive file="$DISK",format=qcow2,if=virtio \
|
||||
-kernel ${kernel}/${kernelFile} \
|
||||
-initrd ${initrd} \
|
||||
-nographic \
|
||||
-append "${kernelParams} init=${systemProfile}/init" \
|
||||
-net nic,model=virtio \
|
||||
-net user,net=10.0.2.0/24,host=10.0.2.2,dns=10.0.2.3,hostfwd=tcp::2222-:22
|
||||
'';
|
||||
|
||||
in pkgs.runCommand "minix-disk" {
|
||||
preferLocalBuild = true;
|
||||
meta.mainProgram = "run-minix-vm";
|
||||
} ''
|
||||
mkdir -p $out/bin
|
||||
ln -s ${startVM} $out/bin/run-minix-vm
|
||||
''
|
||||
63
machines/qemu-vm/configuration.nix
Normal file
63
machines/qemu-vm/configuration.nix
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# System configuration for the QEMU development VM.
|
||||
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
{
|
||||
imports = [
|
||||
./hardware-configuration.nix
|
||||
];
|
||||
|
||||
# Platform
|
||||
nixpkgs.hostPlatform = "x86_64-linux";
|
||||
|
||||
# System
|
||||
system.stateVersion = "25.11";
|
||||
minix.hostname = "minix";
|
||||
|
||||
# Locale and timezone
|
||||
time.timeZone = "UTC";
|
||||
i18n.defaultLocale = "en_US.UTF-8";
|
||||
|
||||
# Kernel
|
||||
boot.kernelPackages = pkgs.linuxPackages_latest;
|
||||
boot.kernelParams = [ "quiet" "panic=-1" ];
|
||||
|
||||
# Users
|
||||
minix.users.root.hashedPassword = "";
|
||||
minix.users.user = {
|
||||
uid = 1000;
|
||||
gid = 100; # users
|
||||
createHome = true;
|
||||
hashedPassword = "";
|
||||
};
|
||||
minix.groups.users.members = [ "user" ];
|
||||
|
||||
# System packages
|
||||
environment.systemPackages = with pkgs; [
|
||||
nix
|
||||
dhcpcd
|
||||
eudev
|
||||
kmod
|
||||
];
|
||||
|
||||
# SSH — stock NixOS module, translated via systemd-compat
|
||||
services.openssh = {
|
||||
enable = true;
|
||||
settings = {
|
||||
PermitRootLogin = "yes";
|
||||
PasswordAuthentication = true;
|
||||
PermitEmptyPasswords = true;
|
||||
};
|
||||
};
|
||||
|
||||
# Allow empty password login (dev VM only)
|
||||
security.pam.services.sshd.allowNullPassword = true;
|
||||
security.pam.services.login.allowNullPassword = true;
|
||||
|
||||
# Nix config — single-user mode (no daemon)
|
||||
# experimental-features (nix-command, flakes, local-overlay-store) from defaults.nix
|
||||
nix.settings.build-users-group = "";
|
||||
|
||||
# Enable turnstile root session for testing (dev VM only)
|
||||
minix.turnstile.rootSession = true;
|
||||
}
|
||||
21
machines/qemu-vm/flake.nix
Normal file
21
machines/qemu-vm/flake.nix
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Machine flake for the QEMU development VM.
|
||||
# Installed to /etc/minix/ on the end machine (mutable, first-boot-only).
|
||||
# root rebuilds with: minix-rebuild build
|
||||
{
|
||||
description = "MiNix QEMU VM";
|
||||
|
||||
inputs = {
|
||||
minix.url = "git+https://git.axiomania.org/lebowski/MiNix.git";
|
||||
nixpkgs-stable.follows = "minix/nixpkgs-stable";
|
||||
nixpkgs-unstable.follows = "minix/nixpkgs-unstable";
|
||||
};
|
||||
|
||||
outputs = { minix, nixpkgs-stable, ... }:
|
||||
let
|
||||
system = "x86_64-linux";
|
||||
in {
|
||||
packages.${system}.system = (minix.lib.minixSystem {
|
||||
modules = [ ./configuration.nix ];
|
||||
}).config.system.build.systemProfile;
|
||||
};
|
||||
}
|
||||
15
machines/qemu-vm/hardware-configuration.nix
Normal file
15
machines/qemu-vm/hardware-configuration.nix
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Hardware configuration for QEMU/KVM virtual machine.
|
||||
# In a real deployment, this file would be generated by minix-generate-config.
|
||||
|
||||
{ config, lib, pkgs, modulesPath, ... }:
|
||||
|
||||
{
|
||||
boot.initrd.availableKernelModules = [ "virtio_blk" "virtio_pci" "virtio_net" ];
|
||||
boot.initrd.kernelModules = [ "virtio_blk" "virtio_pci" "virtio_net" ];
|
||||
boot.kernelParams = [ "console=ttyS0" ];
|
||||
|
||||
fileSystems."/" = {
|
||||
device = "/dev/vda1";
|
||||
fsType = "ext4";
|
||||
};
|
||||
}
|
||||
672
reference.md
Normal file
672
reference.md
Normal file
|
|
@ -0,0 +1,672 @@
|
|||
# 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 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.
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
Layer 2: User Environment (Home Manager, per-user Nix store)
|
||||
Per-user, declarative. Owns $HOME only.
|
||||
Packages, dotfiles, user dinit services.
|
||||
Own Nix store (~/.local/share/nix/store) — no daemon needed.
|
||||
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.
|
||||
|
||||
### Daemon-less Nix 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)
|
||||
|
||||
~/.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
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
### 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)
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```nix
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
-> 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
|
||||
(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)
|
||||
- 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
|
||||
|
||||
### 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
|
||||
|
||||
`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
|
||||
|
||||
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 =` |
|
||||
| `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 |
|
||||
|
||||
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 |
|
||||
| `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.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) |
|
||||
| `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) |
|
||||
|
||||
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 |
|
||||
|---|---|
|
||||
| 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, statically links all systemd internals, only libc is
|
||||
dynamic). 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` (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.
|
||||
|
||||
### Dynamic UID/GID allocation
|
||||
|
||||
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
|
||||
|
||||
### 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.
|
||||
|
||||
---
|
||||
|
||||
## 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) |
|
||||
|
||||
---
|
||||
|
||||
## eudev Compatibility
|
||||
|
||||
`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`
|
||||
|
||||
---
|
||||
|
||||
## NixOS Compatibility
|
||||
|
||||
### 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)
|
||||
|
||||
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 |
|
||||
|
||||
Note: `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)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## nixpkgs Overlay
|
||||
|
||||
```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" ];
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
|
||||
### 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] 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] 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/)
|
||||
|
||||
### Future
|
||||
- [ ] 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)
|
||||
```
|
||||
80
system/eval.nix
Normal file
80
system/eval.nix
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# system/eval.nix — full NixOS module set (blacklist-filtered) + MiNix modules.
|
||||
{ nixpkgs }:
|
||||
|
||||
args @ {
|
||||
modules,
|
||||
specialArgs ? {},
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
lib = nixpkgs.lib;
|
||||
|
||||
# NixOS utils — provides fsNeededForBoot, systemdUtils types, etc.
|
||||
utils = import (nixpkgs + "/nixos/lib/utils.nix") {
|
||||
inherit lib config pkgs;
|
||||
};
|
||||
|
||||
allNixosModules = import (nixpkgs + "/nixos/modules/module-list.nix");
|
||||
|
||||
isExcluded = mod:
|
||||
if !builtins.isPath mod && !builtins.isString mod then false
|
||||
else let p = toString mod; in
|
||||
builtins.any (pat: lib.hasInfix pat p) [
|
||||
"system/boot/systemd" # → systemd-compat.nix (optionsOnly + translator)
|
||||
"system/boot/stage-2.nix" # → systemd-compat.nix
|
||||
"system/activation/" # → systemd-compat.nix (cherry-picks activation-script.nix)
|
||||
# config/system-path.nix — whitelisted (also imported by documentation.nix)
|
||||
"tasks/network-interfaces-systemd.nix" # networkd backend, needs systemd-networkd
|
||||
"system/boot/networkd.nix" # same
|
||||
"system/boot/shutdown.nix" # unconditionally refs systemd poweroff
|
||||
"services/system/nix-daemon" # → systemd-compat.nix (optionsOnly; no daemon)
|
||||
];
|
||||
|
||||
nixosModules = builtins.filter (m: !isExcluded m) allNixosModules;
|
||||
|
||||
minixModules = [
|
||||
./modules/dinit-services.nix
|
||||
./modules/dinit-init.nix
|
||||
./modules/users.nix
|
||||
./modules/system-path.nix
|
||||
./modules/base-etc.nix
|
||||
./modules/systemd-compat.nix
|
||||
./modules/eudev-compat.nix
|
||||
./modules/toplevel.nix
|
||||
./modules/rebuild.nix
|
||||
./modules/generate-config.nix
|
||||
./modules/defaults.nix
|
||||
./modules/turnstile.nix
|
||||
|
||||
# Cherry-picked from blanket-excluded dirs
|
||||
(nixpkgs + "/nixos/modules/system/activation/activation-script.nix")
|
||||
(nixpkgs + "/nixos/modules/system/boot/systemd/tmpfiles.nix")
|
||||
];
|
||||
|
||||
evalModulesMinimal =
|
||||
(import (nixpkgs + "/nixos/lib") {
|
||||
inherit lib;
|
||||
featureFlags.minimalModules = {};
|
||||
})
|
||||
.evalModules;
|
||||
|
||||
baseModules = minixModules ++ nixosModules;
|
||||
|
||||
result = evalModulesMinimal {
|
||||
specialArgs = {
|
||||
modulesPath = builtins.toString (nixpkgs + "/nixos/modules");
|
||||
nixpkgsSrc = nixpkgs;
|
||||
inherit utils;
|
||||
} // specialArgs;
|
||||
modules = baseModules ++ args.modules;
|
||||
};
|
||||
|
||||
# Circular ref — resolved lazily
|
||||
inherit (result) config;
|
||||
pkgs = result._module.args.pkgs;
|
||||
|
||||
in result // {
|
||||
inherit pkgs;
|
||||
inherit lib;
|
||||
}
|
||||
108
system/lib/translator.nix
Normal file
108
system/lib/translator.nix
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
# Common systemd→dinit translator functions.
|
||||
#
|
||||
# Shared by system/modules/systemd-compat.nix (system services) and
|
||||
# system/modules/hm-dinit.nix (user services via Home Manager).
|
||||
#
|
||||
# Pure functions operating on systemd service attrsets — no module config deps.
|
||||
|
||||
{ lib, pkgs }:
|
||||
|
||||
let
|
||||
inherit (lib)
|
||||
optionalString concatStringsSep concatMapStringsSep
|
||||
hasPrefix removePrefix hasSuffix removeSuffix
|
||||
escapeShellArg filter isList toList;
|
||||
|
||||
# systemd convention: ExecStart = ["" cmd] clears upstream default.
|
||||
getScalar = v:
|
||||
if isList v then
|
||||
let nonEmpty = filter (x: x != "" && x != null) v;
|
||||
in if nonEmpty == [] then builtins.head v else lib.last nonEmpty
|
||||
else v;
|
||||
|
||||
scGet = svc: key: default:
|
||||
if svc.serviceConfig ? ${key} then getScalar svc.serviceConfig.${key} else default;
|
||||
|
||||
scGetList = svc: key:
|
||||
if svc.serviceConfig ? ${key} then toList svc.serviceConfig.${key} else [];
|
||||
|
||||
scHas = svc: key: svc.serviceConfig ? ${key};
|
||||
|
||||
scBool = svc: key:
|
||||
if !(scHas svc key) then false
|
||||
else let v = getScalar svc.serviceConfig.${key};
|
||||
in v == true || v == "true" || v == "yes" || v == "on" || v == "1";
|
||||
|
||||
getDinitType = svc:
|
||||
let raw = scGet svc "Type" "simple";
|
||||
in if raw == "simple" || raw == "exec" || raw == "notify"
|
||||
|| raw == "notify-reload" || raw == "idle"
|
||||
then "process"
|
||||
else if raw == "oneshot" then "scripted"
|
||||
else if raw == "forking" then "bgprocess"
|
||||
else "process";
|
||||
|
||||
getDinitRestart = svc:
|
||||
let policy = scGet svc "Restart" "no";
|
||||
in if policy == "always" then "yes"
|
||||
else if policy == "on-failure" || policy == "on-abnormal"
|
||||
|| policy == "on-abort" || policy == "on-watchdog"
|
||||
then "on-failure"
|
||||
else "no";
|
||||
|
||||
# Strip systemd exec prefix chars (-, :, +, !, !!, @) in any combination.
|
||||
stripExecPrefix = cmd:
|
||||
let
|
||||
s = toString cmd;
|
||||
peel = str: foundAt:
|
||||
if str == "" then { inherit str; hasAt = foundAt; }
|
||||
else let c = builtins.substring 0 1 str;
|
||||
tail = builtins.substring 1 (builtins.stringLength str - 1) str;
|
||||
in if c == "-" || c == ":" || c == "+" || c == "!"
|
||||
then peel tail foundAt
|
||||
else if c == "@" then peel tail true
|
||||
else { inherit str; hasAt = foundAt; };
|
||||
result = peel s false;
|
||||
tokens = lib.splitString " " result.str;
|
||||
binary = builtins.head tokens;
|
||||
rest = builtins.tail tokens;
|
||||
args = if result.hasAt && rest != [] then builtins.tail rest else rest;
|
||||
in if args != [] then "${binary} ${concatStringsSep " " args}" else binary;
|
||||
|
||||
mkEnvFile = name: svc:
|
||||
let
|
||||
envVars = lib.filterAttrs (_: v: v != null) svc.environment;
|
||||
lines = lib.mapAttrsToList (k: v: "${k}=${toString v}") envVars;
|
||||
in if lines == [] then null
|
||||
else pkgs.writeText "${name}.env" (concatStringsSep "\n" lines + "\n");
|
||||
|
||||
# Runtime EnvironmentFile paths — strip optional `-` prefix
|
||||
getEnvFilePaths = svc:
|
||||
map (f: let s = toString f; in if hasPrefix "-" s then removePrefix "-" s else s)
|
||||
(scGetList svc "EnvironmentFile");
|
||||
|
||||
buildDinitDeps = name: svc:
|
||||
let
|
||||
stripSuffix = dep: if hasSuffix ".service" dep then removeSuffix ".service" dep else dep;
|
||||
isRelevant = dep:
|
||||
dep != name && !(hasPrefix "systemd-" dep)
|
||||
&& !(hasSuffix ".target" dep) && !(hasSuffix ".socket" dep)
|
||||
&& !(hasSuffix ".mount" dep) && !(hasSuffix ".slice" dep)
|
||||
&& !(hasSuffix ".path" dep);
|
||||
afterSet = filter isRelevant (lib.unique (map stripSuffix svc.after));
|
||||
requireSet = filter isRelevant (lib.unique (map stripSuffix svc.requires));
|
||||
wantsSet = filter isRelevant (lib.unique (map stripSuffix svc.wants));
|
||||
hardDeps = lib.intersectLists afterSet requireSet;
|
||||
# wants + after = waits-for (start dep, wait, don't fail if dep fails)
|
||||
softDeps = lib.intersectLists afterSet wantsSet;
|
||||
in {
|
||||
dependsOn = lib.unique (hardDeps ++ lib.subtractLists afterSet requireSet);
|
||||
waitsFor = softDeps;
|
||||
after = lib.unique (lib.subtractLists (requireSet ++ wantsSet) afterSet);
|
||||
};
|
||||
|
||||
in {
|
||||
inherit getScalar scGet scGetList scHas scBool;
|
||||
inherit getDinitType getDinitRestart;
|
||||
inherit stripExecPrefix mkEnvFile getEnvFilePaths buildDinitDeps;
|
||||
}
|
||||
31
system/modules/base-etc.nix
Normal file
31
system/modules/base-etc.nix
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Base /etc files unique to MiNix.
|
||||
# Stock shells-environment.nix and system-environment.nix handle /etc/profile,
|
||||
# /etc/shells, environment.profiles, and environment.sessionVariables.
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
inherit (lib) mkOption types;
|
||||
in {
|
||||
options.minix.hostname = mkOption {
|
||||
type = types.str;
|
||||
default = "minix";
|
||||
description = "System hostname.";
|
||||
};
|
||||
|
||||
config = {
|
||||
networking.hostName = config.minix.hostname;
|
||||
|
||||
environment.profiles = lib.mkBefore [
|
||||
"/nix/var/nix/profiles/system/sw"
|
||||
];
|
||||
|
||||
# nsswitch.conf is handled by stock nsswitch.nix via system.nssDatabases.
|
||||
# Its defaults (passwd: files, group: files, hosts: files dns) are correct
|
||||
# for MiNix, and using stock keeps the API extensible for modules like
|
||||
# mDNS, LDAP, etc.
|
||||
|
||||
# Override NixOS default (/run/current-system/sw/...) to our profile path
|
||||
environment.sessionVariables.LOCALE_ARCHIVE = lib.mkForce
|
||||
"/nix/var/nix/profiles/system/sw/lib/locale/locale-archive";
|
||||
};
|
||||
}
|
||||
50
system/modules/defaults.nix
Normal file
50
system/modules/defaults.nix
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
# MiNix default configuration — feature flags, distro identity, overlay.
|
||||
{ lib, pkgs, ... }:
|
||||
|
||||
{
|
||||
nixpkgs.overlays = [ (import ../overlay.nix) ];
|
||||
boot.kexec.enable = false;
|
||||
powerManagement.enable = false;
|
||||
networking.resolvconf.enable = lib.mkDefault true;
|
||||
documentation.nixos.enable = false;
|
||||
system.nixos.distroId = lib.mkDefault "minix";
|
||||
system.nixos.distroName = lib.mkDefault "MiNix";
|
||||
system.nixos.vendorId = lib.mkDefault "minix";
|
||||
system.nixos.vendorName = lib.mkDefault "MiNix";
|
||||
|
||||
# Busybox login doesn't support yescrypt ($y$) — use SHA-512 ($6$).
|
||||
# login.defs covers shadow-utils commands that read it directly;
|
||||
# PAM override covers passwd/chpasswd which go through pam_unix.
|
||||
security.loginDefs.settings.ENCRYPT_METHOD = "SHA512";
|
||||
security.pam.services.passwd.rules.password.unix.settings = lib.mkForce {
|
||||
nullok = true;
|
||||
sha512 = true;
|
||||
};
|
||||
security.pam.services.chpasswd.rules.password.unix.settings = lib.mkForce {
|
||||
nullok = true;
|
||||
sha512 = true;
|
||||
};
|
||||
|
||||
# Enable local-overlay-store for per-user Nix stores (Phase 3 L2).
|
||||
# Machine config adds nix-command + flakes; we add local-overlay-store here
|
||||
# as a distro default so all MiNix systems support overlay stores.
|
||||
nix.settings.experimental-features = lib.mkDefault [ "nix-command" "flakes" "local-overlay-store" ];
|
||||
|
||||
# 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;
|
||||
|
||||
# Base system logger (busybox syslogd)
|
||||
dinit.services.syslogd = {
|
||||
type = "process";
|
||||
command = "${lib.getBin pkgs.busybox}/bin/syslogd -n";
|
||||
dependsMs = [ "early" ];
|
||||
};
|
||||
|
||||
# Getty services need to respawn after logout. Stock systemd unit files
|
||||
# inherit Restart=always from upstream presets that we don't have.
|
||||
systemd.services."serial-getty@".serviceConfig.Restart = lib.mkDefault "always";
|
||||
systemd.services."autovt@".serviceConfig.Restart = lib.mkDefault "always";
|
||||
systemd.services."container-getty@".serviceConfig.Restart = lib.mkDefault "always";
|
||||
systemd.services."getty@".serviceConfig.Restart = lib.mkDefault "always";
|
||||
}
|
||||
421
system/modules/dinit-init.nix
Normal file
421
system/modules/dinit-init.nix
Normal file
|
|
@ -0,0 +1,421 @@
|
|||
# dinit-init.nix — generates init wrapper, early boot services, and boot service.
|
||||
#
|
||||
# Replaces runit-init.nix. The init wrapper (system.build.bootStage2) does minimal
|
||||
# pseudo-fs setup and execs dinit as PID 1. Early boot tasks that were previously a
|
||||
# monolithic stage-1 script are now a DAG of dinit services that run in parallel
|
||||
# where possible.
|
||||
#
|
||||
# Service DAG (top-down):
|
||||
# boot (dinit default) → system (all services) → early (pseudo-fs, etc, eudev…)
|
||||
# boot waits-for gettys → gettys depend-ms system (login prompt appears last)
|
||||
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
inherit (lib) mkOption mkDefault mkForce mkIf types
|
||||
optionalString concatStringsSep filterAttrs;
|
||||
|
||||
busybox = pkgs.busybox;
|
||||
eudev = pkgs.eudev;
|
||||
dinit = pkgs.dinit;
|
||||
kmod = pkgs.kmod;
|
||||
|
||||
profilePath = "/nix/var/nix/profiles/system";
|
||||
|
||||
# Collect all services that should auto-start via the system milestone.
|
||||
# Excludes internal milestones and early-* (pulled in via depends-on chain).
|
||||
allAutoStart = let
|
||||
svcs = filterAttrs (name: svc:
|
||||
svc.enable && svc.autoStart && name != "boot" && name != "system"
|
||||
) config.dinit.services;
|
||||
in builtins.attrNames svcs;
|
||||
|
||||
# Serial console gettys from kernel console= params (e.g. console=ttyS0 → serial-getty@ttyS0)
|
||||
serialConsoles = lib.concatMap (p:
|
||||
let m = builtins.match "console=(tty[A-Za-z]+[0-9]+)(,.*)?$" p;
|
||||
in if m != null then [ "serial-getty@${builtins.elemAt m 0}" ] else []
|
||||
) config.boot.kernelParams;
|
||||
|
||||
# --- Init wrapper ---
|
||||
# Minimal PID 1 entry point: mount essential pseudo-fs, install busybox,
|
||||
# then exec dinit with --services-dir pointing to the system profile.
|
||||
initWrapper = pkgs.writeScript "minix-init" ''
|
||||
#!${busybox}/bin/sh
|
||||
export PATH=${busybox}/bin
|
||||
|
||||
mkdir -p /bin /sbin /run /tmp
|
||||
${busybox}/bin/busybox --install -s /bin
|
||||
|
||||
# Ensure essential pseudo-fs (initrd usually handles this; these are fallbacks)
|
||||
mountpoint -q /proc || mount -t proc proc /proc -o nosuid,noexec,nodev
|
||||
mountpoint -q /sys || mount -t sysfs sys /sys -o nosuid,noexec,nodev
|
||||
mountpoint -q /dev || mount -t devtmpfs dev /dev -o mode=0755,nosuid
|
||||
mountpoint -q /run || mount -t tmpfs run /run -o mode=0755,nosuid,nodev
|
||||
|
||||
# Remount root rw (initrd may leave it ro)
|
||||
mount -o remount,rw / 2>/dev/null || true
|
||||
|
||||
PROFILE="$(readlink -f ${profilePath})"
|
||||
exec ${dinit}/bin/dinit --system --services-dir "$PROFILE/dinit.d"
|
||||
'';
|
||||
|
||||
# --- Early boot service scripts ---
|
||||
|
||||
earlyPseudofsScript = pkgs.writeScript "early-pseudofs" ''
|
||||
#!${busybox}/bin/sh
|
||||
# Mount remaining pseudo-filesystems and create standard directories.
|
||||
# proc/sys/dev/run are already mounted by the init wrapper.
|
||||
|
||||
mountpoint -q /tmp || mount -t tmpfs tmp /tmp -o mode=1777,nosuid,nodev
|
||||
|
||||
mkdir -p /dev/pts /dev/shm
|
||||
mountpoint -q /dev/pts || mount -t devpts devpts /dev/pts -o mode=0620,gid=5,ptmxmode=0666,nosuid,noexec
|
||||
mountpoint -q /dev/shm || mount -t tmpfs shm /dev/shm -o mode=1777,nosuid,nodev
|
||||
|
||||
# /run/wrappers — separate tmpfs WITHOUT nosuid so SUID wrappers work
|
||||
mkdir -p /run/wrappers
|
||||
mountpoint -q /run/wrappers || mount -t tmpfs tmpfs /run/wrappers -o mode=0755,nodev
|
||||
|
||||
# cgroup2 unified
|
||||
if [ -d /sys/fs/cgroup ] && ! mountpoint -q /sys/fs/cgroup; then
|
||||
mount -t cgroup2 cgroup2 /sys/fs/cgroup -o nsdelegate 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# /dev symlinks
|
||||
ln -sfn /proc/self/fd /dev/fd
|
||||
ln -sf /proc/self/fd/0 /dev/stdin
|
||||
ln -sf /proc/self/fd/1 /dev/stdout
|
||||
ln -sf /proc/self/fd/2 /dev/stderr
|
||||
|
||||
# Standard directories
|
||||
mkdir -p /run/lock /run/user /var/log /var/empty /var/tmp /var/lib/dhcpcd
|
||||
[ -L /var/run ] || { rm -rf /var/run; ln -s /run /var/run; }
|
||||
ln -sf /proc/mounts /etc/mtab
|
||||
'';
|
||||
|
||||
earlyEudevScript = pkgs.writeScript "early-eudev" ''
|
||||
#!${busybox}/bin/sh
|
||||
# Start eudev daemon, trigger coldplug, and settle.
|
||||
|
||||
# Set firmware path before coldplug
|
||||
if [ -e /sys/module/firmware_class/parameters/path ]; then
|
||||
echo -n "${profilePath}/firmware" > /sys/module/firmware_class/parameters/path
|
||||
fi
|
||||
|
||||
${eudev}/bin/udevd --daemon
|
||||
${eudev}/bin/udevadm trigger --action=add --type=subsystems
|
||||
${eudev}/bin/udevadm trigger --action=add --type=devices
|
||||
${eudev}/bin/udevadm settle
|
||||
'';
|
||||
|
||||
earlyEtcScript = pkgs.writeScript "early-etc" ''
|
||||
#!${busybox}/bin/sh
|
||||
# Populate /etc from the system profile.
|
||||
# etc.nix marks mutable files with .mode sidecar files.
|
||||
# mode=symlink (default) -> symlink to store (immutable)
|
||||
# mode=0644 etc -> copy (writable at runtime)
|
||||
|
||||
PROFILE="$(readlink -f ${profilePath})"
|
||||
|
||||
if [ ! -e "$PROFILE" ]; then
|
||||
echo "ERROR: System profile not found at ${profilePath}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Clean stale symlinks from a previous system profile
|
||||
if [ -d /etc ]; then
|
||||
find /etc -type l 2>/dev/null | while read -r link; do
|
||||
target="$(readlink "$link" 2>/dev/null)" || continue
|
||||
case "$target" in
|
||||
/nix/store/*) rm -f "$link" ;;
|
||||
esac
|
||||
done
|
||||
find /etc -mindepth 1 -type d -empty -delete 2>/dev/null || true
|
||||
fi
|
||||
|
||||
if [ -d "$PROFILE/etc" ]; then
|
||||
cd "$PROFILE/etc"
|
||||
: > /etc/.clean.new
|
||||
find . -type f -o -type l | while read -r f; do
|
||||
f="''${f#./}"
|
||||
case "$f" in
|
||||
*.mode|*.uid|*.gid) continue ;;
|
||||
esac
|
||||
target="/etc/$f"
|
||||
target_dir="$(dirname "$target")"
|
||||
mkdir -p "$target_dir"
|
||||
if [ -f "$PROFILE/etc/''${f}.mode" ]; then
|
||||
mode="$(cat "$PROFILE/etc/''${f}.mode")"
|
||||
if [ "$mode" = "direct-symlink" ]; then
|
||||
cp -a "$PROFILE/etc/$f" "$target"
|
||||
elif [ -e "$target" ]; then
|
||||
# Mutable file already exists — preserve runtime changes
|
||||
true
|
||||
else
|
||||
cp -L "$PROFILE/etc/$f" "$target.tmp"
|
||||
chmod "$mode" "$target.tmp"
|
||||
_uid="0"; _gid="0"
|
||||
[ -f "$PROFILE/etc/''${f}.uid" ] && _uid="$(cat "$PROFILE/etc/''${f}.uid")"
|
||||
[ -f "$PROFILE/etc/''${f}.gid" ] && _gid="$(cat "$PROFILE/etc/''${f}.gid")"
|
||||
_uid="''${_uid#+}"; _gid="''${_gid#+}"
|
||||
if [ "$_uid" != "0" ] || [ "$_gid" != "0" ]; then
|
||||
chown "$_uid:$_gid" "$target.tmp"
|
||||
fi
|
||||
mv -f "$target.tmp" "$target"
|
||||
fi
|
||||
echo "$f" >> /etc/.clean.new
|
||||
elif [ -L "$PROFILE/etc/$f" ]; then
|
||||
cp -a "$PROFILE/etc/$f" "$target"
|
||||
else
|
||||
ln -sf "$PROFILE/etc/$f" "$target"
|
||||
fi
|
||||
done
|
||||
cd /
|
||||
|
||||
# Remove mutable files from previous profile that no longer exist
|
||||
if [ -f /etc/.clean ]; then
|
||||
while read -r _old; do
|
||||
[ -z "$_old" ] && continue
|
||||
if ! grep -qxF "$_old" /etc/.clean.new 2>/dev/null; then
|
||||
rm -f "/etc/$_old"
|
||||
fi
|
||||
done < /etc/.clean
|
||||
fi
|
||||
mv -f /etc/.clean.new /etc/.clean
|
||||
fi
|
||||
'';
|
||||
|
||||
earlyModulesScript = pkgs.writeScript "early-modules" ''
|
||||
#!${busybox}/bin/sh
|
||||
# Link kernel modules and load modules from /etc/modules-load.d/.
|
||||
|
||||
PROFILE="$(readlink -f ${profilePath})"
|
||||
|
||||
if [ -e /proc/modules ] && [ -d "$PROFILE/kernel/modules/lib/modules" ]; then
|
||||
rm -rf /lib/modules
|
||||
ln -sfn "$PROFILE/kernel/modules/lib/modules" /lib/modules
|
||||
fi
|
||||
|
||||
for _f in /etc/modules-load.d/*.conf; do
|
||||
[ -f "$_f" ] || continue
|
||||
while read -r _mod; do
|
||||
case "$_mod" in "#"*|"") continue ;; esac
|
||||
${kmod}/bin/modprobe "$_mod" 2>/dev/null || true
|
||||
done < "$_f"
|
||||
done
|
||||
'';
|
||||
|
||||
earlySysinitScript = pkgs.writeScript "early-sysinit" ''
|
||||
#!${busybox}/bin/sh
|
||||
# System initialization: tmpfiles, sysusers, machine-id, sysctl, fstab,
|
||||
# random seed, nix path registration, store hardening, activation scripts.
|
||||
|
||||
PROFILE="$(readlink -f ${profilePath})"
|
||||
|
||||
# tmpfiles — create directories, symlinks, etc.
|
||||
if [ -x "$PROFILE/sw/bin/systemd-tmpfiles" ]; then
|
||||
"$PROFILE/sw/bin/systemd-tmpfiles" --create --remove --exclude-prefix=/dev 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# sysusers — create/merge system users and groups
|
||||
if [ -x "$PROFILE/sw/bin/systemd-sysusers" ]; then
|
||||
CREDENTIALS_DIRECTORY="$PROFILE/sysusers-credentials" \
|
||||
"$PROFILE/sw/bin/systemd-sysusers" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Generate machine-id if missing
|
||||
if [ ! -f /etc/machine-id ]; then
|
||||
if [ -r /proc/sys/kernel/random/uuid ]; then
|
||||
tr -d '-' < /proc/sys/kernel/random/uuid > /etc/machine-id
|
||||
fi
|
||||
fi
|
||||
|
||||
# Apply sysctl settings
|
||||
for _f in /etc/sysctl.d/*.conf /etc/sysctl.conf; do
|
||||
[ -f "$_f" ] && sysctl -p "$_f" 2>/dev/null || true
|
||||
done
|
||||
|
||||
# Mount additional filesystems from /etc/fstab
|
||||
if [ -f /etc/fstab ]; then
|
||||
mount -a -t nosysfs,noproc,nodevtmpfs,notmpfs -O no_netdev 2>/dev/null || true
|
||||
swapon -a 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Seed the RNG (load saved seed, credit entropy, save new seed)
|
||||
seedrng 2>/dev/null || true
|
||||
|
||||
# Register Nix store paths (first boot on persistent disk)
|
||||
if [ -f /nix-path-registration ] && [ ! -f /nix/var/nix/db/db.sqlite ]; then
|
||||
if [ -x "$PROFILE/sw/bin/nix-store" ]; then
|
||||
HOME=/root "$PROFILE/sw/bin/nix-store" --load-db < /nix-path-registration
|
||||
rm -f /nix-path-registration
|
||||
fi
|
||||
fi
|
||||
|
||||
${optionalString config.minix.hardenStore ''
|
||||
# Harden /nix/store — read-only bind mount
|
||||
mount --bind /nix/store /nix/store
|
||||
mount -o remount,ro,nosuid,nodev,bind /nix/store
|
||||
''}
|
||||
|
||||
# Run activation scripts
|
||||
if [ -x "$PROFILE/activate" ]; then
|
||||
"$PROFILE/activate" "$PROFILE" || echo "WARNING: Activation script failed (non-fatal)" >&2
|
||||
fi
|
||||
|
||||
# Record boot configuration
|
||||
ln -sfn "$PROFILE" /run/booted-system
|
||||
'';
|
||||
|
||||
earlyHostnameScript = pkgs.writeScript "early-hostname" ''
|
||||
#!${busybox}/bin/sh
|
||||
if [ -f /etc/hostname ]; then
|
||||
HOSTNAME="$(cat /etc/hostname)"
|
||||
if [ -n "$HOSTNAME" ]; then
|
||||
printf "%s" "$HOSTNAME" > /proc/sys/kernel/hostname
|
||||
fi
|
||||
fi
|
||||
'';
|
||||
|
||||
earlyLoopbackScript = pkgs.writeScript "early-loopback" ''
|
||||
#!${busybox}/bin/sh
|
||||
${busybox}/bin/ip link set up dev lo 2>/dev/null || \
|
||||
${busybox}/bin/ifconfig lo 127.0.0.1 up 2>/dev/null || true
|
||||
'';
|
||||
|
||||
in {
|
||||
options.minix.hardenStore = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Bind-mount /nix/store read-only (nosuid,nodev). Breaks single-user nix builds.";
|
||||
};
|
||||
|
||||
config = {
|
||||
# Init wrapper — toplevel.nix copies this as $out/init
|
||||
system.build.bootStage2 = initWrapper;
|
||||
|
||||
# dinit in system path (provides dinitctl, shutdown, halt, poweroff, reboot)
|
||||
environment.systemPackages = [ dinit ];
|
||||
|
||||
# Force-empty activation scripts that conflict with early boot services:
|
||||
# - etc: uses Perl (setup-etc.pl) — we populate /etc in early-etc
|
||||
# - specialfs: mounts pseudo-fs — we do this in init wrapper + early-pseudofs
|
||||
# - binsh: creates /bin/sh → bashInteractive — we want busybox
|
||||
system.activationScripts.etc = mkForce "";
|
||||
system.activationScripts.specialfs = mkForce "";
|
||||
system.activationScripts.binsh = mkForce "";
|
||||
|
||||
# --- Early boot services ---
|
||||
|
||||
dinit.services.early-pseudofs = {
|
||||
type = "scripted";
|
||||
command = "${earlyPseudofsScript}";
|
||||
restart = "no";
|
||||
autoStart = false;
|
||||
};
|
||||
|
||||
dinit.services.early-eudev = {
|
||||
type = "scripted";
|
||||
command = "${earlyEudevScript}";
|
||||
stopCommand = "${eudev}/bin/udevadm control --exit";
|
||||
restart = "no";
|
||||
dependsOn = [ "early-pseudofs" ];
|
||||
autoStart = false;
|
||||
};
|
||||
|
||||
dinit.services.early-etc = {
|
||||
type = "scripted";
|
||||
command = "${earlyEtcScript}";
|
||||
restart = "no";
|
||||
dependsOn = [ "early-pseudofs" ];
|
||||
autoStart = false;
|
||||
};
|
||||
|
||||
dinit.services.early-modules = {
|
||||
type = "scripted";
|
||||
command = "${earlyModulesScript}";
|
||||
restart = "no";
|
||||
dependsOn = [ "early-pseudofs" "early-etc" ];
|
||||
autoStart = false;
|
||||
};
|
||||
|
||||
dinit.services.early-sysinit = {
|
||||
type = "scripted";
|
||||
command = "${earlySysinitScript}";
|
||||
restart = "no";
|
||||
dependsOn = [ "early-etc" "early-eudev" "early-modules" ];
|
||||
autoStart = false;
|
||||
};
|
||||
|
||||
dinit.services.early-hostname = {
|
||||
type = "scripted";
|
||||
command = "${earlyHostnameScript}";
|
||||
restart = "no";
|
||||
dependsOn = [ "early-etc" ];
|
||||
autoStart = false;
|
||||
};
|
||||
|
||||
dinit.services.early-loopback = {
|
||||
type = "scripted";
|
||||
command = "${earlyLoopbackScript}";
|
||||
restart = "no";
|
||||
dependsOn = [ "early-pseudofs" ];
|
||||
autoStart = false;
|
||||
};
|
||||
|
||||
# Early milestone — all early boot tasks complete
|
||||
dinit.services.early = {
|
||||
type = "internal";
|
||||
restart = "no";
|
||||
dependsOn = [ "early-sysinit" "early-hostname" "early-loopback" ];
|
||||
autoStart = false;
|
||||
};
|
||||
|
||||
# System milestone — all non-login services started.
|
||||
dinit.services.system = {
|
||||
type = "internal";
|
||||
restart = "no";
|
||||
dependsOn = [ "early" ];
|
||||
waitsFor = mkDefault allAutoStart;
|
||||
autoStart = false;
|
||||
};
|
||||
|
||||
# Boot service — top-level entry point (dinit starts "boot" by default).
|
||||
# depends-on system (all services ready), then waits-for login gettys.
|
||||
dinit.services.boot = {
|
||||
type = "internal";
|
||||
restart = "no";
|
||||
dependsOn = [ "system" ];
|
||||
waitsFor = mkDefault serialConsoles;
|
||||
autoStart = false;
|
||||
};
|
||||
|
||||
# Getty templates wait for system (not just early) so login prompts
|
||||
# appear after all service status lines.
|
||||
dinit.services."serial-getty@".dependsMs = mkForce [ "system" ];
|
||||
dinit.services."autovt@".dependsMs = mkForce [ "system" ];
|
||||
dinit.services."getty@".dependsMs = mkForce [ "system" ];
|
||||
dinit.services."container-getty@".dependsMs = mkForce [ "system" ];
|
||||
|
||||
# Shutdown hook — save random seed before filesystem unmount.
|
||||
# dinit's shutdown utility calls this after stopping services.
|
||||
# Exit non-zero tells dinit to handle unmounting itself.
|
||||
environment.etc."dinit/shutdown-hook" = {
|
||||
mode = "0755";
|
||||
text = ''
|
||||
#!/bin/sh
|
||||
seedrng 2>/dev/null || true
|
||||
exit 1
|
||||
'';
|
||||
};
|
||||
|
||||
# Udev rules for hotplug serial devices.
|
||||
# Built-in ttyS* consoles are handled by serialConsoles (kernel console= param).
|
||||
# USB/ACM serial devices get a getty on hotplug — backgrounded to avoid
|
||||
# blocking udevadm settle during early-eudev.
|
||||
services.udev.extraRules = ''
|
||||
SUBSYSTEM=="tty", KERNEL=="ttyUSB*", TAG+="minix", RUN+="/bin/sh -c '${dinit}/bin/dinitctl start serial-getty@%k &'"
|
||||
SUBSYSTEM=="tty", KERNEL=="ttyACM*", TAG+="minix", RUN+="/bin/sh -c '${dinit}/bin/dinitctl start serial-getty@%k &'"
|
||||
'';
|
||||
};
|
||||
}
|
||||
314
system/modules/dinit-services.nix
Normal file
314
system/modules/dinit-services.nix
Normal file
|
|
@ -0,0 +1,314 @@
|
|||
# Declarative dinit service definitions.
|
||||
# Generates /etc/dinit.d/<name> service files via environment.etc.
|
||||
# Template services (names ending with @) use dinit's native $1 substitution.
|
||||
#
|
||||
# This is the user-facing API for native dinit services. The systemd->dinit
|
||||
# translator in systemd-compat.nix also writes to dinit.services.
|
||||
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
inherit (lib) mkOption mkEnableOption mkIf mkMerge mkDefault
|
||||
mapAttrs mapAttrs' filterAttrs types
|
||||
optionalString concatStringsSep concatMapStringsSep
|
||||
escapeShellArg;
|
||||
|
||||
serviceOpts = types.submodule ({ name, config, ... }: {
|
||||
options = {
|
||||
enable = mkEnableOption "this service" // { default = true; };
|
||||
|
||||
type = mkOption {
|
||||
type = types.enum [ "process" "bgprocess" "scripted" "internal" "triggered" ];
|
||||
default = "process";
|
||||
description = "dinit service type.";
|
||||
};
|
||||
|
||||
command = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Command to start the service. Not needed for internal/triggered types.";
|
||||
};
|
||||
|
||||
stopCommand = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Command to stop the service (optional, default is SIGTERM).";
|
||||
};
|
||||
|
||||
startScript = mkOption {
|
||||
type = types.nullOr types.lines;
|
||||
default = null;
|
||||
description = ''
|
||||
If set, a wrapper script is generated and used as the command.
|
||||
Use this when pre-start setup (mkdir, keygen, etc.) is needed
|
||||
before exec'ing the actual service.
|
||||
'';
|
||||
};
|
||||
|
||||
stopScript = mkOption {
|
||||
type = types.nullOr types.lines;
|
||||
default = null;
|
||||
description = "If set, a wrapper script is generated and used as the stop-command.";
|
||||
};
|
||||
|
||||
runAs = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "User (or user:group) to run the service as.";
|
||||
};
|
||||
|
||||
workingDir = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Working directory for the service.";
|
||||
};
|
||||
|
||||
envFile = mkOption {
|
||||
type = types.nullOr types.path;
|
||||
default = null;
|
||||
description = "Path to a build-time environment file (KEY=VALUE format).";
|
||||
};
|
||||
|
||||
extraEnvFiles = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
description = "Additional runtime environment files (loaded by dinit, KEY=VALUE format).";
|
||||
};
|
||||
|
||||
dependsOn = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
description = "Hard dependencies — service stops if any dependency stops.";
|
||||
};
|
||||
|
||||
dependsMs = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
description = "Milestone dependencies — must start first, but can stop later.";
|
||||
};
|
||||
|
||||
waitsFor = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
description = "Soft dependencies — wait for start/fail, but don't fail if dep fails.";
|
||||
};
|
||||
|
||||
after = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
description = "Ordering only — if named service is starting, wait for it first.";
|
||||
};
|
||||
|
||||
before = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
description = "Reverse ordering — named service waits for this one.";
|
||||
};
|
||||
|
||||
restart = mkOption {
|
||||
type = types.enum [ "yes" "on-failure" "no" ];
|
||||
default = "yes";
|
||||
description = "Restart policy.";
|
||||
};
|
||||
|
||||
logfile = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Log file path for service output.";
|
||||
};
|
||||
|
||||
rlimits = mkOption {
|
||||
type = types.attrsOf types.str;
|
||||
default = {};
|
||||
description = ''
|
||||
Resource limits. Keys are dinit rlimit names (nofile, core, data, addrspace).
|
||||
Values are "soft:hard" or just "limit".
|
||||
'';
|
||||
example = { nofile = "1024:4096"; core = "0"; addrspace = "1073741824"; };
|
||||
};
|
||||
|
||||
capabilities = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Linux capabilities IAB string.";
|
||||
};
|
||||
|
||||
socketListen = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Unix socket path for socket activation (dinit socket-listen).";
|
||||
};
|
||||
|
||||
socketPermissions = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Octal permissions for the socket (default: 0666).";
|
||||
};
|
||||
|
||||
socketUid = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Socket owner (username or UID).";
|
||||
};
|
||||
|
||||
socketGid = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Socket group (groupname or GID).";
|
||||
};
|
||||
|
||||
options = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
description = "dinit options (runs-on-console, no-new-privs, starts-rwfs, etc.).";
|
||||
};
|
||||
|
||||
extraConfig = mkOption {
|
||||
type = types.lines;
|
||||
default = "";
|
||||
description = "Raw lines appended to the dinit service file.";
|
||||
};
|
||||
|
||||
pidFile = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "PID file path (for bgprocess type services).";
|
||||
};
|
||||
|
||||
readyNotification = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Readiness notification mechanism (e.g. pipefd:3, pipevar:NOTIFY_FD).";
|
||||
};
|
||||
|
||||
termSignal = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
description = "Signal to send when stopping (default: TERM).";
|
||||
};
|
||||
|
||||
smoothRecovery = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Restart without stopping dependents on unexpected exit.";
|
||||
};
|
||||
|
||||
autoStart = mkOption {
|
||||
type = types.bool;
|
||||
default = builtins.elem config.type [ "process" "bgprocess" "scripted" ];
|
||||
description = "Whether the boot service should automatically start this service.";
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
allServices = filterAttrs (_: svc: svc.enable) config.dinit.services;
|
||||
|
||||
# Generate the dinit service file content from a service attrset
|
||||
mkServiceFile = name: svc:
|
||||
let
|
||||
# Resolve command: startScript generates a wrapper, otherwise use command directly
|
||||
resolvedCommand =
|
||||
if svc.startScript != null then
|
||||
"${pkgs.writeScript "${name}-start" svc.startScript}"
|
||||
else
|
||||
svc.command;
|
||||
|
||||
resolvedStopCommand =
|
||||
if svc.stopScript != null then
|
||||
"${pkgs.writeScript "${name}-stop" svc.stopScript}"
|
||||
else
|
||||
svc.stopCommand;
|
||||
|
||||
lines = lib.filter (s: s != "") [
|
||||
"type = ${svc.type}"
|
||||
|
||||
(optionalString (resolvedCommand != null)
|
||||
"command = ${resolvedCommand}")
|
||||
|
||||
(optionalString (resolvedStopCommand != null)
|
||||
"stop-command = ${resolvedStopCommand}")
|
||||
|
||||
(optionalString (svc.runAs != null)
|
||||
"run-as = ${svc.runAs}")
|
||||
|
||||
(optionalString (svc.workingDir != null)
|
||||
"working-dir = ${svc.workingDir}")
|
||||
|
||||
(optionalString (svc.pidFile != null)
|
||||
"pid-file = ${svc.pidFile}")
|
||||
|
||||
(optionalString (svc.readyNotification != null)
|
||||
"ready-notification = ${svc.readyNotification}")
|
||||
|
||||
"restart = ${svc.restart}"
|
||||
|
||||
(optionalString (svc.termSignal != null)
|
||||
"term-signal = ${svc.termSignal}")
|
||||
|
||||
(optionalString svc.smoothRecovery
|
||||
"smooth-recovery = true")
|
||||
|
||||
(optionalString (svc.logfile != null)
|
||||
"logfile = ${svc.logfile}")
|
||||
|
||||
# Environment files
|
||||
(optionalString (svc.envFile != null)
|
||||
"env-file = ${svc.envFile}")
|
||||
(concatMapStringsSep "\n" (f: "env-file += ${f}") svc.extraEnvFiles)
|
||||
|
||||
# Dependencies
|
||||
(concatMapStringsSep "\n" (d: "depends-on: ${d}") svc.dependsOn)
|
||||
(concatMapStringsSep "\n" (d: "depends-ms: ${d}") svc.dependsMs)
|
||||
(concatMapStringsSep "\n" (d: "waits-for: ${d}") svc.waitsFor)
|
||||
(concatMapStringsSep "\n" (d: "after: ${d}") svc.after)
|
||||
(concatMapStringsSep "\n" (d: "before: ${d}") svc.before)
|
||||
|
||||
# Resource limits
|
||||
(concatStringsSep "\n" (lib.mapAttrsToList
|
||||
(k: v: "rlimit-${k} = ${v}") svc.rlimits))
|
||||
|
||||
# Capabilities
|
||||
(optionalString (svc.capabilities != null)
|
||||
"capabilities = ${svc.capabilities}")
|
||||
|
||||
# Socket activation
|
||||
(optionalString (svc.socketListen != null)
|
||||
"socket-listen = ${svc.socketListen}")
|
||||
(optionalString (svc.socketPermissions != null)
|
||||
"socket-permissions = ${svc.socketPermissions}")
|
||||
(optionalString (svc.socketUid != null)
|
||||
"socket-uid = ${svc.socketUid}")
|
||||
(optionalString (svc.socketGid != null)
|
||||
"socket-gid = ${svc.socketGid}")
|
||||
|
||||
# Options
|
||||
(optionalString (svc.options != [])
|
||||
"options: ${concatStringsSep " " svc.options}")
|
||||
|
||||
# Extra raw config
|
||||
svc.extraConfig
|
||||
];
|
||||
in
|
||||
concatStringsSep "\n" lines + "\n";
|
||||
|
||||
in {
|
||||
options = {
|
||||
dinit.services = mkOption {
|
||||
type = types.attrsOf serviceOpts;
|
||||
default = {};
|
||||
description = "Attribute set of dinit service definitions.";
|
||||
};
|
||||
};
|
||||
|
||||
config = {
|
||||
# dinit template files are named without the trailing '@' —
|
||||
# e.g. service "getty@" is stored as dinit.d/getty, not dinit.d/getty@.
|
||||
# The '@' is only used when referencing instances (dinitctl start getty@tty1).
|
||||
environment.etc = mapAttrs' (name: svc: {
|
||||
name = "dinit.d/${lib.removeSuffix "@" name}";
|
||||
value = {
|
||||
text = mkServiceFile name svc;
|
||||
};
|
||||
}) allServices;
|
||||
};
|
||||
}
|
||||
170
system/modules/eudev-compat.nix
Normal file
170
system/modules/eudev-compat.nix
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
# eudev-compat.nix — presents eudev as systemd.package for stock NixOS modules.
|
||||
#
|
||||
# NixOS's stage-1.nix does `udev = config.systemd.package` and expects paths like
|
||||
# ${udev}/lib/systemd/systemd-sysctl and ${udev}/lib/systemd/network/*.link that
|
||||
# eudev doesn't provide. This module builds a thin wrapper that fills those gaps,
|
||||
# then sets systemd.package to point at it.
|
||||
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
inherit (lib) concatStringsSep;
|
||||
|
||||
tmpfilesStandalone = pkgs.callPackage ../packages/systemd-tmpfiles-standalone.nix {};
|
||||
sysusersStandalone = pkgs.callPackage ../packages/systemd-sysusers-standalone.nix {};
|
||||
|
||||
# systemd source — hwdb.d data files (PCI/USB/Bluetooth IDs, input devices, sensors, etc.)
|
||||
systemdSrc = pkgs.fetchFromGitHub {
|
||||
owner = "systemd";
|
||||
repo = "systemd";
|
||||
rev = "v260.1";
|
||||
hash = "sha256-FUKj3lvjz8TIsyu8NyJYtiNele+1BhdJPdw7r7nW6as=";
|
||||
};
|
||||
|
||||
# Compile hwdb.bin from systemd hwdb.d data + udev package hwdb files.
|
||||
# Uses eudev's udevadm (same binary format as systemd-hwdb).
|
||||
udevPackages = config.services.udev.packages or [];
|
||||
hwdbBin = pkgs.runCommand "hwdb.bin" {
|
||||
preferLocalBuild = true;
|
||||
allowSubstitutes = false;
|
||||
nativeBuildInputs = [ pkgs.eudev ];
|
||||
} ''
|
||||
mkdir -p etc/udev/hwdb.d
|
||||
|
||||
# systemd hwdb.d data files (35 files: PCI, USB, Bluetooth, ACPI, OUI, etc.)
|
||||
for f in ${systemdSrc}/hwdb.d/*.hwdb; do
|
||||
cp "$f" etc/udev/hwdb.d/
|
||||
done
|
||||
|
||||
# hwdb files from udev packages (eudev itself + extras)
|
||||
for i in ${pkgs.eudev} ${concatStringsSep " " (map toString udevPackages)}; do
|
||||
for j in $i/{etc,lib}/udev/hwdb.d/*; do
|
||||
[ -f "$j" ] && cp "$j" etc/udev/hwdb.d/ || true
|
||||
done
|
||||
done
|
||||
|
||||
udevadm hwdb --update --root=$(pwd)
|
||||
mv etc/udev/hwdb.bin $out
|
||||
'';
|
||||
|
||||
# Wrapper that makes eudev look like systemd from the paths stage-1.nix expects
|
||||
eudevCompat = (pkgs.runCommand "eudev-as-systemd" {} ''
|
||||
mkdir -p $out/bin $out/lib/udev $out/lib/systemd/network
|
||||
|
||||
# Symlink eudev binaries
|
||||
for f in ${pkgs.eudev}/bin/*; do
|
||||
ln -s "$f" $out/bin/
|
||||
done
|
||||
|
||||
# Symlink eudev lib/udev (helpers, rules, etc.)
|
||||
# Handle directories specially — create real dirs with individual symlinks
|
||||
# so we can add stub files (e.g. rules.d needs extra systemd-specific rules)
|
||||
for f in ${pkgs.eudev}/lib/udev/*; do
|
||||
if [ -d "$f" ]; then
|
||||
mkdir -p "$out/lib/udev/$(basename "$f")"
|
||||
for g in "$f"/*; do
|
||||
[ -e "$g" ] && ln -s "$g" "$out/lib/udev/$(basename "$f")/"
|
||||
done
|
||||
else
|
||||
ln -s "$f" $out/lib/udev/
|
||||
fi
|
||||
done
|
||||
|
||||
# systemd-sysctl stub — sysctl is applied by early-sysinit after /etc population
|
||||
cat > $out/lib/systemd/systemd-sysctl << 'STUB'
|
||||
#!/bin/sh
|
||||
exit 0
|
||||
STUB
|
||||
chmod +x $out/lib/systemd/systemd-sysctl
|
||||
|
||||
# systemd-run stub — udev rules reference it by absolute path
|
||||
cat > $out/bin/systemd-run << 'STUB'
|
||||
#!/bin/sh
|
||||
# Stub: run the command directly instead of in a transient service
|
||||
while [ "$#" -gt 0 ] && [ "$1" != "--" ]; do shift; done
|
||||
[ "$1" = "--" ] && shift
|
||||
exec "$@"
|
||||
STUB
|
||||
chmod +x $out/bin/systemd-run
|
||||
|
||||
# systemd-cat stub — strips journal-specific flags, execs the command
|
||||
cat > $out/bin/systemd-cat << 'STUB'
|
||||
#!/bin/sh
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
-t|--identifier|-p|--priority) shift; shift ;;
|
||||
--level-prefix*) shift ;;
|
||||
--) shift; break ;;
|
||||
-*) shift ;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
exec "$@"
|
||||
STUB
|
||||
chmod +x $out/bin/systemd-cat
|
||||
|
||||
# stage-1.nix expects these specific rules files — create empty stubs for any
|
||||
# that eudev doesn't ship (systemd-specific names)
|
||||
mkdir -p $out/lib/udev/rules.d
|
||||
for rule in 60-cdrom_id.rules 60-persistent-storage.rules 75-net-description.rules \
|
||||
80-drivers.rules 80-net-setup-link.rules; do
|
||||
[ -e $out/lib/udev/rules.d/$rule ] || touch $out/lib/udev/rules.d/$rule
|
||||
done
|
||||
|
||||
# stage-1.nix copies *.link from here — provide a default so the glob doesn't fail
|
||||
cat > $out/lib/systemd/network/99-default.link << 'LINK'
|
||||
[Match]
|
||||
OriginalName=*
|
||||
[Link]
|
||||
NamePolicy=keep kernel database onboard slot path
|
||||
MACAddressPolicy=persistent
|
||||
LINK
|
||||
|
||||
# tmpfiles.d configs — stock tmpfiles.nix symlinks to
|
||||
# ''${systemd.package}/example/tmpfiles.d/*.conf.
|
||||
# Provide them from our standalone systemd-tmpfiles build.
|
||||
mkdir -p $out/example
|
||||
ln -s ${tmpfilesStandalone}/example/tmpfiles.d $out/example/tmpfiles.d
|
||||
'') // {
|
||||
# stage-1.nix accesses lib.getLib udev.kmod for libkmod
|
||||
kmod = pkgs.kmod;
|
||||
};
|
||||
|
||||
in {
|
||||
config = {
|
||||
# Point NixOS modules at our eudev wrapper
|
||||
systemd.package = lib.mkDefault eudevCompat;
|
||||
|
||||
# Standalone systemd tools built from upstream source
|
||||
environment.systemPackages = [ tmpfilesStandalone sysusersStandalone ];
|
||||
|
||||
# Stock modprobe.nix points at ${systemd.package}/lib/modprobe.d/systemd.conf
|
||||
# which eudev doesn't ship — override with empty file to prevent dangling symlink.
|
||||
environment.etc."modprobe.d/systemd.conf".text = lib.mkForce "";
|
||||
|
||||
# stage-1.nix copies binaries from eudev-as-systemd, but our wrapper uses
|
||||
# symlinks into ${pkgs.eudev}. cp -pdv preserves those symlinks, and nuke-refs
|
||||
# only processes regular files — so the eudev store path leaks into the initrd.
|
||||
# Fix: dereference all absolute symlinks, then replace udevd.
|
||||
boot.initrd.extraUtilsCommands = ''
|
||||
for f in $out/bin/* $out/lib/udev/*; do
|
||||
if [ -L "$f" ]; then
|
||||
target=$(readlink "$f")
|
||||
case "$target" in
|
||||
/nix/store/*) real=$(readlink -f "$f"); rm "$f"; cp "$real" "$f" ;;
|
||||
esac
|
||||
fi
|
||||
done
|
||||
rm -f $out/bin/systemd-udevd
|
||||
cp ${pkgs.eudev}/bin/udevd $out/bin/systemd-udevd
|
||||
'';
|
||||
|
||||
# Test that eudev's daemon works in the patched initrd
|
||||
boot.initrd.extraUtilsCommandsTest = ''
|
||||
$out/bin/systemd-udevd --version
|
||||
'';
|
||||
|
||||
# hwdb.bin — compiled from systemd hwdb.d data files using eudev's udevadm
|
||||
environment.etc."udev/hwdb.bin".source = lib.mkForce hwdbBin;
|
||||
};
|
||||
}
|
||||
58
system/modules/generate-config.nix
Normal file
58
system/modules/generate-config.nix
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# generate-config.nix — packages minix-generate-config from stock nixos-generate-config.pl
|
||||
# with MiNix-specific substitutions. Adds to system packages.
|
||||
|
||||
{ config, pkgs, lib, nixpkgsSrc, ... }:
|
||||
|
||||
let
|
||||
detectVirt = pkgs.writeShellScript "detect-virt" ''
|
||||
# Virtualization detection — wraps virt-what, translates to systemd-detect-virt names.
|
||||
out=$(${pkgs.virt-what}/bin/virt-what 2>/dev/null | head -1)
|
||||
case "$out" in
|
||||
virtualbox) echo "oracle" ;;
|
||||
hyperv) echo "microsoft" ;;
|
||||
"") echo "none" ;;
|
||||
*) echo "$out" ;;
|
||||
esac
|
||||
'';
|
||||
|
||||
minixGenerateConfig = pkgs.replaceVarsWith {
|
||||
name = "minix-generate-config";
|
||||
src = "${nixpkgsSrc}/nixos/modules/installer/tools/nixos-generate-config.pl";
|
||||
dir = "bin";
|
||||
isExecutable = true;
|
||||
replacements = {
|
||||
perl = "${pkgs.perl.withPackages (p: [
|
||||
p.FileSlurp
|
||||
p.ConfigIniFiles
|
||||
])}/bin/perl";
|
||||
hostPlatformSystem = pkgs.stdenv.hostPlatform.system;
|
||||
detectvirt = "${detectVirt}";
|
||||
btrfs = "${pkgs.btrfs-progs}/bin/btrfs";
|
||||
configuration = ''
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
{
|
||||
imports =
|
||||
[ ./hardware-configuration.nix
|
||||
];
|
||||
|
||||
$bootLoaderConfig
|
||||
system.stateVersion = "25.11";
|
||||
|
||||
}'';
|
||||
desktopConfiguration = "";
|
||||
flake = ''
|
||||
{
|
||||
description = "MiNix system configuration";
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
|
||||
outputs = inputs@{ self, nixpkgs, ... }: {
|
||||
# Configure with minixSystem
|
||||
};
|
||||
}'';
|
||||
xserverEnabled = "0";
|
||||
};
|
||||
};
|
||||
|
||||
in {
|
||||
config.environment.systemPackages = [ minixGenerateConfig ];
|
||||
}
|
||||
150
system/modules/hm-dinit.nix
Normal file
150
system/modules/hm-dinit.nix
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
# hm-dinit.nix — Home Manager module for dinit user service integration.
|
||||
#
|
||||
# Consumed via flake output: minix.homeModules.default
|
||||
# User flakes import it as: modules = [ minix.homeModules.default ./home.nix ];
|
||||
#
|
||||
# Two responsibilities:
|
||||
# A. Translate systemd.user.services → dinit user service files (~/.config/dinit.d/)
|
||||
# B. Replace HM's reloadSystemd activation with dinitctl --user
|
||||
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
inherit (lib)
|
||||
mkDefault mkForce mkMerge
|
||||
mapAttrs mapAttrs' filterAttrs attrValues
|
||||
optionalString concatStringsSep concatMapStringsSep
|
||||
hasPrefix removePrefix hasSuffix removeSuffix
|
||||
escapeShellArg filter isList toList;
|
||||
|
||||
# Common translator functions (shared with systemd-compat.nix)
|
||||
tr = import ../lib/translator.nix { inherit lib pkgs; };
|
||||
inherit (tr) getScalar scGet scGetList getDinitType getDinitRestart
|
||||
stripExecPrefix mkEnvFile getEnvFilePaths buildDinitDeps;
|
||||
|
||||
# --- Translate user services ---
|
||||
|
||||
userServices = filterAttrs (name: svc:
|
||||
svc.enable
|
||||
&& (svc.serviceConfig ? ExecStart)
|
||||
&& !(hasPrefix "systemd-" name)
|
||||
) (config.systemd.user.services or {});
|
||||
|
||||
translateService = name: svc:
|
||||
let
|
||||
deps = buildDinitDeps name svc;
|
||||
envFile = mkEnvFile name svc;
|
||||
envFiles = getEnvFilePaths svc;
|
||||
execStart = stripExecPrefix (getScalar svc.serviceConfig.ExecStart);
|
||||
rawWorkDir = scGet svc "WorkingDirectory" "";
|
||||
workDir = let s = if hasPrefix "-" rawWorkDir then removePrefix "-" rawWorkDir else rawWorkDir;
|
||||
in if s == "" || s == "~" then null else s;
|
||||
pidFile = scGet svc "PIDFile" "";
|
||||
execStartPre = scGetList svc "ExecStartPre";
|
||||
execStartPost = scGetList svc "ExecStartPost";
|
||||
execStartList = map (cmd: stripExecPrefix (toString cmd))
|
||||
(filter (x: x != "" && x != null) (toList svc.serviceConfig.ExecStart));
|
||||
needsScript = execStartPre != [] || execStartPost != []
|
||||
|| builtins.length execStartList > 1;
|
||||
|
||||
fmtCmd = cmd:
|
||||
let c = toString cmd; opt = hasPrefix "-" c;
|
||||
in "${stripExecPrefix c}${optionalString opt " || true"}";
|
||||
|
||||
wrapperScript = "#!/bin/sh\nset -e\n"
|
||||
+ optionalString (execStartPre != []) (concatMapStringsSep "\n" fmtCmd execStartPre + "\n")
|
||||
+ (if getDinitType svc == "scripted" then
|
||||
concatMapStringsSep "\n" (cmd: cmd) execStartList
|
||||
+ optionalString (execStartPost != []) ("\n" + concatMapStringsSep "\n" fmtCmd execStartPost)
|
||||
else
|
||||
optionalString (execStartPost != [])
|
||||
"( set +e; sleep 1; ${concatMapStringsSep "; " fmtCmd execStartPost} ) &\n"
|
||||
+ "exec ${execStart}")
|
||||
+ "\n";
|
||||
in {
|
||||
type = mkDefault (getDinitType svc);
|
||||
restart = getDinitRestart svc;
|
||||
dependsOn = deps.dependsOn;
|
||||
waitsFor = deps.waitsFor;
|
||||
after = deps.after;
|
||||
}
|
||||
// lib.optionalAttrs (envFile != null) { envFile = mkDefault envFile; }
|
||||
// lib.optionalAttrs (envFiles != []) { extraEnvFiles = mkDefault envFiles; }
|
||||
// lib.optionalAttrs (workDir != null) { workingDir = mkDefault workDir; }
|
||||
// lib.optionalAttrs (pidFile != "") { pidFile = mkDefault pidFile; }
|
||||
// (if needsScript then { startScript = mkDefault wrapperScript; }
|
||||
else { command = mkDefault execStart; });
|
||||
|
||||
dinitUserServices = mapAttrs translateService userServices;
|
||||
|
||||
# --- Generate dinit service file content ---
|
||||
mkServiceFile = name: svc:
|
||||
let
|
||||
resolvedCommand =
|
||||
if svc ? startScript && svc.startScript != null then
|
||||
"${pkgs.writeScript "${name}-start" svc.startScript}"
|
||||
else svc.command or null;
|
||||
resolvedStopCommand =
|
||||
if svc ? stopScript && svc.stopScript != null then
|
||||
"${pkgs.writeScript "${name}-stop" svc.stopScript}"
|
||||
else svc.stopCommand or null;
|
||||
lines = filter (s: s != "") [
|
||||
"type = ${svc.type}"
|
||||
(optionalString (resolvedCommand != null) "command = ${resolvedCommand}")
|
||||
(optionalString (resolvedStopCommand != null) "stop-command = ${resolvedStopCommand}")
|
||||
(optionalString (svc.workingDir or null != null) "working-dir = ${svc.workingDir}")
|
||||
(optionalString (svc.pidFile or null != null) "pid-file = ${svc.pidFile}")
|
||||
"restart = ${svc.restart}"
|
||||
(optionalString (svc.envFile or null != null) "env-file = ${svc.envFile}")
|
||||
(concatMapStringsSep "\n" (f: "env-file += ${f}") (svc.extraEnvFiles or []))
|
||||
(concatMapStringsSep "\n" (d: "depends-on: ${d}") (svc.dependsOn or []))
|
||||
(concatMapStringsSep "\n" (d: "waits-for: ${d}") (svc.waitsFor or []))
|
||||
(concatMapStringsSep "\n" (d: "after: ${d}") (svc.after or []))
|
||||
(svc.extraConfig or "")
|
||||
];
|
||||
in concatStringsSep "\n" lines + "\n";
|
||||
|
||||
serviceFiles = mapAttrs' (name: svc: {
|
||||
name = "dinit.d/${lib.removeSuffix "@" name}";
|
||||
value = { text = mkServiceFile name svc; };
|
||||
}) dinitUserServices;
|
||||
|
||||
in {
|
||||
config = mkMerge [
|
||||
# Install translated user service files into ~/.config/dinit.d/
|
||||
(lib.mkIf (dinitUserServices != {}) {
|
||||
xdg.configFile = serviceFiles;
|
||||
})
|
||||
|
||||
# Replace HM's systemd reload activation with dinit reload
|
||||
{
|
||||
home.activation.reloadSystemd = mkForce "";
|
||||
|
||||
home.activation.reloadDinit = lib.hm.dag.entryAfter [ "linkGeneration" ] ''
|
||||
if command -v dinitctl >/dev/null 2>&1 && dinitctl --user list >/dev/null 2>&1; then
|
||||
_dinit_new="$newGenPath/home-files/.config/dinit.d"
|
||||
_dinit_old="''${oldGenPath:+$oldGenPath/home-files/.config/dinit.d}"
|
||||
|
||||
if [ -d "$_dinit_new" ]; then
|
||||
for _svc_file in "$_dinit_new"/*; do
|
||||
[ -f "$_svc_file" ] || continue
|
||||
_svc_name="$(basename "$_svc_file")"
|
||||
|
||||
_changed=0
|
||||
if [ -z "$_dinit_old" ] || [ ! -f "$_dinit_old/$_svc_name" ]; then
|
||||
_changed=1
|
||||
elif ! diff -q "$_svc_file" "$_dinit_old/$_svc_name" >/dev/null 2>&1; then
|
||||
_changed=1
|
||||
fi
|
||||
|
||||
if [ "$_changed" = "1" ]; then
|
||||
dinitctl --user stop "$_svc_name" 2>/dev/null || true
|
||||
dinitctl --user start "$_svc_name" 2>/dev/null || true
|
||||
fi
|
||||
done
|
||||
fi
|
||||
fi
|
||||
'';
|
||||
}
|
||||
];
|
||||
}
|
||||
151
system/modules/rebuild.nix
Normal file
151
system/modules/rebuild.nix
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
# 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, ... }:
|
||||
|
||||
let
|
||||
cfg = config.minix;
|
||||
in {
|
||||
options.minix.flakeRef = lib.mkOption {
|
||||
type = lib.types.str;
|
||||
default = "/etc/minix";
|
||||
description = "Default flake reference for minix-rebuild build.";
|
||||
};
|
||||
|
||||
options.minix.machineFlake = lib.mkOption {
|
||||
type = lib.types.nullOr lib.types.path;
|
||||
default = null;
|
||||
description = ''
|
||||
Path to the machine flake directory (e.g. ./machines/qemu-vm).
|
||||
Its contents are installed to /etc/minix/ as mutable files —
|
||||
written on first boot, preserved across reboots so user edits persist.
|
||||
'';
|
||||
};
|
||||
|
||||
config = lib.mkMerge [
|
||||
{
|
||||
environment.systemPackages = [
|
||||
# minix-rebuild — system profile builder (root)
|
||||
(pkgs.writeShellScriptBin "minix-rebuild" ''
|
||||
set -e
|
||||
|
||||
usage() {
|
||||
echo "Usage: minix-rebuild <command> [args]"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " build [FLAKE_REF] Build system profile and switch to it"
|
||||
echo " switch PATH Switch to a pre-built system profile"
|
||||
echo ""
|
||||
echo "If FLAKE_REF is omitted, uses ${cfg.flakeRef}"
|
||||
echo "After switching, reboot to activate the new profile."
|
||||
exit 1
|
||||
}
|
||||
|
||||
do_switch() {
|
||||
RESULT="$(readlink -f "$1")"
|
||||
if [ ! -e "$RESULT" ]; then
|
||||
echo "Error: $1 does not exist"
|
||||
exit 1
|
||||
fi
|
||||
echo "Setting system profile to: $RESULT"
|
||||
nix-env --profile /nix/var/nix/profiles/system --set "$RESULT"
|
||||
echo "Done. Reboot to activate the new system profile."
|
||||
}
|
||||
|
||||
[ $# -lt 1 ] && usage
|
||||
|
||||
case "$1" in
|
||||
build)
|
||||
FLAKE="''${2:-${cfg.flakeRef}}"
|
||||
OUTLINK="/tmp/minix-rebuild-$$"
|
||||
echo "Building system profile from: $FLAKE#packages.${pkgs.stdenv.hostPlatform.system}.system"
|
||||
nix build "''${FLAKE}#packages.${pkgs.stdenv.hostPlatform.system}.system" -o "$OUTLINK"
|
||||
do_switch "$OUTLINK"
|
||||
rm -f "$OUTLINK"
|
||||
;;
|
||||
switch)
|
||||
[ -z "$2" ] && usage
|
||||
do_switch "$2"
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
;;
|
||||
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
|
||||
'')
|
||||
];
|
||||
}
|
||||
|
||||
# Install machine flake to /etc/minix/ as mutable files.
|
||||
# early-etc copies files with .mode sidecar on first boot only —
|
||||
# subsequent boots preserve user edits (configuration.nix, home.nix, etc.).
|
||||
(lib.mkIf (cfg.machineFlake != null) {
|
||||
environment.etc = let
|
||||
flakeDir = cfg.machineFlake;
|
||||
# Install a file from the machine flake dir as a mutable /etc/minix/ entry.
|
||||
mkMutable = etcPath: srcPath: {
|
||||
"minix/${etcPath}" = {
|
||||
source = srcPath;
|
||||
mode = "0644";
|
||||
};
|
||||
};
|
||||
in lib.mkMerge [
|
||||
(mkMutable "flake.nix" (flakeDir + "/flake.nix"))
|
||||
(lib.mkIf (builtins.pathExists (flakeDir + "/flake.lock"))
|
||||
(mkMutable "flake.lock" (flakeDir + "/flake.lock")))
|
||||
(mkMutable "configuration.nix" (flakeDir + "/configuration.nix"))
|
||||
(lib.mkIf (builtins.pathExists (flakeDir + "/hardware-configuration.nix"))
|
||||
(mkMutable "hardware-configuration.nix" (flakeDir + "/hardware-configuration.nix")))
|
||||
];
|
||||
})
|
||||
];
|
||||
}
|
||||
21
system/modules/system-path.nix
Normal file
21
system/modules/system-path.nix
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# Override stock config/system-path.nix defaults with busybox-based minimal set.
|
||||
# Stock module declares all options; we just override corePackages and defaultPackages.
|
||||
{ pkgs, lib, ... }:
|
||||
|
||||
{
|
||||
# Replace NixOS corePackages (coreutils, bash, curl, etc.) with busybox + essentials.
|
||||
environment.corePackages = lib.mkForce (with pkgs; [
|
||||
busybox
|
||||
util-linux
|
||||
iproute2
|
||||
procps
|
||||
kmod
|
||||
getent
|
||||
]);
|
||||
|
||||
# Drop stock defaultPackages (perl, rsync, strace).
|
||||
environment.defaultPackages = lib.mkForce [];
|
||||
|
||||
# Disable documentation to avoid pulling in heavy deps.
|
||||
documentation.enable = lib.mkDefault false;
|
||||
}
|
||||
446
system/modules/systemd-compat.nix
Normal file
446
system/modules/systemd-compat.nix
Normal file
|
|
@ -0,0 +1,446 @@
|
|||
# systemd-compat.nix — Stock systemd option imports + systemd→dinit translator.
|
||||
#
|
||||
# optionsOnly imports stock NixOS module options without config sections.
|
||||
# Translates systemd.services → dinit.services, systemd.timers → snooze,
|
||||
# systemd.sockets → dinit socket-listen (Unix sockets only).
|
||||
|
||||
{ config, lib, pkgs, utils, modulesPath, ... } @ args:
|
||||
|
||||
let
|
||||
optionsOnly = path:
|
||||
let mod = (import path) args;
|
||||
in { options = mod.options or {}; imports = mod.imports or []; };
|
||||
|
||||
inherit (lib)
|
||||
mkIf mkMerge mkDefault
|
||||
mapAttrs filterAttrs attrValues
|
||||
optionalString concatStringsSep concatMapStringsSep
|
||||
toList hasPrefix removePrefix hasSuffix removeSuffix
|
||||
escapeShellArg filter isList;
|
||||
|
||||
# Common translator functions (shared with hm-dinit.nix)
|
||||
tr = import ../lib/translator.nix { inherit lib pkgs; };
|
||||
inherit (tr) getScalar scGet scGetList scHas scBool
|
||||
getDinitType getDinitRestart stripExecPrefix mkEnvFile getEnvFilePaths buildDinitDeps;
|
||||
|
||||
cfg = config.systemd.services;
|
||||
|
||||
isTemplate = name: hasSuffix "@" name;
|
||||
|
||||
# All translatable services: enabled, has ExecStart, not systemd-internal
|
||||
translatedServices = filterAttrs (name: svc:
|
||||
svc.enable
|
||||
&& (svc.serviceConfig ? ExecStart)
|
||||
&& !(hasPrefix "systemd-" name)
|
||||
) cfg;
|
||||
|
||||
# Split for boot waitsFor (templates are started via target wants, not boot)
|
||||
regularServices = filterAttrs (n: _: !(isTemplate n)) translatedServices;
|
||||
|
||||
# Template instance names from target wants/requires (e.g. "getty@tty1")
|
||||
templateInstanceNames = let
|
||||
refs = lib.concatLists (lib.mapAttrsToList (_: t:
|
||||
map (removeSuffix ".service") ((t.wants or []) ++ (t.requires or []))
|
||||
) config.systemd.targets);
|
||||
in filter (n: builtins.match "(.+)@(.+)" n != null) refs;
|
||||
|
||||
# Directory provisioning (StateDirectory, RuntimeDirectory, etc.)
|
||||
buildDirProvisioning = svc:
|
||||
let
|
||||
user = scGet svc "User" "";
|
||||
group = scGet svc "Group" "";
|
||||
own = if user != "" then (if group != "" then "${user}:${group}" else user) else "";
|
||||
mkLines = { prefix, key, modeKey }:
|
||||
let
|
||||
raw = if scHas svc key then svc.serviceConfig.${key} else null;
|
||||
dirs = if raw == null then [] else if isList raw then map toString raw else [ (toString raw) ];
|
||||
mode = scGet svc modeKey "0755";
|
||||
in concatMapStringsSep "\n" (d:
|
||||
let path = escapeShellArg "${prefix}/${d}"; in
|
||||
"mkdir -p ${path}"
|
||||
+ optionalString (own != "") " && chown ${own} ${path}"
|
||||
+ " && chmod ${mode} ${path}"
|
||||
) dirs;
|
||||
in concatStringsSep "\n" (filter (s: s != "") (map mkLines [
|
||||
{ prefix = "/var/lib"; key = "StateDirectory"; modeKey = "StateDirectoryMode"; }
|
||||
{ prefix = "/run"; key = "RuntimeDirectory"; modeKey = "RuntimeDirectoryMode"; }
|
||||
{ prefix = "/var/cache"; key = "CacheDirectory"; modeKey = "CacheDirectoryMode"; }
|
||||
{ prefix = "/var/log"; key = "LogsDirectory"; modeKey = "LogsDirectoryMode"; }
|
||||
{ prefix = "/etc"; key = "ConfigurationDirectory"; modeKey = "ConfigurationDirectoryMode"; }
|
||||
]));
|
||||
|
||||
getDinitRlimits = svc:
|
||||
let
|
||||
add = sdKey: dinitKey:
|
||||
let v = scGet svc sdKey "";
|
||||
in if v != "" && v != "infinity" then { ${dinitKey} = toString v; } else {};
|
||||
in (add "LimitNOFILE" "nofile") // (add "LimitCORE" "core")
|
||||
// (add "LimitDATA" "data") // (add "LimitAS" "addrspace");
|
||||
|
||||
templateSubst = s: builtins.replaceStrings ["%I" "%i"] ["$1" "$1"] s;
|
||||
|
||||
# bwrap sandboxing is incompatible with AmbientCapabilities — bwrap's
|
||||
# --cap-drop/--cap-add can't restore caps already lost via setpriv user switch.
|
||||
# Services with ambient caps use dinit's native capabilities + run-as instead.
|
||||
hasSandboxing = svc:
|
||||
let sc = svc.serviceConfig; in
|
||||
!(sc ? AmbientCapabilities) &&
|
||||
((sc ? ProtectSystem) || (sc ? ProtectHome) || (sc ? PrivateTmp)
|
||||
|| (sc ? PrivateDevices) || (sc ? ProtectKernelTunables)
|
||||
|| (sc ? ProtectControlGroups) || (sc ? ProtectHostname)
|
||||
|| (sc ? NoNewPrivileges) || (sc ? PrivateNetwork)
|
||||
|| (sc ? ReadOnlyPaths) || (sc ? ReadWritePaths)
|
||||
|| (sc ? CapabilityBoundingSet));
|
||||
|
||||
collectWritableDirs = svc:
|
||||
let
|
||||
getDirs = key: prefix:
|
||||
let raw = if scHas svc key then svc.serviceConfig.${key} else null;
|
||||
in if raw == null then []
|
||||
else if isList raw then map (d: "${prefix}/${toString d}") raw
|
||||
else [ "${prefix}/${toString raw}" ];
|
||||
in (getDirs "StateDirectory" "/var/lib") ++ (getDirs "RuntimeDirectory" "/run")
|
||||
++ (getDirs "CacheDirectory" "/var/cache") ++ (getDirs "LogsDirectory" "/var/log")
|
||||
++ (getDirs "ConfigurationDirectory" "/etc");
|
||||
|
||||
buildBwrapFlags = svc:
|
||||
let
|
||||
protectSystem = scGet svc "ProtectSystem" "";
|
||||
protectHome = scGet svc "ProtectHome" "";
|
||||
flags = filter (s: s != "") [
|
||||
(if protectSystem == "strict" then "--ro-bind / / --proc /proc --dev-bind /dev /dev --bind /run /run --bind /var /var --bind /tmp /tmp"
|
||||
else if protectSystem == "full" then "--ro-bind /usr /usr --ro-bind /boot /boot --ro-bind /etc /etc"
|
||||
else if protectSystem == true || protectSystem == "true"
|
||||
then "--ro-bind /usr /usr --ro-bind /boot /boot"
|
||||
else "")
|
||||
(if protectSystem == "strict" then
|
||||
concatStringsSep " " (map (p: "--bind ${p} ${p}") (collectWritableDirs svc))
|
||||
else "")
|
||||
(concatStringsSep " " (map (p: "--bind ${toString p} ${toString p}") (scGetList svc "ReadWritePaths")))
|
||||
(concatStringsSep " " (map (p: "--ro-bind ${toString p} ${toString p}") (scGetList svc "ReadOnlyPaths")))
|
||||
(if protectHome == true || protectHome == "true" || protectHome == "yes"
|
||||
then "--tmpfs /home --tmpfs /root --tmpfs /run/user"
|
||||
else if protectHome == "read-only" then "--ro-bind /home /home --ro-bind /root /root"
|
||||
else if protectHome == "tmpfs" then "--tmpfs /home --tmpfs /root --tmpfs /run/user"
|
||||
else "")
|
||||
(if scBool svc "PrivateTmp" then "--tmpfs /tmp --tmpfs /var/tmp" else "")
|
||||
(if scBool svc "PrivateDevices" then "--dev /dev" else "")
|
||||
(if scBool svc "PrivateNetwork" then "--unshare-net" else "")
|
||||
(if scBool svc "ProtectKernelTunables" then "--ro-bind /proc/sys /proc/sys" else "")
|
||||
(if scBool svc "ProtectControlGroups" then "--ro-bind /sys/fs/cgroup /sys/fs/cgroup" else "")
|
||||
(if scBool svc "ProtectHostname" then "--unshare-uts" else "")
|
||||
(if scBool svc "NoNewPrivileges" then "--new-session" else "")
|
||||
(let caps = scGetList svc "CapabilityBoundingSet";
|
||||
in if caps != [] then
|
||||
"--cap-drop ALL " + concatStringsSep " " (map (c: "--cap-add ${toString c}") caps)
|
||||
else "")
|
||||
];
|
||||
in concatStringsSep " \\\n " flags;
|
||||
|
||||
# Wrapper script — needed for ExecStartPre, dir provisioning, UMask, bwrap, multi-ExecStart, ExecStartPost
|
||||
needsWrapper = svc:
|
||||
let execStartList = filter (x: x != "" && x != null) (toList svc.serviceConfig.ExecStart);
|
||||
in scGetList svc "ExecStartPre" != [] || scGetList svc "ExecStartPost" != []
|
||||
|| buildDirProvisioning svc != "" || scGet svc "UMask" "" != ""
|
||||
|| hasSandboxing svc || builtins.length execStartList > 1;
|
||||
|
||||
mkSetpriv = svc:
|
||||
let user = scGet svc "User" ""; group = scGet svc "Group" "";
|
||||
hasCaps = scGetList svc "AmbientCapabilities" != [];
|
||||
# Skip setpriv for services with AmbientCapabilities — setpriv drops
|
||||
# ambient caps on uid change. Let these run as root with dinit caps.
|
||||
in if user == "" || hasCaps then ""
|
||||
else "${pkgs.util-linux}/bin/setpriv --reuid=${user}${optionalString (group != "") " --regid=${group}"} --init-groups --";
|
||||
|
||||
mkWrapperScript = name: svc:
|
||||
let
|
||||
dinitType = getDinitType svc;
|
||||
dirProv = buildDirProvisioning svc;
|
||||
umask = scGet svc "UMask" "";
|
||||
useBwrap = hasSandboxing svc;
|
||||
bwrapFlags = if useBwrap then buildBwrapFlags svc else "";
|
||||
setpriv = mkSetpriv svc;
|
||||
execStart = stripExecPrefix (getScalar svc.serviceConfig.ExecStart);
|
||||
execStartPre = scGetList svc "ExecStartPre";
|
||||
execStartPost = scGetList svc "ExecStartPost";
|
||||
execStartList = map (cmd: stripExecPrefix (toString cmd))
|
||||
(filter (x: x != "" && x != null) (toList svc.serviceConfig.ExecStart));
|
||||
wrapCmd = cmd:
|
||||
"${optionalString (setpriv != "") "${setpriv} "}${optionalString useBwrap
|
||||
"${pkgs.bubblewrap}/bin/bwrap \\\n ${bwrapFlags} \\\n -- "}${cmd}";
|
||||
fmtCmd = cmd:
|
||||
let c = toString cmd; opt = hasPrefix "-" c;
|
||||
in "${stripExecPrefix c}${optionalString opt " || true"}";
|
||||
body = concatStringsSep "\n" (filter (s: s != "") [
|
||||
dirProv
|
||||
(optionalString (umask != "") "umask ${umask}")
|
||||
(optionalString (execStartPre != []) (concatMapStringsSep "\n" fmtCmd execStartPre))
|
||||
]);
|
||||
in "#!/bin/sh\nset -e\n"
|
||||
+ optionalString (body != "") (body + "\n")
|
||||
+ (if dinitType == "scripted" then
|
||||
concatMapStringsSep "\n" (cmd: wrapCmd cmd) execStartList
|
||||
+ optionalString (execStartPost != []) ("\n" + concatMapStringsSep "\n" fmtCmd execStartPost)
|
||||
else
|
||||
optionalString (execStartPost != [])
|
||||
"( set +e; sleep 1; ${concatMapStringsSep "; " fmtCmd execStartPost} ) &\n"
|
||||
+ "exec ${wrapCmd execStart}")
|
||||
+ "\n";
|
||||
|
||||
getStopAttrs = name: svc:
|
||||
let
|
||||
stops = scGetList svc "ExecStop";
|
||||
posts = scGetList svc "ExecStopPost";
|
||||
all = stops ++ posts;
|
||||
isTempl = isTemplate name;
|
||||
t = s: if isTempl then templateSubst s else s;
|
||||
in if all == [] then {}
|
||||
else if builtins.length all == 1 && posts == [] then
|
||||
{ stopCommand = mkDefault (t (stripExecPrefix (toString (builtins.head all)))); }
|
||||
else { stopScript = mkDefault (t ("#!/bin/sh\n"
|
||||
+ concatMapStringsSep "\n" (cmd: stripExecPrefix (toString cmd)) all + "\n")); };
|
||||
|
||||
mkDinitService = name: svc:
|
||||
let
|
||||
deps = buildDinitDeps name svc;
|
||||
envFile = mkEnvFile name svc;
|
||||
envFiles = getEnvFilePaths svc;
|
||||
wrapper = needsWrapper svc;
|
||||
execStart = stripExecPrefix (getScalar svc.serviceConfig.ExecStart);
|
||||
user = scGet svc "User" "";
|
||||
rawWorkDir = scGet svc "WorkingDirectory" "";
|
||||
workDir = let s = if hasPrefix "-" rawWorkDir then removePrefix "-" rawWorkDir else rawWorkDir;
|
||||
in if s == "" || s == "~" then null else s;
|
||||
isTempl = isTemplate name;
|
||||
t = s: if isTempl then templateSubst s else s;
|
||||
ambientCaps = scGetList svc "AmbientCapabilities";
|
||||
dinitCaps = if ambientCaps == [] then null
|
||||
else concatStringsSep "," (map (c: "^${c}") ambientCaps);
|
||||
pidFile = scGet svc "PIDFile" "";
|
||||
in {
|
||||
type = mkDefault (getDinitType svc);
|
||||
restart = getDinitRestart svc;
|
||||
dependsMs = [ "early" ];
|
||||
dependsOn = deps.dependsOn;
|
||||
waitsFor = deps.waitsFor;
|
||||
after = deps.after;
|
||||
rlimits = mkDefault (getDinitRlimits svc);
|
||||
options = mkDefault (lib.optional (scBool svc "NoNewPrivileges") "no-new-privs");
|
||||
}
|
||||
// lib.optionalAttrs (envFile != null) { envFile = mkDefault envFile; }
|
||||
// lib.optionalAttrs (envFiles != []) { extraEnvFiles = mkDefault envFiles; }
|
||||
// lib.optionalAttrs (workDir != null) { workingDir = mkDefault (t workDir); }
|
||||
// lib.optionalAttrs (dinitCaps != null) { capabilities = mkDefault dinitCaps; }
|
||||
// lib.optionalAttrs (pidFile != "") { pidFile = mkDefault pidFile; }
|
||||
// getStopAttrs name svc
|
||||
// (if wrapper then { startScript = mkDefault (t (mkWrapperScript name svc)); }
|
||||
else { command = mkDefault (t execStart); runAs = mkDefault (if user != "" then user else null); })
|
||||
// lib.optionalAttrs (builtins.elem name dormantServiceNames || isTemplate name) { autoStart = mkDefault false; };
|
||||
|
||||
anyUseBwrap = lib.any (svc: hasSandboxing svc) (attrValues regularServices);
|
||||
|
||||
# Dormant: no wantedBy + timer-triggered → no auto-start
|
||||
dormantServiceNames = lib.unique (
|
||||
builtins.attrNames (filterAttrs (_: svc: svc.wantedBy == []) regularServices)
|
||||
++ timerTriggeredServices
|
||||
);
|
||||
|
||||
# Getty template instances → boot.waitsFor (login services start after system milestone).
|
||||
# Regular services are covered by dinit-init.nix's allAutoStart via autoStart flag.
|
||||
bootWaitsFor = templateInstanceNames;
|
||||
|
||||
# --- Timer → snooze translation ---
|
||||
# Each timer becomes a dinit service: snooze sleeps, execs, dinit restarts.
|
||||
|
||||
parseTimespanSec = span:
|
||||
let
|
||||
m = builtins.match "([0-9]+)(s|sec|m|min|h|hr|d|day)?" (lib.toLower (lib.trim (toString span)));
|
||||
n = if m != null then lib.toInt (builtins.elemAt m 0) else 0;
|
||||
unit = if m != null then builtins.elemAt m 1 else null;
|
||||
in if unit == "m" || unit == "min" then n * 60
|
||||
else if unit == "h" || unit == "hr" then n * 3600
|
||||
else if unit == "d" || unit == "day" then n * 86400
|
||||
else n;
|
||||
|
||||
dowMap = { mon = "1"; tue = "2"; wed = "3"; thu = "4"; fri = "5"; sat = "6"; sun = "0"; };
|
||||
translateDow = d:
|
||||
let dl = lib.toLower d;
|
||||
parts = builtins.match "([a-z]+)\\.\\.([a-z]+)" dl;
|
||||
in if dowMap ? ${dl} then dowMap.${dl}
|
||||
else if parts != null then "${dowMap.${builtins.elemAt parts 0}}-${dowMap.${builtins.elemAt parts 1}}"
|
||||
else d;
|
||||
|
||||
# OnCalendar spec → snooze flags. Snooze defaults: -d* -m* -w* -H0 -M0 -S0.
|
||||
calendarToSnoozeFlags = spec:
|
||||
let
|
||||
s = lib.toLower (lib.trim spec);
|
||||
shorthands = {
|
||||
minutely = "-H'*' -M'*'"; hourly = "-H'*'"; daily = "";
|
||||
weekly = "-w0"; monthly = "-d1"; yearly = "-m1 -d1"; annually = "-m1 -d1";
|
||||
quarterly = "-m1,4,7,10 -d1"; semiannually = "-m1,7 -d1";
|
||||
};
|
||||
parseTime = t:
|
||||
let p = lib.splitString ":" t;
|
||||
in { hour = builtins.elemAt p 0;
|
||||
minute = if builtins.length p >= 2 then builtins.elemAt p 1 else "0";
|
||||
second = if builtins.length p >= 3 then builtins.elemAt p 2 else null; };
|
||||
parseDate = d:
|
||||
let p = lib.splitString "-" d;
|
||||
in if builtins.length p == 3 then { month = builtins.elemAt p 1; dom = builtins.elemAt p 2; }
|
||||
else if builtins.length p == 2 then { month = builtins.elemAt p 0; dom = builtins.elemAt p 1; }
|
||||
else { month = null; dom = null; };
|
||||
f = flag: v: if v == null || v == "*" then "" else "${flag}${v}";
|
||||
fs = flag: v: if v == null then "" else if v == "*" then "${flag}'*'" else "${flag}${v}";
|
||||
join = args: concatStringsSep " " (filter (x: x != "") args);
|
||||
tokens = lib.splitString " " (lib.trim spec);
|
||||
nTokens = builtins.length tokens;
|
||||
timeFlags = t: [ (fs "-H" t.hour) (fs "-M" t.minute) (fs "-S" t.second) ];
|
||||
in
|
||||
if shorthands ? ${s} then shorthands.${s}
|
||||
else if nTokens == 1 then join (timeFlags (parseTime (builtins.elemAt tokens 0)))
|
||||
else if nTokens == 2 then
|
||||
let first = builtins.elemAt tokens 0; second = builtins.elemAt tokens 1;
|
||||
in if builtins.match ".*-.*" first != null then
|
||||
let d = parseDate first; t = parseTime second;
|
||||
in join ([ (f "-m" d.month) (f "-d" d.dom) ] ++ timeFlags t)
|
||||
else join ([ "-w${translateDow first}" ] ++ timeFlags (parseTime second))
|
||||
else if nTokens == 3 then
|
||||
let d = parseDate (builtins.elemAt tokens 1); t = parseTime (builtins.elemAt tokens 2);
|
||||
in join ([ "-w${translateDow (builtins.elemAt tokens 0)}" (f "-m" d.month) (f "-d" d.dom) ] ++ timeFlags t)
|
||||
else builtins.throw "systemd-compat: unsupported calendar spec '${spec}'";
|
||||
|
||||
timerServicesFromStartAt = filterAttrs (_: svc: svc.enable && svc.startAt != []) cfg;
|
||||
autoTimers = mapAttrs (_: svc: { wantedBy = [ "timers.target" ]; timerConfig.OnCalendar = svc.startAt; }) timerServicesFromStartAt;
|
||||
allTimers = filterAttrs (_: t: t.enable or true) (autoTimers // config.systemd.timers);
|
||||
timerServiceName = name: timer:
|
||||
let u = timer.timerConfig.Unit or null;
|
||||
in if u != null then removeSuffix ".service" (toString u) else name;
|
||||
|
||||
mkTimerService = name: timer:
|
||||
let
|
||||
tc = timer.timerConfig;
|
||||
chk = key: if (tc.${key} or null) != null
|
||||
then builtins.throw "systemd-compat: ${key} not supported (timer '${name}'). Use OnCalendar."
|
||||
else null;
|
||||
_m = builtins.seq (chk "OnBootSec") (builtins.seq (chk "OnStartupSec")
|
||||
(builtins.seq (chk "OnUnitActiveSec") (chk "OnUnitInactiveSec")));
|
||||
svcName = timerServiceName name timer;
|
||||
onCal = tc.OnCalendar or null;
|
||||
specs = if onCal == null then []
|
||||
else filter (s: s != "") (if isList onCal then map toString onCal else [ (toString onCal) ]);
|
||||
persistent = tc.Persistent or false;
|
||||
flags = concatStringsSep " " (filter (x: x != "") [
|
||||
(if specs != [] then calendarToSnoozeFlags (builtins.head specs) else "")
|
||||
(let v = tc.RandomizedDelaySec or null; in if v != null then "-R${toString (parseTimespanSec v)}" else "")
|
||||
(let v = tc.AccuracySec or null; in if v != null then "-s${toString (parseTimespanSec v)}" else "")
|
||||
(if persistent then "-t /var/lib/snooze/${name}.timefile" else "")
|
||||
]);
|
||||
dinitctl = "${pkgs.dinit}/bin/dinitctl";
|
||||
cmd = if persistent then
|
||||
"${pkgs.snooze}/bin/snooze ${flags} sh -c '${dinitctl} start ${svcName} || true; touch /var/lib/snooze/${name}.timefile'"
|
||||
else "${pkgs.snooze}/bin/snooze ${flags} ${dinitctl} start ${svcName}";
|
||||
in builtins.seq _m { type = "process"; command = mkDefault cmd; restart = "yes"; autoStart = true; dependsMs = [ "early" ]; };
|
||||
|
||||
timerTriggeredServices = lib.unique (lib.mapAttrsToList (name: timer: timerServiceName name timer) allTimers);
|
||||
hasTimers = allTimers != {};
|
||||
timerDinitServices = mapAttrs mkTimerService allTimers;
|
||||
|
||||
# --- Socket activation (Unix domain sockets only) ---
|
||||
|
||||
eligibleSockets = filterAttrs (_: sock:
|
||||
sock.enable && (sock.socketConfig ? ListenStream || sock.listenStreams != [])
|
||||
) config.systemd.sockets;
|
||||
|
||||
mkSocketAttrs = name: sock:
|
||||
let
|
||||
sc = sock.socketConfig;
|
||||
streams = sock.listenStreams ++ (if sc ? ListenStream then toList sc.ListenStream else []);
|
||||
unixStreams = filter (s: hasPrefix "/" (toString s)) streams;
|
||||
tcpStreams = filter (s: !(hasPrefix "/" (toString s))) streams;
|
||||
_warn = if tcpStreams != [] then
|
||||
builtins.trace "systemd-compat: TCP/UDP socket '${name}' skipped (dinit only supports Unix sockets)" null
|
||||
else null;
|
||||
socketPath = if unixStreams != [] then toString (builtins.head unixStreams) else null;
|
||||
svcName = if sc ? Service then removeSuffix ".service" (toString sc.Service) else name;
|
||||
in builtins.seq _warn (if socketPath == null then {} else {
|
||||
${svcName} = { socketListen = mkDefault socketPath; }
|
||||
// lib.optionalAttrs (sc ? SocketMode) { socketPermissions = mkDefault (toString sc.SocketMode); }
|
||||
// lib.optionalAttrs (sc ? SocketUser) { socketUid = mkDefault (toString sc.SocketUser); }
|
||||
// lib.optionalAttrs (sc ? SocketGroup) { socketGid = mkDefault (toString sc.SocketGroup); };
|
||||
});
|
||||
|
||||
socketDinitAttrs = lib.foldl' (acc: attrs: acc // attrs) {}
|
||||
(lib.mapAttrsToList mkSocketAttrs eligibleSockets);
|
||||
|
||||
in {
|
||||
|
||||
imports =
|
||||
map (p: optionsOnly (modulesPath + p)) [
|
||||
"/system/boot/systemd.nix"
|
||||
"/system/boot/systemd/user.nix"
|
||||
"/system/boot/systemd/sysusers.nix"
|
||||
"/system/boot/systemd/coredump.nix"
|
||||
"/system/boot/systemd/oomd.nix"
|
||||
"/system/boot/systemd/nspawn.nix"
|
||||
"/system/boot/systemd/sysupdate.nix"
|
||||
"/system/boot/systemd/tpm2.nix"
|
||||
"/system/boot/systemd/repart.nix"
|
||||
"/system/boot/systemd/shutdown.nix"
|
||||
"/system/boot/systemd/journald.nix"
|
||||
"/system/boot/systemd/journald-gateway.nix"
|
||||
"/system/boot/systemd/journald-remote.nix"
|
||||
"/system/boot/systemd/journald-upload.nix"
|
||||
"/system/boot/systemd/logind.nix"
|
||||
"/system/boot/systemd/homed.nix"
|
||||
"/system/boot/systemd/userdbd.nix"
|
||||
"/system/boot/systemd/dm-verity.nix"
|
||||
"/system/boot/systemd/fido2.nix"
|
||||
"/system/boot/networkd.nix"
|
||||
"/system/boot/systemd/initrd.nix"
|
||||
"/system/boot/stage-2.nix"
|
||||
"/system/activation/top-level.nix"
|
||||
"/system/activation/bootspec.nix"
|
||||
"/system/activation/activatable-system.nix"
|
||||
"/system/activation/switchable-system.nix"
|
||||
"/system/activation/nixos-init.nix"
|
||||
"/services/system/nix-daemon.nix"
|
||||
]
|
||||
++ [
|
||||
# specialisation.nix needs extendModules which evalModulesMinimal doesn't provide
|
||||
({ lib, ... }: {
|
||||
options.isSpecialisation = lib.mkOption { type = lib.types.bool; internal = true; default = false; };
|
||||
options.specialisation = lib.mkOption {
|
||||
type = lib.types.attrsOf lib.types.anything; default = {};
|
||||
description = "Additional configurations to build (no-op in MiNix).";
|
||||
};
|
||||
})
|
||||
];
|
||||
|
||||
config = mkMerge [
|
||||
{ system.build.toplevel = mkDefault config.system.build.systemProfile; }
|
||||
|
||||
{ system.activationScripts.postBootCommands =
|
||||
mkIf (config.boot.postBootCommands != "") config.boot.postBootCommands; }
|
||||
|
||||
# systemd.services → dinit.services (regular + templates)
|
||||
{ dinit.services = mapAttrs mkDinitService translatedServices; }
|
||||
|
||||
# Getty template instances → boot.waitsFor (gettys start after system milestone)
|
||||
{ dinit.services.boot.waitsFor = mkDefault bootWaitsFor; }
|
||||
|
||||
# Timers → snooze dinit services
|
||||
(mkIf hasTimers {
|
||||
dinit.services = lib.mapAttrs' (name: svc: {
|
||||
name = "timer-${name}";
|
||||
value = svc;
|
||||
}) timerDinitServices;
|
||||
})
|
||||
|
||||
# Socket activation → dinit socket-listen
|
||||
(mkIf (socketDinitAttrs != {}) { dinit.services = socketDinitAttrs; })
|
||||
|
||||
(mkIf anyUseBwrap { environment.systemPackages = [ pkgs.bubblewrap ]; })
|
||||
|
||||
{ security.pam.services.login.updateWtmp = lib.mkForce false; }
|
||||
];
|
||||
}
|
||||
67
system/modules/toplevel.nix
Normal file
67
system/modules/toplevel.nix
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# Assembles the system profile derivation.
|
||||
# Follows the stock NixOS top-level.nix pattern: $out/init with @systemConfig@ substitution.
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
inherit (lib) mkOption types optionalString;
|
||||
|
||||
etcDir = config.system.build.etc;
|
||||
kernel = config.boot.kernelPackages.kernel;
|
||||
kernelFile = config.system.boot.loader.kernelFile;
|
||||
|
||||
activateScript = pkgs.writeShellScript "activate"
|
||||
config.system.activationScripts.script;
|
||||
|
||||
in {
|
||||
options.system.build.systemProfile = mkOption {
|
||||
internal = true;
|
||||
description = "The assembled system profile derivation.";
|
||||
};
|
||||
|
||||
config.system.build.systemProfile = pkgs.runCommand "minix-system-profile" {} ''
|
||||
mkdir -p $out
|
||||
|
||||
# init wrapper — copy as executable
|
||||
cp ${config.system.build.bootStage2} $out/init
|
||||
chmod +x $out/init
|
||||
|
||||
# activate — substitute @out@ with final profile path
|
||||
cp ${activateScript} $out/activate
|
||||
substituteInPlace $out/activate --subst-var-by out $out
|
||||
chmod +x $out/activate
|
||||
|
||||
ln -s ${etcDir}/etc $out/etc
|
||||
|
||||
if [ -d ${etcDir}/etc/dinit.d ]; then
|
||||
ln -s ${etcDir}/etc/dinit.d $out/dinit.d
|
||||
else
|
||||
mkdir -p $out/dinit.d
|
||||
fi
|
||||
|
||||
ln -s ${config.system.path} $out/sw
|
||||
|
||||
mkdir -p $out/kernel
|
||||
ln -s ${kernel}/${kernelFile} $out/kernel/vmlinuz
|
||||
ln -s ${config.system.modulesTree} $out/kernel/modules
|
||||
# Stock NixOS layout — packages reference /run/booted-system/kernel-modules
|
||||
ln -s ${config.system.modulesTree} $out/kernel-modules
|
||||
|
||||
${optionalString config.boot.initrd.enable ''
|
||||
ln -s ${config.system.build.initialRamdisk}/initrd $out/initrd
|
||||
''}
|
||||
|
||||
echo -n "${lib.concatStringsSep " " config.boot.kernelParams}" > $out/kernel-params
|
||||
|
||||
${optionalString (config.hardware.deviceTree.package != null) ''
|
||||
ln -s ${config.hardware.deviceTree.package} $out/dtbs
|
||||
''}
|
||||
|
||||
ln -s ${config.hardware.firmware}/lib/firmware $out/firmware
|
||||
|
||||
# sysusers credentials (hashed passwords for systemd-sysusers)
|
||||
ln -s ${config.system.build.sysusersCredentials} $out/sysusers-credentials
|
||||
|
||||
echo -n "${config.system.nixos.version}" > $out/nixos-version
|
||||
echo -n "${pkgs.stdenv.hostPlatform.system}" > $out/system
|
||||
'';
|
||||
}
|
||||
158
system/modules/turnstile.nix
Normal file
158
system/modules/turnstile.nix
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
# turnstile.nix — session/login tracker + per-user dinit instance spawning.
|
||||
#
|
||||
# Integrates turnstile (Chimera Linux project) into MiNix:
|
||||
# - turnstiled system service (dinit process type, starts before gettys)
|
||||
# - pam_turnstile.so in PAM session rules for interactive services
|
||||
# - XDG_RUNTIME_DIR management (/run/user/$UID)
|
||||
# - dinit backend config adapted for MiNix paths
|
||||
# - /etc/dinit.d/user/ for system-provided user services
|
||||
|
||||
{ config, lib, pkgs, ... }:
|
||||
|
||||
let
|
||||
inherit (lib) mkDefault mkForce mkIf mkOption types concatStringsSep;
|
||||
|
||||
turnstile = pkgs.callPackage ../packages/turnstile.nix {
|
||||
dinit = pkgs.dinit;
|
||||
};
|
||||
|
||||
pamMinixNs = pkgs.callPackage ../packages/pam-minix-ns.nix {};
|
||||
|
||||
cfg = config.minix.turnstile;
|
||||
|
||||
# PAM services that should get pam_turnstile.so session rule
|
||||
interactiveServices = [ "login" "sshd" "su" "sudo" ];
|
||||
|
||||
turnstileConf = pkgs.writeText "turnstiled.conf" (lib.concatStringsSep "\n" [
|
||||
"backend = dinit"
|
||||
"manage_rundir = ${if cfg.manageRundir then "yes" else "no"}"
|
||||
"linger = ${cfg.linger}"
|
||||
"rundir_path = /run/user/%u"
|
||||
"export_dbus_address = yes"
|
||||
"root_session = ${if cfg.rootSession then "yes" else "no"}"
|
||||
"login_timeout = ${toString cfg.loginTimeout}"
|
||||
""
|
||||
]);
|
||||
|
||||
# dinit backend config — sourced by the backend shell script.
|
||||
# Uses ''$ to escape Nix interpolation (these are shell variables).
|
||||
dinitBackendConf = pkgs.writeText "dinit.conf" (lib.concatStringsSep "\n" [
|
||||
''boot_dir="''${HOME}/.config/dinit.d/boot.d"''
|
||||
''system_boot_dir="/etc/dinit.d/user/boot.d"''
|
||||
''services_dir1="''${HOME}/.config/dinit.d"''
|
||||
''services_dir2="/etc/dinit.d/user"''
|
||||
""
|
||||
]);
|
||||
|
||||
in {
|
||||
|
||||
options.minix.turnstile = {
|
||||
enable = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Enable turnstile session tracking and per-user dinit instances.";
|
||||
};
|
||||
|
||||
manageRundir = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Whether turnstile manages XDG_RUNTIME_DIR (/run/user/$UID).";
|
||||
};
|
||||
|
||||
rootSession = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Whether to spawn a user dinit instance for root.";
|
||||
};
|
||||
|
||||
linger = mkOption {
|
||||
type = types.enum [ "yes" "no" "maybe" ];
|
||||
default = "maybe";
|
||||
description = ''
|
||||
Whether to keep user services running after last logout.
|
||||
"maybe" checks /var/lib/turnstiled/linger/<username>.
|
||||
'';
|
||||
};
|
||||
|
||||
loginTimeout = mkOption {
|
||||
type = types.int;
|
||||
default = 60;
|
||||
description = "Timeout (seconds) for user service manager startup during login.";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable {
|
||||
|
||||
# --- turnstiled system service ---
|
||||
dinit.services.turnstiled = {
|
||||
type = "process";
|
||||
command = "${turnstile}/bin/turnstiled";
|
||||
logfile = "/var/log/turnstiled.log";
|
||||
dependsMs = [ "early" ];
|
||||
};
|
||||
|
||||
# --- Configuration files ---
|
||||
environment.etc."turnstile/turnstiled.conf".source = turnstileConf;
|
||||
environment.etc."turnstile/backend/dinit.conf".source = dinitBackendConf;
|
||||
|
||||
# --- System-provided user service directories (empty for now) ---
|
||||
# Future: D-Bus session bus, PipeWire, etc. go here
|
||||
environment.etc."dinit.d/user/.keep".text = "";
|
||||
environment.etc."dinit.d/user/boot.d/.keep".text = "";
|
||||
|
||||
# --- PAM integration ---
|
||||
# pam_turnstile.so — session tracking for interactive services.
|
||||
# pam_minix_ns.so — enters per-user Nix overlay store namespace (setns).
|
||||
# Order: pam_unix (10200) → pam_turnstile (11900) → pam_minix_ns (11950).
|
||||
security.pam.services = lib.listToAttrs (map (name: {
|
||||
inherit name;
|
||||
value.rules.session = {
|
||||
turnstile = {
|
||||
enable = true;
|
||||
control = "optional";
|
||||
modulePath = "${turnstile}/lib/security/pam_turnstile.so";
|
||||
order = 11900;
|
||||
};
|
||||
minix-ns = {
|
||||
enable = true;
|
||||
control = "optional";
|
||||
modulePath = "${pamMinixNs}/lib/security/pam_minix_ns.so";
|
||||
order = 11950;
|
||||
};
|
||||
};
|
||||
}) interactiveServices)
|
||||
// {
|
||||
# The daemon itself uses PAM service "turnstiled" when spawning backend processes.
|
||||
# pam_minix_ns runs BEFORE pam_turnstile here (order 11800) so the forked
|
||||
# backend process inherits the mount namespace.
|
||||
turnstiled = {
|
||||
rootOK = true;
|
||||
rules.session = {
|
||||
minix-ns = {
|
||||
enable = true;
|
||||
control = "optional";
|
||||
modulePath = "${pamMinixNs}/lib/security/pam_minix_ns.so";
|
||||
order = 11800;
|
||||
};
|
||||
turnstile = {
|
||||
enable = true;
|
||||
control = "required";
|
||||
modulePath = "${turnstile}/lib/security/pam_turnstile.so";
|
||||
args = [ "turnstiled" ];
|
||||
order = 11900;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# --- State directories ---
|
||||
# turnstiled needs /var/lib/turnstiled for session state and linger files
|
||||
systemd.tmpfiles.rules = [
|
||||
"d /var/lib/turnstiled 0755 root root - -"
|
||||
"d /var/lib/turnstiled/linger 0755 root root - -"
|
||||
];
|
||||
|
||||
# turnstile package on system PATH (for dinitctl --user, loginctl-like queries)
|
||||
environment.systemPackages = [ turnstile ];
|
||||
};
|
||||
}
|
||||
373
system/modules/users.nix
Normal file
373
system/modules/users.nix
Normal file
|
|
@ -0,0 +1,373 @@
|
|||
# Minimal user/group management.
|
||||
# Generates /etc/sysusers.d/00-minix.conf from declarative config.
|
||||
# At boot, systemd-sysusers (standalone) creates passwd/shadow/group/gshadow
|
||||
# with merge semantics — new entries added without touching existing ones.
|
||||
#
|
||||
# Phase 3 L2: also creates per-user Nix overlay store directories, generates
|
||||
# user nix.conf (local-overlay-store), and per-user mount namespace with
|
||||
# overlay store. Users run: home-manager switch --flake /etc/minix#<username>
|
||||
|
||||
{ config, pkgs, lib, ... }:
|
||||
|
||||
let
|
||||
inherit (lib) mkOption mkDefault mkForce mkMerge types concatLines mapAttrsToList
|
||||
mapAttrs filterAttrs filter optionalString escapeShellArg;
|
||||
|
||||
userOpts = { name, config, ... }: {
|
||||
options = {
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = "Username.";
|
||||
};
|
||||
|
||||
uid = mkOption {
|
||||
type = types.int;
|
||||
description = "User ID.";
|
||||
};
|
||||
|
||||
gid = mkOption {
|
||||
type = types.int;
|
||||
default = config.uid;
|
||||
description = "Primary group ID.";
|
||||
};
|
||||
|
||||
home = mkOption {
|
||||
type = types.path;
|
||||
default = if name == "root" then "/root" else "/home/${name}";
|
||||
description = "Home directory.";
|
||||
};
|
||||
|
||||
shell = mkOption {
|
||||
type = types.str;
|
||||
default = "/bin/sh";
|
||||
description = "Login shell.";
|
||||
};
|
||||
|
||||
password = mkOption {
|
||||
type = types.str;
|
||||
default = "x";
|
||||
description = "Password field for /etc/passwd. Use 'x' to defer to /etc/shadow.";
|
||||
};
|
||||
|
||||
hashedPassword = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = "Hashed password for /etc/shadow. Empty string = no password.";
|
||||
};
|
||||
|
||||
description = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = "GECOS field.";
|
||||
};
|
||||
|
||||
createHome = mkOption {
|
||||
type = types.bool;
|
||||
default = false;
|
||||
description = "Whether to create and chown the home directory at boot.";
|
||||
};
|
||||
};
|
||||
|
||||
config = {
|
||||
name = mkDefault name;
|
||||
};
|
||||
};
|
||||
|
||||
groupOpts = { name, config, ... }: {
|
||||
options = {
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = "Group name.";
|
||||
};
|
||||
|
||||
gid = mkOption {
|
||||
type = types.int;
|
||||
description = "Group ID.";
|
||||
};
|
||||
|
||||
members = mkOption {
|
||||
type = types.listOf types.str;
|
||||
default = [];
|
||||
description = "Group members.";
|
||||
};
|
||||
};
|
||||
|
||||
config = {
|
||||
name = mkDefault name;
|
||||
};
|
||||
};
|
||||
|
||||
cfg = config.minix.users;
|
||||
grp = config.minix.groups;
|
||||
|
||||
# Generate /etc/sysusers.d/00-minix.conf — systemd-sysusers config format.
|
||||
# u <name> <uid>:<gid> "<gecos>" <home> <shell>
|
||||
# g <name> <gid>
|
||||
# m <user> <group>
|
||||
sysusersConfig = pkgs.writeTextDir "00-minix.conf" ''
|
||||
${concatLines (mapAttrsToList (_: u:
|
||||
''u ${u.name} ${toString u.uid}:${toString u.gid} "${u.description}" ${u.home} ${u.shell}''
|
||||
) cfg)}
|
||||
${concatLines (mapAttrsToList (_: g:
|
||||
"g ${g.name} ${toString g.gid}"
|
||||
) grp)}
|
||||
${concatLines (lib.flatten (mapAttrsToList (_: g:
|
||||
map (member: "m ${member} ${g.name}") g.members
|
||||
) grp))}
|
||||
'';
|
||||
|
||||
# Credentials directory for initial passwords.
|
||||
# systemd-sysusers reads passwd.hashed-password.<user> files from $CREDENTIALS_DIRECTORY.
|
||||
# On first boot: sets hashed passwords. On subsequent boots: existing entries preserved.
|
||||
sysusersCredentials = pkgs.runCommand "sysusers-credentials" {} ''
|
||||
mkdir -p $out
|
||||
${concatLines (mapAttrsToList (_: u:
|
||||
"printf '%s' ${escapeShellArg u.hashedPassword} > $out/passwd.hashed-password.${u.name}"
|
||||
) cfg)}
|
||||
'';
|
||||
|
||||
# Dynamic UID/GID allocation for users/groups without well-known IDs.
|
||||
# Bridge from users.users/users.groups (stock NixOS API) → minix.users/minix.groups.
|
||||
dynamicBase = 800;
|
||||
|
||||
dynamicGroupNames = builtins.sort builtins.lessThan (
|
||||
filter (name:
|
||||
let ng = config.users.groups.${name};
|
||||
in !(ng.gid != null) && !(config.ids.gids ? ${name})
|
||||
) (builtins.attrNames config.users.groups)
|
||||
);
|
||||
|
||||
dynamicGidMap = lib.listToAttrs (lib.imap0 (i: name: {
|
||||
inherit name;
|
||||
value = dynamicBase + i;
|
||||
}) dynamicGroupNames);
|
||||
|
||||
dynamicUserNames = builtins.sort builtins.lessThan (
|
||||
filter (name:
|
||||
let nu = config.users.users.${name};
|
||||
in !(nu.uid != null) && !(config.ids.uids ? ${name})
|
||||
) (builtins.attrNames config.users.users)
|
||||
);
|
||||
|
||||
dynamicUidMap = lib.listToAttrs (lib.imap0 (i: name: {
|
||||
inherit name;
|
||||
value = dynamicBase + builtins.length dynamicGroupNames + i;
|
||||
}) dynamicUserNames);
|
||||
|
||||
# --- Per-user Nix overlay store support ---
|
||||
|
||||
# Users eligible for per-user stores: non-system interactive users
|
||||
perUserStoreUsers = filterAttrs (_: u: u.createHome && u.uid >= 1000) cfg;
|
||||
|
||||
# Per-user Nix config template (local-overlay-store)
|
||||
mkUserNixConf = u: pkgs.writeText "nix-conf-${u.name}" ''
|
||||
store = local-overlay://?lower-store=local&upper-layer=${u.home}/.local/share/nix/upper&state=${u.home}/.local/share/nix/var/nix&check-mount=true
|
||||
experimental-features = nix-command flakes local-overlay-store
|
||||
'';
|
||||
|
||||
in {
|
||||
options = {
|
||||
minix.users = mkOption {
|
||||
type = types.attrsOf (types.submodule userOpts);
|
||||
default = {};
|
||||
description = "System user definitions.";
|
||||
};
|
||||
|
||||
minix.groups = mkOption {
|
||||
type = types.attrsOf (types.submodule groupOpts);
|
||||
default = {};
|
||||
description = "System group definitions.";
|
||||
};
|
||||
};
|
||||
|
||||
config = mkMerge [
|
||||
# Default system users — UIDs from NixOS ids.nix
|
||||
{
|
||||
minix.users = {
|
||||
root = {
|
||||
uid = config.ids.uids.root;
|
||||
gid = config.ids.gids.root;
|
||||
home = "/root";
|
||||
shell = mkForce "/bin/sh";
|
||||
createHome = true;
|
||||
hashedPassword = mkDefault "";
|
||||
};
|
||||
nobody = {
|
||||
uid = config.ids.uids.nobody;
|
||||
gid = config.ids.gids.nogroup;
|
||||
home = "/var/empty";
|
||||
shell = "/bin/sh";
|
||||
};
|
||||
};
|
||||
|
||||
# Default system groups — GIDs from NixOS ids.nix
|
||||
minix.groups = {
|
||||
root = { gid = config.ids.gids.root; };
|
||||
nogroup = { gid = config.ids.gids.nogroup; };
|
||||
tty = { gid = config.ids.gids.tty; };
|
||||
wheel = { gid = config.ids.gids.wheel; };
|
||||
audio = { gid = config.ids.gids.audio; };
|
||||
video = { gid = config.ids.gids.video; };
|
||||
disk = { gid = config.ids.gids.disk; };
|
||||
input = { gid = config.ids.gids.input; };
|
||||
kvm = { gid = config.ids.gids.kvm; };
|
||||
shadow = { gid = config.ids.gids.shadow; };
|
||||
users = { gid = config.ids.gids.users; };
|
||||
dialout = { gid = config.ids.gids.dialout; };
|
||||
utmp = { gid = config.ids.gids.utmp; };
|
||||
adm = { gid = config.ids.gids.adm; };
|
||||
};
|
||||
}
|
||||
|
||||
# Override stock Perl-based activation scripts — sysusers handles users/groups at boot.
|
||||
{
|
||||
system.activationScripts.users = mkForce "";
|
||||
system.activationScripts.groups = mkForce "";
|
||||
system.activationScripts.hashes = mkForce "";
|
||||
users.manageLingering = mkForce false;
|
||||
}
|
||||
|
||||
# Bridge: users.users → minix.users (stock NixOS API → our format)
|
||||
{
|
||||
minix.users = mapAttrs (name: nu: let
|
||||
resolveUid =
|
||||
if nu.uid != null then nu.uid
|
||||
else if config.ids.uids ? ${name} then config.ids.uids.${name}
|
||||
else if dynamicUidMap ? ${name} then dynamicUidMap.${name}
|
||||
else builtins.throw
|
||||
"users.users.${name}: could not allocate UID";
|
||||
groupName = if nu.group != "" then nu.group else name;
|
||||
resolveGid =
|
||||
if config.ids.gids ? ${groupName} then config.ids.gids.${groupName}
|
||||
else if dynamicGidMap ? ${groupName} then dynamicGidMap.${groupName}
|
||||
else resolveUid;
|
||||
in {
|
||||
uid = mkDefault resolveUid;
|
||||
gid = mkDefault resolveGid;
|
||||
home = mkDefault (toString nu.home);
|
||||
shell = mkDefault (
|
||||
let s = if nu.shell != null then toString nu.shell else "/bin/sh";
|
||||
in if lib.hasPrefix "/nix/store/" s then "/bin/sh" else s);
|
||||
description = mkDefault (
|
||||
if nu.description != null then nu.description
|
||||
else "");
|
||||
hashedPassword = mkDefault (
|
||||
if nu ? hashedPassword && nu.hashedPassword != null then nu.hashedPassword
|
||||
else if nu ? initialHashedPassword && nu.initialHashedPassword != null then nu.initialHashedPassword
|
||||
else "");
|
||||
}) config.users.users;
|
||||
|
||||
minix.groups = mapAttrs (name: ng: let
|
||||
resolveGid =
|
||||
if ng.gid != null then ng.gid
|
||||
else if config.ids.gids ? ${name} then config.ids.gids.${name}
|
||||
else if dynamicGidMap ? ${name} then dynamicGidMap.${name}
|
||||
else builtins.throw
|
||||
"users.groups.${name}: could not allocate GID";
|
||||
in {
|
||||
gid = mkDefault resolveGid;
|
||||
members = mkDefault ng.members;
|
||||
}) config.users.groups;
|
||||
}
|
||||
|
||||
# Bridge: users.users.*.extraGroups → minix.groups.*.members
|
||||
{
|
||||
minix.groups = let
|
||||
allMemberships = lib.foldlAttrs (acc: userName: nu:
|
||||
builtins.foldl' (a: grpName:
|
||||
a // { ${grpName} = (a.${grpName} or []) ++ [userName]; }
|
||||
) acc (nu.extraGroups or [])
|
||||
) {} config.users.users;
|
||||
in mapAttrs (_: members: {
|
||||
members = mkDefault members;
|
||||
}) allMemberships;
|
||||
}
|
||||
|
||||
# Wire into environment.etc — sysusers.d config (sysusers creates passwd/shadow/group/gshadow at boot)
|
||||
{
|
||||
environment.etc."sysusers.d".source = sysusersConfig;
|
||||
|
||||
# Expose credentials dir so toplevel.nix can link it into the profile
|
||||
system.build.sysusersCredentials = sysusersCredentials;
|
||||
|
||||
# Create home directories + per-user store setup via a one-shot dinit service.
|
||||
dinit.services.user-init = let
|
||||
homeUsers = lib.filterAttrs (_: u: u.createHome) cfg;
|
||||
in {
|
||||
type = "scripted";
|
||||
dependsMs = [ "early" ];
|
||||
command = toString (pkgs.writeScript "user-init" ''
|
||||
#!/bin/sh
|
||||
${concatLines (mapAttrsToList (name: u: ''
|
||||
mkdir -p ${escapeShellArg u.home}
|
||||
chown ${toString u.uid}:${toString u.gid} ${escapeShellArg u.home}
|
||||
'') homeUsers)}
|
||||
# Make system store db accessible to overlay store users.
|
||||
# Directory: nix checks writability. big-lock: opened O_RDWR for flock().
|
||||
# sqlite-shm/wal: WAL mode readers need write access.
|
||||
# Runs every boot to self-heal after rebuilds.
|
||||
chgrp users /nix/var/nix/db /nix/var/nix/db/big-lock /nix/var/nix/db/db.sqlite-shm /nix/var/nix/db/db.sqlite-wal
|
||||
chmod 0775 /nix/var/nix/db
|
||||
chmod 0660 /nix/var/nix/db/big-lock
|
||||
chmod 0664 /nix/var/nix/db/db.sqlite-shm /nix/var/nix/db/db.sqlite-wal
|
||||
|
||||
${concatLines (mapAttrsToList (name: u: let
|
||||
nixDir = "${u.home}/.local/share/nix";
|
||||
configDir = "${u.home}/.config";
|
||||
unshare = "${pkgs.util-linux}/bin/unshare";
|
||||
in ''
|
||||
# --- Per-user Nix overlay store: ${u.name} ---
|
||||
mkdir -p "${nixDir}/upper" "${nixDir}/work" "${nixDir}/merged/nix/store"
|
||||
mkdir -p "${nixDir}/var/nix/db" "${nixDir}/var/nix/profiles" "${nixDir}/var/nix/gcroots"
|
||||
chown -R ${toString u.uid}:${toString u.gid} "${u.home}/.local"
|
||||
|
||||
# Per-user nix.conf (mutable — only create if missing)
|
||||
mkdir -p "${configDir}/nix"
|
||||
if [ ! -f "${configDir}/nix/nix.conf" ]; then
|
||||
cp ${mkUserNixConf u} "${configDir}/nix/nix.conf"
|
||||
chmod 644 "${configDir}/nix/nix.conf"
|
||||
fi
|
||||
|
||||
chown -R ${toString u.uid}:${toString u.gid} "${configDir}/nix"
|
||||
|
||||
# --- Mount namespace with overlay store ---
|
||||
# Strategy: start a helper in a new mount ns, bind-mount the ns
|
||||
# handle from the parent (no propagation needed), set up overlay
|
||||
# via nsenter, then kill the helper (ns persists via bind-mount).
|
||||
# Non-fatal: login continues without overlay if this fails.
|
||||
mkdir -p "/run/user/${toString u.uid}/ns"
|
||||
chmod 0700 "/run/user/${toString u.uid}"
|
||||
chown ${toString u.uid}:${toString u.gid} "/run/user/${toString u.uid}"
|
||||
touch "/run/user/${toString u.uid}/ns/mnt"
|
||||
${unshare} --mount sleep infinity &
|
||||
_ns_pid=$!
|
||||
# Wait for child to call unshare(2) — ns inode changes
|
||||
_parent_ns=$(readlink /proc/self/ns/mnt)
|
||||
while [ "$(readlink /proc/$_ns_pid/ns/mnt 2>/dev/null)" = "$_parent_ns" ]; do
|
||||
sleep 0.01
|
||||
done
|
||||
if mount --bind "/proc/$_ns_pid/ns/mnt" "/run/user/${toString u.uid}/ns/mnt" 2>/dev/null; then
|
||||
nsenter --mount="/run/user/${toString u.uid}/ns/mnt" sh -c '
|
||||
mount --make-rprivate /
|
||||
mount -t overlay overlay \
|
||||
-o "lowerdir=/nix/store,upperdir=${nixDir}/upper,workdir=${nixDir}/work" \
|
||||
"${nixDir}/merged/nix/store"
|
||||
mount --bind "${nixDir}/merged/nix/store" /nix/store
|
||||
'
|
||||
else
|
||||
echo "warning: namespace bind-mount failed for ${u.name}"
|
||||
fi
|
||||
kill $_ns_pid 2>/dev/null; wait $_ns_pid 2>/dev/null; true
|
||||
'') perUserStoreUsers)}
|
||||
'');
|
||||
};
|
||||
}
|
||||
|
||||
# home-manager CLI on system PATH
|
||||
{
|
||||
environment.systemPackages = [ pkgs.home-manager ];
|
||||
}
|
||||
];
|
||||
}
|
||||
20
system/overlay.nix
Normal file
20
system/overlay.nix
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
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"
|
||||
];
|
||||
});
|
||||
}
|
||||
21
system/packages/pam-minix-ns.nix
Normal file
21
system/packages/pam-minix-ns.nix
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
# pam_minix_ns.so — PAM session module that enters per-user Nix overlay
|
||||
# store mount namespaces. Created at boot by user-init, persisted at
|
||||
# /run/user/$UID/ns/mnt. See pam_minix_ns.c for details.
|
||||
{ stdenv, pam }:
|
||||
|
||||
stdenv.mkDerivation {
|
||||
pname = "pam-minix-ns";
|
||||
version = "0.1.0";
|
||||
|
||||
dontUnpack = true;
|
||||
|
||||
buildInputs = [ pam ];
|
||||
|
||||
buildPhase = ''
|
||||
$CC -shared -fPIC -o pam_minix_ns.so ${./pam_minix_ns.c} -lpam
|
||||
'';
|
||||
|
||||
installPhase = ''
|
||||
install -Dm755 pam_minix_ns.so $out/lib/security/pam_minix_ns.so
|
||||
'';
|
||||
}
|
||||
47
system/packages/pam_minix_ns.c
Normal file
47
system/packages/pam_minix_ns.c
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* pam_minix_ns — enter per-user Nix overlay store mount namespace.
|
||||
*
|
||||
* At boot, user-init creates a mount namespace per user with overlayfs
|
||||
* merging user packages onto /nix/store, persisted at /run/user/$UID/ns/mnt.
|
||||
*
|
||||
* This PAM session module calls setns() to move the login process (and
|
||||
* turnstile backend) into that namespace. Runs as root in the PAM stack,
|
||||
* so CAP_SYS_ADMIN is available. Always returns PAM_SUCCESS — if the
|
||||
* namespace doesn't exist or setns fails, the session continues without
|
||||
* the overlay (degraded but functional).
|
||||
*/
|
||||
|
||||
#define _GNU_SOURCE
|
||||
#include <security/pam_modules.h>
|
||||
#include <sched.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <pwd.h>
|
||||
|
||||
PAM_EXTERN int pam_sm_open_session(pam_handle_t *pamh, int flags,
|
||||
int argc, const char **argv) {
|
||||
const char *user;
|
||||
if (pam_get_user(pamh, &user, NULL) != PAM_SUCCESS)
|
||||
return PAM_SUCCESS;
|
||||
|
||||
struct passwd *pw = getpwnam(user);
|
||||
if (!pw || pw->pw_uid < 1000)
|
||||
return PAM_SUCCESS;
|
||||
|
||||
char ns_path[256];
|
||||
snprintf(ns_path, sizeof(ns_path), "/run/user/%d/ns/mnt", pw->pw_uid);
|
||||
|
||||
int fd = open(ns_path, O_RDONLY);
|
||||
if (fd < 0)
|
||||
return PAM_SUCCESS;
|
||||
|
||||
setns(fd, CLONE_NEWNS);
|
||||
close(fd);
|
||||
return PAM_SUCCESS;
|
||||
}
|
||||
|
||||
PAM_EXTERN int pam_sm_close_session(pam_handle_t *pamh, int flags,
|
||||
int argc, const char **argv) {
|
||||
return PAM_SUCCESS;
|
||||
}
|
||||
86
system/packages/systemd-sysusers-standalone.nix
Normal file
86
system/packages/systemd-sysusers-standalone.nix
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
# Standalone systemd-sysusers binary, built from upstream systemd source.
|
||||
# Uses -Dstandalone-binaries=true to produce a self-contained binary that
|
||||
# statically links all systemd internals (only libc is dynamic).
|
||||
#
|
||||
# Handles user/group creation with merge semantics: creates missing entries
|
||||
# without touching existing ones. Reads hashed passwords from
|
||||
# $CREDENTIALS_DIRECTORY/passwd.hashed-password.<user>.
|
||||
|
||||
{ lib
|
||||
, stdenv
|
||||
, fetchFromGitHub
|
||||
, meson
|
||||
, ninja
|
||||
, pkg-config
|
||||
, gperf
|
||||
, python3
|
||||
, libcap
|
||||
, libxcrypt
|
||||
, util-linux
|
||||
, linuxHeaders ? stdenv.cc.libc.linuxHeaders
|
||||
}:
|
||||
|
||||
stdenv.mkDerivation {
|
||||
pname = "systemd-sysusers-standalone";
|
||||
version = "260.1";
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "systemd";
|
||||
repo = "systemd";
|
||||
rev = "v260.1";
|
||||
hash = "sha256-FUKj3lvjz8TIsyu8NyJYtiNele+1BhdJPdw7r7nW6as=";
|
||||
};
|
||||
|
||||
nativeBuildInputs = [
|
||||
meson
|
||||
ninja
|
||||
pkg-config
|
||||
gperf
|
||||
(python3.withPackages (ps: [ ps.jinja2 ]))
|
||||
];
|
||||
|
||||
buildInputs = [
|
||||
libcap
|
||||
libxcrypt
|
||||
util-linux
|
||||
linuxHeaders
|
||||
];
|
||||
|
||||
postPatch = ''
|
||||
patchShebangs tools/ src/
|
||||
|
||||
# xml_helper.py needs lxml which we don't have — skip the check
|
||||
substituteInPlace man/meson.build \
|
||||
--replace-fail \
|
||||
"have_lxml = run_command(xml_helper_py, check: false).returncode() == 0" \
|
||||
"have_lxml = false"
|
||||
'';
|
||||
|
||||
# Disable all auto-features, then enable only what we need.
|
||||
mesonAutoFeatures = "disabled";
|
||||
|
||||
mesonFlags = [
|
||||
"-Dstandalone-binaries=true"
|
||||
"-Dsysusers=true"
|
||||
"-Dmode=release"
|
||||
"-Dstatic-libsystemd=pic"
|
||||
"-Dtests=false"
|
||||
"-Dinstall-tests=false"
|
||||
];
|
||||
|
||||
ninjaFlags = [
|
||||
"systemd-sysusers.standalone"
|
||||
];
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out/bin
|
||||
cp systemd-sysusers.standalone $out/bin/systemd-sysusers
|
||||
'';
|
||||
|
||||
meta = with lib; {
|
||||
description = "Standalone systemd-sysusers binary (statically links systemd internals)";
|
||||
homepage = "https://github.com/systemd/systemd";
|
||||
license = licenses.lgpl21Plus;
|
||||
platforms = platforms.linux;
|
||||
};
|
||||
}
|
||||
97
system/packages/systemd-tmpfiles-standalone.nix
Normal file
97
system/packages/systemd-tmpfiles-standalone.nix
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
# Standalone systemd-tmpfiles binary, built from upstream systemd source.
|
||||
# Uses -Dstandalone-binaries=true to produce a self-contained binary that
|
||||
# statically links all systemd internals (only libc is dynamic).
|
||||
|
||||
{ lib
|
||||
, stdenv
|
||||
, fetchFromGitHub
|
||||
, meson
|
||||
, ninja
|
||||
, pkg-config
|
||||
, gperf
|
||||
, python3
|
||||
, libcap
|
||||
, libxcrypt
|
||||
, util-linux
|
||||
, linuxHeaders ? stdenv.cc.libc.linuxHeaders
|
||||
}:
|
||||
|
||||
stdenv.mkDerivation {
|
||||
pname = "systemd-tmpfiles-standalone";
|
||||
version = "260.1";
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "systemd";
|
||||
repo = "systemd";
|
||||
rev = "v260.1";
|
||||
hash = "sha256-FUKj3lvjz8TIsyu8NyJYtiNele+1BhdJPdw7r7nW6as=";
|
||||
};
|
||||
|
||||
nativeBuildInputs = [
|
||||
meson
|
||||
ninja
|
||||
pkg-config
|
||||
gperf
|
||||
(python3.withPackages (ps: [ ps.jinja2 ]))
|
||||
];
|
||||
|
||||
buildInputs = [
|
||||
libcap
|
||||
libxcrypt
|
||||
util-linux
|
||||
linuxHeaders
|
||||
];
|
||||
|
||||
postPatch = ''
|
||||
patchShebangs tools/ src/
|
||||
|
||||
# xml_helper.py needs lxml which we don't have — skip the check
|
||||
substituteInPlace man/meson.build \
|
||||
--replace-fail \
|
||||
"have_lxml = run_command(xml_helper_py, check: false).returncode() == 0" \
|
||||
"have_lxml = false"
|
||||
'';
|
||||
|
||||
# Disable all auto-features, then enable only what we need.
|
||||
mesonAutoFeatures = "disabled";
|
||||
|
||||
mesonFlags = [
|
||||
"-Dstandalone-binaries=true"
|
||||
"-Dtmpfiles=true"
|
||||
"-Dmode=release"
|
||||
"-Dstatic-libsystemd=pic"
|
||||
"-Dtests=false"
|
||||
"-Dinstall-tests=false"
|
||||
];
|
||||
|
||||
# Build the standalone binary + jinja2-generated tmpfiles.d configs.
|
||||
# The .conf.in files use custom_target() so they need explicit ninja targets.
|
||||
ninjaFlags = [
|
||||
"systemd-tmpfiles.standalone"
|
||||
"tmpfiles.d/var.conf"
|
||||
"tmpfiles.d/systemd.conf"
|
||||
"tmpfiles.d/static-nodes-permissions.conf"
|
||||
];
|
||||
|
||||
installPhase = ''
|
||||
mkdir -p $out/bin $out/example/tmpfiles.d
|
||||
cp systemd-tmpfiles.standalone $out/bin/systemd-tmpfiles
|
||||
|
||||
# Install tmpfiles.d configs (matches stock nixpkgs path convention).
|
||||
# Plain .conf from the source tree:
|
||||
for f in $src/tmpfiles.d/*.conf; do
|
||||
[ -f "$f" ] && install -m644 "$f" $out/example/tmpfiles.d/
|
||||
done
|
||||
# Jinja2-generated .conf from the build dir:
|
||||
for f in tmpfiles.d/*.conf; do
|
||||
[ -f "$f" ] && install -m644 "$f" $out/example/tmpfiles.d/
|
||||
done
|
||||
'';
|
||||
|
||||
meta = with lib; {
|
||||
description = "Standalone systemd-tmpfiles binary (statically links systemd internals)";
|
||||
homepage = "https://github.com/systemd/systemd";
|
||||
license = licenses.lgpl21Plus;
|
||||
platforms = platforms.linux;
|
||||
};
|
||||
}
|
||||
77
system/packages/turnstile.nix
Normal file
77
system/packages/turnstile.nix
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
# turnstile — session/login tracker with dinit user instance spawning.
|
||||
# Chimera Linux project (BSD-2-Clause). Provides:
|
||||
# - turnstiled daemon (tracks sessions, spawns per-user dinit)
|
||||
# - pam_turnstile.so (PAM session module)
|
||||
# - dinit backend script (shell script that launches dinit --user)
|
||||
#
|
||||
# Creates XDG_RUNTIME_DIR, manages user dinit lifecycle, exports
|
||||
# DBUS_SESSION_BUS_ADDRESS.
|
||||
|
||||
{ lib
|
||||
, stdenv
|
||||
, fetchFromGitHub
|
||||
, meson
|
||||
, ninja
|
||||
, pkg-config
|
||||
, scdoc
|
||||
, pam
|
||||
, dinit
|
||||
}:
|
||||
|
||||
stdenv.mkDerivation {
|
||||
pname = "turnstile";
|
||||
version = "0.1.11";
|
||||
|
||||
src = fetchFromGitHub {
|
||||
owner = "chimera-linux";
|
||||
repo = "turnstile";
|
||||
rev = "v0.1.11";
|
||||
hash = "sha256-94J+w0RHxzw7wS70LcpEzMvgevAqAwl0EtiANUmdRYU=";
|
||||
};
|
||||
|
||||
nativeBuildInputs = [
|
||||
meson
|
||||
ninja
|
||||
pkg-config
|
||||
scdoc
|
||||
];
|
||||
|
||||
buildInputs = [
|
||||
pam
|
||||
];
|
||||
|
||||
postPatch = ''
|
||||
# Patch dinit backend script to use Nix store paths
|
||||
substituteInPlace backend/dinit \
|
||||
--replace-fail 'exec dinitctl ' 'exec ${dinit}/bin/dinitctl ' \
|
||||
--replace-fail 'exec dinit ' 'exec ${dinit}/bin/dinit ' \
|
||||
--replace-fail '/usr/bin/dinit-monitor' '${dinit}/bin/dinit-monitor'
|
||||
|
||||
# CONF_PATH must resolve to /etc/turnstile at runtime — that's where
|
||||
# our NixOS module generates the configs via environment.etc.
|
||||
# Default meson computes $out/etc/turnstile (Nix store), unreachable at runtime.
|
||||
substituteInPlace meson.build \
|
||||
--replace-fail \
|
||||
"get_option('prefix'), get_option('sysconfdir'), 'turnstile'" \
|
||||
"'/', 'etc', 'turnstile'"
|
||||
|
||||
'';
|
||||
|
||||
mesonFlags = [
|
||||
"-Ddinit=enabled"
|
||||
"-Drunit=disabled"
|
||||
"-Dmanage_rundir=true"
|
||||
"-Dlibrary=disabled"
|
||||
"-Ddefault_backend=dinit"
|
||||
"-Drundir=/run"
|
||||
"-Dpam_moddir=${placeholder "out"}/lib/security"
|
||||
"-Dpamdir=${placeholder "out"}/etc/pam.d"
|
||||
];
|
||||
|
||||
meta = with lib; {
|
||||
description = "Session/login tracker with per-user service manager spawning";
|
||||
homepage = "https://github.com/chimera-linux/turnstile";
|
||||
license = licenses.bsd2;
|
||||
platforms = platforms.linux;
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue