fix: review fixes + feat: imports, assertions, merge engine, per-package overrides

Review fixes (8 blocking issues):
- Extract ParseError to shared error.hpp (ODR fix)
- Remove dead package.cpp/package.hpp + tomlplusplus dep
- Safe parse_int() helper replacing crash-prone std::stoi
- consume_string() now accepts bare numbers and idents
- line_at() fixed for post-EOF line numbers
- Subcommand validation before file read in CLI
- KwService/KwAssert/KwImport added to consume_ident()
- root partition promoted to first-class BootBlock field

New features:
- Imports: imports = [...] with recursive merge resolution
- Assertions: assert { "msg" : field op value } in both parsers
- Merge engine: resolve_package() with features/config merge + force support
- Per-package overrides: /kappa/system/builds/<name>.kap
- .gitignore: added vcpkg_installed/ and kappa binary
This commit is contained in:
2026-07-29 23:52:03 -04:00
parent d5f7d397ed
commit 0c1e82d66b
18 changed files with 325 additions and 212 deletions
+89 -8
View File
@@ -1,17 +1,15 @@
#include "kappa/dsl/system.hpp"
#include "kappa/dsl/error.hpp"
#include "kappa/dsl/lexer.hpp"
#include <filesystem>
#include <format>
#include <fstream>
#include <sstream>
#include <stdexcept>
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)) {}
};
class SysParser {
public:
explicit SysParser(std::string_view source) : lexer_(source) { advance(); }
@@ -58,6 +56,9 @@ Token SysParser::consume(TokenType type) {
std::string SysParser::consume_ident() {
if (at(TokenType::Ident) ||
at(TokenType::KwEnv) ||
at(TokenType::KwService) ||
at(TokenType::KwAssert) ||
at(TokenType::KwImport) ||
at(TokenType::KwConfig) ||
at(TokenType::KwFeatures) ||
at(TokenType::KwVersion) ||
@@ -87,6 +88,7 @@ std::string SysParser::consume_ident() {
std::string SysParser::consume_string() {
if (at(TokenType::KwTrue)) { advance(); return "true"; }
if (at(TokenType::KwFalse)) { advance(); return "false"; }
if (at(TokenType::Ident)) { auto v = current_.lexeme; advance(); return v; }
return consume(TokenType::String).lexeme;
}
@@ -108,7 +110,50 @@ SystemConfig SysParser::parse() {
auto kw = consume_ident();
if (kw == "system") { parse_system_block(cfg); }
if (kw == "imports") {
consume(TokenType::Equals);
consume(TokenType::Lbracket);
skip_newlines();
while (!at(TokenType::Rbracket) && !at(TokenType::Eof)) {
cfg.imports.push_back(consume(TokenType::String).lexeme);
skip_newlines();
if (at(TokenType::Comma)) { consume(TokenType::Comma); }
skip_newlines();
}
consume(TokenType::Rbracket);
} else if (kw == "assert") {
consume(TokenType::Lbrace);
skip_newlines();
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
if (at(TokenType::Newline)) { advance(); continue; }
Assertion a;
a.message = consume(TokenType::String).lexeme;
consume(TokenType::Ident); // ":"
a.field = current_.lexeme; advance();
if (at(TokenType::Equals)) {
advance(); // first =
if (at(TokenType::Equals)) {
a.op = "=="; advance(); // second =
} else {
a.op = "=";
}
} else {
a.op = current_.lexeme; advance();
}
if (at(TokenType::String)) {
a.value = consume(TokenType::String).lexeme;
} else if (at(TokenType::KwTrue)) {
a.value = "true"; advance();
} else if (at(TokenType::KwFalse)) {
a.value = "false"; advance();
} else {
a.value = current_.lexeme; advance();
}
cfg.assertions.push_back(std::move(a));
skip_newlines();
}
consume(TokenType::Rbrace);
} else if (kw == "system") { parse_system_block(cfg); }
else if (kw == "packages") { parse_packages_block(cfg); }
else if (kw == "services") { parse_services_block(cfg); }
else if (kw == "boot") { parse_boot_block(cfg); }
@@ -184,7 +229,8 @@ void SysParser::parse_system_block(SystemConfig& cfg) {
auto k = consume_ident();
consume(TokenType::Equals);
if (k == "keep") {
cfg.system.rollback.keep = std::stoi(std::string(current_.lexeme));
cfg.system.rollback.keep = parse_int(current_.line, current_.col,
current_.lexeme);
advance();
}
skip_newlines();
@@ -299,6 +345,7 @@ void SysParser::parse_boot_block(SystemConfig& cfg) {
else if (key == "init") { cfg.boot.init = std::move(val); }
else if (key == "efi") { cfg.boot.efi = std::move(val); }
else if (key == "swap") { cfg.boot.swap = std::move(val); }
else if (key == "root") { cfg.boot.root = std::move(val); }
else if (key == "bootloader"){ cfg.boot.bootloader = std::move(val); }
else { cfg.boot.params[key] = std::move(val); }
skip_newlines();
@@ -355,4 +402,38 @@ SystemConfig parse_system_config(std::string_view source) {
return p.parse();
}
static void merge_config(SystemConfig& base, SystemConfig&& imported) {
if (!imported.system.hostname.empty()) { base.system.hostname = std::move(imported.system.hostname); }
if (!imported.system.timezone.empty()) { base.system.timezone = std::move(imported.system.timezone); }
for (auto& e : imported.system.env) { base.system.env.push_back(std::move(e)); }
for (auto& [k, v] : imported.system.config) { base.system.config[k] = std::move(v); }
for (auto& [k, v] : imported.system.features) { base.system.features[k] = std::move(v); }
if (imported.system.rollback.keep > 0) { base.system.rollback.keep = imported.system.rollback.keep; }
for (auto& p : imported.packages) { base.packages.push_back(std::move(p)); }
for (auto& s : imported.services) { base.services.push_back(std::move(s)); }
for (auto& u : imported.users) { base.users.push_back(std::move(u)); }
if (!imported.boot.kernel.empty()) { base.boot.kernel = std::move(imported.boot.kernel); }
if (!imported.boot.init.empty()) { base.boot.init = std::move(imported.boot.init); }
if (!imported.boot.efi.empty()) { base.boot.efi = std::move(imported.boot.efi); }
if (!imported.boot.swap.empty()) { base.boot.swap = std::move(imported.boot.swap); }
if (!imported.boot.root.empty()) { base.boot.root = std::move(imported.boot.root); }
if (!imported.boot.bootloader.empty()) { base.boot.bootloader = std::move(imported.boot.bootloader); }
for (auto& [k, v] : imported.boot.params) { base.boot.params[k] = std::move(v); }
}
SystemConfig resolve_imports(const SystemConfig& cfg, const std::string& base_dir) {
auto resolved = cfg;
for (auto& import_path : cfg.imports) {
auto full_path = std::filesystem::path(base_dir) / import_path;
std::ifstream in(full_path);
if (!in) { continue; }
std::ostringstream buf;
buf << in.rdbuf();
auto imported = parse_system_config(buf.str());
merge_config(resolved, resolve_imports(imported, base_dir));
}
resolved.imports.clear();
return resolved;
}
} // namespace kappa::dsl