feat: system-agnostic package manager — 5 inits, 2 bootloaders, parallel scheduler

Complete rewrite of kappa from a sequential build tool into a
system-agnostic package manager with runtime init switching.

Core additions:
- 5 init system backends: systemd, openrc, s6, runit, dinit
  (service file generation, enable/disable, init_paths)
- 2 bootloader backends: grub, limine (config generation, fallback entries)
- Parallel scheduler with worker pool, depth-based priority (Beta/Alpha/Zeta),
  atomic claiming, dependency tracking, deduplication, and failure propagation
- Init-switch impact analysis: only rebuild packages using ${enabledinit}
- Init-agnostic service definitions: flat NamedService blocks replace
  per-init nesting
- Package conflicts: mutual incompatibility detection in resolver
- System groups: init-agnostic group creation in DSL
- Init-agnostic hostname/timezone: direct /etc/hostname and /etc/localtime writes
- Source tarball caching at /kappa/cache/ with atomic write-then-rename
- Package recipe caching with remote fetching and version comparison
- remotes = [...] block in system config for package repositories
- Auto-fetch: rebuild resolves missing packages from remotes
- uninstall phase in package definitions
- ${enabledinit} eval variable for init-conditional builds
- Shared util module (to_lower, shell_escape)
- 54 integration tests across two shell test suites
- Comprehensive README and CONTRIBUTING guide

Bug fixes from review:
- CRITICAL: Replace std::system() with fork+execvp (command injection)
- CRITICAL: Fix scheduler deadlock on successful completion
- CRITICAL: Fix rebuild init/kernel/bootloader change detection
- HIGH: Fix path traversal via unsanitized package names in cache
- HIGH: Fix TOCTOU race in cache write with atomic rename
- HIGH: Fix formatter dropping remotes/imports blocks
- HIGH: Fix formatter stripping empty-string assert values
- HIGH: Fix formatter non-idempotent output (sorted key iteration)
- HIGH: Populate ${enabledinit} from boot.init in BuildStep
- MEDIUM: Fix data race on non-atomic scheduler stop flag
- MEDIUM: Fix compute_depths() traversal direction
- MEDIUM: Add runit to doctor supported-init warning
- MEDIUM: Extract to_lower/shell_escape to shared kappa::util
- MEDIUM: Consolidate generator declarations in headers
This commit is contained in:
2026-07-31 05:32:57 -04:00
parent d6612d0a4a
commit dd984f96d4
40 changed files with 3047 additions and 117 deletions
+49
View File
@@ -1,10 +1,55 @@
/*
* Kappa system configuration.
* Lives at /kappa/system/config.kap
*
* INIT SYSTEM SELECTION
* =====================
* The `boot.init` field (line ~84) selects which init system manages this
* machine. Valid values (case-insensitive):
*
* systemd — system and service manager
* openrc — OpenRC dependency-based init
* s6 — s6 supervision suite
* dinit — dinit service manager / init system
*
* The `boot.bootloader` field (line ~122) selects which bootloader config
* kappa generates (grub or limine).
*
* This setting determines:
* 1. Which backend generates service files at install time
* (systemd → .service units, openrc → init.d scripts, etc.)
* 2. What `${enabledinit}` resolves to during package builds
*
* Package definitions do NOT specify per-init service blocks. A package
* defines its service once (see examples/foo.kap) and the selected init
* system's backend handles the translation.
*
* SERVICES BLOCK
* ==============
* The `services` section enables or disables services declared by
* installed packages. Each entry maps to a package's service name:
*
* services {
* nginx { enable = true } // package "nginx", default "main" service
* postgresql.main { enable = true } // package "postgresql", named service "main"
* postgresql.checkpointer { enable = false }
* }
*
* For single-service packages, the service name defaults to "main" and
* can be omitted. For multi-service packages, use dot-notation
* (pkgname.servicename) to target a specific named service.
*
* Additional keys in each service block (port, ssl, etc.) are passed as
* custom config to the service definition.
*/
imports = []
remotes = [
"https://packages.kappa-os.org/stable/",
"https://packages.kappa-os.org/contrib/",
]
assert {
"efi partition required for UEFI boot" : boot.efi != ""
"root partition must be set" : boot.root != ""
@@ -75,11 +120,15 @@ services {
}
}
// Select init system — determines which backend generates service files.
// Valid: systemd, openrc, s6, dinit (case-insensitive).
boot {
kernel = "linux"
init = "s6"
efi = "/dev/sda2"
swap = "/dev/sda3"
// Bootloader — generates the appropriate config at install time.
// Valid: grub, limine (case-insensitive).
bootloader = "limine"
root = "/dev/sda1"
}
+64 -13
View File
@@ -1,6 +1,51 @@
/*
* foo — a web server with optional SSL and GUI support.
* Demonstrates the full kappa DSL surface.
*
* SERVICE MODEL
* =============
* Kappa service definitions are init-system-agnostic. The `service` block
* describes what the service IS (exec, type, ports, user) — NOT how each init
* system runs it. The system config's `boot.init` field (see config.kap)
* determines which init system's service files get generated at install time:
*
* boot.init = "systemd" → generates .service unit files
* boot.init = "openrc" → generates /etc/init.d scripts
* boot.init = "s6" → generates s6 service directories
* boot.init = "dinit" → generates dinit service descriptors
* boot.init = "runit" → generates runit service directories
*
* Per-init blocks (service { systemd { ... } s6 { ... } }) do NOT exist.
* If a package genuinely needs init-specific behaviour (e.g. different
* ./configure flags for systemd vs. openrc), use ${enabledinit} in the
* build phase — see examples/postgres.kap for that pattern.
*
* SERVICE TYPE VALUES
* ===================
* These are semantic, not init-specific. Each backend translates them
* into its own vocabulary:
*
* "simple" — foreground process; init manages lifecycle directly.
* systemd: Type=simple openrc: command_background=false
* s6: type=longrun dinit: type=process
*
* "forking" — process daemonises itself; init tracks the forked PID.
* systemd: Type=forking openrc: command_background=true
* s6: type=longrun dinit: type=bgprocess
*
* "notify" — foreground process that signals readiness (sd_notify).
* systemd: Type=notify openrc: command_background=true
* s6: type=longrun dinit: type=process
*
* "oneshot" — runs once and exits (startup tasks, database migrations).
* systemd: Type=oneshot openrc: command_background=false
* s6: type=oneshot dinit: type=scripted
*
* "longrun" — long-running supervised process (s6/runit idiom).
* systemd: Type=simple openrc: command_background=true
* s6: type=longrun dinit: type=process
*
* The backend generators handle all translation. Package authors only
* need to pick the semantic type that describes their daemon's behaviour.
*/
package "foo" {
const version = "1.2.3"
@@ -8,7 +53,8 @@ package "foo" {
sha256 = "e127a709cba24c76de8936cb7083dd768f28cd37eb010492e2f19b71eb1294e4"
license = "MIT"
provides = ["libfoo.so.1", "foo"]
provides = ["libfoo.so.1", "foo"]
conflicts = [] // packages this cannot coexist with (e.g. ["eudev"] if this were systemd)
patches = [
{
@@ -60,18 +106,19 @@ package "foo" {
CFLAGS ?= "-g"
}
// --- service ----------------------------------------------------------
// Init-agnostic service definition. The `type` field is semantic
// ("forking") — the selected init system's backend translates it into
// the appropriate native format. If the package ships multiple
// services, use named blocks (see examples/postgres.kap).
service {
runit {
exec = "/usr/bin/foo --daemon"
type = "forking"
user = "foo"
}
s6 {
exec = "/usr/bin/foo"
type = "longrun"
ports = [80, 443]
user = "foo"
}
exec = "/usr/bin/foo"
type = "forking" // daemonises itself
user = "foo"
ports = [80, 443]
description = "Foo web server"
after = "network" // ordering hint — systemd After=, OpenRC need, etc.
restart = "on-failure" // "always" | "on-failure" | "never"
}
prepare {
@@ -90,4 +137,8 @@ package "foo" {
install {
make DESTDIR=${destdir} install
}
uninstall {
make -C build uninstall
}
}
+148
View File
@@ -0,0 +1,148 @@
/*
* postgresql — a database server shipping multiple services.
*
* MULTI-SERVICE PACKAGES
* ======================
* Packages that install more than one long-running process can declare
* multiple named `service` blocks. Each has its own exec, type, ports,
* and lifecycle config. The system config enables them individually using
* dot-notation (see config.kap):
*
* services {
* postgresql.main { enable = true }
* postgresql.checkpointer { enable = true }
* postgresql.walwriter { enable = true }
* }
*
* Omitting the dot selects the service named "main".
*
*
* INIT-CONDITIONAL BUILDS
* =======================
* The variable ${enabledinit} exposes the configured init system name
* (from boot.init in config.kap) during the build phase. Use shell
* conditionals — no DSL if/else needed:
*
* build {
* case ${enabledinit} in
* systemd) ./configure --with-systemd --prefix=${prefix} ;;
* openrc) ./configure --with-openrc --prefix=${prefix} ;;
* s6|dinit) ./configure --prefix=${prefix} ;;
* esac
* make -j${jobs}
* }
*
* ${enabledinit} is interpolated to the literal init system name
* ("systemd", "openrc", "s6", or "dinit") before the shell executes the
* block. No DSL context-sensitive parsing required.
*
*
* SERVICE TYPE TRANSLATION (for reference)
* ========================================
* semantic │ systemd │ openrc │ s6 │ dinit
* ──────────┼────────────┼─────────────────────┼──────────┼──────────
* simple │ Type=simple│ bg=false │ longrun │ process
* forking │ Type=fork │ bg=true │ longrun │ bgprocess
* notify │ Type=notify│ bg=true │ longrun │ process
* oneshot │ Type=one │ bg=false, args="" │ oneshot │ scripted
* longrun │ Type=simple│ bg=true │ longrun │ process
*/
package "postgresql" {
const version = "16.3"
const source = "https://ftp.postgresql.org/pub/source/v${version}/postgresql-${version}.tar.gz"
sha256 = "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
license = "PostgreSQL"
depends = [
{ name = "readline", version = ">=8" },
{ name = "zlib", version = ">=1.2" },
{ name = "openssl" },
]
conflicts = [] // mutually exclusive packages (e.g. systemd vs eudev)
features {
ssl = { enabled = true, flag = "--with-ssl=openssl" }
nls = { enabled = true, flag = "--enable-nls" }
systemd = { enabled = false, flag = "--with-systemd" }
}
config {
file "etc/postgresql/data/postgresql.conf" mode = "default" {
port = ${cfg.port ? 5432}
max_connections = ${cfg.max_conn ? 100}
shared_buffers = ${cfg.shared_buf ? 128MB}
}
}
env {
CFLAGS = "-O2"
LDFLAGS = "-Wl,--as-needed"
}
// --- services ---------------------------------------------------------
// PostgreSQL ships the main server plus several auxiliary processes.
// Each runs as a separate service under the init system.
// Default service (name = "main"). Enabled via: postgresql { enable = true }
service main {
exec = "/usr/bin/postgres -D /var/lib/postgresql/data"
type = "forking" // postmaster daemonises itself
user = "postgres"
ports = [5432]
description = "PostgreSQL database server"
after = "network"
restart = "always"
working_dir = "/var/lib/postgresql"
}
// Background writer — handles checkpoint I/O.
service checkpointer {
exec = "/usr/bin/postgres-checkpointer"
type = "longrun"
user = "postgres"
description = "PostgreSQL checkpointer process"
restart = "always"
}
// WAL writer — flushes write-ahead log to disk.
service walwriter {
exec = "/usr/bin/postgres-walwriter"
type = "longrun"
user = "postgres"
restart = "always"
}
// --- build phases ------------------------------------------------------
prepare {
tar xf postgresql-${version}.tar.gz
}
// init-conditional build: PostgreSQL optionally links against systemd
// for socket activation and service notification. Use ${enabledinit}
// to decide configure flags without per-init service blocks.
build {
case ${enabledinit} in
systemd) ./configure --with-systemd --with-ssl=openssl --prefix=${prefix} ;;
*) ./configure --with-ssl=openssl --prefix=${prefix} ;;
esac
make -j${jobs} world
}
check {
make check
}
install {
make DESTDIR=${destdir} install-world
}
uninstall {
make DESTDIR=${destdir} uninstall-world
}
assert {
"data directory must exist" : system.config.data_dir != ""
}
}