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
440 lines
16 KiB
C++
440 lines
16 KiB
C++
#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 SysParser {
|
|
public:
|
|
explicit SysParser(std::string_view source) : lexer_(source) { advance(); }
|
|
|
|
SystemConfig parse();
|
|
|
|
private:
|
|
void advance();
|
|
Token consume(TokenType type);
|
|
void skip_newlines();
|
|
bool at(TokenType type) const { return current_.type == type; }
|
|
|
|
bool parse_bool();
|
|
void parse_system_block(SystemConfig& cfg);
|
|
void parse_packages_block(SystemConfig& cfg);
|
|
void parse_services_block(SystemConfig& cfg);
|
|
void parse_boot_block(SystemConfig& cfg);
|
|
void parse_users_block(SystemConfig& cfg);
|
|
|
|
std::string consume_ident();
|
|
std::string consume_string();
|
|
|
|
Token current_;
|
|
Lexer lexer_;
|
|
};
|
|
|
|
void SysParser::advance() { current_ = lexer_.next(); }
|
|
|
|
void SysParser::skip_newlines() {
|
|
while (at(TokenType::Newline)) { advance(); }
|
|
}
|
|
|
|
Token SysParser::consume(TokenType type) {
|
|
if (!at(type)) {
|
|
throw ParseError(current_.line, current_.col,
|
|
std::format("expected '{}', got '{}'",
|
|
token_name(type), token_name(current_.type)));
|
|
}
|
|
Token t = std::move(current_);
|
|
advance();
|
|
return t;
|
|
}
|
|
|
|
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) ||
|
|
at(TokenType::KwSource) ||
|
|
at(TokenType::KwLicense) ||
|
|
at(TokenType::KwPatches) ||
|
|
at(TokenType::KwConst) ||
|
|
at(TokenType::KwPackage) ||
|
|
at(TokenType::KwDepends) ||
|
|
at(TokenType::KwProvides) ||
|
|
at(TokenType::KwOutputs) ||
|
|
at(TokenType::KwPrepare) ||
|
|
at(TokenType::KwBuild) ||
|
|
at(TokenType::KwCheck) ||
|
|
at(TokenType::KwInstall) ||
|
|
at(TokenType::KwTrue) ||
|
|
at(TokenType::KwFalse)) {
|
|
auto lexeme = current_.lexeme;
|
|
advance();
|
|
return lexeme;
|
|
}
|
|
throw ParseError(current_.line, current_.col,
|
|
std::format("expected identifier, got '{}'",
|
|
token_name(current_.type)));
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
bool SysParser::parse_bool() {
|
|
if (at(TokenType::KwTrue)) { advance(); return true; }
|
|
if (at(TokenType::KwFalse)) { advance(); return false; }
|
|
throw ParseError(current_.line, current_.col,
|
|
std::format("expected 'true' or 'false', got '{}'",
|
|
token_name(current_.type)));
|
|
}
|
|
|
|
SystemConfig SysParser::parse() {
|
|
skip_newlines();
|
|
SystemConfig cfg;
|
|
|
|
while (!at(TokenType::Eof)) {
|
|
skip_newlines();
|
|
if (at(TokenType::Eof)) { break; }
|
|
|
|
auto kw = consume_ident();
|
|
|
|
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); }
|
|
else if (kw == "users") { parse_users_block(cfg); }
|
|
else {
|
|
throw ParseError(current_.line, current_.col,
|
|
std::format("unknown section '{}'", kw));
|
|
}
|
|
}
|
|
|
|
return cfg;
|
|
}
|
|
|
|
void SysParser::parse_system_block(SystemConfig& cfg) {
|
|
consume(TokenType::Lbrace);
|
|
skip_newlines();
|
|
|
|
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
|
skip_newlines();
|
|
if (at(TokenType::Rbrace)) { break; }
|
|
|
|
auto key = consume_ident();
|
|
if (key == "hostname") {
|
|
consume(TokenType::Equals);
|
|
cfg.system.hostname = consume_string();
|
|
} else if (key == "timezone") {
|
|
consume(TokenType::Equals);
|
|
cfg.system.timezone = consume_string();
|
|
} else if (key == "env") {
|
|
consume(TokenType::Lbrace);
|
|
skip_newlines();
|
|
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
|
if (at(TokenType::Newline)) { advance(); continue; }
|
|
auto k = consume_ident();
|
|
bool soft = false;
|
|
if (at(TokenType::Ident) && current_.lexeme == "?=") {
|
|
soft = true;
|
|
advance();
|
|
} else {
|
|
consume(TokenType::Equals);
|
|
}
|
|
cfg.system.env.push_back({std::move(k), consume_string(), soft});
|
|
skip_newlines();
|
|
}
|
|
consume(TokenType::Rbrace);
|
|
} else if (key == "config") {
|
|
consume(TokenType::Lbrace);
|
|
skip_newlines();
|
|
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
|
if (at(TokenType::Newline)) { advance(); continue; }
|
|
auto k = consume_ident();
|
|
consume(TokenType::Equals);
|
|
cfg.system.config[k] = consume_string();
|
|
skip_newlines();
|
|
}
|
|
consume(TokenType::Rbrace);
|
|
} else if (key == "features") {
|
|
consume(TokenType::Lbrace);
|
|
skip_newlines();
|
|
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
|
if (at(TokenType::Newline)) { advance(); continue; }
|
|
auto k = consume_ident();
|
|
consume(TokenType::Equals);
|
|
cfg.system.features[k] = FeatureDef{parse_bool(), false, ""};
|
|
skip_newlines();
|
|
}
|
|
consume(TokenType::Rbrace);
|
|
} else if (key == "rollback") {
|
|
consume(TokenType::Lbrace);
|
|
skip_newlines();
|
|
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
|
if (at(TokenType::Newline)) { advance(); continue; }
|
|
auto k = consume_ident();
|
|
consume(TokenType::Equals);
|
|
if (k == "keep") {
|
|
cfg.system.rollback.keep = parse_int(current_.line, current_.col,
|
|
current_.lexeme);
|
|
advance();
|
|
}
|
|
skip_newlines();
|
|
}
|
|
consume(TokenType::Rbrace);
|
|
} else {
|
|
throw ParseError(current_.line, current_.col,
|
|
std::format("unknown system key '{}'", key));
|
|
}
|
|
}
|
|
consume(TokenType::Rbrace);
|
|
}
|
|
|
|
void SysParser::parse_packages_block(SystemConfig& cfg) {
|
|
consume(TokenType::Lbrace);
|
|
skip_newlines();
|
|
|
|
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
|
skip_newlines();
|
|
if (at(TokenType::Rbrace)) { break; }
|
|
|
|
PackageRef pkg;
|
|
pkg.name = consume_ident();
|
|
|
|
if (at(TokenType::Lbrace)) {
|
|
consume(TokenType::Lbrace);
|
|
skip_newlines();
|
|
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
|
skip_newlines();
|
|
if (at(TokenType::Rbrace)) { break; }
|
|
auto key = consume_ident();
|
|
if (key == "version") {
|
|
consume(TokenType::Equals);
|
|
pkg.version = consume_string();
|
|
} else if (key == "features") {
|
|
consume(TokenType::Lbrace);
|
|
skip_newlines();
|
|
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
|
if (at(TokenType::Newline)) { advance(); continue; }
|
|
auto k = consume_ident();
|
|
consume(TokenType::Equals);
|
|
pkg.features[k] = FeatureDef{parse_bool(), false, ""};
|
|
skip_newlines();
|
|
}
|
|
consume(TokenType::Rbrace);
|
|
} else if (key == "config") {
|
|
consume(TokenType::Lbrace);
|
|
skip_newlines();
|
|
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
|
if (at(TokenType::Newline)) { advance(); continue; }
|
|
auto k = consume_ident();
|
|
consume(TokenType::Equals);
|
|
pkg.config[k] = current_.lexeme;
|
|
advance();
|
|
skip_newlines();
|
|
}
|
|
consume(TokenType::Rbrace);
|
|
}
|
|
}
|
|
consume(TokenType::Rbrace);
|
|
}
|
|
cfg.packages.push_back(std::move(pkg));
|
|
}
|
|
consume(TokenType::Rbrace);
|
|
}
|
|
|
|
void SysParser::parse_services_block(SystemConfig& cfg) {
|
|
consume(TokenType::Lbrace);
|
|
skip_newlines();
|
|
|
|
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
|
skip_newlines();
|
|
if (at(TokenType::Rbrace)) { break; }
|
|
|
|
ServiceRef svc;
|
|
svc.name = consume_ident();
|
|
|
|
if (at(TokenType::Lbrace)) {
|
|
consume(TokenType::Lbrace);
|
|
skip_newlines();
|
|
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
|
if (at(TokenType::Newline)) { advance(); continue; }
|
|
auto key = consume_ident();
|
|
if (key == "enable") {
|
|
consume(TokenType::Equals);
|
|
svc.enable = parse_bool();
|
|
} else {
|
|
consume(TokenType::Equals);
|
|
svc.config[key] = current_.lexeme;
|
|
advance();
|
|
}
|
|
skip_newlines();
|
|
}
|
|
consume(TokenType::Rbrace);
|
|
}
|
|
cfg.services.push_back(std::move(svc));
|
|
}
|
|
consume(TokenType::Rbrace);
|
|
}
|
|
|
|
void SysParser::parse_boot_block(SystemConfig& cfg) {
|
|
consume(TokenType::Lbrace);
|
|
skip_newlines();
|
|
|
|
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
|
skip_newlines();
|
|
if (at(TokenType::Rbrace)) { break; }
|
|
auto key = consume_ident();
|
|
consume(TokenType::Equals);
|
|
auto val = consume_string();
|
|
if (key == "kernel") { cfg.boot.kernel = std::move(val); }
|
|
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();
|
|
}
|
|
consume(TokenType::Rbrace);
|
|
}
|
|
|
|
void SysParser::parse_users_block(SystemConfig& cfg) {
|
|
consume(TokenType::Lbrace);
|
|
skip_newlines();
|
|
|
|
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
|
skip_newlines();
|
|
if (at(TokenType::Rbrace)) { break; }
|
|
|
|
UserRef u;
|
|
u.name = consume_ident();
|
|
|
|
if (at(TokenType::Lbrace)) {
|
|
consume(TokenType::Lbrace);
|
|
skip_newlines();
|
|
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
|
if (at(TokenType::Newline)) { advance(); continue; }
|
|
auto key = consume_ident();
|
|
if (key == "shell") {
|
|
consume(TokenType::Equals);
|
|
u.shell = consume_string();
|
|
} else if (key == "groups") {
|
|
consume(TokenType::Equals);
|
|
consume(TokenType::Lbracket);
|
|
skip_newlines();
|
|
while (!at(TokenType::Rbracket) && !at(TokenType::Eof)) {
|
|
u.groups.push_back(consume_string());
|
|
skip_newlines();
|
|
if (at(TokenType::Comma)) { consume(TokenType::Comma); }
|
|
skip_newlines();
|
|
}
|
|
consume(TokenType::Rbracket);
|
|
} else {
|
|
consume(TokenType::Equals);
|
|
u.extra[key] = consume_string();
|
|
}
|
|
skip_newlines();
|
|
}
|
|
consume(TokenType::Rbrace);
|
|
}
|
|
cfg.users.push_back(std::move(u));
|
|
}
|
|
consume(TokenType::Rbrace);
|
|
}
|
|
|
|
SystemConfig parse_system_config(std::string_view source) {
|
|
SysParser p(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
|