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:
Executable
+350
@@ -0,0 +1,350 @@
|
||||
#!/bin/bash
|
||||
# kappa init-switching integration test via systemd-nspawn
|
||||
# Requires: systemd-nspawn, debootstrap (or pacstrap), kappa binary
|
||||
set -euo pipefail
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m'
|
||||
|
||||
KAPPA_BIN="${KAPPA_BIN:-./build/kappa}"
|
||||
ROOTFS="./test-rootfs"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
cleanup() {
|
||||
echo ""
|
||||
echo "Cleaning up..."
|
||||
rm -rf "$ROOTFS" /tmp/kappa-test-*.kap 2>/dev/null || true
|
||||
}
|
||||
|
||||
trap cleanup EXIT
|
||||
|
||||
check() {
|
||||
local desc="$1" cmd="$2" expect="$3"
|
||||
echo -n " $desc ... "
|
||||
local out
|
||||
out=$(eval "$cmd" 2>&1) || true
|
||||
if echo "$out" | grep -q "$expect"; then
|
||||
echo -e "${GREEN}OK${NC}"
|
||||
PASS=$((PASS + 1))
|
||||
else
|
||||
echo -e "${RED}FAIL${NC}"
|
||||
echo " expected: $expect"
|
||||
echo " got: $(echo "$out" | head -3 | tr '\n' ' ')"
|
||||
FAIL=$((FAIL + 1))
|
||||
fi
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " kappa init-switching integration tests"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 1. Create a minimal rootfs directory structure
|
||||
# ------------------------------------------------------------------
|
||||
echo "--- Setting up test rootfs ---"
|
||||
|
||||
mkdir -p "$ROOTFS"/{etc,usr/local/bin,kappa/{boot,db,store,temp},var/service}
|
||||
cp "$KAPPA_BIN" "$ROOTFS/usr/local/bin/kappa"
|
||||
chmod +x "$ROOTFS/usr/local/bin/kappa"
|
||||
|
||||
export KAPPA_ROOT="$ROOTFS/kappa"
|
||||
|
||||
# Create a minimal package registry (simulating installed packages)
|
||||
mkdir -p "$KAPPA_ROOT/db"
|
||||
|
||||
# Simulate an installed system with nginx (has services), zlib (no services),
|
||||
# and postgresql (multi-service, uses ${enabledinit})
|
||||
cat > /tmp/kappa-test-nginx.kap << 'EOF'
|
||||
package "nginx" {
|
||||
version = "1.24"
|
||||
source = "https://nginx.org/nginx-1.24.tar.gz"
|
||||
service {
|
||||
exec = "/usr/bin/nginx"
|
||||
type = "forking"
|
||||
ports = [80, 443]
|
||||
user = "www-data"
|
||||
description = "Nginx web server"
|
||||
}
|
||||
build { ./configure --prefix=${prefix}; make -j${jobs} }
|
||||
install { make DESTDIR=${destdir} install }
|
||||
}
|
||||
EOF
|
||||
|
||||
cat > /tmp/kappa-test-zlib.kap << 'EOF'
|
||||
package "zlib" {
|
||||
version = "1.3"
|
||||
source = "https://zlib.net/zlib-1.3.tar.gz"
|
||||
build { ./configure --prefix=${prefix}; make -j${jobs} }
|
||||
install { make DESTDIR=${destdir} install }
|
||||
}
|
||||
EOF
|
||||
|
||||
cat > /tmp/kappa-test-postgres.kap << 'EOF'
|
||||
package "postgresql" {
|
||||
version = "16.3"
|
||||
source = "https://ftp.postgresql.org/source/postgresql-16.3.tar.gz"
|
||||
service main {
|
||||
exec = "/usr/bin/postgres"
|
||||
type = "forking"
|
||||
ports = [5432]
|
||||
user = "postgres"
|
||||
description = "PostgreSQL database server"
|
||||
}
|
||||
service checkpointer {
|
||||
exec = "/usr/bin/postgres-checkpointer"
|
||||
type = "longrun"
|
||||
user = "postgres"
|
||||
}
|
||||
build {
|
||||
case ${enabledinit} in
|
||||
systemd) ./configure --with-systemd --prefix=${prefix} ;;
|
||||
*) ./configure --prefix=${prefix} ;;
|
||||
esac
|
||||
make -j${jobs}
|
||||
}
|
||||
install { make DESTDIR=${destdir} install }
|
||||
}
|
||||
EOF
|
||||
|
||||
# Write installed DB (simulate nginx, zlib, postgresql already installed)
|
||||
# Format: name version hash [provides...]
|
||||
mkdir -p "$(dirname "$KAPPA_ROOT/db/installed")"
|
||||
cat > "$KAPPA_ROOT/db/installed" << 'EOF'
|
||||
nginx 1.24 a1b2c3d4e5f6a7b8
|
||||
zlib 1.3 b2c3d4e5f6a7b8c9
|
||||
postgresql 16.3 c3d4e5f6a7b8c9d0
|
||||
init a1b2c3d4e5f6a7b8
|
||||
kernel d4e5f6a7b8c9d0e1
|
||||
bootloader e5f6a7b8c9d0e1f2
|
||||
EOF
|
||||
|
||||
echo ""
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 2. Init system switching tests
|
||||
# ------------------------------------------------------------------
|
||||
echo "--- Init system switching ---"
|
||||
|
||||
# Base config template
|
||||
config_template() {
|
||||
local init="$1"
|
||||
cat << EOF
|
||||
system { hostname = "kappa-test" }
|
||||
packages {
|
||||
nginx {}
|
||||
zlib {}
|
||||
postgresql {}
|
||||
$init {}
|
||||
}
|
||||
services {
|
||||
nginx { enable = true }
|
||||
postgresql { enable = true }
|
||||
postgresql.checkpointer { enable = true }
|
||||
}
|
||||
boot {
|
||||
kernel = "linux"
|
||||
init = "$init"
|
||||
root = "/dev/sda1"
|
||||
bootloader = "limine"
|
||||
}
|
||||
groups {
|
||||
wheel { gid = 998 }
|
||||
}
|
||||
users {
|
||||
root { shell = "/bin/zsh" }
|
||||
}
|
||||
EOF
|
||||
}
|
||||
|
||||
# Test systemd config
|
||||
config_template "systemd" > /tmp/kappa-test-systemd.kap
|
||||
check "parse systemd config" \
|
||||
"$KAPPA_BIN parse-config /tmp/kappa-test-systemd.kap" \
|
||||
'systemd — system and service manager'
|
||||
|
||||
check "systemd bootloader in config" \
|
||||
"$KAPPA_BIN parse-config /tmp/kappa-test-systemd.kap" \
|
||||
'Limine'
|
||||
|
||||
# Test openrc config
|
||||
config_template "openrc" > /tmp/kappa-test-openrc.kap
|
||||
check "parse openrc config" \
|
||||
"$KAPPA_BIN parse-config /tmp/kappa-test-openrc.kap" \
|
||||
'OpenRC — dependency-based init'
|
||||
|
||||
# Test s6 config
|
||||
config_template "s6" > /tmp/kappa-test-s6.kap
|
||||
check "parse s6 config" \
|
||||
"$KAPPA_BIN parse-config /tmp/kappa-test-s6.kap" \
|
||||
's6 — s6 supervision suite'
|
||||
|
||||
# Test runit config
|
||||
config_template "runit" > /tmp/kappa-test-runit.kap
|
||||
check "parse runit config" \
|
||||
"$KAPPA_BIN parse-config /tmp/kappa-test-runit.kap" \
|
||||
'runit — supervision suite'
|
||||
|
||||
# Test dinit config
|
||||
config_template "dinit" > /tmp/kappa-test-dinit.kap
|
||||
check "parse dinit config" \
|
||||
"$KAPPA_BIN parse-config /tmp/kappa-test-dinit.kap" \
|
||||
'dinit — service manager'
|
||||
|
||||
echo ""
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 3. Service generation verification
|
||||
# ------------------------------------------------------------------
|
||||
echo "--- Service file generation ---"
|
||||
|
||||
# Verify service specs are parsed correctly per init
|
||||
for init in systemd openrc s6 runit dinit; do
|
||||
check "service block present for $init config" \
|
||||
"$KAPPA_BIN format /tmp/kappa-test-$init.kap" \
|
||||
'enable = true'
|
||||
done
|
||||
|
||||
# Verify the format output contains correct service fields
|
||||
check "nginx service has type forking" \
|
||||
"$KAPPA_BIN format /tmp/kappa-test-nginx.kap" \
|
||||
'type = "forking"'
|
||||
|
||||
check "postgresql has named service checkpointer" \
|
||||
"$KAPPA_BIN format /tmp/kappa-test-postgres.kap" \
|
||||
'service checkpointer'
|
||||
|
||||
check "postgresql build uses enabledinit" \
|
||||
"$KAPPA_BIN format /tmp/kappa-test-postgres.kap" \
|
||||
'enabledinit'
|
||||
|
||||
echo ""
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 4. Rebuild impact analysis
|
||||
# ------------------------------------------------------------------
|
||||
echo "--- Rebuild impact analysis ---"
|
||||
|
||||
# Write an initial installed state matching s6 config
|
||||
# (reuse the same installed DB from above)
|
||||
|
||||
config_template "s6" > /tmp/kappa-test-rebuild-s6.kap
|
||||
# The packages list includes "s6" which isn't in the installed DB,
|
||||
# so rebuild correctly shows it as a new package to build.
|
||||
check "rebuild detects new init package" \
|
||||
"$KAPPA_BIN rebuild /tmp/kappa-test-rebuild-s6.kap 2>&1" \
|
||||
'packages to rebuild'
|
||||
|
||||
# Now test switching FROM s6 TO systemd
|
||||
config_template "systemd" > /tmp/kappa-test-rebuild-systemd.kap
|
||||
|
||||
# The rebuild should detect init_changed
|
||||
# Note: this requires the installed DB to have the old init hash
|
||||
check "rebuild detects init change" \
|
||||
"$KAPPA_BIN rebuild /tmp/kappa-test-rebuild-systemd.kap 2>&1" \
|
||||
'packages to rebuild'
|
||||
|
||||
echo ""
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 5. Bootloader switching tests
|
||||
# ------------------------------------------------------------------
|
||||
echo "--- Bootloader switching ---"
|
||||
|
||||
for bl in grub limine; do
|
||||
cat > "/tmp/kappa-test-bl-$bl.kap" << KAPEOF
|
||||
system { hostname = "test" }
|
||||
packages { nginx {} }
|
||||
services {}
|
||||
boot {
|
||||
kernel = "linux"
|
||||
init = "s6"
|
||||
root = "/dev/sda1"
|
||||
bootloader = "$bl"
|
||||
}
|
||||
users { root { shell = "/bin/sh" } }
|
||||
KAPEOF
|
||||
check "bootloader $bl recognized in config" \
|
||||
"$KAPPA_BIN parse-config /tmp/kappa-test-bl-$bl.kap" \
|
||||
"$bl"
|
||||
done
|
||||
|
||||
# Test unknown bootloader
|
||||
cat > /tmp/kappa-test-badbl.kap << 'KAPEOF'
|
||||
system { hostname = "test" }
|
||||
packages {}
|
||||
services {}
|
||||
boot {
|
||||
kernel = "linux"
|
||||
init = "s6"
|
||||
root = "/dev/sda1"
|
||||
bootloader = "lilo"
|
||||
}
|
||||
users { root { shell = "/bin/sh" } }
|
||||
KAPEOF
|
||||
check "doctor warns on unknown bootloader" \
|
||||
"$KAPPA_BIN doctor /tmp/kappa-test-badbl.kap 2>&1" \
|
||||
'not a recognized bootloader'
|
||||
|
||||
echo ""
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# 6. Groups and conflicts
|
||||
# ------------------------------------------------------------------
|
||||
echo "--- Groups and conflicts ---"
|
||||
|
||||
cat > /tmp/kappa-test-groups2.kap << 'KAPEOF'
|
||||
system { hostname = "test" }
|
||||
packages {}
|
||||
services {}
|
||||
boot {
|
||||
kernel = "linux"
|
||||
init = "s6"
|
||||
root = "/dev/sda1"
|
||||
bootloader = "limine"
|
||||
}
|
||||
groups {
|
||||
wheel { gid = 998 }
|
||||
audio {}
|
||||
docker { gid = 995 }
|
||||
}
|
||||
users { root { shell = "/bin/sh" } }
|
||||
KAPEOF
|
||||
|
||||
check "groups with gid format correctly" \
|
||||
"$KAPPA_BIN format /tmp/kappa-test-groups2.kap" \
|
||||
'gid = 998'
|
||||
|
||||
check "groups without gid format as empty" \
|
||||
"$KAPPA_BIN format /tmp/kappa-test-groups2.kap" \
|
||||
'audio {}'
|
||||
|
||||
# Conflicts test
|
||||
cat > /tmp/kappa-test-conflict-pkg.kap << 'KAPEOF'
|
||||
package "systemd" {
|
||||
version = "255"
|
||||
source = "https://example.com/systemd.tar.gz"
|
||||
provides = ["udev", "logind"]
|
||||
conflicts = ["eudev", "elogind"]
|
||||
build { make }
|
||||
install { make install }
|
||||
}
|
||||
KAPEOF
|
||||
|
||||
check "conflicts parse correctly" \
|
||||
"$KAPPA_BIN parse-package /tmp/kappa-test-conflict-pkg.kap" \
|
||||
'valid'
|
||||
|
||||
check "conflicts format round-trips" \
|
||||
"$KAPPA_BIN format /tmp/kappa-test-conflict-pkg.kap" \
|
||||
'conflicts = \['
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo -e " Results: ${GREEN}$PASS passed${NC}, ${RED}$FAIL failed${NC}"
|
||||
echo "============================================"
|
||||
|
||||
[ "$FAIL" -eq 0 ] || exit 1
|
||||
Reference in New Issue
Block a user