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
155 lines
5.1 KiB
Markdown
155 lines
5.1 KiB
Markdown
<p align="center">
|
|
<img src="https://git.spectoria.dev/repo-avatars/f111d66d4a9972c13eb8aa4dddc7fe2c39aabdf0ab0ff0dc27a31a9aa1c2e968" width="180" alt="kappa mascot" />
|
|
</p>
|
|
|
|
# kappa
|
|
|
|
**Anywhere, any init, anytime.**
|
|
|
|
A declarative, source-based package manager that doesn't care what init system
|
|
you run. Or what bootloader. Or what CPU architecture. Kappa builds your entire
|
|
system from source — and lets you swap the init system like you'd swap a
|
|
wallpaper.
|
|
|
|
---
|
|
|
|
### Why
|
|
|
|
Every other package manager picked a side. `apt` married systemd. `pacman`
|
|
shackled itself to Arch's ecosystem. `emerge` gave you choice but at the cost
|
|
of your weekend. Nix gave you reproducibility but took your filesystem with it.
|
|
|
|
Kappa is what you get when you stop negotiating. You declare what your system
|
|
*is*, and kappa figures out how to build it. Change your mind about the init
|
|
system? Rebuild only the packages that care — the other 800 stay put.
|
|
|
|
### What it does
|
|
|
|
```
|
|
# Your system, in one file:
|
|
boot {
|
|
kernel = "linux"
|
|
init = "s6" # swap to "systemd" anytime
|
|
bootloader = "limine" # or "grub"
|
|
root = "/dev/sda1"
|
|
}
|
|
|
|
packages {
|
|
nginx { version = ">=1.24" }
|
|
postgresql {}
|
|
zlib {}
|
|
}
|
|
|
|
services {
|
|
nginx { enable = true }
|
|
}
|
|
```
|
|
|
|
Then:
|
|
|
|
```sh
|
|
kappa rebuild config.kap # builds everything, generates service files
|
|
kappa rebuild config.kap # boot.init = "openrc" — only 5 packages actually rebuild
|
|
```
|
|
|
|
### Features that nobody else has
|
|
|
|
- **Init-system-as-configuration.** `boot.init = "s6"` → generates s6 service
|
|
directories. Change it to `"systemd"` → regenerates `.service` units. Change
|
|
it to `"openrc"` → generates init.d scripts. The package definitions don't
|
|
know or care which init you picked. That's kappa's problem.
|
|
|
|
- **Post-install init switching.** Change `boot.init`, run `kappa rebuild`, reboot.
|
|
You're now on a different init system. Only packages that actually use
|
|
`${enabledinit}` in their build scripts need recompiling. Everything else
|
|
just gets new service files generated.
|
|
|
|
- **Bootloader rollback.** Every rebuild creates a fallback boot entry pointing
|
|
at the previous generation's init. If the new one doesn't boot, the old one
|
|
is one reboot away.
|
|
|
|
- **Parallel scheduler.** `-w 4 -j 8` means four packages building
|
|
simultaneously, eight jobs each. The scheduler uses depth-based priority
|
|
grouping so leaf dependencies unblock as much work as possible first.
|
|
|
|
- **Package recipe caching.** Declare `remotes = ["https://repo.example.com/"]`
|
|
in your config. Kappa fetches `.kap` files on demand, caches them, and only
|
|
re-fetches when the remote version is newer.
|
|
|
|
- **Source tarball caching.** Downloaded once, stored at `/kappa/cache/`.
|
|
Rebuilds don't touch the network unless versions change.
|
|
|
|
- **Conflicts.** `systemd` declares `conflicts = ["eudev", "elogind"]`. The
|
|
resolver catches mutual incompatibility before a build starts.
|
|
|
|
- **Init-agnostic system config.** `groups { wheel { gid = 998 } }` — kappa
|
|
creates the groups. `system { hostname = "mybox" }` — kappa writes
|
|
`/etc/hostname`. No `systemctl`, no `rc-update`, no init dependency.
|
|
|
|
### 5 init systems. 2 bootloaders. Zero lock-in.
|
|
|
|
| Init | Service location | Enable command |
|
|
|------|-----------------|----------------|
|
|
| systemd | `/etc/systemd/system/{name}.service` | `systemctl enable` |
|
|
| openrc | `/etc/init.d/{name}` | `rc-update add` |
|
|
| s6 | `/etc/s6/sv/{name}/run` | `s6-rc-bundle-update` |
|
|
| runit | `/etc/sv/{name}/run` | `ln -sf /etc/sv/{name} /var/service/` |
|
|
| dinit | `/etc/dinit.d/{name}` | `dinitctl enable` |
|
|
|
|
| Bootloader | Config path |
|
|
|-----------|------------|
|
|
| limine | `/boot/limine.cfg` |
|
|
| grub | `/boot/grub/grub.cfg` |
|
|
|
|
### Quick start
|
|
|
|
```sh
|
|
# Build kappa (needs Clang 17+, CMake 3.20+, C++23)
|
|
cmake -B build -G Ninja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
|
|
cmake --build build
|
|
|
|
# Write a config
|
|
cat > system.kap << 'EOF'
|
|
system { hostname = "kappa.local" }
|
|
packages { nginx {} }
|
|
services { nginx { enable = true } }
|
|
boot {
|
|
kernel = "linux"; init = "s6"; root = "/dev/sda1"; bootloader = "limine"
|
|
}
|
|
users { root { shell = "/bin/zsh" } }
|
|
remotes = ["https://packages.kappa-os.org/stable/"]
|
|
EOF
|
|
|
|
# Parse it
|
|
build/kappa parse-config system.kap
|
|
|
|
# Rebuild
|
|
build/kappa rebuild system.kap
|
|
```
|
|
|
|
### Subcommands
|
|
|
|
| Command | What it does |
|
|
|---------|-------------|
|
|
| `parse-package <file>` | Validate a `.kap` package definition |
|
|
| `parse-config <file>` | Validate a system configuration |
|
|
| `validate <file>` | Validate any kappa file |
|
|
| `format <file>` | Pretty-print to canonical style |
|
|
| `doctor <file>` | Check for issues and warnings |
|
|
| `resolve <config>` | Compute a build plan |
|
|
| `fetch <package>` | Download and verify source tarballs |
|
|
| `fetch-package <name>` | Fetch a package recipe from remotes |
|
|
| `build <package>` | Build a single package |
|
|
| `rebuild <config>` | Diff config against installed state, rebuild changed |
|
|
| `list` | Show installed packages |
|
|
| `rollback` | Show available generations |
|
|
|
|
### License
|
|
|
|
BSD 2-Clause. Do whatever you want. Just don't sue us.
|
|
|
|
### Contributing
|
|
|
|
See [CONTRIBUTING.md](CONTRIBUTING.md). We're opinionated but we merge good
|
|
code.
|