Files
kappa/src/install/install.cpp
T
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

202 lines
6.3 KiB
C++

#include "kappa/install/install.hpp"
#include "kappa/paths.hpp"
#include <algorithm>
#include <cstdint>
#include <filesystem>
#include <format>
#include <fstream>
#include <sstream>
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();
}
static std::uint64_t fnv1a(std::string_view s) {
std::uint64_t h = 14695981039346656037ULL;
for (char c : s) { h ^= static_cast<std::uint64_t>(static_cast<unsigned char>(c)); h *= 1099511628211ULL; }
return h;
}
InstallResult install(const resolve::BuildStep& step,
const fs::path& work_dir) {
InstallResult result;
auto hash = std::format("{:016x}",
fnv1a(step.name + "\0" + step.resolved.original.version));
result.hash = hash;
auto dest = fs::path(paths::bin_dir()) / hash;
result.store_path = dest;
std::error_code ec;
fs::create_directories(dest, ec);
if (ec) {
result.error = "cannot create store directory";
return result;
}
auto src = work_dir / "destdir";
if (fs::exists(src)) {
for (auto& entry : fs::recursive_directory_iterator(src, ec)) {
if (ec) { break; }
auto rel = fs::relative(entry.path(), src);
auto target = dest / rel;
if (entry.is_directory()) {
fs::create_directories(target, ec);
} else {
fs::create_directories(target.parent_path(), ec);
if (!ec) { fs::rename(entry.path(), target, ec); }
}
if (ec) { break; }
}
}
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, ch, step.resolved.original.provides});
if (!write_installed(entries)) {
result.error = "failed to write installed DB";
return result;
}
std::vector<std::string> hashes;
for (auto& e : entries) { hashes.push_back(e.hash); }
record_generation(hashes, 5);
result.ok = true;
return result;
}
std::vector<DbEntry> read_installed() {
std::vector<DbEntry> entries;
std::ifstream in(db_file());
if (!in) { return entries; }
std::string line;
while (std::getline(in, line)) {
if (line.empty()) { continue; }
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));
}
return entries;
}
bool write_installed(const std::vector<DbEntry>& entries) {
std::error_code ec;
fs::create_directories(paths::db_dir(), ec);
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;
}
}
std::filesystem::rename(tmp, db_file(), ec);
return !ec;
}
bool record_generation(const std::vector<std::string>& hashes, int keep) {
auto gen_dir = fs::path(paths::db_dir()) / "generations";
std::error_code ec;
fs::create_directories(gen_dir, ec);
int gen = 1;
for (auto& entry : fs::directory_iterator(gen_dir, ec)) {
auto name = entry.path().filename().string();
if (name.starts_with("gen-")) { ++gen; }
}
auto gen_file = gen_dir / std::format("gen-{:04d}", gen);
std::ofstream out(gen_file);
if (!out) { return false; }
for (auto& h : hashes) { out << h << '\n'; }
std::vector<fs::path> gens;
for (auto& entry : fs::directory_iterator(gen_dir, ec)) {
gens.push_back(entry.path());
}
std::sort(gens.begin(), gens.end());
while (static_cast<int>(gens.size()) > keep && keep > 0) {
fs::remove_all(gens.front(), ec);
gens.erase(gens.begin());
}
return true;
}
} // namespace kappa::install