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/
|
||||
compile_commands.json
|
||||
vcpkg_installed/
|
||||
kappa
|
||||
|
||||
|
||||
+1
-4
@@ -19,17 +19,14 @@ if(DEFINED ENV{VCPKG_ROOT})
|
||||
CACHE STRING "vcpkg toolchain")
|
||||
endif()
|
||||
|
||||
find_package(tomlplusplus CONFIG REQUIRED)
|
||||
|
||||
add_executable(kappa
|
||||
src/main.cpp
|
||||
src/package.cpp
|
||||
src/paths.cpp
|
||||
src/dsl/lexer.cpp
|
||||
src/dsl/parser.cpp
|
||||
src/dsl/system.cpp
|
||||
src/eval/vars.cpp
|
||||
src/cli/diagnostic.cpp
|
||||
src/config/merge.cpp
|
||||
)
|
||||
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.
|
||||
* Lives at /kappa/system/config.kap
|
||||
*/
|
||||
|
||||
imports = []
|
||||
|
||||
assert {
|
||||
"efi partition required for UEFI boot" : boot.efi != ""
|
||||
"root partition must be set" : boot.root != ""
|
||||
}
|
||||
|
||||
system {
|
||||
hostname = "kappa.local"
|
||||
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;
|
||||
};
|
||||
|
||||
struct Assertion {
|
||||
std::string message;
|
||||
std::string field;
|
||||
std::string op; // "==" or "!="
|
||||
std::string value;
|
||||
};
|
||||
|
||||
struct PackageDef {
|
||||
std::string name;
|
||||
std::string version;
|
||||
@@ -65,6 +72,7 @@ struct PackageDef {
|
||||
std::vector<Patch> patches;
|
||||
std::vector<EnvEntry> env_entries;
|
||||
std::unordered_map<std::string, ServiceInit> service;
|
||||
std::vector<Assertion> assertions;
|
||||
std::unordered_set<std::string> const_keys;
|
||||
Phase prepare;
|
||||
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 efi;
|
||||
std::string swap;
|
||||
std::string root;
|
||||
std::string bootloader;
|
||||
std::unordered_map<std::string, std::string> params;
|
||||
};
|
||||
@@ -52,13 +53,16 @@ struct ServiceRef {
|
||||
};
|
||||
|
||||
struct SystemConfig {
|
||||
std::vector<std::string> imports;
|
||||
SystemBlock system;
|
||||
std::vector<PackageRef> packages;
|
||||
std::vector<ServiceRef> services;
|
||||
BootBlock boot;
|
||||
std::vector<UserRef> users;
|
||||
std::vector<Assertion> assertions;
|
||||
};
|
||||
|
||||
SystemConfig parse_system_config(std::string_view source);
|
||||
SystemConfig resolve_imports(const SystemConfig& cfg, const std::string& base_dir);
|
||||
|
||||
} // namespace kappa::dsl
|
||||
|
||||
@@ -35,6 +35,8 @@ enum class TokenType {
|
||||
KwPatches,
|
||||
KwEnv,
|
||||
KwService,
|
||||
KwAssert,
|
||||
KwImport,
|
||||
KwPrepare,
|
||||
KwBuild,
|
||||
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) {
|
||||
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) {
|
||||
if (current == target_line) { start = i; break; }
|
||||
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);
|
||||
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},
|
||||
{"env", TokenType::KwEnv},
|
||||
{"service", TokenType::KwService},
|
||||
{"assert", TokenType::KwAssert},
|
||||
{"import", TokenType::KwImport},
|
||||
{"prepare", TokenType::KwPrepare},
|
||||
{"build", TokenType::KwBuild},
|
||||
{"check", TokenType::KwCheck},
|
||||
@@ -53,6 +55,8 @@ std::string_view token_name(TokenType type) {
|
||||
case TokenType::KwPatches: return "patches";
|
||||
case TokenType::KwEnv: return "env";
|
||||
case TokenType::KwService: return "service";
|
||||
case TokenType::KwAssert: return "assert";
|
||||
case TokenType::KwImport: return "import";
|
||||
case TokenType::KwPrepare: return "prepare";
|
||||
case TokenType::KwBuild: return "build";
|
||||
case TokenType::KwCheck: return "check";
|
||||
|
||||
+42
-9
@@ -1,4 +1,5 @@
|
||||
#include "kappa/dsl/parser.hpp"
|
||||
#include "kappa/dsl/error.hpp"
|
||||
#include "kappa/dsl/lexer.hpp"
|
||||
|
||||
#include <format>
|
||||
@@ -8,12 +9,6 @@
|
||||
|
||||
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 {
|
||||
public:
|
||||
explicit Parser(std::string_view source) : lexer_(source) { advance(); }
|
||||
@@ -215,7 +210,7 @@ void Parser::parse_body(PackageDef& pkg) {
|
||||
skip_newlines();
|
||||
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||
if (at(TokenType::Newline)) { advance(); continue; }
|
||||
auto init_name = current_.lexeme; // e.g. "runit", "s6"
|
||||
auto init_name = current_.lexeme;
|
||||
advance();
|
||||
consume(TokenType::Lbrace);
|
||||
skip_newlines();
|
||||
@@ -236,7 +231,8 @@ void Parser::parse_body(PackageDef& pkg) {
|
||||
skip_newlines();
|
||||
while (!at(TokenType::Rbracket) && !at(TokenType::Eof)) {
|
||||
si.ports.push_back(
|
||||
std::stoi(std::string(current_.lexeme)));
|
||||
parse_int(current_.line, current_.col,
|
||||
current_.lexeme));
|
||||
advance();
|
||||
skip_newlines();
|
||||
if (at(TokenType::Comma)) { consume(TokenType::Comma); }
|
||||
@@ -255,6 +251,42 @@ void Parser::parse_body(PackageDef& pkg) {
|
||||
consume(TokenType::Rbrace);
|
||||
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:
|
||||
consume(TokenType::KwPrepare);
|
||||
pkg.prepare = parse_phase();
|
||||
@@ -420,7 +452,8 @@ Patch Parser::parse_patch_item() {
|
||||
} else if (key == "sha256") {
|
||||
p.sha256 = consume(TokenType::String).lexeme;
|
||||
} else if (key == "level") {
|
||||
p.level = std::stoi(std::string(current_.lexeme));
|
||||
p.level = parse_int(current_.line, current_.col,
|
||||
current_.lexeme);
|
||||
advance();
|
||||
}
|
||||
skip_newlines();
|
||||
|
||||
+89
-8
@@ -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
|
||||
|
||||
+22
-13
@@ -48,24 +48,25 @@ static std::string read_file(const char* path) {
|
||||
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 ": "
|
||||
try {
|
||||
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);
|
||||
|
||||
cli::print_error(std::cerr, source,
|
||||
{path, line, col}, message,
|
||||
"check the syntax at this location");
|
||||
} else {
|
||||
std::cerr << "error: " << e.what() << '\n';
|
||||
cli::print_error(std::cerr, source,
|
||||
{path, line, col}, message,
|
||||
"check the syntax at this location");
|
||||
return;
|
||||
} catch (const std::exception&) {}
|
||||
}
|
||||
std::cerr << "error: " << e.what() << '\n';
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
@@ -86,6 +87,16 @@ int main(int argc, char* argv[]) {
|
||||
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)
|
||||
const char* file_arg = nullptr;
|
||||
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";
|
||||
print_usage();
|
||||
return 1;
|
||||
return 1; // unreachable
|
||||
}
|
||||
|
||||
-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",
|
||||
"name": "kappa",
|
||||
"version": "0.1.0",
|
||||
"dependencies": [
|
||||
"tomlplusplus"
|
||||
]
|
||||
"dependencies": []
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user