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:
@@ -27,7 +27,9 @@ add_executable(kappa
|
||||
src/paths.cpp
|
||||
src/dsl/lexer.cpp
|
||||
src/dsl/parser.cpp
|
||||
src/dsl/system.cpp
|
||||
src/eval/vars.cpp
|
||||
src/cli/diagnostic.cpp
|
||||
)
|
||||
target_include_directories(kappa PRIVATE include)
|
||||
target_link_libraries(kappa PRIVATE tomlplusplus::tomlplusplus)
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Kappa system configuration.
|
||||
* Lives at /kappa/system/config.kap
|
||||
*/
|
||||
system {
|
||||
hostname = "kappa.local"
|
||||
timezone = "America/New_York"
|
||||
|
||||
features {
|
||||
ssl = true
|
||||
wayland = false
|
||||
pulseaudio = false
|
||||
}
|
||||
|
||||
env {
|
||||
CFLAGS = "-O2 -march=native"
|
||||
LDFLAGS = "-Wl,--as-needed"
|
||||
MAKEFLAGS = "-j8"
|
||||
}
|
||||
|
||||
config {
|
||||
prefix = "/usr"
|
||||
log_dir = "/var/log"
|
||||
}
|
||||
|
||||
rollback {
|
||||
keep = 5
|
||||
}
|
||||
}
|
||||
|
||||
packages {
|
||||
foo {
|
||||
version = ">=1.2"
|
||||
features {
|
||||
gui = true
|
||||
}
|
||||
config {
|
||||
port = "9090"
|
||||
ssl = true
|
||||
}
|
||||
}
|
||||
bar {}
|
||||
baz {
|
||||
features {
|
||||
ssl = false
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// Per-package overrides can also live in
|
||||
// /kappa/system/builds/<name>.kap
|
||||
*/
|
||||
}
|
||||
|
||||
services {
|
||||
nginx {
|
||||
enable = true
|
||||
port = 80
|
||||
ssl = true
|
||||
}
|
||||
sshd {
|
||||
enable = true
|
||||
port = 22
|
||||
}
|
||||
cron {
|
||||
enable = false
|
||||
}
|
||||
}
|
||||
|
||||
boot {
|
||||
kernel = "linux"
|
||||
init = "s6"
|
||||
efi = "/dev/sda2"
|
||||
swap = "/dev/sda3"
|
||||
bootloader = "limine"
|
||||
root = "/dev/sda1"
|
||||
}
|
||||
|
||||
users {
|
||||
specter {
|
||||
shell = "/bin/zsh"
|
||||
groups = ["wheel", "audio", "docker"]
|
||||
}
|
||||
root {
|
||||
shell = "/bin/zsh"
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,20 @@ package "foo" {
|
||||
CFLAGS ?= "-g"
|
||||
}
|
||||
|
||||
service {
|
||||
runit {
|
||||
exec = "/usr/bin/foo --daemon"
|
||||
type = "forking"
|
||||
user = "foo"
|
||||
}
|
||||
s6 {
|
||||
exec = "/usr/bin/foo"
|
||||
type = "longrun"
|
||||
ports = [80, 443]
|
||||
user = "foo"
|
||||
}
|
||||
}
|
||||
|
||||
prepare {
|
||||
patch "fix-build.patch"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace kappa::cli {
|
||||
|
||||
struct SourceLocation {
|
||||
std::string_view file;
|
||||
int line = 0;
|
||||
int col = 0;
|
||||
};
|
||||
|
||||
void print_error(std::ostream& os,
|
||||
std::string_view source,
|
||||
SourceLocation loc,
|
||||
std::string_view message);
|
||||
|
||||
void print_error(std::ostream& os,
|
||||
std::string_view source,
|
||||
SourceLocation loc,
|
||||
std::string_view message,
|
||||
std::string_view help);
|
||||
|
||||
void print_note(std::ostream& os,
|
||||
std::string_view source,
|
||||
SourceLocation loc,
|
||||
std::string_view message);
|
||||
|
||||
} // namespace kappa::cli
|
||||
@@ -44,6 +44,14 @@ struct EnvEntry {
|
||||
bool soft = false;
|
||||
};
|
||||
|
||||
struct ServiceInit {
|
||||
std::string exec;
|
||||
std::string type;
|
||||
std::string user;
|
||||
std::vector<int> ports;
|
||||
std::unordered_map<std::string, std::string> env;
|
||||
};
|
||||
|
||||
struct PackageDef {
|
||||
std::string name;
|
||||
std::string version;
|
||||
@@ -56,6 +64,7 @@ struct PackageDef {
|
||||
std::vector<ConfigFile> config_files;
|
||||
std::vector<Patch> patches;
|
||||
std::vector<EnvEntry> env_entries;
|
||||
std::unordered_map<std::string, ServiceInit> service;
|
||||
std::unordered_set<std::string> const_keys;
|
||||
Phase prepare;
|
||||
Phase build;
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
#pragma once
|
||||
|
||||
#include "kappa/dsl/ast.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace kappa::dsl {
|
||||
|
||||
struct RollbackConfig {
|
||||
int keep = 0;
|
||||
};
|
||||
|
||||
struct SystemBlock {
|
||||
std::string hostname;
|
||||
std::string timezone;
|
||||
std::vector<EnvEntry> env;
|
||||
std::unordered_map<std::string, std::string> config;
|
||||
std::unordered_map<std::string, FeatureDef> features;
|
||||
RollbackConfig rollback;
|
||||
};
|
||||
|
||||
struct PackageRef {
|
||||
std::string name;
|
||||
std::string version;
|
||||
std::unordered_map<std::string, FeatureDef> features;
|
||||
std::unordered_map<std::string, std::string> config;
|
||||
};
|
||||
|
||||
struct BootBlock {
|
||||
std::string kernel;
|
||||
std::string init;
|
||||
std::string efi;
|
||||
std::string swap;
|
||||
std::string bootloader;
|
||||
std::unordered_map<std::string, std::string> params;
|
||||
};
|
||||
|
||||
struct UserRef {
|
||||
std::string name;
|
||||
std::string shell;
|
||||
std::vector<std::string> groups;
|
||||
std::unordered_map<std::string, std::string> extra;
|
||||
};
|
||||
|
||||
struct ServiceRef {
|
||||
std::string name;
|
||||
bool enable = false;
|
||||
std::unordered_map<std::string, std::string> config;
|
||||
};
|
||||
|
||||
struct SystemConfig {
|
||||
SystemBlock system;
|
||||
std::vector<PackageRef> packages;
|
||||
std::vector<ServiceRef> services;
|
||||
BootBlock boot;
|
||||
std::vector<UserRef> users;
|
||||
};
|
||||
|
||||
SystemConfig parse_system_config(std::string_view source);
|
||||
|
||||
} // namespace kappa::dsl
|
||||
@@ -34,6 +34,7 @@ enum class TokenType {
|
||||
KwLicense,
|
||||
KwPatches,
|
||||
KwEnv,
|
||||
KwService,
|
||||
KwPrepare,
|
||||
KwBuild,
|
||||
KwCheck,
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
#include "kappa/cli/diagnostic.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <format>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace kappa::cli {
|
||||
|
||||
using namespace std::string_view_literals;
|
||||
|
||||
static constexpr auto bold = "\033[1m"sv;
|
||||
static constexpr auto red = "\033[31m"sv;
|
||||
static constexpr auto cyan = "\033[36m"sv;
|
||||
static constexpr auto yellow = "\033[33m"sv;
|
||||
static constexpr auto reset = "\033[0m"sv;
|
||||
|
||||
static std::string_view line_at(std::string_view source, int target_line) {
|
||||
int current = 1;
|
||||
std::size_t start = 0;
|
||||
for (std::size_t i = 0; i < source.size(); ++i) {
|
||||
if (current == target_line) { start = i; break; }
|
||||
if (source[i] == '\n') { ++current; }
|
||||
}
|
||||
if (current != target_line) { return ""sv; }
|
||||
|
||||
auto end = source.find('\n', start);
|
||||
if (end == std::string_view::npos) { end = source.size(); }
|
||||
return source.substr(start, end - start);
|
||||
}
|
||||
|
||||
static void print_header(std::ostream& os,
|
||||
std::string_view severity,
|
||||
std::string_view color,
|
||||
std::string_view file,
|
||||
int line, int col,
|
||||
std::string_view message) {
|
||||
os << color << bold << severity << ": " << reset
|
||||
<< color << message << reset << '\n'
|
||||
<< " " << cyan << "\u250C\u2500 " << file << ':' << line << ':' << col
|
||||
<< reset << '\n';
|
||||
}
|
||||
|
||||
void print_error(std::ostream& os,
|
||||
std::string_view source,
|
||||
SourceLocation loc,
|
||||
std::string_view message) {
|
||||
print_header(os, "error", red, loc.file, loc.line, loc.col, message);
|
||||
|
||||
auto line_src = line_at(source, loc.line);
|
||||
if (!line_src.empty()) {
|
||||
auto line_num = std::format("{}", loc.line);
|
||||
os << " " << cyan << "\u2502" << reset << '\n'
|
||||
<< ' ' << line_num << " " << cyan << "\u2502 " << reset
|
||||
<< line_src << '\n';
|
||||
|
||||
os << " " << std::string(line_num.size(), ' ')
|
||||
<< ' ' << cyan << "\u2502 " << reset;
|
||||
for (int i = 0; i < loc.col - 1; ++i) { os << ' '; }
|
||||
os << red << bold << '^' << reset << '\n';
|
||||
}
|
||||
|
||||
os << " " << cyan << "\u2502" << reset << '\n';
|
||||
}
|
||||
|
||||
void print_error(std::ostream& os,
|
||||
std::string_view source,
|
||||
SourceLocation loc,
|
||||
std::string_view message,
|
||||
std::string_view help) {
|
||||
print_error(os, source, loc, message);
|
||||
os << " " << cyan << "\u2570\u2500\u2500 " << reset
|
||||
<< yellow << "help: " << reset << help << '\n';
|
||||
}
|
||||
|
||||
void print_note(std::ostream& os,
|
||||
std::string_view source,
|
||||
SourceLocation loc,
|
||||
std::string_view message) {
|
||||
print_header(os, "note", cyan, loc.file, loc.line, loc.col, message);
|
||||
auto line_src = line_at(source, loc.line);
|
||||
if (!line_src.empty()) {
|
||||
auto line_num = std::format("{}", loc.line);
|
||||
os << " " << cyan << "\u2502" << reset << '\n'
|
||||
<< ' ' << line_num << " " << cyan << "\u2502 " << reset
|
||||
<< line_src << '\n'
|
||||
<< " " << std::string(line_num.size(), ' ')
|
||||
<< ' ' << cyan << "\u2502" << reset << '\n';
|
||||
}
|
||||
os << " " << cyan << "\u2570\u2500\u2500 " << reset
|
||||
<< cyan << "note: " << reset << message << '\n';
|
||||
}
|
||||
|
||||
} // namespace kappa::cli
|
||||
@@ -19,6 +19,7 @@ static const std::unordered_map<std::string_view, TokenType> keywords = {
|
||||
{"license", TokenType::KwLicense},
|
||||
{"patches", TokenType::KwPatches},
|
||||
{"env", TokenType::KwEnv},
|
||||
{"service", TokenType::KwService},
|
||||
{"prepare", TokenType::KwPrepare},
|
||||
{"build", TokenType::KwBuild},
|
||||
{"check", TokenType::KwCheck},
|
||||
@@ -51,6 +52,7 @@ std::string_view token_name(TokenType type) {
|
||||
case TokenType::KwLicense: return "license";
|
||||
case TokenType::KwPatches: return "patches";
|
||||
case TokenType::KwEnv: return "env";
|
||||
case TokenType::KwService: return "service";
|
||||
case TokenType::KwPrepare: return "prepare";
|
||||
case TokenType::KwBuild: return "build";
|
||||
case TokenType::KwCheck: return "check";
|
||||
|
||||
@@ -209,6 +209,52 @@ void Parser::parse_body(PackageDef& pkg) {
|
||||
consume(TokenType::Rbrace);
|
||||
break;
|
||||
|
||||
case TokenType::KwService:
|
||||
consume(TokenType::KwService);
|
||||
consume(TokenType::Lbrace);
|
||||
skip_newlines();
|
||||
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||
if (at(TokenType::Newline)) { advance(); continue; }
|
||||
auto init_name = current_.lexeme; // e.g. "runit", "s6"
|
||||
advance();
|
||||
consume(TokenType::Lbrace);
|
||||
skip_newlines();
|
||||
ServiceInit si;
|
||||
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||
if (at(TokenType::Newline)) { advance(); continue; }
|
||||
auto key = current_.lexeme;
|
||||
advance();
|
||||
consume(TokenType::Equals);
|
||||
if (key == "exec") {
|
||||
si.exec = consume(TokenType::String).lexeme;
|
||||
} else if (key == "type") {
|
||||
si.type = consume(TokenType::String).lexeme;
|
||||
} else if (key == "user") {
|
||||
si.user = consume(TokenType::String).lexeme;
|
||||
} else if (key == "ports") {
|
||||
consume(TokenType::Lbracket);
|
||||
skip_newlines();
|
||||
while (!at(TokenType::Rbracket) && !at(TokenType::Eof)) {
|
||||
si.ports.push_back(
|
||||
std::stoi(std::string(current_.lexeme)));
|
||||
advance();
|
||||
skip_newlines();
|
||||
if (at(TokenType::Comma)) { consume(TokenType::Comma); }
|
||||
skip_newlines();
|
||||
}
|
||||
consume(TokenType::Rbracket);
|
||||
} else {
|
||||
si.env[key] = consume(TokenType::String).lexeme;
|
||||
}
|
||||
skip_newlines();
|
||||
}
|
||||
consume(TokenType::Rbrace);
|
||||
pkg.service[std::string(init_name)] = std::move(si);
|
||||
skip_newlines();
|
||||
}
|
||||
consume(TokenType::Rbrace);
|
||||
break;
|
||||
|
||||
case TokenType::KwPrepare:
|
||||
consume(TokenType::KwPrepare);
|
||||
pkg.prepare = parse_phase();
|
||||
|
||||
@@ -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
|
||||
+138
-15
@@ -1,28 +1,151 @@
|
||||
#include "kappa/package.hpp"
|
||||
#include "kappa/cli/diagnostic.hpp"
|
||||
#include "kappa/dsl/parser.hpp"
|
||||
#include "kappa/dsl/system.hpp"
|
||||
|
||||
#include <toml++/toml.hpp>
|
||||
#include <cstdlib>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string_view>
|
||||
|
||||
using namespace std::string_view_literals;
|
||||
using namespace kappa;
|
||||
|
||||
static constexpr auto version = "kappa 0.1.0"sv;
|
||||
|
||||
static void print_usage() {
|
||||
std::cout << R"(kappa — a declarative source-based package manager
|
||||
|
||||
Usage:
|
||||
kappa <subcommand> [options] <file>
|
||||
|
||||
Subcommands:
|
||||
parse-package <file> Parse and validate a package definition (.kap)
|
||||
parse-config <file> Parse and validate a system configuration
|
||||
validate <file> Validate any kappa file (package or config)
|
||||
|
||||
Options:
|
||||
-h, --help Show this help message
|
||||
-V, --version Show version information
|
||||
)";
|
||||
}
|
||||
|
||||
static bool is_flag(std::string_view arg) {
|
||||
return arg == "-h" || arg == "--help" || arg == "-V" || arg == "--version";
|
||||
}
|
||||
|
||||
static std::string read_file(const char* path) {
|
||||
std::ifstream in(path);
|
||||
if (!in) {
|
||||
std::cerr << "error: cannot open '" << path << "'\n";
|
||||
std::exit(1);
|
||||
}
|
||||
std::ostringstream buf;
|
||||
buf << in.rdbuf();
|
||||
return buf.str();
|
||||
}
|
||||
|
||||
static void handle_parse_error(const char* path,
|
||||
std::string_view source,
|
||||
const std::runtime_error& e) {
|
||||
// ParseError format: "line:col: message"
|
||||
auto msg = std::string_view(e.what());
|
||||
auto first_colon = msg.find(':');
|
||||
auto second_colon = msg.find(':', first_colon + 1);
|
||||
|
||||
if (first_colon != std::string_view::npos &&
|
||||
second_colon != std::string_view::npos) {
|
||||
int line = std::stoi(std::string(msg.substr(0, first_colon)));
|
||||
int col = std::stoi(std::string(
|
||||
msg.substr(first_colon + 1, second_colon - first_colon - 1)));
|
||||
auto message = msg.substr(second_colon + 2); // skip ": "
|
||||
|
||||
cli::print_error(std::cerr, source,
|
||||
{path, line, col}, message,
|
||||
"check the syntax at this location");
|
||||
} else {
|
||||
std::cerr << "error: " << e.what() << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
std::filesystem::path toml_path = (argc > 1) ? argv[1] : "package.toml";
|
||||
|
||||
if (!std::filesystem::exists(toml_path)) {
|
||||
std::cerr << "Error: '" << toml_path << "' not found\n";
|
||||
if (argc < 2) {
|
||||
std::cerr << "kappa: missing subcommand\n\n";
|
||||
print_usage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
auto pkg = kappa::parse_package(toml_path.native());
|
||||
kappa::print_package(std::cout, pkg);
|
||||
auto subcommand = std::string_view(argv[1]);
|
||||
|
||||
if (subcommand == "-h" || subcommand == "--help") {
|
||||
print_usage();
|
||||
return 0;
|
||||
} catch (const toml::parse_error& e) {
|
||||
std::cerr << "Parse error:\n" << e << '\n';
|
||||
}
|
||||
if (subcommand == "-V" || subcommand == "--version") {
|
||||
std::cout << version << '\n';
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Find the file argument (skip flags)
|
||||
const char* file_arg = nullptr;
|
||||
for (int i = 2; i < argc; ++i) {
|
||||
if (!is_flag(argv[i])) {
|
||||
file_arg = argv[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (file_arg == nullptr) {
|
||||
std::cerr << "error: no input file specified\n";
|
||||
return 1;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Error: " << e.what() << '\n';
|
||||
}
|
||||
|
||||
auto source = read_file(file_arg);
|
||||
|
||||
if (subcommand == "parse-package") {
|
||||
try {
|
||||
auto pkg = dsl::parse(source);
|
||||
std::cout << "package \"" << pkg.name << "\" " << pkg.version
|
||||
<< " — valid\n";
|
||||
return 0;
|
||||
} catch (const std::runtime_error& e) {
|
||||
handle_parse_error(file_arg, source, e);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (subcommand == "parse-config") {
|
||||
try {
|
||||
auto cfg = dsl::parse_system_config(source);
|
||||
std::cout << "system config — valid ("
|
||||
<< cfg.packages.size() << " packages, "
|
||||
<< cfg.services.size() << " services, "
|
||||
<< cfg.users.size() << " users)\n";
|
||||
return 0;
|
||||
} catch (const std::runtime_error& e) {
|
||||
handle_parse_error(file_arg, source, e);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (subcommand == "validate") {
|
||||
try {
|
||||
dsl::parse(source);
|
||||
std::cout << file_arg << ": valid package definition\n";
|
||||
return 0;
|
||||
} catch (const std::runtime_error&) {
|
||||
try {
|
||||
dsl::parse_system_config(source);
|
||||
std::cout << file_arg << ": valid system configuration\n";
|
||||
return 0;
|
||||
} catch (const std::runtime_error& e) {
|
||||
handle_parse_error(file_arg, source, e);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::cerr << "error: unknown subcommand '" << subcommand << "'\n\n";
|
||||
print_usage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user