feat: system config DSL, services, CLI with rustc diagnostics
- System config parser (system/packages/services/boot/users blocks) - Boot block: kernel, init, efi, swap, bootloader, root partition - System-wide features, rollback config, swap partition - Service block: per-init variants (runit, s6, etc.), system init selection via boot.init - Feature-gated dependencies, config values accept numbers/bools/strings - CLI: parse-package, parse-config, validate subcommands - Rustc-style error diagnostics with source context, carets, help text - Diagnostic module with ANSI color output
This commit is contained in:
@@ -0,0 +1,358 @@
|
||||
#include "kappa/dsl/system.hpp"
|
||||
#include "kappa/dsl/lexer.hpp"
|
||||
|
||||
#include <format>
|
||||
#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(); }
|
||||
|
||||
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::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"; }
|
||||
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 == "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 = std::stoi(std::string(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 == "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();
|
||||
}
|
||||
|
||||
} // namespace kappa::dsl
|
||||
Reference in New Issue
Block a user