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:
@@ -6,6 +6,7 @@ Checks: >
|
|||||||
portability-*,
|
portability-*,
|
||||||
readability-*,
|
readability-*,
|
||||||
-modernize-use-trailing-return-type,
|
-modernize-use-trailing-return-type,
|
||||||
|
-portability-avoid-pragma-once,
|
||||||
-readability-identifier-length,
|
-readability-identifier-length,
|
||||||
-readability-magic-numbers,
|
-readability-magic-numbers,
|
||||||
-readability-identifier-naming
|
-readability-identifier-naming
|
||||||
|
|||||||
+7
-1
@@ -34,7 +34,13 @@
|
|||||||
|
|
||||||
# Build
|
# Build
|
||||||
build/
|
build/
|
||||||
|
build-gcc/
|
||||||
compile_commands.json
|
compile_commands.json
|
||||||
vcpkg_installed/
|
vcpkg_installed/
|
||||||
kappa
|
/kappa
|
||||||
|
|
||||||
|
# Agent session state
|
||||||
|
.omo/
|
||||||
|
.logs/
|
||||||
|
.opencode/
|
||||||
|
|
||||||
|
|||||||
+6
-4
@@ -1,11 +1,9 @@
|
|||||||
cmake_minimum_required(VERSION 3.20)
|
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)
|
project(kappa VERSION 0.1.0 LANGUAGES CXX)
|
||||||
|
|
||||||
|
find_package(Threads REQUIRED)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 23)
|
set(CMAKE_CXX_STANDARD 23)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||||
@@ -53,3 +51,7 @@ add_executable(kappa
|
|||||||
src/system/activate.cpp
|
src/system/activate.cpp
|
||||||
)
|
)
|
||||||
target_include_directories(kappa PRIVATE include)
|
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)
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ These aren't guidelines. They're the deal.
|
|||||||
|
|
||||||
### 1. C++23 or don't bother
|
### 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
|
polyfill for `std::format` or can't handle designated initializers, it
|
||||||
doesn't belong here. The standard library is your only dependency. Zero
|
doesn't belong here. The standard library is your only dependency. Zero
|
||||||
external C++ libraries. Not even Boost.
|
external C++ libraries. Not even Boost.
|
||||||
|
|||||||
@@ -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
|
in your config. Kappa fetches `.kap` files on demand, caches them, and only
|
||||||
re-fetches when the remote version is newer.
|
re-fetches when the remote version is newer.
|
||||||
|
|
||||||
- **Source tarball caching.** Downloaded once, stored at `/kappa/cache/`.
|
- **Source tarball caching.** Downloaded once, stored at `$KAPPA_ROOT/cache/`
|
||||||
Rebuilds don't touch the network unless versions change.
|
(default: `/usr/local/kappa/cache/`). Rebuilds don't touch the network
|
||||||
|
unless versions change.
|
||||||
|
|
||||||
- **Conflicts.** `systemd` declares `conflicts = ["eudev", "elogind"]`. The
|
- **Conflicts.** `systemd` declares `conflicts = ["eudev", "elogind"]`. The
|
||||||
resolver catches mutual incompatibility before a build starts.
|
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
|
### 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
|
```sh
|
||||||
# Build kappa (needs Clang 17+, CMake 3.20+, C++23)
|
# Build kappa (needs Clang 17+, CMake 3.20+, C++23)
|
||||||
cmake -B build -G Ninja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
|
cmake -B build -G Ninja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Per-package override for foo.
|
* 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.
|
* Uses system config syntax — only features/config need to be specified.
|
||||||
* The rest inherits from the package definition and system config.
|
* The rest inherits from the package definition and system config.
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Kappa system configuration.
|
* Kappa system configuration.
|
||||||
* Lives at /kappa/system/config.kap
|
* Lives at /usr/local/kappa/system/config.kap
|
||||||
*
|
*
|
||||||
* INIT SYSTEM SELECTION
|
* INIT SYSTEM SELECTION
|
||||||
* =====================
|
* =====================
|
||||||
@@ -101,7 +101,7 @@ packages {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
// Per-package overrides can also live in
|
// Per-package overrides can also live in
|
||||||
// /kappa/system/builds/<name>.kap
|
// /usr/local/kappa/system/builds/<name>.kap
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +1,13 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "kappa/dsl/parse_util.hpp"
|
||||||
|
|
||||||
#include <format>
|
#include <format>
|
||||||
#include <stdexcept>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
|
||||||
namespace kappa::dsl {
|
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) {
|
inline std::string msg_expected(std::string_view expected, std::string_view got) {
|
||||||
return std::format("expected {}, got '{}'", expected, got);
|
return std::format("expected {}, got '{}'", expected, got);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <format>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
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
|
||||||
@@ -17,6 +17,7 @@ std::filesystem::path builds_dir();
|
|||||||
std::filesystem::path cache_dir();
|
std::filesystem::path cache_dir();
|
||||||
std::filesystem::path packages_dir();
|
std::filesystem::path packages_dir();
|
||||||
|
|
||||||
void ensure_directories();
|
bool ensure_directories();
|
||||||
|
bool directories_exist();
|
||||||
|
|
||||||
} // namespace kappa::paths
|
} // namespace kappa::paths
|
||||||
|
|||||||
+18
-1
@@ -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
|
// install_bootloader_config — write the generated config file to disk
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -41,7 +53,12 @@ BootloaderInstallResult install_bootloader_config(Bootloader bl,
|
|||||||
return {false, {}, "Unknown bootloader"};
|
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()) {
|
if (content.empty()) {
|
||||||
return {false, {}, "Failed to generate bootloader config"};
|
return {false, {}, "Failed to generate bootloader config"};
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
#include <filesystem>
|
||||||
#include <format>
|
#include <format>
|
||||||
#include <ranges>
|
#include <ranges>
|
||||||
|
|
||||||
@@ -48,12 +49,12 @@ BootloaderPaths bootloader_paths(Bootloader bl, std::string_view prefix) {
|
|||||||
switch (bl) {
|
switch (bl) {
|
||||||
case Bootloader::Grub:
|
case Bootloader::Grub:
|
||||||
return {
|
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",
|
.install_cmd = "grub-install",
|
||||||
};
|
};
|
||||||
case Bootloader::Limine:
|
case Bootloader::Limine:
|
||||||
return {
|
return {
|
||||||
.config_path = std::format("{}boot/limine/limine.cfg", prefix),
|
.config_path = (std::filesystem::path(prefix) / "boot/limine/limine.cfg").string(),
|
||||||
.install_cmd = "limine",
|
.install_cmd = "limine",
|
||||||
};
|
};
|
||||||
case Bootloader::Unknown:
|
case Bootloader::Unknown:
|
||||||
|
|||||||
+2
-2
@@ -382,9 +382,9 @@ Dependency Parser::parse_dependency_item() {
|
|||||||
auto raw = consume(TokenType::String).lexeme;
|
auto raw = consume(TokenType::String).lexeme;
|
||||||
auto colon = raw.find(':');
|
auto colon = raw.find(':');
|
||||||
if (colon != std::string::npos) {
|
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);
|
consume(TokenType::Lbrace);
|
||||||
|
|||||||
+20
-7
@@ -19,8 +19,13 @@ using namespace std::string_view_literals;
|
|||||||
|
|
||||||
static int exec_cmd(const std::vector<std::string>& argv) {
|
static int exec_cmd(const std::vector<std::string>& argv) {
|
||||||
if (argv.empty()) { return -1; }
|
if (argv.empty()) { return -1; }
|
||||||
|
std::vector<std::vector<char>> argv_storage(argv.size());
|
||||||
std::vector<char*> cargs;
|
std::vector<char*> cargs;
|
||||||
for (auto& a : argv) { cargs.push_back(const_cast<char*>(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);
|
cargs.push_back(nullptr);
|
||||||
|
|
||||||
pid_t pid = fork();
|
pid_t pid = fork();
|
||||||
@@ -45,8 +50,13 @@ static std::string exec_capture(const std::vector<std::string>& argv) {
|
|||||||
dup2(pipefd[1], STDOUT_FILENO);
|
dup2(pipefd[1], STDOUT_FILENO);
|
||||||
close(pipefd[1]);
|
close(pipefd[1]);
|
||||||
|
|
||||||
|
std::vector<std::vector<char>> argv_storage(argv.size());
|
||||||
std::vector<char*> cargs;
|
std::vector<char*> cargs;
|
||||||
for (auto& a : argv) { cargs.push_back(const_cast<char*>(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);
|
cargs.push_back(nullptr);
|
||||||
execvp(cargs[0], cargs.data());
|
execvp(cargs[0], cargs.data());
|
||||||
_exit(127);
|
_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";
|
result.error = "hash verification failed";
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -186,8 +196,7 @@ FetchResult fetch(const dsl::PackageDef& pkg) {
|
|||||||
if (rc2 != 0) { result.error = "extraction failed"; return result; }
|
if (rc2 != 0) { result.error = "extraction failed"; return result; }
|
||||||
} else {
|
} else {
|
||||||
int rc2 = exec_cmd({"tar", "xf", dest_file.string(),
|
int rc2 = exec_cmd({"tar", "xf", dest_file.string(),
|
||||||
"-C", paths::temp_dir().string(),
|
"-C", paths::temp_dir().string()});
|
||||||
"--no-same-owner", "--no-same-permissions"});
|
|
||||||
if (rc2 != 0) { result.error = "extraction failed"; return result; }
|
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),
|
int prc = exec_cmd({"patch", "-p" + std::to_string(patch.level),
|
||||||
"-d", result.work_dir.string(), "-i", patch_file});
|
"-d", result.work_dir.string(), "-i", patch_file});
|
||||||
|
if (prc != 0) {
|
||||||
|
result.error = "patch failed: " + patch.url;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
+74
-8
@@ -1,6 +1,7 @@
|
|||||||
#include "kappa/install/install.hpp"
|
#include "kappa/install/install.hpp"
|
||||||
#include "kappa/paths.hpp"
|
#include "kappa/paths.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <format>
|
#include <format>
|
||||||
@@ -11,6 +12,42 @@ namespace kappa::install {
|
|||||||
|
|
||||||
namespace fs = std::filesystem;
|
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() {
|
static std::string db_file() {
|
||||||
return (fs::path(paths::db_dir()) / "installed").string();
|
return (fs::path(paths::db_dir()) / "installed").string();
|
||||||
}
|
}
|
||||||
@@ -56,8 +93,12 @@ InstallResult install(const resolve::BuildStep& step,
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto entries = read_installed();
|
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,
|
entries.push_back({step.name, step.resolved.original.version,
|
||||||
hash, step.resolved.original.provides});
|
hash, ch, step.resolved.original.provides});
|
||||||
if (!write_installed(entries)) {
|
if (!write_installed(entries)) {
|
||||||
result.error = "failed to write installed DB";
|
result.error = "failed to write installed DB";
|
||||||
return result;
|
return result;
|
||||||
@@ -82,6 +123,19 @@ std::vector<DbEntry> read_installed() {
|
|||||||
std::istringstream iss(line);
|
std::istringstream iss(line);
|
||||||
DbEntry e;
|
DbEntry e;
|
||||||
iss >> e.name >> e.version >> e.hash;
|
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;
|
std::string prov;
|
||||||
while (iss >> prov) { e.provides.push_back(prov); }
|
while (iss >> prov) { e.provides.push_back(prov); }
|
||||||
entries.push_back(std::move(e));
|
entries.push_back(std::move(e));
|
||||||
@@ -92,14 +146,26 @@ std::vector<DbEntry> read_installed() {
|
|||||||
bool write_installed(const std::vector<DbEntry>& entries) {
|
bool write_installed(const std::vector<DbEntry>& entries) {
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
fs::create_directories(paths::db_dir(), ec);
|
fs::create_directories(paths::db_dir(), ec);
|
||||||
std::ofstream out(db_file());
|
auto tmp = db_file() + ".tmp";
|
||||||
if (!out) { return false; }
|
{
|
||||||
for (auto& e : entries) {
|
std::ofstream out(tmp);
|
||||||
out << e.name << ' ' << e.version << ' ' << e.hash;
|
if (!out) return false;
|
||||||
for (auto& p : e.provides) { out << ' ' << p; }
|
for (auto& e : entries) {
|
||||||
out << '\n';
|
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) {
|
bool record_generation(const std::vector<std::string>& hashes, int keep) {
|
||||||
|
|||||||
+82
-45
@@ -16,9 +16,11 @@
|
|||||||
|
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <format>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <stdexcept>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
|
||||||
using namespace std::string_view_literals;
|
using namespace std::string_view_literals;
|
||||||
@@ -49,6 +51,7 @@ Subcommands:
|
|||||||
Options:
|
Options:
|
||||||
-h, --help Show this help message
|
-h, --help Show this help message
|
||||||
-V, --version Show version information
|
-V, --version Show version information
|
||||||
|
--root <path> 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) {
|
static std::string read_file(const char* path) {
|
||||||
std::ifstream in(path);
|
std::ifstream in(path);
|
||||||
if (!in) {
|
if (!in) {
|
||||||
std::cerr << "error: cannot open '" << path << "'\n";
|
throw std::runtime_error(std::format("cannot open '{}'", path));
|
||||||
std::exit(1);
|
|
||||||
}
|
}
|
||||||
std::ostringstream buf;
|
std::ostringstream buf;
|
||||||
buf << in.rdbuf();
|
buf << in.rdbuf();
|
||||||
@@ -114,8 +116,10 @@ static resolve::Registry build_registry(const dsl::SystemConfig& cfg) {
|
|||||||
registry[pkg.name] = std::move(pkg);
|
registry[pkg.name] = std::move(pkg);
|
||||||
found = true;
|
found = true;
|
||||||
break;
|
break;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "warning: parse error in " << sp << ": " << e.what() << "\n";
|
||||||
} catch (...) {
|
} 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());
|
auto pkg = dsl::parse(buf.str());
|
||||||
registry[pkg.name] = std::move(pkg);
|
registry[pkg.name] = std::move(pkg);
|
||||||
found = true;
|
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[]) {
|
int main(int argc, char* argv[]) {
|
||||||
paths::ensure_directories();
|
|
||||||
|
|
||||||
if (argc < 2) {
|
if (argc < 2) {
|
||||||
std::cerr << "kappa: missing subcommand\n\n";
|
std::cerr << "kappa: missing subcommand\n\n";
|
||||||
print_usage();
|
print_usage();
|
||||||
return 1;
|
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") {
|
if (subcommand == "-h" || subcommand == "--help") {
|
||||||
print_usage();
|
print_usage();
|
||||||
@@ -185,7 +198,7 @@ int main(int argc, char* argv[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const char* file_arg = nullptr;
|
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"
|
if (std::string_view(argv[i]) == "-h"
|
||||||
|| std::string_view(argv[i]) == "--help") {
|
|| std::string_view(argv[i]) == "--help") {
|
||||||
print_usage();
|
print_usage();
|
||||||
@@ -249,6 +262,59 @@ int main(int argc, char* argv[]) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (subcommand == "fetch-package") {
|
||||||
|
if (file_arg == nullptr) {
|
||||||
|
std::cerr << "error: no package name specified\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
std::vector<std::string> 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
|
if (file_arg == nullptr
|
||||||
&& subcommand != "list"
|
&& subcommand != "list"
|
||||||
&& subcommand != "rollback") {
|
&& subcommand != "rollback") {
|
||||||
@@ -256,7 +322,8 @@ int main(int argc, char* argv[]) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto source = file_arg ? read_file(file_arg) : "";
|
try {
|
||||||
|
auto source = file_arg ? read_file(file_arg) : "";
|
||||||
|
|
||||||
if (subcommand == "parse-package") {
|
if (subcommand == "parse-package") {
|
||||||
try {
|
try {
|
||||||
@@ -345,7 +412,7 @@ int main(int argc, char* argv[]) {
|
|||||||
auto pkg = dsl::parse(source);
|
auto pkg = dsl::parse(source);
|
||||||
|
|
||||||
int jobs = 1;
|
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]);
|
auto arg = std::string_view(argv[i]);
|
||||||
if ((arg == "-j" || arg == "--jobs") && i + 1 < argc) {
|
if ((arg == "-j" || arg == "--jobs") && i + 1 < argc) {
|
||||||
jobs = std::stoi(argv[++i]);
|
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<std::string> 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") {
|
if (subcommand == "rebuild") {
|
||||||
try {
|
try {
|
||||||
auto cfg = dsl::parse_system_config(source);
|
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
|
return 1; // unreachable
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-2
@@ -9,7 +9,7 @@ static std::filesystem::path g_root = [] {
|
|||||||
if (auto* env = std::getenv("KAPPA_ROOT"); env != nullptr) {
|
if (auto* env = std::getenv("KAPPA_ROOT"); env != nullptr) {
|
||||||
return std::filesystem::path{env};
|
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; }
|
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 cache_dir() { return g_root / "cache"; }
|
||||||
std::filesystem::path packages_dir() { return cache_dir() / "packages"; }
|
std::filesystem::path packages_dir() { return cache_dir() / "packages"; }
|
||||||
|
|
||||||
void ensure_directories() {
|
bool ensure_directories() {
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
std::filesystem::create_directories(bin_dir(), ec);
|
std::filesystem::create_directories(bin_dir(), ec);
|
||||||
|
if (ec) return false;
|
||||||
std::filesystem::create_directories(temp_dir(), ec);
|
std::filesystem::create_directories(temp_dir(), ec);
|
||||||
|
if (ec) return false;
|
||||||
std::filesystem::create_directories(db_dir(), ec);
|
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);
|
std::filesystem::create_directories(builds_dir(), ec);
|
||||||
|
if (ec) return false;
|
||||||
std::filesystem::create_directories(cache_dir(), ec);
|
std::filesystem::create_directories(cache_dir(), ec);
|
||||||
|
if (ec) return false;
|
||||||
std::filesystem::create_directories(packages_dir(), ec);
|
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
|
} // namespace kappa::paths
|
||||||
|
|||||||
+12
-3
@@ -28,9 +28,18 @@ ChangeSet compute_changes(const dsl::SystemConfig& cfg) {
|
|||||||
if (e.name != p.name) { continue; }
|
if (e.name != p.name) { continue; }
|
||||||
found = true;
|
found = true;
|
||||||
|
|
||||||
bool changed = !p.version.empty()
|
bool changed = false;
|
||||||
|| !p.features.empty()
|
if (!p.version.empty() && p.version != e.version) {
|
||||||
|| !p.config.empty();
|
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); }
|
if (changed) { cs.changed.push_back(p.name); }
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -55,12 +55,9 @@ std::string generate_dinit_service(const ServiceSpec& spec) {
|
|||||||
out << std::format("run-as = {}\n", spec.user);
|
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()) {
|
if (!spec.env.empty()) {
|
||||||
out << "\n# Environment variables:\n";
|
out << std::format("env-file = {}.env\n", spec.name);
|
||||||
for (const auto& [key, value] : spec.env) {
|
|
||||||
out << std::format("# {}={}\n", key, value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// description
|
// description
|
||||||
|
|||||||
+12
-1
@@ -110,7 +110,7 @@ ServiceInstallResult install_service(InitSystem is,
|
|||||||
return {false, {},
|
return {false, {},
|
||||||
std::format("Failed to write {}", run_path.string())};
|
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 << "# Generated by kappa — do not edit manually\n";
|
||||||
out << std::format("# s6 service: {}\n", spec.name);
|
out << std::format("# s6 service: {}\n", spec.name);
|
||||||
if (!spec.working_dir.empty()) {
|
if (!spec.working_dir.empty()) {
|
||||||
@@ -196,6 +196,17 @@ ServiceInstallResult install_service(InitSystem is,
|
|||||||
out << content;
|
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(), {}};
|
return {true, file_path.string(), {}};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ namespace kappa::service {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
static constexpr std::string_view openrc_run_shebang = "#!/sbin/openrc-run\n";
|
||||||
|
|
||||||
bool is_background_type(std::string_view type) {
|
bool is_background_type(std::string_view type) {
|
||||||
return type == "longrun" || type == "notify" || type == "forking";
|
return type == "longrun" || type == "notify" || type == "forking";
|
||||||
}
|
}
|
||||||
@@ -18,7 +20,7 @@ std::string generate_openrc_service(const ServiceSpec& spec) {
|
|||||||
std::ostringstream os;
|
std::ostringstream os;
|
||||||
|
|
||||||
// Shebang and header
|
// Shebang and header
|
||||||
os << "#!/sbin/openrc-run\n";
|
os << openrc_run_shebang;
|
||||||
os << "# Generated by kappa — do not edit manually\n";
|
os << "# Generated by kappa — do not edit manually\n";
|
||||||
|
|
||||||
// Description
|
// Description
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@ std::string generate_s6_service(const ServiceSpec& spec) {
|
|||||||
|
|
||||||
// --- run file content ---
|
// --- run file content ---
|
||||||
std::ostringstream run;
|
std::ostringstream run;
|
||||||
run << "#!/bin/execlineb -P\n";
|
run << s6_execline_shebang;
|
||||||
run << "# Generated by kappa — do not edit manually\n";
|
run << "# Generated by kappa — do not edit manually\n";
|
||||||
run << std::format("# s6 service: {}\n", spec.name);
|
run << std::format("# s6 service: {}\n", spec.name);
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
#include <filesystem>
|
||||||
#include <format>
|
#include <format>
|
||||||
#include <ranges>
|
#include <ranges>
|
||||||
|
|
||||||
@@ -61,31 +62,31 @@ InitPaths init_paths(InitSystem is, std::string_view prefix) {
|
|||||||
switch (is) {
|
switch (is) {
|
||||||
case InitSystem::Systemd:
|
case InitSystem::Systemd:
|
||||||
return {
|
return {
|
||||||
.service_dir = std::format("{}etc/systemd/system", prefix),
|
.service_dir = (std::filesystem::path(prefix) / "etc/systemd/system").string(),
|
||||||
.enable_cmd = "systemctl enable",
|
.enable_cmd = "systemctl enable",
|
||||||
.disable_cmd = "systemctl disable",
|
.disable_cmd = "systemctl disable",
|
||||||
};
|
};
|
||||||
case InitSystem::OpenRC:
|
case InitSystem::OpenRC:
|
||||||
return {
|
return {
|
||||||
.service_dir = std::format("{}etc/init.d", prefix),
|
.service_dir = (std::filesystem::path(prefix) / "etc/init.d").string(),
|
||||||
.enable_cmd = "rc-update add",
|
.enable_cmd = "rc-update add",
|
||||||
.disable_cmd = "rc-update del",
|
.disable_cmd = "rc-update del",
|
||||||
};
|
};
|
||||||
case InitSystem::S6:
|
case InitSystem::S6:
|
||||||
return {
|
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",
|
.enable_cmd = "s6-rc-bundle-update",
|
||||||
.disable_cmd = "s6-rc-bundle-update",
|
.disable_cmd = "s6-rc-bundle-update delete",
|
||||||
};
|
};
|
||||||
case InitSystem::Runit:
|
case InitSystem::Runit:
|
||||||
return {
|
return {
|
||||||
.service_dir = std::format("{}etc/sv", prefix),
|
.service_dir = (std::filesystem::path(prefix) / "etc/sv").string(),
|
||||||
.enable_cmd = "ln -sf /etc/sv",
|
.enable_cmd = std::format("ln -sf {}/etc/sv/{{name}} {}/var/service/", prefix, prefix),
|
||||||
.disable_cmd = "rm -f /var/service",
|
.disable_cmd = std::format("rm -f {}/var/service/{{name}}", prefix),
|
||||||
};
|
};
|
||||||
case InitSystem::Dinit:
|
case InitSystem::Dinit:
|
||||||
return {
|
return {
|
||||||
.service_dir = std::format("{}etc/dinit.d", prefix),
|
.service_dir = (std::filesystem::path(prefix) / "etc/dinit.d").string(),
|
||||||
.enable_cmd = "dinitctl enable",
|
.enable_cmd = "dinitctl enable",
|
||||||
.disable_cmd = "dinitctl disable",
|
.disable_cmd = "dinitctl disable",
|
||||||
};
|
};
|
||||||
|
|||||||
+29
-5
@@ -1,8 +1,10 @@
|
|||||||
#include "kappa/system/activate.hpp"
|
#include "kappa/system/activate.hpp"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <format>
|
#include <format>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
namespace kappa::system {
|
namespace kappa::system {
|
||||||
|
|
||||||
@@ -33,15 +35,37 @@ ActivateResult write_timezone(const std::string& timezone,
|
|||||||
return {false, "timezone is empty"};
|
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 localtime = std::filesystem::path(prefix) / "etc/localtime";
|
||||||
std::filesystem::path zoneinfo = std::filesystem::path(prefix) / "usr/share/zoneinfo" / timezone;
|
|
||||||
|
|
||||||
std::error_code ec;
|
static constexpr std::array<std::string_view, 4> zoneinfo_dirs = {
|
||||||
if (!std::filesystem::exists(zoneinfo, ec)) {
|
"usr/share/zoneinfo", // glibc/FHS standard
|
||||||
return {false, std::format("timezone data not found: {}", zoneinfo.string())};
|
"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);
|
std::filesystem::create_directories(localtime.parent_path(), ec);
|
||||||
// Remove existing symlink/file if present
|
// Remove existing symlink/file if present
|
||||||
std::filesystem::remove(localtime, ec);
|
std::filesystem::remove(localtime, ec);
|
||||||
|
|||||||
@@ -59,11 +59,6 @@ std::vector<Diagnostic> 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()) {
|
if (!pkg.assertions.empty()) {
|
||||||
diags.push_back({DiagSeverity::Warning,
|
diags.push_back({DiagSeverity::Warning,
|
||||||
std::to_string(pkg.assertions.size())
|
std::to_string(pkg.assertions.size())
|
||||||
|
|||||||
Reference in New Issue
Block a user