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:
@@ -35,4 +35,6 @@
|
|||||||
# Build
|
# Build
|
||||||
build/
|
build/
|
||||||
compile_commands.json
|
compile_commands.json
|
||||||
|
vcpkg_installed/
|
||||||
|
kappa
|
||||||
|
|
||||||
|
|||||||
+1
-4
@@ -19,17 +19,14 @@ if(DEFINED ENV{VCPKG_ROOT})
|
|||||||
CACHE STRING "vcpkg toolchain")
|
CACHE STRING "vcpkg toolchain")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
find_package(tomlplusplus CONFIG REQUIRED)
|
|
||||||
|
|
||||||
add_executable(kappa
|
add_executable(kappa
|
||||||
src/main.cpp
|
src/main.cpp
|
||||||
src/package.cpp
|
|
||||||
src/paths.cpp
|
src/paths.cpp
|
||||||
src/dsl/lexer.cpp
|
src/dsl/lexer.cpp
|
||||||
src/dsl/parser.cpp
|
src/dsl/parser.cpp
|
||||||
src/dsl/system.cpp
|
src/dsl/system.cpp
|
||||||
src/eval/vars.cpp
|
src/eval/vars.cpp
|
||||||
src/cli/diagnostic.cpp
|
src/cli/diagnostic.cpp
|
||||||
|
src/config/merge.cpp
|
||||||
)
|
)
|
||||||
target_include_directories(kappa PRIVATE include)
|
target_include_directories(kappa PRIVATE include)
|
||||||
target_link_libraries(kappa PRIVATE tomlplusplus::tomlplusplus)
|
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
* Per-package override for foo.
|
||||||
|
* Lives at /kappa/system/builds/foo.kap
|
||||||
|
*
|
||||||
|
* Uses system config syntax — only features/config need to be specified.
|
||||||
|
* The rest inherits from the package definition and system config.
|
||||||
|
*/
|
||||||
|
packages {
|
||||||
|
foo {
|
||||||
|
features {
|
||||||
|
debug = true
|
||||||
|
}
|
||||||
|
config {
|
||||||
|
port = "9090"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,14 @@
|
|||||||
* Kappa system configuration.
|
* Kappa system configuration.
|
||||||
* Lives at /kappa/system/config.kap
|
* Lives at /kappa/system/config.kap
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
imports = []
|
||||||
|
|
||||||
|
assert {
|
||||||
|
"efi partition required for UEFI boot" : boot.efi != ""
|
||||||
|
"root partition must be set" : boot.root != ""
|
||||||
|
}
|
||||||
|
|
||||||
system {
|
system {
|
||||||
hostname = "kappa.local"
|
hostname = "kappa.local"
|
||||||
timezone = "America/New_York"
|
timezone = "America/New_York"
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "kappa/dsl/ast.hpp"
|
||||||
|
#include "kappa/dsl/system.hpp"
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace kappa::config {
|
||||||
|
|
||||||
|
struct ResolvedPackage {
|
||||||
|
dsl::PackageDef original;
|
||||||
|
std::unordered_map<std::string, dsl::FeatureDef> features;
|
||||||
|
std::unordered_map<std::string, std::string> config;
|
||||||
|
};
|
||||||
|
|
||||||
|
ResolvedPackage resolve_package(
|
||||||
|
const dsl::PackageDef& pkg,
|
||||||
|
const dsl::SystemBlock& system,
|
||||||
|
const std::optional<dsl::PackageRef>& overrides);
|
||||||
|
|
||||||
|
std::unordered_map<std::string, std::string> resolve_config(
|
||||||
|
const dsl::PackageDef& pkg,
|
||||||
|
const dsl::SystemBlock& system,
|
||||||
|
const std::optional<dsl::PackageRef>& overrides);
|
||||||
|
|
||||||
|
} // namespace kappa::config
|
||||||
@@ -52,6 +52,13 @@ struct ServiceInit {
|
|||||||
std::unordered_map<std::string, std::string> env;
|
std::unordered_map<std::string, std::string> env;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct Assertion {
|
||||||
|
std::string message;
|
||||||
|
std::string field;
|
||||||
|
std::string op; // "==" or "!="
|
||||||
|
std::string value;
|
||||||
|
};
|
||||||
|
|
||||||
struct PackageDef {
|
struct PackageDef {
|
||||||
std::string name;
|
std::string name;
|
||||||
std::string version;
|
std::string version;
|
||||||
@@ -65,6 +72,7 @@ struct PackageDef {
|
|||||||
std::vector<Patch> patches;
|
std::vector<Patch> patches;
|
||||||
std::vector<EnvEntry> env_entries;
|
std::vector<EnvEntry> env_entries;
|
||||||
std::unordered_map<std::string, ServiceInit> service;
|
std::unordered_map<std::string, ServiceInit> service;
|
||||||
|
std::vector<Assertion> assertions;
|
||||||
std::unordered_set<std::string> const_keys;
|
std::unordered_set<std::string> const_keys;
|
||||||
Phase prepare;
|
Phase prepare;
|
||||||
Phase build;
|
Phase build;
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
#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)) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
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 integer, got '{}'", lexeme));
|
||||||
|
}
|
||||||
|
return val;
|
||||||
|
} catch (const std::invalid_argument&) {
|
||||||
|
throw ParseError(line, col,
|
||||||
|
std::format("expected integer, got '{}'", lexeme));
|
||||||
|
} catch (const std::out_of_range&) {
|
||||||
|
throw ParseError(line, col,
|
||||||
|
std::format("integer out of range: '{}'", lexeme));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace kappa::dsl
|
||||||
@@ -34,6 +34,7 @@ struct BootBlock {
|
|||||||
std::string init;
|
std::string init;
|
||||||
std::string efi;
|
std::string efi;
|
||||||
std::string swap;
|
std::string swap;
|
||||||
|
std::string root;
|
||||||
std::string bootloader;
|
std::string bootloader;
|
||||||
std::unordered_map<std::string, std::string> params;
|
std::unordered_map<std::string, std::string> params;
|
||||||
};
|
};
|
||||||
@@ -52,13 +53,16 @@ struct ServiceRef {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct SystemConfig {
|
struct SystemConfig {
|
||||||
|
std::vector<std::string> imports;
|
||||||
SystemBlock system;
|
SystemBlock system;
|
||||||
std::vector<PackageRef> packages;
|
std::vector<PackageRef> packages;
|
||||||
std::vector<ServiceRef> services;
|
std::vector<ServiceRef> services;
|
||||||
BootBlock boot;
|
BootBlock boot;
|
||||||
std::vector<UserRef> users;
|
std::vector<UserRef> users;
|
||||||
|
std::vector<Assertion> assertions;
|
||||||
};
|
};
|
||||||
|
|
||||||
SystemConfig parse_system_config(std::string_view source);
|
SystemConfig parse_system_config(std::string_view source);
|
||||||
|
SystemConfig resolve_imports(const SystemConfig& cfg, const std::string& base_dir);
|
||||||
|
|
||||||
} // namespace kappa::dsl
|
} // namespace kappa::dsl
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ enum class TokenType {
|
|||||||
KwPatches,
|
KwPatches,
|
||||||
KwEnv,
|
KwEnv,
|
||||||
KwService,
|
KwService,
|
||||||
|
KwAssert,
|
||||||
|
KwImport,
|
||||||
KwPrepare,
|
KwPrepare,
|
||||||
KwBuild,
|
KwBuild,
|
||||||
KwCheck,
|
KwCheck,
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <iosfwd>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace kappa {
|
|
||||||
|
|
||||||
struct PackageConfig {
|
|
||||||
std::string name;
|
|
||||||
std::string version;
|
|
||||||
std::string description;
|
|
||||||
std::string source_url;
|
|
||||||
std::string source_extension;
|
|
||||||
std::string source_hash;
|
|
||||||
std::vector<std::string> dependencies;
|
|
||||||
std::string build_system;
|
|
||||||
};
|
|
||||||
|
|
||||||
PackageConfig parse_package(const std::string& toml_path);
|
|
||||||
void print_package(std::ostream& os, const PackageConfig& pkg);
|
|
||||||
|
|
||||||
} // namespace kappa
|
|
||||||
@@ -17,12 +17,12 @@ static constexpr auto reset = "\033[0m"sv;
|
|||||||
|
|
||||||
static std::string_view line_at(std::string_view source, int target_line) {
|
static std::string_view line_at(std::string_view source, int target_line) {
|
||||||
int current = 1;
|
int current = 1;
|
||||||
std::size_t start = 0;
|
std::size_t start = std::string_view::npos;
|
||||||
for (std::size_t i = 0; i < source.size(); ++i) {
|
for (std::size_t i = 0; i < source.size(); ++i) {
|
||||||
if (current == target_line) { start = i; break; }
|
if (current == target_line) { start = i; break; }
|
||||||
if (source[i] == '\n') { ++current; }
|
if (source[i] == '\n') { ++current; }
|
||||||
}
|
}
|
||||||
if (current != target_line) { return ""sv; }
|
if (start == std::string_view::npos) { return ""sv; }
|
||||||
|
|
||||||
auto end = source.find('\n', start);
|
auto end = source.find('\n', start);
|
||||||
if (end == std::string_view::npos) { end = source.size(); }
|
if (end == std::string_view::npos) { end = source.size(); }
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
#include "kappa/config/merge.hpp"
|
||||||
|
|
||||||
|
namespace kappa::config {
|
||||||
|
|
||||||
|
ResolvedPackage resolve_package(
|
||||||
|
const dsl::PackageDef& pkg,
|
||||||
|
const dsl::SystemBlock& system,
|
||||||
|
const std::optional<dsl::PackageRef>& overrides)
|
||||||
|
{
|
||||||
|
ResolvedPackage rp;
|
||||||
|
rp.original = pkg;
|
||||||
|
|
||||||
|
rp.features = pkg.features;
|
||||||
|
|
||||||
|
for (auto& [key, sys_feat] : system.features) {
|
||||||
|
auto it = rp.features.find(key);
|
||||||
|
if (it == rp.features.end()) {
|
||||||
|
rp.features[key] = sys_feat;
|
||||||
|
} else if (!it->second.force) {
|
||||||
|
it->second.enabled = sys_feat.enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overrides) {
|
||||||
|
for (auto& [key, ovr_feat] : overrides->features) {
|
||||||
|
auto it = rp.features.find(key);
|
||||||
|
if (it == rp.features.end()) {
|
||||||
|
rp.features[key] = ovr_feat;
|
||||||
|
} else if (!it->second.force) {
|
||||||
|
it->second.enabled = ovr_feat.enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rp.config = resolve_config(pkg, system, overrides);
|
||||||
|
|
||||||
|
return rp;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unordered_map<std::string, std::string> resolve_config(
|
||||||
|
const dsl::PackageDef& pkg,
|
||||||
|
const dsl::SystemBlock& system,
|
||||||
|
const std::optional<dsl::PackageRef>& overrides)
|
||||||
|
{
|
||||||
|
std::unordered_map<std::string, std::string> cfg = system.config;
|
||||||
|
|
||||||
|
for (auto& cf : pkg.config_files) {
|
||||||
|
for (auto& [key, val] : cf.entries) {
|
||||||
|
cfg.try_emplace(key, val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overrides) {
|
||||||
|
for (auto& [key, val] : overrides->config) {
|
||||||
|
cfg[key] = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace kappa::config
|
||||||
@@ -20,6 +20,8 @@ static const std::unordered_map<std::string_view, TokenType> keywords = {
|
|||||||
{"patches", TokenType::KwPatches},
|
{"patches", TokenType::KwPatches},
|
||||||
{"env", TokenType::KwEnv},
|
{"env", TokenType::KwEnv},
|
||||||
{"service", TokenType::KwService},
|
{"service", TokenType::KwService},
|
||||||
|
{"assert", TokenType::KwAssert},
|
||||||
|
{"import", TokenType::KwImport},
|
||||||
{"prepare", TokenType::KwPrepare},
|
{"prepare", TokenType::KwPrepare},
|
||||||
{"build", TokenType::KwBuild},
|
{"build", TokenType::KwBuild},
|
||||||
{"check", TokenType::KwCheck},
|
{"check", TokenType::KwCheck},
|
||||||
@@ -53,6 +55,8 @@ std::string_view token_name(TokenType type) {
|
|||||||
case TokenType::KwPatches: return "patches";
|
case TokenType::KwPatches: return "patches";
|
||||||
case TokenType::KwEnv: return "env";
|
case TokenType::KwEnv: return "env";
|
||||||
case TokenType::KwService: return "service";
|
case TokenType::KwService: return "service";
|
||||||
|
case TokenType::KwAssert: return "assert";
|
||||||
|
case TokenType::KwImport: return "import";
|
||||||
case TokenType::KwPrepare: return "prepare";
|
case TokenType::KwPrepare: return "prepare";
|
||||||
case TokenType::KwBuild: return "build";
|
case TokenType::KwBuild: return "build";
|
||||||
case TokenType::KwCheck: return "check";
|
case TokenType::KwCheck: return "check";
|
||||||
|
|||||||
+42
-9
@@ -1,4 +1,5 @@
|
|||||||
#include "kappa/dsl/parser.hpp"
|
#include "kappa/dsl/parser.hpp"
|
||||||
|
#include "kappa/dsl/error.hpp"
|
||||||
#include "kappa/dsl/lexer.hpp"
|
#include "kappa/dsl/lexer.hpp"
|
||||||
|
|
||||||
#include <format>
|
#include <format>
|
||||||
@@ -8,12 +9,6 @@
|
|||||||
|
|
||||||
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)) {}
|
|
||||||
};
|
|
||||||
|
|
||||||
class Parser {
|
class Parser {
|
||||||
public:
|
public:
|
||||||
explicit Parser(std::string_view source) : lexer_(source) { advance(); }
|
explicit Parser(std::string_view source) : lexer_(source) { advance(); }
|
||||||
@@ -215,7 +210,7 @@ void Parser::parse_body(PackageDef& pkg) {
|
|||||||
skip_newlines();
|
skip_newlines();
|
||||||
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||||
if (at(TokenType::Newline)) { advance(); continue; }
|
if (at(TokenType::Newline)) { advance(); continue; }
|
||||||
auto init_name = current_.lexeme; // e.g. "runit", "s6"
|
auto init_name = current_.lexeme;
|
||||||
advance();
|
advance();
|
||||||
consume(TokenType::Lbrace);
|
consume(TokenType::Lbrace);
|
||||||
skip_newlines();
|
skip_newlines();
|
||||||
@@ -236,7 +231,8 @@ void Parser::parse_body(PackageDef& pkg) {
|
|||||||
skip_newlines();
|
skip_newlines();
|
||||||
while (!at(TokenType::Rbracket) && !at(TokenType::Eof)) {
|
while (!at(TokenType::Rbracket) && !at(TokenType::Eof)) {
|
||||||
si.ports.push_back(
|
si.ports.push_back(
|
||||||
std::stoi(std::string(current_.lexeme)));
|
parse_int(current_.line, current_.col,
|
||||||
|
current_.lexeme));
|
||||||
advance();
|
advance();
|
||||||
skip_newlines();
|
skip_newlines();
|
||||||
if (at(TokenType::Comma)) { consume(TokenType::Comma); }
|
if (at(TokenType::Comma)) { consume(TokenType::Comma); }
|
||||||
@@ -255,6 +251,42 @@ void Parser::parse_body(PackageDef& pkg) {
|
|||||||
consume(TokenType::Rbrace);
|
consume(TokenType::Rbrace);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case TokenType::KwAssert:
|
||||||
|
consume(TokenType::KwAssert);
|
||||||
|
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();
|
||||||
|
if (at(TokenType::Equals)) {
|
||||||
|
a.op = "=="; advance();
|
||||||
|
} 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();
|
||||||
|
}
|
||||||
|
pkg.assertions.push_back(std::move(a));
|
||||||
|
skip_newlines();
|
||||||
|
}
|
||||||
|
consume(TokenType::Rbrace);
|
||||||
|
break;
|
||||||
|
|
||||||
case TokenType::KwPrepare:
|
case TokenType::KwPrepare:
|
||||||
consume(TokenType::KwPrepare);
|
consume(TokenType::KwPrepare);
|
||||||
pkg.prepare = parse_phase();
|
pkg.prepare = parse_phase();
|
||||||
@@ -420,7 +452,8 @@ Patch Parser::parse_patch_item() {
|
|||||||
} else if (key == "sha256") {
|
} else if (key == "sha256") {
|
||||||
p.sha256 = consume(TokenType::String).lexeme;
|
p.sha256 = consume(TokenType::String).lexeme;
|
||||||
} else if (key == "level") {
|
} else if (key == "level") {
|
||||||
p.level = std::stoi(std::string(current_.lexeme));
|
p.level = parse_int(current_.line, current_.col,
|
||||||
|
current_.lexeme);
|
||||||
advance();
|
advance();
|
||||||
}
|
}
|
||||||
skip_newlines();
|
skip_newlines();
|
||||||
|
|||||||
+89
-8
@@ -1,17 +1,15 @@
|
|||||||
#include "kappa/dsl/system.hpp"
|
#include "kappa/dsl/system.hpp"
|
||||||
|
#include "kappa/dsl/error.hpp"
|
||||||
#include "kappa/dsl/lexer.hpp"
|
#include "kappa/dsl/lexer.hpp"
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
#include <format>
|
#include <format>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
|
|
||||||
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)) {}
|
|
||||||
};
|
|
||||||
|
|
||||||
class SysParser {
|
class SysParser {
|
||||||
public:
|
public:
|
||||||
explicit SysParser(std::string_view source) : lexer_(source) { advance(); }
|
explicit SysParser(std::string_view source) : lexer_(source) { advance(); }
|
||||||
@@ -58,6 +56,9 @@ Token SysParser::consume(TokenType type) {
|
|||||||
std::string SysParser::consume_ident() {
|
std::string SysParser::consume_ident() {
|
||||||
if (at(TokenType::Ident) ||
|
if (at(TokenType::Ident) ||
|
||||||
at(TokenType::KwEnv) ||
|
at(TokenType::KwEnv) ||
|
||||||
|
at(TokenType::KwService) ||
|
||||||
|
at(TokenType::KwAssert) ||
|
||||||
|
at(TokenType::KwImport) ||
|
||||||
at(TokenType::KwConfig) ||
|
at(TokenType::KwConfig) ||
|
||||||
at(TokenType::KwFeatures) ||
|
at(TokenType::KwFeatures) ||
|
||||||
at(TokenType::KwVersion) ||
|
at(TokenType::KwVersion) ||
|
||||||
@@ -87,6 +88,7 @@ std::string SysParser::consume_ident() {
|
|||||||
std::string SysParser::consume_string() {
|
std::string SysParser::consume_string() {
|
||||||
if (at(TokenType::KwTrue)) { advance(); return "true"; }
|
if (at(TokenType::KwTrue)) { advance(); return "true"; }
|
||||||
if (at(TokenType::KwFalse)) { advance(); return "false"; }
|
if (at(TokenType::KwFalse)) { advance(); return "false"; }
|
||||||
|
if (at(TokenType::Ident)) { auto v = current_.lexeme; advance(); return v; }
|
||||||
return consume(TokenType::String).lexeme;
|
return consume(TokenType::String).lexeme;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,7 +110,50 @@ SystemConfig SysParser::parse() {
|
|||||||
|
|
||||||
auto kw = consume_ident();
|
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 == "packages") { parse_packages_block(cfg); }
|
||||||
else if (kw == "services") { parse_services_block(cfg); }
|
else if (kw == "services") { parse_services_block(cfg); }
|
||||||
else if (kw == "boot") { parse_boot_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();
|
auto k = consume_ident();
|
||||||
consume(TokenType::Equals);
|
consume(TokenType::Equals);
|
||||||
if (k == "keep") {
|
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();
|
advance();
|
||||||
}
|
}
|
||||||
skip_newlines();
|
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 == "init") { cfg.boot.init = std::move(val); }
|
||||||
else if (key == "efi") { cfg.boot.efi = 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 == "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 if (key == "bootloader"){ cfg.boot.bootloader = std::move(val); }
|
||||||
else { cfg.boot.params[key] = std::move(val); }
|
else { cfg.boot.params[key] = std::move(val); }
|
||||||
skip_newlines();
|
skip_newlines();
|
||||||
@@ -355,4 +402,38 @@ SystemConfig parse_system_config(std::string_view source) {
|
|||||||
return p.parse();
|
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
|
} // namespace kappa::dsl
|
||||||
|
|||||||
+22
-13
@@ -48,24 +48,25 @@ static std::string read_file(const char* path) {
|
|||||||
static void handle_parse_error(const char* path,
|
static void handle_parse_error(const char* path,
|
||||||
std::string_view source,
|
std::string_view source,
|
||||||
const std::runtime_error& e) {
|
const std::runtime_error& e) {
|
||||||
// ParseError format: "line:col: message"
|
|
||||||
auto msg = std::string_view(e.what());
|
auto msg = std::string_view(e.what());
|
||||||
auto first_colon = msg.find(':');
|
auto first_colon = msg.find(':');
|
||||||
auto second_colon = msg.find(':', first_colon + 1);
|
auto second_colon = msg.find(':', first_colon + 1);
|
||||||
|
|
||||||
if (first_colon != std::string_view::npos &&
|
if (first_colon != std::string_view::npos &&
|
||||||
second_colon != std::string_view::npos) {
|
second_colon != std::string_view::npos) {
|
||||||
int line = std::stoi(std::string(msg.substr(0, first_colon)));
|
try {
|
||||||
int col = std::stoi(std::string(
|
int line = std::stoi(std::string(msg.substr(0, first_colon)));
|
||||||
msg.substr(first_colon + 1, second_colon - first_colon - 1)));
|
int col = std::stoi(std::string(
|
||||||
auto message = msg.substr(second_colon + 2); // skip ": "
|
msg.substr(first_colon + 1, second_colon - first_colon - 1)));
|
||||||
|
auto message = msg.substr(second_colon + 2);
|
||||||
|
|
||||||
cli::print_error(std::cerr, source,
|
cli::print_error(std::cerr, source,
|
||||||
{path, line, col}, message,
|
{path, line, col}, message,
|
||||||
"check the syntax at this location");
|
"check the syntax at this location");
|
||||||
} else {
|
return;
|
||||||
std::cerr << "error: " << e.what() << '\n';
|
} catch (const std::exception&) {}
|
||||||
}
|
}
|
||||||
|
std::cerr << "error: " << e.what() << '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
int main(int argc, char* argv[]) {
|
int main(int argc, char* argv[]) {
|
||||||
@@ -86,6 +87,16 @@ int main(int argc, char* argv[]) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool valid_subcommand = (subcommand == "parse-package")
|
||||||
|
|| (subcommand == "parse-config")
|
||||||
|
|| (subcommand == "validate");
|
||||||
|
|
||||||
|
if (!valid_subcommand) {
|
||||||
|
std::cerr << "error: unknown subcommand '" << subcommand << "'\n\n";
|
||||||
|
print_usage();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
// Find the file argument (skip flags)
|
// Find the file argument (skip flags)
|
||||||
const char* file_arg = nullptr;
|
const char* file_arg = nullptr;
|
||||||
for (int i = 2; i < argc; ++i) {
|
for (int i = 2; i < argc; ++i) {
|
||||||
@@ -145,7 +156,5 @@ int main(int argc, char* argv[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::cerr << "error: unknown subcommand '" << subcommand << "'\n\n";
|
return 1; // unreachable
|
||||||
print_usage();
|
|
||||||
return 1;
|
|
||||||
}
|
}
|
||||||
|
|||||||
-150
@@ -1,150 +0,0 @@
|
|||||||
#include "kappa/package.hpp"
|
|
||||||
|
|
||||||
#include <toml++/toml.hpp>
|
|
||||||
#include <algorithm>
|
|
||||||
#include <array>
|
|
||||||
#include <ostream>
|
|
||||||
#include <string>
|
|
||||||
#include <string_view>
|
|
||||||
|
|
||||||
namespace kappa {
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
using namespace std::string_view_literals;
|
|
||||||
|
|
||||||
static constexpr std::array hash_keys = {"sha512"sv, "sha256"sv, "md5"sv};
|
|
||||||
|
|
||||||
static constexpr std::array known_extensions = {
|
|
||||||
"tar.xz"sv, "tar.gz"sv, "tar.zst"sv, "zip"sv, "tar"sv, "zst"sv, "git"sv, "bs2"sv
|
|
||||||
};
|
|
||||||
|
|
||||||
static std::string_view find_hash(const toml::table& src) {
|
|
||||||
for (auto key : hash_keys) {
|
|
||||||
if (src.contains(key)) { return key; }
|
|
||||||
}
|
|
||||||
return ""sv;
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::string extract_extension(std::string_view url) {
|
|
||||||
auto path = url.substr(0, url.find_first_of("?#"));
|
|
||||||
auto filename = path.substr(path.rfind('/') + 1);
|
|
||||||
|
|
||||||
for (auto ext : known_extensions) {
|
|
||||||
auto dotted = std::string(".") + std::string(ext);
|
|
||||||
if (filename.ends_with(dotted)) { return std::string(ext); }
|
|
||||||
}
|
|
||||||
|
|
||||||
auto dot = filename.rfind('.');
|
|
||||||
if (dot != std::string_view::npos) { return std::string(filename.substr(dot + 1)); }
|
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool is_known_extension(std::string_view ext) {
|
|
||||||
return std::any_of(known_extensions.begin(), known_extensions.end(),
|
|
||||||
[ext](auto k) { return ext == k; });
|
|
||||||
}
|
|
||||||
|
|
||||||
static std::string interpolate(std::string raw, const toml::table& vars) {
|
|
||||||
std::string result;
|
|
||||||
result.reserve(raw.size());
|
|
||||||
std::size_t i = 0;
|
|
||||||
|
|
||||||
while (i < raw.size()) {
|
|
||||||
if (raw[i] == '$' && i + 1 < raw.size() && raw[i + 1] == '{') {
|
|
||||||
auto end = raw.find('}', i + 2);
|
|
||||||
if (end == std::string::npos) {
|
|
||||||
result += raw.substr(i);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
auto key = raw.substr(i + 2, end - (i + 2));
|
|
||||||
result += vars[key].value_or("");
|
|
||||||
i = end + 1;
|
|
||||||
} else {
|
|
||||||
result += raw[i];
|
|
||||||
++i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
PackageConfig parse_package(const std::string& toml_path) {
|
|
||||||
auto tbl = toml::parse_file(toml_path);
|
|
||||||
PackageConfig pkg;
|
|
||||||
|
|
||||||
if (auto* pkg_tbl = tbl["package"].as_table()) {
|
|
||||||
pkg.name = (*pkg_tbl)["name"].value_or("");
|
|
||||||
pkg.version = (*pkg_tbl)["version"].value_or("");
|
|
||||||
pkg.description = (*pkg_tbl)["description"].value_or("");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auto* src = tbl["source"].as_table()) {
|
|
||||||
auto url_str = (*src)["url"].value_or(""sv);
|
|
||||||
auto* pkg_tbl = tbl["package"].as_table();
|
|
||||||
auto resolved = interpolate(std::string(url_str),
|
|
||||||
pkg_tbl != nullptr ? *pkg_tbl : toml::table{});
|
|
||||||
pkg.source_url = std::move(resolved);
|
|
||||||
|
|
||||||
pkg.source_extension = extract_extension(pkg.source_url);
|
|
||||||
|
|
||||||
auto hash_name = find_hash(*src);
|
|
||||||
if (!hash_name.empty()) {
|
|
||||||
pkg.source_hash = std::string(hash_name) + ':'
|
|
||||||
+ std::string((*src)[hash_name].value_or(""sv));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auto* deps_tbl = tbl["dependencies"].as_table()) {
|
|
||||||
if (auto* arr = (*deps_tbl)["deps"].as_array()) {
|
|
||||||
for (std::size_t i = 0; i < arr->size(); ++i) {
|
|
||||||
pkg.dependencies.emplace_back((*arr)[i].value_or(""sv));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (auto* bld = tbl["build"].as_table()) {
|
|
||||||
pkg.build_system = (*bld)["system"].value_or(""sv);
|
|
||||||
}
|
|
||||||
|
|
||||||
return pkg;
|
|
||||||
}
|
|
||||||
|
|
||||||
void print_package(std::ostream& os, const PackageConfig& pkg) {
|
|
||||||
os << "package.name = " << pkg.name << '\n';
|
|
||||||
os << "package.version = " << pkg.version << '\n';
|
|
||||||
os << "package.description = " << pkg.description << '\n';
|
|
||||||
|
|
||||||
if (!pkg.source_url.empty()) {
|
|
||||||
os << "source.url = " << pkg.source_url << '\n';
|
|
||||||
}
|
|
||||||
if (!pkg.source_extension.empty()) {
|
|
||||||
os << "source.extension = " << pkg.source_extension;
|
|
||||||
if (!is_known_extension(pkg.source_extension)) {
|
|
||||||
os << " (unrecognised)";
|
|
||||||
}
|
|
||||||
os << '\n';
|
|
||||||
}
|
|
||||||
if (!pkg.source_hash.empty()) {
|
|
||||||
os << "source.hash = " << pkg.source_hash << '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!pkg.dependencies.empty()) {
|
|
||||||
os << "dependencies = [";
|
|
||||||
for (std::size_t i = 0; i < pkg.dependencies.size(); ++i) {
|
|
||||||
if (i > 0) { os << ", "; }
|
|
||||||
os << '"' << pkg.dependencies[i] << '"';
|
|
||||||
}
|
|
||||||
os << "]\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!pkg.build_system.empty()) {
|
|
||||||
os << "build.system = " << pkg.build_system << '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
os << '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace kappa
|
|
||||||
+1
-3
@@ -2,7 +2,5 @@
|
|||||||
"$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json",
|
"$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json",
|
||||||
"name": "kappa",
|
"name": "kappa",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": [
|
"dependencies": []
|
||||||
"tomlplusplus"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user