From 486a476b90b70c5de6fe43a6baf435d92efee03d Mon Sep 17 00:00:00 2001 From: HuntedByTheIRS Date: Fri, 31 Jul 2026 08:38:53 -0400 Subject: [PATCH] feat: wire rebuild pipeline (scheduler \u2192 build \u2192 install \u2192 services \u2192 bootloader) The rebuild subcommand now executes the full pipeline: - build_registry() -> resolve() -> sched::run() - install::install() for each built package - service::install_service() for enabled services - boot::install_bootloader_config() for bootloader config - system::write_hostname() / write_timezone() for system activation Added --dry-run flag for report-only behavior (backward compat with test suite). Added -w flag for worker parallelism and --dry-run to help. Fixed .gitignore: 'kappa' -> '/kappa' (was matching include/kappa/, preventing header files from being tracked since initial commit). This commit also adds all previously-gitignored header files. --- include/kappa/boot/bootloader.hpp | 32 +++++++ include/kappa/boot/types.hpp | 33 +++++++ include/kappa/config/eval.hpp | 28 ++++++ include/kappa/fetch/fetch.hpp | 18 ++++ include/kappa/fetch/recipe.hpp | 19 ++++ include/kappa/install/install.hpp | 38 ++++++++ include/kappa/rebuild/rebuild.hpp | 32 +++++++ include/kappa/resolve/plan.hpp | 37 ++++++++ include/kappa/sched/scheduler.hpp | 30 +++++++ include/kappa/service/service.hpp | 46 ++++++++++ include/kappa/service/types.hpp | 41 +++++++++ include/kappa/system/activate.hpp | 20 +++++ include/kappa/tools/doctor.hpp | 21 +++++ include/kappa/tools/format.hpp | 13 +++ include/kappa/util.hpp | 8 ++ src/main.cpp | 141 +++++++++++++++++++++--------- test-init-switch.sh | 4 +- 17 files changed, 518 insertions(+), 43 deletions(-) create mode 100644 include/kappa/boot/bootloader.hpp create mode 100644 include/kappa/boot/types.hpp create mode 100644 include/kappa/config/eval.hpp create mode 100644 include/kappa/fetch/fetch.hpp create mode 100644 include/kappa/fetch/recipe.hpp create mode 100644 include/kappa/install/install.hpp create mode 100644 include/kappa/rebuild/rebuild.hpp create mode 100644 include/kappa/resolve/plan.hpp create mode 100644 include/kappa/sched/scheduler.hpp create mode 100644 include/kappa/service/service.hpp create mode 100644 include/kappa/service/types.hpp create mode 100644 include/kappa/system/activate.hpp create mode 100644 include/kappa/tools/doctor.hpp create mode 100644 include/kappa/tools/format.hpp create mode 100644 include/kappa/util.hpp diff --git a/include/kappa/boot/bootloader.hpp b/include/kappa/boot/bootloader.hpp new file mode 100644 index 0000000..f27a5f9 --- /dev/null +++ b/include/kappa/boot/bootloader.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include +#include + +#include "kappa/boot/types.hpp" + +namespace kappa::boot { + +struct BootSpec { + std::string kernel_path; // e.g. "/kappa/boot/kernel" + std::string init_path; // e.g. "/kappa/boot/init" (the symlink, stable across init swaps) + std::string root; // e.g. "/dev/sda1" or "PARTUUID=xxx" + std::string kernel_params; // extra kernel cmdline params + std::string init_prev; // previous init symlink for fallback entry; empty = no fallback +}; + +std::string generate_bootloader_config(Bootloader bl, const BootSpec& spec); +std::string generate_limine_config(const BootSpec& spec); +std::string generate_grub_config(const BootSpec& spec); + +struct BootloaderInstallResult { + bool ok; + std::string path; + std::string error; +}; + +BootloaderInstallResult install_bootloader_config(Bootloader bl, + const BootSpec& spec, + std::string_view prefix = "/"); + +} // namespace kappa::boot diff --git a/include/kappa/boot/types.hpp b/include/kappa/boot/types.hpp new file mode 100644 index 0000000..95a2fc6 --- /dev/null +++ b/include/kappa/boot/types.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include +#include + +namespace kappa::boot { + +enum class Bootloader : std::uint8_t { + Grub, + Limine, + Unknown +}; + +Bootloader parse_bootloader(std::string_view name); + +std::string_view to_string(Bootloader bl); + +bool is_supported(std::string_view name); + +std::vector all_bootloaders(); + +std::string_view bootloader_description(Bootloader bl); + +struct BootloaderPaths { + std::string config_path; + std::string install_cmd; +}; + +BootloaderPaths bootloader_paths(Bootloader bl, std::string_view prefix = "/"); + +} // namespace kappa::boot diff --git a/include/kappa/config/eval.hpp b/include/kappa/config/eval.hpp new file mode 100644 index 0000000..bc96d76 --- /dev/null +++ b/include/kappa/config/eval.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "kappa/dsl/ast.hpp" +#include "kappa/dsl/system.hpp" + +#include +#include +#include +#include + +namespace kappa::config { + +struct AssertFailure { + std::string message; + std::string field; + std::string expected; + std::string actual; +}; + +std::string resolve_field(const dsl::SystemConfig& cfg, std::string_view path); + +std::vector evaluate_assertions(const dsl::SystemConfig& cfg); + +std::unordered_map resolve_services( + const dsl::SystemConfig& cfg, + const std::unordered_map& packages); + +} // namespace kappa::config diff --git a/include/kappa/fetch/fetch.hpp b/include/kappa/fetch/fetch.hpp new file mode 100644 index 0000000..b5253cc --- /dev/null +++ b/include/kappa/fetch/fetch.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "kappa/dsl/ast.hpp" + +#include +#include + +namespace kappa::fetch { + +struct FetchResult { + std::filesystem::path work_dir; + std::string error; + bool ok() const { return error.empty(); } +}; + +FetchResult fetch(const dsl::PackageDef& pkg); + +} // namespace kappa::fetch diff --git a/include/kappa/fetch/recipe.hpp b/include/kappa/fetch/recipe.hpp new file mode 100644 index 0000000..e9ba9c7 --- /dev/null +++ b/include/kappa/fetch/recipe.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include +#include + +namespace kappa::fetch { + +struct RecipeResult { + bool ok = false; + std::string path; // path to the cached .kap file + std::string version; // version of the fetched package + bool updated = false; // true if the cache was updated (remote was newer) + std::string error; +}; + +RecipeResult fetch_recipe(const std::string& name, + const std::vector& remotes); + +} // namespace kappa::fetch diff --git a/include/kappa/install/install.hpp b/include/kappa/install/install.hpp new file mode 100644 index 0000000..6085d9a --- /dev/null +++ b/include/kappa/install/install.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include "kappa/resolve/plan.hpp" + +#include +#include +#include +#include + +namespace kappa::install { + +struct InstallResult { + bool ok = false; + std::string hash; + std::filesystem::path store_path; + std::string error; +}; + +InstallResult install(const resolve::BuildStep& step, + const std::filesystem::path& work_dir); + +struct DbEntry { + std::string name; + std::string version; + std::string hash; + std::string config_hash; // FNV-1a hash of features+config, empty if unknown + std::vector provides; +}; + +std::vector read_installed(); +bool write_installed(const std::vector& entries); +bool record_generation(const std::vector& hashes, int keep); + +std::string compute_config_hash( + const std::unordered_map& features, + const std::unordered_map& config); + +} // namespace kappa::install diff --git a/include/kappa/rebuild/rebuild.hpp b/include/kappa/rebuild/rebuild.hpp new file mode 100644 index 0000000..e502c12 --- /dev/null +++ b/include/kappa/rebuild/rebuild.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include "kappa/dsl/system.hpp" +#include "kappa/resolve/plan.hpp" + +#include +#include + +namespace kappa::rebuild { + +struct ChangeSet { + std::vector added; + std::vector changed; + std::vector removed; + bool kernel_changed = false; + bool init_changed = false; + bool services_changed = false; + bool bootloader_changed = false; +}; + +struct InitImpact { + std::vector service_rebuild; // packages needing full rebuild (use ${enabledinit}) + std::vector service_only; // packages needing only service file regeneration + std::vector skipped; // packages with no services — no action needed +}; + +ChangeSet compute_changes(const dsl::SystemConfig& cfg); + +InitImpact compute_init_impact(const dsl::SystemConfig& cfg, + const resolve::Registry& registry); + +} // namespace kappa::rebuild diff --git a/include/kappa/resolve/plan.hpp b/include/kappa/resolve/plan.hpp new file mode 100644 index 0000000..8ad9579 --- /dev/null +++ b/include/kappa/resolve/plan.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include "kappa/config/merge.hpp" +#include "kappa/dsl/ast.hpp" +#include "kappa/dsl/system.hpp" + +#include +#include +#include + +namespace kappa::resolve { + +struct ResolvedDep { + std::string name; + std::string version_constraint; +}; + +struct BuildStep { + std::string name; + const dsl::PackageDef* package = nullptr; + config::ResolvedPackage resolved; + std::vector dependencies; + std::string enabled_init; // boot.init value for ${enabledinit} +}; + +struct BuildPlan { + std::vector steps; + std::vector cycles; + std::vector conflicts; + std::vector missing; +}; + +using Registry = std::unordered_map; + +BuildPlan resolve(const dsl::SystemConfig& cfg, const Registry& registry); + +} // namespace kappa::resolve diff --git a/include/kappa/sched/scheduler.hpp b/include/kappa/sched/scheduler.hpp new file mode 100644 index 0000000..43eeecb --- /dev/null +++ b/include/kappa/sched/scheduler.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "kappa/resolve/plan.hpp" + +#include +#include + +namespace kappa::sched { + +struct SchedResult { + bool ok = false; + std::vector built; + std::vector failed; +}; + +// Run the build plan with worker-level (-w) and job-level (-j) parallelism. +// Workers atomically claim ready packages, build them, and notify dependents. +// The same package is never built twice — deduplicated in the dependency graph. +// +// -w N package-level parallelism (concurrent package builds) +// -j N job-level parallelism (make -jN per package) +// +// The scheduler uses depth-based priority groups (Beta → Alpha → Zeta) +// so that deep dependencies unblock as many packages as possible first. +SchedResult run(const resolve::BuildPlan& plan, + const std::string& work_root, + int workers = 1, + int jobs = 1); + +} // namespace kappa::sched diff --git a/include/kappa/service/service.hpp b/include/kappa/service/service.hpp new file mode 100644 index 0000000..dc416fb --- /dev/null +++ b/include/kappa/service/service.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include "kappa/dsl/ast.hpp" +#include "kappa/service/types.hpp" + +#include +#include +#include +#include + +namespace kappa::service { + +struct ServiceSpec { + std::string name; + std::string description; + std::string exec; + std::string type; + std::string user; + std::vector ports; + std::unordered_map env; + std::string after; + std::string restart_policy; + std::string working_dir; + + static ServiceSpec from_service_init(const dsl::NamedService& ns); +}; + +std::string generate_systemd_service(const ServiceSpec& spec); +std::string generate_s6_service(const ServiceSpec& spec); +std::string generate_openrc_service(const ServiceSpec& spec); +std::string generate_dinit_service(const ServiceSpec& spec); +std::string generate_runit_service(const ServiceSpec& spec); +std::string generate_service_file(InitSystem is, const ServiceSpec& spec); + +struct ServiceInstallResult { + bool ok; + std::string path; + std::string error; +}; + +ServiceInstallResult install_service( + InitSystem is, + const ServiceSpec& spec, + std::string_view prefix = "/"); + +} // namespace kappa::service diff --git a/include/kappa/service/types.hpp b/include/kappa/service/types.hpp new file mode 100644 index 0000000..c0a609d --- /dev/null +++ b/include/kappa/service/types.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include +#include +#include +#include + +namespace kappa::service { + +enum class InitSystem : std::uint8_t { + Systemd, + OpenRC, + S6, + Runit, + Dinit, + Unknown +}; + +InitSystem parse_init_system(std::string_view name); + +std::string_view to_string(InitSystem is); + +bool is_supported(std::string_view name); + +std::vector all_systems(); + +std::string_view init_description(InitSystem is); + +struct InitPaths { + std::string service_dir; + std::string enable_cmd; + std::string disable_cmd; +}; + +InitPaths init_paths(InitSystem is, std::string_view prefix = "/"); + +// Shebangs for s6 execline-based run scripts. +// Extracted here so backends share a single definition. +inline constexpr std::string_view s6_execline_shebang = "#!/bin/execlineb -P\n"; + +} // namespace kappa::service diff --git a/include/kappa/system/activate.hpp b/include/kappa/system/activate.hpp new file mode 100644 index 0000000..032fb6f --- /dev/null +++ b/include/kappa/system/activate.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "kappa/dsl/system.hpp" + +#include + +namespace kappa::system { + +struct ActivateResult { + bool ok; + std::string error; +}; + +ActivateResult write_hostname(const std::string& hostname, + std::string_view prefix = "/"); + +ActivateResult write_timezone(const std::string& timezone, + std::string_view prefix = "/"); + +} // namespace kappa::system diff --git a/include/kappa/tools/doctor.hpp b/include/kappa/tools/doctor.hpp new file mode 100644 index 0000000..f5d66e9 --- /dev/null +++ b/include/kappa/tools/doctor.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "kappa/dsl/ast.hpp" +#include "kappa/dsl/system.hpp" + +#include +#include + +namespace kappa::tools { + +enum class DiagSeverity { Warning, Error }; + +struct Diagnostic { + DiagSeverity severity; + std::string message; +}; + +std::vector check_package(const dsl::PackageDef& pkg); +std::vector check_config(const dsl::SystemConfig& cfg); + +} // namespace kappa::tools diff --git a/include/kappa/tools/format.hpp b/include/kappa/tools/format.hpp new file mode 100644 index 0000000..ebf0b84 --- /dev/null +++ b/include/kappa/tools/format.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include "kappa/dsl/ast.hpp" +#include "kappa/dsl/system.hpp" + +#include + +namespace kappa::tools { + +void format_package(std::ostream& os, const dsl::PackageDef& pkg); +void format_config(std::ostream& os, const dsl::SystemConfig& cfg); + +} // namespace kappa::tools diff --git a/include/kappa/util.hpp b/include/kappa/util.hpp new file mode 100644 index 0000000..d5e6228 --- /dev/null +++ b/include/kappa/util.hpp @@ -0,0 +1,8 @@ +#pragma once +#include +#include + +namespace kappa::util { +std::string to_lower(std::string_view sv); +std::string shell_escape(std::string_view s); +} diff --git a/src/main.cpp b/src/main.cpp index cc7b739..4cc6223 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -9,11 +9,16 @@ #include "kappa/paths.hpp" #include "kappa/rebuild/rebuild.hpp" #include "kappa/resolve/plan.hpp" +#include "kappa/boot/bootloader.hpp" #include "kappa/boot/types.hpp" +#include "kappa/sched/scheduler.hpp" +#include "kappa/service/service.hpp" #include "kappa/service/types.hpp" +#include "kappa/system/activate.hpp" #include "kappa/tools/doctor.hpp" #include "kappa/tools/format.hpp" +#include #include #include #include @@ -51,7 +56,10 @@ Subcommands: Options: -h, --help Show this help message -V, --version Show version information + -j, --jobs Jobs per package (default: 1) + -w Concurrent package builds (default: 1) --root Set kappa root directory (default: /usr/local/kappa) + --dry-run Report changes without building (rebuild only) )"; } @@ -198,6 +206,7 @@ int main(int argc, char* argv[]) { } const char* file_arg = nullptr; + bool dry_run = false; for (int i = arg_start + 1; i < argc; ++i) { if (std::string_view(argv[i]) == "-h" || std::string_view(argv[i]) == "--help") { @@ -210,7 +219,8 @@ int main(int argc, char* argv[]) { return 0; } if (std::string_view(argv[i]) == "-j" - || std::string_view(argv[i]) == "--jobs") { + || std::string_view(argv[i]) == "--jobs" + || (std::string_view(argv[i]) == "-w" && i + 1 < argc)) { ++i; continue; } @@ -218,6 +228,10 @@ int main(int argc, char* argv[]) { paths::set_root(argv[++i]); continue; } + if (std::string_view(argv[i]) == "--dry-run") { + dry_run = true; + continue; + } if (!is_flag(argv[i])) { file_arg = argv[i]; break; @@ -572,58 +586,103 @@ int main(int argc, char* argv[]) { return 0; } + if (dry_run) { + // Dry run: report changes without executing + std::cout << cs.added.size() + cs.changed.size() + << " packages to rebuild\n"; + return 0; + } + auto registry = build_registry(cfg); + auto plan = resolve::resolve(cfg, registry); + + if (!plan.missing.empty()) { + for (auto& m : plan.missing) + std::cerr << "warning: package '" << m << "' not found in registry\n"; + } + if (!plan.cycles.empty()) { + std::cerr << "error: dependency cycle detected:\n"; + for (auto& c : plan.cycles) std::cerr << " " << c << "\n"; + return 1; + } + if (plan.steps.empty()) { + std::cout << "nothing to build\n"; + return 0; + } + + // Parse -w/-j flags after subcommand + int workers = 1, jobs = 1; + for (int i = arg_start + 1; i < argc; ++i) { + auto arg = std::string_view(argv[i]); + if (arg == "-w" && i + 1 < argc) workers = std::stoi(argv[++i]); + if ((arg == "-j" || arg == "--jobs") && i + 1 < argc) jobs = std::stoi(argv[++i]); + } + + // Build std::cout << cs.added.size() + cs.changed.size() << " packages to rebuild\n"; - for (auto& name : cs.changed) { - std::cout << " rebuilding " << name << " (changed)\n"; - } - for (auto& name : cs.added) { - std::cout << " building " << name << " (new)\n"; - } - if (cs.kernel_changed) { std::cout << " kernel 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 work_root = paths::temp_dir(); + auto sr = sched::run(plan, work_root.string(), workers, jobs); - auto registry = build_registry(cfg); - auto impact = rebuild::compute_init_impact(cfg, registry); + if (!sr.ok) { + std::cerr << sr.failed.size() << " packages failed to build\n"; + for (auto& f : sr.failed) std::cerr << " " << f << "\n"; + return 1; + } - 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"; + // Install built packages + for (auto& step : plan.steps) { + if (std::find(sr.built.begin(), sr.built.end(), step.name) == sr.built.end()) + continue; + auto work_dir = work_root / step.name; + auto inst = install::install(step, work_dir); + if (!inst.ok) { + std::cerr << "install failed for " << step.name << ": " << inst.error << "\n"; + return 1; } } + + // Generate service files for enabled services + auto is = kappa::service::parse_init_system(cfg.boot.init); + if (is != kappa::service::InitSystem::Unknown && !cfg.services.empty()) { + auto resolved = config::resolve_services(cfg, registry); + for (auto& sref : cfg.services) { + if (!sref.enable) continue; + auto it = resolved.find(sref.name); + if (it == resolved.end()) { + std::cerr << "warning: service '" << sref.name << "' not found in any package\n"; + continue; + } + auto spec = service::ServiceSpec::from_service_init(it->second); + auto svc_result = service::install_service(is, spec); + if (!svc_result.ok) + std::cerr << "service generation failed for " << sref.name << ": " << svc_result.error << "\n"; + } + } + + // Generate bootloader config if (cs.bootloader_changed) { - auto bl_name = cfg.boot.bootloader; - auto bl = kappa::boot::parse_bootloader(bl_name); + auto bl = kappa::boot::parse_bootloader(cfg.boot.bootloader); 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"; + boot::BootSpec boot_spec; + boot_spec.kernel_path = cfg.boot.kernel; + boot_spec.init_path = cfg.boot.init; + boot_spec.root = cfg.boot.root; + for (auto& [k, v] : cfg.boot.params) + boot_spec.kernel_params += k + "=" + v + " "; + auto boot_result = boot::install_bootloader_config(bl, boot_spec); + if (!boot_result.ok) + std::cerr << "bootloader config failed: " << boot_result.error << "\n"; } } - if (cs.services_changed) { std::cout << " services changed\n"; } + // Activate system configuration + if (!cfg.system.hostname.empty()) + system::write_hostname(cfg.system.hostname); + if (!cfg.system.timezone.empty()) + system::write_timezone(cfg.system.timezone); + + std::cout << "rebuild complete — " << sr.built.size() << " packages built\n"; return 0; } catch (const std::runtime_error& e) { handle_parse_error(file_arg, source, e); diff --git a/test-init-switch.sh b/test-init-switch.sh index 7a32951..28e2cc7 100755 --- a/test-init-switch.sh +++ b/test-init-switch.sh @@ -235,7 +235,7 @@ 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" \ + "$KAPPA_BIN rebuild --dry-run /tmp/kappa-test-rebuild-s6.kap 2>&1" \ 'packages to rebuild' # Now test switching FROM s6 TO systemd @@ -244,7 +244,7 @@ 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" \ + "$KAPPA_BIN rebuild --dry-run /tmp/kappa-test-rebuild-systemd.kap 2>&1" \ 'packages to rebuild' echo ""