#include "kappa/install/install.hpp" #include "kappa/paths.hpp" #include #include #include #include #include #include namespace kappa::install { namespace fs = std::filesystem; std::string compute_config_hash( const std::unordered_map& features, const std::unordered_map& config) { // Serialize features (sorted by key): "key1=enabled/flag|key2=..." std::vector> 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> 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(static_cast(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(static_cast(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 hashes; for (auto& e : entries) { hashes.push_back(e.hash); } record_generation(hashes, 5); result.ok = true; return result; } std::vector read_installed() { std::vector 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& 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& 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 gens; for (auto& entry : fs::directory_iterator(gen_dir, ec)) { gens.push_back(entry.path()); } std::sort(gens.begin(), gens.end()); while (static_cast(gens.size()) > keep && keep > 0) { fs::remove_all(gens.front(), ec); gens.erase(gens.begin()); } return true; } } // namespace kappa::install