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.
This commit is contained in:
2026-07-31 08:28:49 -04:00
parent dd984f96d4
commit 7e93db2d07
25 changed files with 353 additions and 133 deletions
+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) {