Compare commits

..
2 Commits
Author SHA1 Message Date
huntedbytheirs 486a476b90 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.
2026-07-31 08:38:53 -04:00
huntedbytheirs 7e93db2d07 feat: portability, correctness, and quality improvements for v0.2
Portability (Linux distro-agnostic):
- Remove hardcoded Clang compiler enforcement; GCC now builds
- Add find_package(Threads REQUIRED) for older glibc
- Add cmake install() target
- FHS 3.0 default root: /kappa -> /usr/local/kappa
- fs::path operator/ for all init/bootloader paths (fixes prefix fragility)
- Multi-distro zoneinfo search (FHS, NixOS, Guix, alt)
- Portable tar extraction (drop GNU-only --no-same-permissions)
- Runit enable/disable commands now prefix-aware
- --root CLI flag before/after subcommand, lazy directory creation
- Shebang constants de-duplicated to types.hpp

Correctness (race conditions, UB, corruption):
- Fix CWD race in scheduler: per-child chdir() instead of process-global
- Fix UB const_cast in exec_cmd/exec_capture: mutable argv buffers
- Fix non-atomic installed DB writes: tmp+rename pattern
- Fix read_file() no longer calls exit(1), throws instead
- Fix silent catch(...) parse errors now print diagnostics
- Fix rebuild false positives with config_hash change detection
- Fix s6 disable_cmd copy-paste bug (was identical to enable)
- Fix runit enable_cmd incomplete, disable_cmd wrong target
- Fix dinit env vars: functional env-file + companion .env

Quality:
- Add -Wall -Wextra -Wpedantic to CMake, fix 2 pre-existing warnings
- Move parse_int from error.hpp to parse_util.hpp
- Fix hash verification guard checks all three hash types
- Check patch return code in fetch.cpp
- Add explicit system_dir creation in ensure_directories()
- Add resolve to needs_dirs for build_registry() consistency
- Update stale /kappa path references in examples
- Remove inaccurate -Werror claim in CONTRIBUTING.md
- Add build-gcc/ and agent dirs to .gitignore
- Suppress clang-tidy portability-avoid-pragma-once
- Fix .gitignore /kappa pattern (was matching include/kappa/)
- Delete stale vcpkg_installed/ directory

54/54 tests pass. Builds on Clang and GCC with 0 warnings.
2026-07-31 08:28:49 -04:00
41 changed files with 871 additions and 176 deletions
+1
View File
@@ -6,6 +6,7 @@ Checks: >
portability-*,
readability-*,
-modernize-use-trailing-return-type,
-portability-avoid-pragma-once,
-readability-identifier-length,
-readability-magic-numbers,
-readability-identifier-naming
+7 -1
View File
@@ -34,7 +34,13 @@
# Build
build/
build-gcc/
compile_commands.json
vcpkg_installed/
kappa
/kappa
# Agent session state
.omo/
.logs/
.opencode/
+6 -4
View File
@@ -1,11 +1,9 @@
cmake_minimum_required(VERSION 3.20)
# Enforce Clang
set(CMAKE_C_COMPILER clang)
set(CMAKE_CXX_COMPILER clang++)
project(kappa VERSION 0.1.0 LANGUAGES CXX)
find_package(Threads REQUIRED)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
@@ -53,3 +51,7 @@ add_executable(kappa
src/system/activate.cpp
)
target_include_directories(kappa PRIVATE include)
target_link_libraries(kappa PRIVATE Threads::Threads)
target_compile_options(kappa PRIVATE -Wall -Wextra -Wpedantic)
install(TARGETS kappa RUNTIME DESTINATION bin)
+1 -1
View File
@@ -10,7 +10,7 @@ These aren't guidelines. They're the deal.
### 1. C++23 or don't bother
We compile with Clang, `-std=c++23`, and `-Werror`. If your code needs a
We compile with Clang, `-std=c++23`, and zero warnings. If your code needs a
polyfill for `std::format` or can't handle designated initializers, it
doesn't belong here. The standard library is your only dependency. Zero
external C++ libraries. Not even Boost.
+7 -2
View File
@@ -76,8 +76,9 @@ kappa rebuild config.kap # boot.init = "openrc" — only 5 packages actually r
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.
- **Source tarball caching.** Downloaded once, stored at `$KAPPA_ROOT/cache/`
(default: `/usr/local/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.
@@ -103,6 +104,10 @@ kappa rebuild config.kap # boot.init = "openrc" — only 5 packages actually r
### Quick start
> **Note for 0.1.x users**: The default store root has moved from `/kappa` to
> `/usr/local/kappa` (FHS 3.0). Set `KAPPA_ROOT` to your existing `/kappa`
> directory to keep using the old location.
```sh
# Build kappa (needs Clang 17+, CMake 3.20+, C++23)
cmake -B build -G Ninja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
+1 -1
View File
@@ -1,6 +1,6 @@
/*
* Per-package override for foo.
* Lives at /kappa/system/builds/foo.kap
* Lives at /usr/local/kappa/system/builds/foo.kap
*
* Uses system config syntax — only features/config need to be specified.
* The rest inherits from the package definition and system config.
+2 -2
View File
@@ -1,6 +1,6 @@
/*
* Kappa system configuration.
* Lives at /kappa/system/config.kap
* Lives at /usr/local/kappa/system/config.kap
*
* INIT SYSTEM SELECTION
* =====================
@@ -101,7 +101,7 @@ packages {
/*
// Per-package overrides can also live in
// /kappa/system/builds/<name>.kap
// /usr/local/kappa/system/builds/<name>.kap
*/
}
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include <string>
#include <string_view>
#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
+33
View File
@@ -0,0 +1,33 @@
#pragma once
#include <cstdint>
#include <string>
#include <string_view>
#include <vector>
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<Bootloader> 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
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include "kappa/dsl/ast.hpp"
#include "kappa/dsl/system.hpp"
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
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<AssertFailure> evaluate_assertions(const dsl::SystemConfig& cfg);
std::unordered_map<std::string, dsl::NamedService> resolve_services(
const dsl::SystemConfig& cfg,
const std::unordered_map<std::string, dsl::PackageDef>& packages);
} // namespace kappa::config
+2 -25
View File
@@ -1,36 +1,13 @@
#pragma once
#include "kappa/dsl/parse_util.hpp"
#include <format>
#include <stdexcept>
#include <string>
#include <string_view>
namespace kappa::dsl {
class ParseError : public std::runtime_error {
public:
ParseError(int line, int col, const std::string& msg)
: std::runtime_error(std::format("{}:{}: {}", line, col, msg)) {}
};
inline int parse_int(int line, int col, const std::string& lexeme) {
try {
std::size_t pos = 0;
int val = std::stoi(lexeme, &pos);
if (pos != lexeme.size()) {
throw ParseError(line, col,
std::format("expected an integer, got '{}'", lexeme));
}
return val;
} catch (const std::invalid_argument&) {
throw ParseError(line, col,
std::format("expected an integer, got '{}'", lexeme));
} catch (const std::out_of_range&) {
throw ParseError(line, col,
std::format("integer out of range: '{}'", lexeme));
}
}
inline std::string msg_expected(std::string_view expected, std::string_view got) {
return std::format("expected {}, got '{}'", expected, got);
}
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include <format>
#include <stdexcept>
#include <string>
namespace kappa::dsl {
class ParseError : public std::runtime_error {
public:
ParseError(int line, int col, const std::string& msg)
: std::runtime_error(std::format("{}:{}: {}", line, col, msg)) {}
};
/// Parse an integer from a DSL token lexeme.
/// Throws ParseError on invalid input or overflow.
inline int parse_int(int line, int col, const std::string& lexeme) {
try {
std::size_t pos = 0;
int val = std::stoi(lexeme, &pos);
if (pos != lexeme.size()) {
throw ParseError(line, col,
std::format("expected an integer, got '{}'", lexeme));
}
return val;
} catch (const std::invalid_argument&) {
throw ParseError(line, col,
std::format("expected an integer, got '{}'", lexeme));
} catch (const std::out_of_range&) {
throw ParseError(line, col,
std::format("integer out of range: '{}'", lexeme));
}
}
} // namespace kappa::dsl
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include "kappa/dsl/ast.hpp"
#include <filesystem>
#include <string>
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
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include <string>
#include <vector>
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<std::string>& remotes);
} // namespace kappa::fetch
+38
View File
@@ -0,0 +1,38 @@
#pragma once
#include "kappa/resolve/plan.hpp"
#include <filesystem>
#include <string>
#include <unordered_map>
#include <vector>
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<std::string> provides;
};
std::vector<DbEntry> read_installed();
bool write_installed(const std::vector<DbEntry>& entries);
bool record_generation(const std::vector<std::string>& hashes, int keep);
std::string compute_config_hash(
const std::unordered_map<std::string, dsl::FeatureDef>& features,
const std::unordered_map<std::string, std::string>& config);
} // namespace kappa::install
+2 -1
View File
@@ -17,6 +17,7 @@ std::filesystem::path builds_dir();
std::filesystem::path cache_dir();
std::filesystem::path packages_dir();
void ensure_directories();
bool ensure_directories();
bool directories_exist();
} // namespace kappa::paths
+32
View File
@@ -0,0 +1,32 @@
#pragma once
#include "kappa/dsl/system.hpp"
#include "kappa/resolve/plan.hpp"
#include <string>
#include <vector>
namespace kappa::rebuild {
struct ChangeSet {
std::vector<std::string> added;
std::vector<std::string> changed;
std::vector<std::string> removed;
bool kernel_changed = false;
bool init_changed = false;
bool services_changed = false;
bool bootloader_changed = false;
};
struct InitImpact {
std::vector<std::string> service_rebuild; // packages needing full rebuild (use ${enabledinit})
std::vector<std::string> service_only; // packages needing only service file regeneration
std::vector<std::string> 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
+37
View File
@@ -0,0 +1,37 @@
#pragma once
#include "kappa/config/merge.hpp"
#include "kappa/dsl/ast.hpp"
#include "kappa/dsl/system.hpp"
#include <string>
#include <unordered_map>
#include <vector>
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<ResolvedDep> dependencies;
std::string enabled_init; // boot.init value for ${enabledinit}
};
struct BuildPlan {
std::vector<BuildStep> steps;
std::vector<std::string> cycles;
std::vector<std::string> conflicts;
std::vector<std::string> missing;
};
using Registry = std::unordered_map<std::string, dsl::PackageDef>;
BuildPlan resolve(const dsl::SystemConfig& cfg, const Registry& registry);
} // namespace kappa::resolve
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include "kappa/resolve/plan.hpp"
#include <string>
#include <vector>
namespace kappa::sched {
struct SchedResult {
bool ok = false;
std::vector<std::string> built;
std::vector<std::string> 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
+46
View File
@@ -0,0 +1,46 @@
#pragma once
#include "kappa/dsl/ast.hpp"
#include "kappa/service/types.hpp"
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
namespace kappa::service {
struct ServiceSpec {
std::string name;
std::string description;
std::string exec;
std::string type;
std::string user;
std::vector<int> ports;
std::unordered_map<std::string, std::string> 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
+41
View File
@@ -0,0 +1,41 @@
#pragma once
#include <cstdint>
#include <string>
#include <string_view>
#include <vector>
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<InitSystem> 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
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include "kappa/dsl/system.hpp"
#include <string>
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
+21
View File
@@ -0,0 +1,21 @@
#pragma once
#include "kappa/dsl/ast.hpp"
#include "kappa/dsl/system.hpp"
#include <string>
#include <vector>
namespace kappa::tools {
enum class DiagSeverity { Warning, Error };
struct Diagnostic {
DiagSeverity severity;
std::string message;
};
std::vector<Diagnostic> check_package(const dsl::PackageDef& pkg);
std::vector<Diagnostic> check_config(const dsl::SystemConfig& cfg);
} // namespace kappa::tools
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include "kappa/dsl/ast.hpp"
#include "kappa/dsl/system.hpp"
#include <ostream>
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
+8
View File
@@ -0,0 +1,8 @@
#pragma once
#include <string>
#include <string_view>
namespace kappa::util {
std::string to_lower(std::string_view sv);
std::string shell_escape(std::string_view s);
}
+18 -1
View File
@@ -27,6 +27,18 @@ std::string generate_bootloader_config(Bootloader bl, const BootSpec& spec) {
}
}
namespace {
std::string read_current_init(std::string_view prefix) {
namespace fs = std::filesystem;
fs::path init_link = fs::path(prefix) / "boot/init";
std::error_code ec;
if (!fs::is_symlink(init_link, ec)) return {};
auto target = fs::read_symlink(init_link, ec);
if (ec) return {};
return target.string();
}
} // namespace
// ---------------------------------------------------------------------------
// install_bootloader_config — write the generated config file to disk
// ---------------------------------------------------------------------------
@@ -41,7 +53,12 @@ BootloaderInstallResult install_bootloader_config(Bootloader bl,
return {false, {}, "Unknown bootloader"};
}
std::string content = generate_bootloader_config(bl, spec);
BootSpec spec_copy = spec;
if (spec_copy.init_prev.empty()) {
spec_copy.init_prev = read_current_init(prefix);
}
std::string content = generate_bootloader_config(bl, spec_copy);
if (content.empty()) {
return {false, {}, "Failed to generate bootloader config"};
}
+3 -2
View File
@@ -3,6 +3,7 @@
#include <algorithm>
#include <cctype>
#include <filesystem>
#include <format>
#include <ranges>
@@ -48,12 +49,12 @@ BootloaderPaths bootloader_paths(Bootloader bl, std::string_view prefix) {
switch (bl) {
case Bootloader::Grub:
return {
.config_path = std::format("{}boot/grub/grub.cfg", prefix),
.config_path = (std::filesystem::path(prefix) / "boot/grub/grub.cfg").string(),
.install_cmd = "grub-install",
};
case Bootloader::Limine:
return {
.config_path = std::format("{}boot/limine/limine.cfg", prefix),
.config_path = (std::filesystem::path(prefix) / "boot/limine/limine.cfg").string(),
.install_cmd = "limine",
};
case Bootloader::Unknown:
+2 -2
View File
@@ -382,9 +382,9 @@ Dependency Parser::parse_dependency_item() {
auto raw = consume(TokenType::String).lexeme;
auto colon = raw.find(':');
if (colon != std::string::npos) {
return {raw.substr(0, colon), "", raw.substr(colon + 1)};
return {raw.substr(0, colon), "", raw.substr(colon + 1), ""};
}
return {std::move(raw), "", ""};
return {std::move(raw), "", "", ""};
}
consume(TokenType::Lbrace);
+20 -7
View File
@@ -19,8 +19,13 @@ using namespace std::string_view_literals;
static int exec_cmd(const std::vector<std::string>& argv) {
if (argv.empty()) { return -1; }
std::vector<std::vector<char>> argv_storage(argv.size());
std::vector<char*> cargs;
for (auto& a : argv) { cargs.push_back(const_cast<char*>(a.c_str())); }
for (size_t i = 0; i < argv.size(); ++i) {
argv_storage[i].assign(argv[i].begin(), argv[i].end());
argv_storage[i].push_back('\0');
cargs.push_back(argv_storage[i].data());
}
cargs.push_back(nullptr);
pid_t pid = fork();
@@ -45,8 +50,13 @@ static std::string exec_capture(const std::vector<std::string>& argv) {
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
std::vector<std::vector<char>> argv_storage(argv.size());
std::vector<char*> cargs;
for (auto& a : argv) { cargs.push_back(const_cast<char*>(a.c_str())); }
for (size_t i = 0; i < argv.size(); ++i) {
argv_storage[i].assign(argv[i].begin(), argv[i].end());
argv_storage[i].push_back('\0');
cargs.push_back(argv_storage[i].data());
}
cargs.push_back(nullptr);
execvp(cargs[0], cargs.data());
_exit(127);
@@ -174,7 +184,7 @@ FetchResult fetch(const dsl::PackageDef& pkg) {
}
}
}
if (!verified && !pkg.sha256.empty()) {
if (!verified && (!pkg.sha256.empty() || !pkg.sha512.empty() || !pkg.md5.empty())) {
result.error = "hash verification failed";
return result;
}
@@ -186,8 +196,7 @@ FetchResult fetch(const dsl::PackageDef& pkg) {
if (rc2 != 0) { result.error = "extraction failed"; return result; }
} else {
int rc2 = exec_cmd({"tar", "xf", dest_file.string(),
"-C", paths::temp_dir().string(),
"--no-same-owner", "--no-same-permissions"});
"-C", paths::temp_dir().string()});
if (rc2 != 0) { result.error = "extraction failed"; return result; }
}
@@ -215,8 +224,12 @@ FetchResult fetch(const dsl::PackageDef& pkg) {
}
}
exec_cmd({"patch", "-p" + std::to_string(patch.level),
"-d", result.work_dir.string(), "-i", patch_file});
int prc = exec_cmd({"patch", "-p" + std::to_string(patch.level),
"-d", result.work_dir.string(), "-i", patch_file});
if (prc != 0) {
result.error = "patch failed: " + patch.url;
return result;
}
}
return result;
+74 -8
View File
@@ -1,6 +1,7 @@
#include "kappa/install/install.hpp"
#include "kappa/paths.hpp"
#include <algorithm>
#include <cstdint>
#include <filesystem>
#include <format>
@@ -11,6 +12,42 @@ namespace kappa::install {
namespace fs = std::filesystem;
std::string compute_config_hash(
const std::unordered_map<std::string, dsl::FeatureDef>& features,
const std::unordered_map<std::string, std::string>& config) {
// Serialize features (sorted by key): "key1=enabled/flag|key2=..."
std::vector<std::pair<std::string_view, const dsl::FeatureDef*>> feat_sorted;
for (auto& [k, v] : features) feat_sorted.emplace_back(k, &v);
std::sort(feat_sorted.begin(), feat_sorted.end());
std::string serialized;
for (auto& [k, f] : feat_sorted) {
if (!serialized.empty()) serialized += '|';
serialized += k;
serialized += '=';
if (f->force) serialized += "force:";
serialized += f->enabled ? "1:" : "0:";
serialized += f->flag;
}
serialized += '\n';
// Serialize config (sorted by key): "key1=val1|key2=val2"
std::vector<std::pair<std::string_view, std::string_view>> cfg_sorted;
for (auto& [k, v] : config) cfg_sorted.emplace_back(k, v);
std::sort(cfg_sorted.begin(), cfg_sorted.end());
for (auto& [k, v] : cfg_sorted) {
serialized += k;
serialized += '=';
serialized += v;
serialized += '|';
}
// FNV-1a 64-bit hash
std::uint64_t h = 14695981039346656037ULL;
for (char c : serialized) {
h ^= static_cast<std::uint64_t>(static_cast<unsigned char>(c));
h *= 1099511628211ULL;
}
return std::format("{:016x}", h);
}
static std::string db_file() {
return (fs::path(paths::db_dir()) / "installed").string();
}
@@ -56,8 +93,12 @@ InstallResult install(const resolve::BuildStep& step,
}
auto entries = read_installed();
std::string ch = "0000000000000000";
if (!step.resolved.features.empty() || !step.resolved.config.empty()) {
ch = compute_config_hash(step.resolved.features, step.resolved.config);
}
entries.push_back({step.name, step.resolved.original.version,
hash, step.resolved.original.provides});
hash, ch, step.resolved.original.provides});
if (!write_installed(entries)) {
result.error = "failed to write installed DB";
return result;
@@ -82,6 +123,19 @@ std::vector<DbEntry> read_installed() {
std::istringstream iss(line);
DbEntry e;
iss >> e.name >> e.version >> e.hash;
std::string token;
auto pos = iss.tellg();
if (iss >> token && token.size() == 16
&& std::all_of(token.begin(), token.end(), [](char c) {
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f');
})) {
e.config_hash = token;
} else {
if (pos != std::streampos(-1)) {
iss.clear();
iss.seekg(pos);
}
}
std::string prov;
while (iss >> prov) { e.provides.push_back(prov); }
entries.push_back(std::move(e));
@@ -92,14 +146,26 @@ std::vector<DbEntry> read_installed() {
bool write_installed(const std::vector<DbEntry>& entries) {
std::error_code ec;
fs::create_directories(paths::db_dir(), ec);
std::ofstream out(db_file());
if (!out) { return false; }
for (auto& e : entries) {
out << e.name << ' ' << e.version << ' ' << e.hash;
for (auto& p : e.provides) { out << ' ' << p; }
out << '\n';
auto tmp = db_file() + ".tmp";
{
std::ofstream out(tmp);
if (!out) return false;
for (auto& e : entries) {
out << e.name << ' ' << e.version << ' ' << e.hash;
if (!e.config_hash.empty() && e.config_hash != "0000000000000000") {
out << ' ' << e.config_hash;
}
for (auto& p : e.provides) { out << ' ' << p; }
out << '\n';
}
out.close();
if (out.fail()) {
std::filesystem::remove(tmp, ec);
return false;
}
}
return true;
std::filesystem::rename(tmp, db_file(), ec);
return !ec;
}
bool record_generation(const std::vector<std::string>& hashes, int keep) {
+182 -86
View File
@@ -9,16 +9,23 @@
#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 <algorithm>
#include <cstdlib>
#include <filesystem>
#include <format>
#include <fstream>
#include <iostream>
#include <sstream>
#include <stdexcept>
#include <string_view>
using namespace std::string_view_literals;
@@ -49,6 +56,10 @@ Subcommands:
Options:
-h, --help Show this help message
-V, --version Show version information
-j, --jobs <n> Jobs per package (default: 1)
-w <n> Concurrent package builds (default: 1)
--root <path> Set kappa root directory (default: /usr/local/kappa)
--dry-run Report changes without building (rebuild only)
)";
}
@@ -59,8 +70,7 @@ static bool is_flag(std::string_view arg) {
static std::string read_file(const char* path) {
std::ifstream in(path);
if (!in) {
std::cerr << "error: cannot open '" << path << "'\n";
std::exit(1);
throw std::runtime_error(std::format("cannot open '{}'", path));
}
std::ostringstream buf;
buf << in.rdbuf();
@@ -114,8 +124,10 @@ static resolve::Registry build_registry(const dsl::SystemConfig& cfg) {
registry[pkg.name] = std::move(pkg);
found = true;
break;
} catch (const std::exception& e) {
std::cerr << "warning: parse error in " << sp << ": " << e.what() << "\n";
} catch (...) {
continue;
std::cerr << "warning: unknown parse error in " << sp << "\n";
}
}
@@ -131,7 +143,11 @@ static resolve::Registry build_registry(const dsl::SystemConfig& cfg) {
auto pkg = dsl::parse(buf.str());
registry[pkg.name] = std::move(pkg);
found = true;
} catch (...) {}
} catch (const std::exception& e) {
std::cerr << "warning: parse error in remote recipe " << pref.name << ": " << e.what() << "\n";
} catch (...) {
std::cerr << "warning: unknown parse error in remote recipe " << pref.name << "\n";
}
}
}
}
@@ -146,15 +162,20 @@ static resolve::Registry build_registry(const dsl::SystemConfig& cfg) {
}
int main(int argc, char* argv[]) {
paths::ensure_directories();
if (argc < 2) {
std::cerr << "kappa: missing subcommand\n\n";
print_usage();
return 1;
}
auto subcommand = std::string_view(argv[1]);
// Parse leading --root before subcommand
int arg_start = 1;
while (arg_start < argc && std::string_view(argv[arg_start]) == "--root" && arg_start + 1 < argc) {
paths::set_root(argv[arg_start + 1]);
arg_start += 2;
}
auto subcommand = std::string_view(argv[arg_start]);
if (subcommand == "-h" || subcommand == "--help") {
print_usage();
@@ -185,7 +206,8 @@ int main(int argc, char* argv[]) {
}
const char* file_arg = nullptr;
for (int i = 2; i < argc; ++i) {
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") {
print_usage();
@@ -197,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;
}
@@ -205,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;
@@ -249,6 +276,59 @@ int main(int argc, char* argv[]) {
return 0;
}
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 (const std::exception& e) {
std::cerr << "warning: config parse error: " << e.what() << "\n";
} catch (...) {
std::cerr << "warning: unknown config parse error\n";
}
}
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;
}
}
// Subcommands that MUST have writable directories to function.
bool needs_write = (subcommand == "build"
|| subcommand == "fetch" || subcommand == "fetch-package");
// rebuild benefits from cache dirs for registry lookups but
// degrades gracefully — it only reports, never writes.
bool needs_dirs = needs_write || subcommand == "rebuild" || subcommand == "resolve";
if (needs_dirs && !paths::directories_exist()) {
if (!paths::ensure_directories()) {
if (needs_write) {
std::cerr << "error: cannot create kappa directories (check permissions)\n";
return 1;
}
}
}
if (file_arg == nullptr
&& subcommand != "list"
&& subcommand != "rollback") {
@@ -256,7 +336,8 @@ int main(int argc, char* argv[]) {
return 1;
}
auto source = file_arg ? read_file(file_arg) : "";
try {
auto source = file_arg ? read_file(file_arg) : "";
if (subcommand == "parse-package") {
try {
@@ -345,7 +426,7 @@ int main(int argc, char* argv[]) {
auto pkg = dsl::parse(source);
int jobs = 1;
for (int i = 2; i < argc; ++i) {
for (int i = arg_start + 1; i < argc; ++i) {
auto arg = std::string_view(argv[i]);
if ((arg == "-j" || arg == "--jobs") && i + 1 < argc) {
jobs = std::stoi(argv[++i]);
@@ -493,41 +574,6 @@ 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);
@@ -540,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);
@@ -599,5 +690,10 @@ int main(int argc, char* argv[]) {
}
}
} catch (const std::exception& e) {
std::cerr << "error: " << e.what() << "\n";
return 1;
}
return 1; // unreachable
}
+22 -2
View File
@@ -9,7 +9,7 @@ static std::filesystem::path g_root = [] {
if (auto* env = std::getenv("KAPPA_ROOT"); env != nullptr) {
return std::filesystem::path{env};
}
return std::filesystem::path{"/kappa"};
return std::filesystem::path{"/usr/local/kappa"};
}();
void set_root(std::string_view path) { g_root = path; }
@@ -23,14 +23,34 @@ std::filesystem::path builds_dir() { return g_root / "system" / "builds"; }
std::filesystem::path cache_dir() { return g_root / "cache"; }
std::filesystem::path packages_dir() { return cache_dir() / "packages"; }
void ensure_directories() {
bool ensure_directories() {
std::error_code ec;
std::filesystem::create_directories(bin_dir(), ec);
if (ec) return false;
std::filesystem::create_directories(temp_dir(), ec);
if (ec) return false;
std::filesystem::create_directories(db_dir(), ec);
if (ec) return false;
std::filesystem::create_directories(system_dir(), ec);
if (ec) return false;
std::filesystem::create_directories(builds_dir(), ec);
if (ec) return false;
std::filesystem::create_directories(cache_dir(), ec);
if (ec) return false;
std::filesystem::create_directories(packages_dir(), ec);
if (ec) return false;
return true;
}
bool directories_exist() {
return std::filesystem::exists(g_root)
&& std::filesystem::exists(bin_dir())
&& std::filesystem::exists(temp_dir())
&& std::filesystem::exists(db_dir())
&& std::filesystem::exists(system_dir())
&& std::filesystem::exists(builds_dir())
&& std::filesystem::exists(cache_dir())
&& std::filesystem::exists(packages_dir());
}
} // namespace kappa::paths
+12 -3
View File
@@ -28,9 +28,18 @@ ChangeSet compute_changes(const dsl::SystemConfig& cfg) {
if (e.name != p.name) { continue; }
found = true;
bool changed = !p.version.empty()
|| !p.features.empty()
|| !p.config.empty();
bool changed = false;
if (!p.version.empty() && p.version != e.version) {
changed = true;
}
if (!changed && (!p.features.empty() || !p.config.empty())) {
if (e.config_hash.empty()) {
changed = true;
} else {
auto ch = install::compute_config_hash(p.features, p.config);
changed = (ch != e.config_hash);
}
}
if (changed) { cs.changed.push_back(p.name); }
break;
+2 -5
View File
@@ -55,12 +55,9 @@ std::string generate_dinit_service(const ServiceSpec& spec) {
out << std::format("run-as = {}\n", spec.user);
}
// Environment variables (as comments — dinit doesn't support inline env)
// Environment variables — dinit uses env-file directive
if (!spec.env.empty()) {
out << "\n# Environment variables:\n";
for (const auto& [key, value] : spec.env) {
out << std::format("# {}={}\n", key, value);
}
out << std::format("env-file = {}.env\n", spec.name);
}
// description
+12 -1
View File
@@ -110,7 +110,7 @@ ServiceInstallResult install_service(InitSystem is,
return {false, {},
std::format("Failed to write {}", run_path.string())};
}
out << "#!/bin/execlineb -P\n";
out << s6_execline_shebang;
out << "# Generated by kappa — do not edit manually\n";
out << std::format("# s6 service: {}\n", spec.name);
if (!spec.working_dir.empty()) {
@@ -196,6 +196,17 @@ ServiceInstallResult install_service(InitSystem is,
out << content;
}
// Dinit: also write companion .env file
if (is == InitSystem::Dinit && !spec.env.empty()) {
fs::path env_path = fs::path(paths.service_dir) / std::format("{}.env", spec.name);
std::ofstream env_out(env_path);
if (env_out) {
for (const auto& [key, value] : spec.env) {
env_out << key << "=" << value << "\n";
}
}
}
return {true, file_path.string(), {}};
}
+3 -1
View File
@@ -8,6 +8,8 @@ namespace kappa::service {
namespace {
static constexpr std::string_view openrc_run_shebang = "#!/sbin/openrc-run\n";
bool is_background_type(std::string_view type) {
return type == "longrun" || type == "notify" || type == "forking";
}
@@ -18,7 +20,7 @@ std::string generate_openrc_service(const ServiceSpec& spec) {
std::ostringstream os;
// Shebang and header
os << "#!/sbin/openrc-run\n";
os << openrc_run_shebang;
os << "# Generated by kappa — do not edit manually\n";
// Description
+1 -1
View File
@@ -19,7 +19,7 @@ std::string generate_s6_service(const ServiceSpec& spec) {
// --- run file content ---
std::ostringstream run;
run << "#!/bin/execlineb -P\n";
run << s6_execline_shebang;
run << "# Generated by kappa — do not edit manually\n";
run << std::format("# s6 service: {}\n", spec.name);
+9 -8
View File
@@ -3,6 +3,7 @@
#include <algorithm>
#include <cctype>
#include <filesystem>
#include <format>
#include <ranges>
@@ -61,31 +62,31 @@ InitPaths init_paths(InitSystem is, std::string_view prefix) {
switch (is) {
case InitSystem::Systemd:
return {
.service_dir = std::format("{}etc/systemd/system", prefix),
.service_dir = (std::filesystem::path(prefix) / "etc/systemd/system").string(),
.enable_cmd = "systemctl enable",
.disable_cmd = "systemctl disable",
};
case InitSystem::OpenRC:
return {
.service_dir = std::format("{}etc/init.d", prefix),
.service_dir = (std::filesystem::path(prefix) / "etc/init.d").string(),
.enable_cmd = "rc-update add",
.disable_cmd = "rc-update del",
};
case InitSystem::S6:
return {
.service_dir = std::format("{}etc/s6/sv", prefix),
.service_dir = (std::filesystem::path(prefix) / "etc/s6/sv").string(),
.enable_cmd = "s6-rc-bundle-update",
.disable_cmd = "s6-rc-bundle-update",
.disable_cmd = "s6-rc-bundle-update delete",
};
case InitSystem::Runit:
return {
.service_dir = std::format("{}etc/sv", prefix),
.enable_cmd = "ln -sf /etc/sv",
.disable_cmd = "rm -f /var/service",
.service_dir = (std::filesystem::path(prefix) / "etc/sv").string(),
.enable_cmd = std::format("ln -sf {}/etc/sv/{{name}} {}/var/service/", prefix, prefix),
.disable_cmd = std::format("rm -f {}/var/service/{{name}}", prefix),
};
case InitSystem::Dinit:
return {
.service_dir = std::format("{}etc/dinit.d", prefix),
.service_dir = (std::filesystem::path(prefix) / "etc/dinit.d").string(),
.enable_cmd = "dinitctl enable",
.disable_cmd = "dinitctl disable",
};
+29 -5
View File
@@ -1,8 +1,10 @@
#include "kappa/system/activate.hpp"
#include <array>
#include <filesystem>
#include <format>
#include <fstream>
#include <string_view>
namespace kappa::system {
@@ -33,15 +35,37 @@ ActivateResult write_timezone(const std::string& timezone,
return {false, "timezone is empty"};
}
// /etc/localtime is a symlink to /usr/share/zoneinfo/{timezone}
// /etc/localtime is a symlink to zoneinfo data
std::filesystem::path localtime = std::filesystem::path(prefix) / "etc/localtime";
std::filesystem::path zoneinfo = std::filesystem::path(prefix) / "usr/share/zoneinfo" / timezone;
std::error_code ec;
if (!std::filesystem::exists(zoneinfo, ec)) {
return {false, std::format("timezone data not found: {}", zoneinfo.string())};
static constexpr std::array<std::string_view, 4> zoneinfo_dirs = {
"usr/share/zoneinfo", // glibc/FHS standard
"etc/zoneinfo", // NixOS
"share/zoneinfo", // Guix, some prefix installs
"usr/lib/zoneinfo", // alternative
};
std::filesystem::path zoneinfo;
for (auto dir : zoneinfo_dirs) {
auto candidate = std::filesystem::path(prefix) / dir / timezone;
if (std::filesystem::exists(candidate)) {
zoneinfo = candidate;
break;
}
}
if (zoneinfo.empty()) {
std::string tried;
for (size_t i = 0; i < zoneinfo_dirs.size(); ++i) {
if (i > 0) tried += ", ";
tried += (std::filesystem::path(prefix) / zoneinfo_dirs[i] / timezone).string();
}
return {false,
std::format("timezone data '{}' not found in any known location: tried {}",
timezone, tried)};
}
std::error_code ec;
std::filesystem::create_directories(localtime.parent_path(), ec);
// Remove existing symlink/file if present
std::filesystem::remove(localtime, ec);
-5
View File
@@ -59,11 +59,6 @@ std::vector<Diagnostic> check_package(const dsl::PackageDef& pkg) {
}
}
bool has_forced_features = false;
for (auto& [_, f] : pkg.features) {
if (f.force) { has_forced_features = true; break; }
}
if (!pkg.assertions.empty()) {
diags.push_back({DiagSeverity::Warning,
std::to_string(pkg.assertions.size())
+2 -2
View File
@@ -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 ""