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:
+156
-10
@@ -4,10 +4,13 @@
|
||||
#include "kappa/dsl/parser.hpp"
|
||||
#include "kappa/dsl/system.hpp"
|
||||
#include "kappa/fetch/fetch.hpp"
|
||||
#include "kappa/fetch/recipe.hpp"
|
||||
#include "kappa/install/install.hpp"
|
||||
#include "kappa/paths.hpp"
|
||||
#include "kappa/rebuild/rebuild.hpp"
|
||||
#include "kappa/resolve/plan.hpp"
|
||||
#include "kappa/boot/types.hpp"
|
||||
#include "kappa/service/types.hpp"
|
||||
#include "kappa/tools/doctor.hpp"
|
||||
#include "kappa/tools/format.hpp"
|
||||
|
||||
@@ -37,6 +40,7 @@ Subcommands:
|
||||
doctor <file> Check a .kap file for issues and warnings
|
||||
resolve <config> Resolve a build plan from a system config
|
||||
fetch <package> Download and verify source for a package
|
||||
fetch-package <name> Fetch a package recipe from configured remotes
|
||||
build <package> Build a package from its source directory
|
||||
rebuild <config> Compare config to installed state, rebuild changed
|
||||
list List installed packages
|
||||
@@ -87,6 +91,60 @@ static void handle_parse_error(const char* path,
|
||||
std::cerr << "error: " << e.what() << '\n';
|
||||
}
|
||||
|
||||
static resolve::Registry build_registry(const dsl::SystemConfig& cfg) {
|
||||
resolve::Registry registry;
|
||||
|
||||
for (const auto& pref : cfg.packages) {
|
||||
bool found = false;
|
||||
|
||||
// Check standard locations: local .kap, examples/, cache/packages/
|
||||
std::vector<std::string> search_paths = {
|
||||
std::string(pref.name) + ".kap",
|
||||
std::string("examples/") + pref.name + ".kap",
|
||||
(paths::packages_dir() / (pref.name + ".kap")).string(),
|
||||
};
|
||||
|
||||
for (const auto& sp : search_paths) {
|
||||
std::ifstream in(sp);
|
||||
if (!in) continue;
|
||||
std::ostringstream buf;
|
||||
buf << in.rdbuf();
|
||||
try {
|
||||
auto pkg = dsl::parse(buf.str());
|
||||
registry[pkg.name] = std::move(pkg);
|
||||
found = true;
|
||||
break;
|
||||
} catch (...) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If not found locally, try remotes
|
||||
if (!found && !cfg.remotes.empty()) {
|
||||
auto result = fetch::fetch_recipe(pref.name, cfg.remotes);
|
||||
if (result.ok && !result.path.empty()) {
|
||||
std::ifstream in(result.path);
|
||||
if (in) {
|
||||
std::ostringstream buf;
|
||||
buf << in.rdbuf();
|
||||
try {
|
||||
auto pkg = dsl::parse(buf.str());
|
||||
registry[pkg.name] = std::move(pkg);
|
||||
found = true;
|
||||
} catch (...) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
std::cerr << "warning: package '" << pref.name
|
||||
<< "' not found locally or in remotes\n";
|
||||
}
|
||||
}
|
||||
|
||||
return registry;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
paths::ensure_directories();
|
||||
|
||||
@@ -117,7 +175,8 @@ int main(int argc, char* argv[]) {
|
||||
|| (subcommand == "build")
|
||||
|| (subcommand == "rebuild")
|
||||
|| (subcommand == "list")
|
||||
|| (subcommand == "rollback");
|
||||
|| (subcommand == "fetch-package")
|
||||
|| (subcommand == "rollback");
|
||||
|
||||
if (!valid_subcommand) {
|
||||
std::cerr << "error: unknown subcommand '" << subcommand << "'\n\n";
|
||||
@@ -218,6 +277,26 @@ int main(int argc, char* argv[]) {
|
||||
<< cfg.packages.size() << " packages, "
|
||||
<< cfg.services.size() << " services, "
|
||||
<< cfg.users.size() << " users)\n";
|
||||
if (!cfg.boot.init.empty()) {
|
||||
auto is = kappa::service::parse_init_system(cfg.boot.init);
|
||||
std::cout << " init: " << cfg.boot.init;
|
||||
if (is != kappa::service::InitSystem::Unknown) {
|
||||
std::cout << " (" << kappa::service::init_description(is) << ")";
|
||||
} else {
|
||||
std::cout << " (unrecognized)";
|
||||
}
|
||||
std::cout << "\n";
|
||||
}
|
||||
if (!cfg.boot.bootloader.empty()) {
|
||||
auto bl = kappa::boot::parse_bootloader(cfg.boot.bootloader);
|
||||
std::cout << " bootloader: " << cfg.boot.bootloader;
|
||||
if (bl != kappa::boot::Bootloader::Unknown) {
|
||||
std::cout << " (" << kappa::boot::bootloader_description(bl) << ")";
|
||||
} else {
|
||||
std::cout << " (unrecognized)";
|
||||
}
|
||||
std::cout << "\n";
|
||||
}
|
||||
return 0;
|
||||
} catch (const std::runtime_error& e) {
|
||||
handle_parse_error(file_arg, source, e);
|
||||
@@ -358,13 +437,7 @@ int main(int argc, char* argv[]) {
|
||||
try {
|
||||
auto cfg = dsl::parse_system_config(source);
|
||||
|
||||
resolve::Registry registry;
|
||||
auto* pkg_arg = (argc > 3) ? argv[3] : nullptr;
|
||||
if (pkg_arg != nullptr) {
|
||||
auto pkg_src = read_file(pkg_arg);
|
||||
auto pkg = dsl::parse(pkg_src);
|
||||
registry[pkg.name] = std::move(pkg);
|
||||
}
|
||||
auto registry = build_registry(cfg);
|
||||
|
||||
auto plan = resolve::resolve(cfg, registry);
|
||||
|
||||
@@ -420,6 +493,41 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
}
|
||||
|
||||
if (subcommand == "fetch-package") {
|
||||
if (file_arg == nullptr) {
|
||||
std::cerr << "error: no package name specified\n";
|
||||
return 1;
|
||||
}
|
||||
try {
|
||||
std::vector<std::string> remotes;
|
||||
auto config_path = std::filesystem::path(paths::system_dir()) / "config.kap";
|
||||
if (std::filesystem::exists(config_path)) {
|
||||
auto cfg_src = read_file(config_path.c_str());
|
||||
try {
|
||||
auto cfg = dsl::parse_system_config(cfg_src);
|
||||
remotes = cfg.remotes;
|
||||
} catch (...) {
|
||||
}
|
||||
}
|
||||
auto result = fetch::fetch_recipe(file_arg, remotes);
|
||||
if (result.ok) {
|
||||
if (result.updated) {
|
||||
std::cout << "fetched " << file_arg << " " << result.version
|
||||
<< " → " << result.path << "\n";
|
||||
} else {
|
||||
std::cout << file_arg << " " << result.version
|
||||
<< " (cached, up to date)\n";
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
std::cerr << "fetch failed: " << result.error << "\n";
|
||||
return 1;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "fetch error: " << e.what() << "\n";
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (subcommand == "rebuild") {
|
||||
try {
|
||||
auto cfg = dsl::parse_system_config(source);
|
||||
@@ -442,8 +550,46 @@ int main(int argc, char* argv[]) {
|
||||
std::cout << " building " << name << " (new)\n";
|
||||
}
|
||||
if (cs.kernel_changed) { std::cout << " kernel changed\n"; }
|
||||
if (cs.init_changed) { std::cout << " init changed\n"; }
|
||||
if (cs.bootloader_changed) { std::cout << " bootloader changed\n"; }
|
||||
if (cs.init_changed) {
|
||||
auto is_name = cfg.boot.init;
|
||||
auto is = kappa::service::parse_init_system(is_name);
|
||||
if (is != kappa::service::InitSystem::Unknown) {
|
||||
std::cout << " init system: " << is_name << " ("
|
||||
<< kappa::service::init_description(is) << ")\n";
|
||||
} else {
|
||||
std::cout << " init changed (" << is_name
|
||||
<< " — unrecognized)\n";
|
||||
}
|
||||
|
||||
auto registry = build_registry(cfg);
|
||||
auto impact = rebuild::compute_init_impact(cfg, registry);
|
||||
|
||||
if (!impact.service_rebuild.empty()) {
|
||||
std::cout << " full rebuild (uses ${enabledinit}): "
|
||||
<< impact.service_rebuild.size()
|
||||
<< " packages\n";
|
||||
}
|
||||
if (!impact.service_only.empty()) {
|
||||
std::cout << " service files only: "
|
||||
<< impact.service_only.size()
|
||||
<< " packages\n";
|
||||
}
|
||||
if (!impact.skipped.empty()) {
|
||||
std::cout << " no services — skipped: "
|
||||
<< impact.skipped.size() << " packages\n";
|
||||
}
|
||||
}
|
||||
if (cs.bootloader_changed) {
|
||||
auto bl_name = cfg.boot.bootloader;
|
||||
auto bl = kappa::boot::parse_bootloader(bl_name);
|
||||
if (bl != kappa::boot::Bootloader::Unknown) {
|
||||
std::cout << " bootloader: " << bl_name << " ("
|
||||
<< kappa::boot::bootloader_description(bl) << ")\n";
|
||||
} else {
|
||||
std::cout << " bootloader changed (" << bl_name
|
||||
<< " — unrecognized)\n";
|
||||
}
|
||||
}
|
||||
if (cs.services_changed) { std::cout << " services changed\n"; }
|
||||
|
||||
return 0;
|
||||
|
||||
Reference in New Issue
Block a user