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.
202 lines
6.3 KiB
C++
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
|