From 7e93db2d07475aeac4781339b327f7c81b27e9db Mon Sep 17 00:00:00 2001 From: HuntedByTheIRS Date: Fri, 31 Jul 2026 08:28:49 -0400 Subject: [PATCH] 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. --- .clang-tidy | 1 + .gitignore | 8 +- CMakeLists.txt | 10 ++- CONTRIBUTING.md | 2 +- README.md | 9 ++- examples/builds/foo.kap | 2 +- examples/config.kap | 4 +- include/kappa/dsl/error.hpp | 27 +------ include/kappa/dsl/parse_util.hpp | 35 +++++++++ include/kappa/paths.hpp | 3 +- src/boot/install.cpp | 19 ++++- src/boot/types.cpp | 5 +- src/dsl/parser.cpp | 4 +- src/fetch/fetch.cpp | 27 +++++-- src/install/install.cpp | 82 ++++++++++++++++++-- src/main.cpp | 127 ++++++++++++++++++++----------- src/paths.cpp | 24 +++++- src/rebuild/rebuild.cpp | 15 +++- src/service/dinit.cpp | 7 +- src/service/install.cpp | 13 +++- src/service/openrc.cpp | 4 +- src/service/s6.cpp | 2 +- src/service/types.cpp | 17 +++-- src/system/activate.cpp | 34 +++++++-- src/tools/doctor.cpp | 5 -- 25 files changed, 353 insertions(+), 133 deletions(-) create mode 100644 include/kappa/dsl/parse_util.hpp diff --git a/.clang-tidy b/.clang-tidy index e8ea98a..fbd9b8b 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -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 diff --git a/.gitignore b/.gitignore index 9f2113e..443e28b 100644 --- a/.gitignore +++ b/.gitignore @@ -34,7 +34,13 @@ # Build build/ +build-gcc/ compile_commands.json vcpkg_installed/ -kappa +/kappa + +# Agent session state +.omo/ +.logs/ +.opencode/ diff --git a/CMakeLists.txt b/CMakeLists.txt index a2a4935..a84326d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b245e25..f7171a2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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. diff --git a/README.md b/README.md index 4bac920..6bb2724 100644 --- a/README.md +++ b/README.md @@ -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++ diff --git a/examples/builds/foo.kap b/examples/builds/foo.kap index 1f4004a..f617fa1 100644 --- a/examples/builds/foo.kap +++ b/examples/builds/foo.kap @@ -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. diff --git a/examples/config.kap b/examples/config.kap index 2d11db3..de9b668 100644 --- a/examples/config.kap +++ b/examples/config.kap @@ -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/.kap + // /usr/local/kappa/system/builds/.kap */ } diff --git a/include/kappa/dsl/error.hpp b/include/kappa/dsl/error.hpp index 51e106a..68804bd 100644 --- a/include/kappa/dsl/error.hpp +++ b/include/kappa/dsl/error.hpp @@ -1,36 +1,13 @@ #pragma once +#include "kappa/dsl/parse_util.hpp" + #include -#include #include #include 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); } diff --git a/include/kappa/dsl/parse_util.hpp b/include/kappa/dsl/parse_util.hpp new file mode 100644 index 0000000..c6d1ac7 --- /dev/null +++ b/include/kappa/dsl/parse_util.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include + +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 diff --git a/include/kappa/paths.hpp b/include/kappa/paths.hpp index 92b1168..e4c107a 100644 --- a/include/kappa/paths.hpp +++ b/include/kappa/paths.hpp @@ -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 diff --git a/src/boot/install.cpp b/src/boot/install.cpp index 238c644..dcd792c 100644 --- a/src/boot/install.cpp +++ b/src/boot/install.cpp @@ -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"}; } diff --git a/src/boot/types.cpp b/src/boot/types.cpp index e3fb2dc..c7a4f2a 100644 --- a/src/boot/types.cpp +++ b/src/boot/types.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -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: diff --git a/src/dsl/parser.cpp b/src/dsl/parser.cpp index 3c5dfba..e9ade7f 100644 --- a/src/dsl/parser.cpp +++ b/src/dsl/parser.cpp @@ -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); diff --git a/src/fetch/fetch.cpp b/src/fetch/fetch.cpp index c19809f..d009651 100644 --- a/src/fetch/fetch.cpp +++ b/src/fetch/fetch.cpp @@ -19,8 +19,13 @@ using namespace std::string_view_literals; static int exec_cmd(const std::vector& argv) { if (argv.empty()) { return -1; } + std::vector> argv_storage(argv.size()); std::vector cargs; - for (auto& a : argv) { cargs.push_back(const_cast(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& argv) { dup2(pipefd[1], STDOUT_FILENO); close(pipefd[1]); + std::vector> argv_storage(argv.size()); std::vector cargs; - for (auto& a : argv) { cargs.push_back(const_cast(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; diff --git a/src/install/install.cpp b/src/install/install.cpp index cd6921c..1296ea2 100644 --- a/src/install/install.cpp +++ b/src/install/install.cpp @@ -1,6 +1,7 @@ #include "kappa/install/install.hpp" #include "kappa/paths.hpp" +#include #include #include #include @@ -11,6 +12,42 @@ 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(); } @@ -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 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 read_installed() { bool write_installed(const std::vector& 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& hashes, int keep) { diff --git a/src/main.cpp b/src/main.cpp index b264130..cc7b739 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -16,9 +16,11 @@ #include #include +#include #include #include #include +#include #include using namespace std::string_view_literals; @@ -49,6 +51,7 @@ Subcommands: Options: -h, --help Show this help message -V, --version Show version information + --root Set kappa root directory (default: /usr/local/kappa) )"; } @@ -59,8 +62,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 +116,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 +135,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 +154,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 +198,7 @@ int main(int argc, char* argv[]) { } const char* file_arg = nullptr; - for (int i = 2; i < argc; ++i) { + for (int i = arg_start + 1; i < argc; ++i) { if (std::string_view(argv[i]) == "-h" || std::string_view(argv[i]) == "--help") { print_usage(); @@ -249,6 +262,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 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 +322,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 +412,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 +560,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 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); @@ -599,5 +631,10 @@ int main(int argc, char* argv[]) { } } + } catch (const std::exception& e) { + std::cerr << "error: " << e.what() << "\n"; + return 1; + } + return 1; // unreachable } diff --git a/src/paths.cpp b/src/paths.cpp index a3eb99d..810f73d 100644 --- a/src/paths.cpp +++ b/src/paths.cpp @@ -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 diff --git a/src/rebuild/rebuild.cpp b/src/rebuild/rebuild.cpp index 5dc7e95..e5d5aff 100644 --- a/src/rebuild/rebuild.cpp +++ b/src/rebuild/rebuild.cpp @@ -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; diff --git a/src/service/dinit.cpp b/src/service/dinit.cpp index 81c5411..764b807 100644 --- a/src/service/dinit.cpp +++ b/src/service/dinit.cpp @@ -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 diff --git a/src/service/install.cpp b/src/service/install.cpp index 97a16d9..64c2b30 100644 --- a/src/service/install.cpp +++ b/src/service/install.cpp @@ -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(), {}}; } diff --git a/src/service/openrc.cpp b/src/service/openrc.cpp index f6f72ef..9378e0c 100644 --- a/src/service/openrc.cpp +++ b/src/service/openrc.cpp @@ -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 diff --git a/src/service/s6.cpp b/src/service/s6.cpp index f5270e5..f2b50cd 100644 --- a/src/service/s6.cpp +++ b/src/service/s6.cpp @@ -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); diff --git a/src/service/types.cpp b/src/service/types.cpp index 56a7e53..ad28c3a 100644 --- a/src/service/types.cpp +++ b/src/service/types.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include @@ -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", }; diff --git a/src/system/activate.cpp b/src/system/activate.cpp index 07963ed..19198cb 100644 --- a/src/system/activate.cpp +++ b/src/system/activate.cpp @@ -1,8 +1,10 @@ #include "kappa/system/activate.hpp" +#include #include #include #include +#include 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 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); diff --git a/src/tools/doctor.cpp b/src/tools/doctor.cpp index 05dce7e..e4c2140 100644 --- a/src/tools/doctor.cpp +++ b/src/tools/doctor.cpp @@ -59,11 +59,6 @@ std::vector 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())