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:
2026-07-29 23:52:03 -04:00
parent d5f7d397ed
commit 0c1e82d66b
18 changed files with 325 additions and 212 deletions
+2
View File
@@ -35,4 +35,6 @@
# Build # Build
build/ build/
compile_commands.json compile_commands.json
vcpkg_installed/
kappa
+1 -4
View File
@@ -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)
+17
View File
@@ -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"
}
}
}
+8
View File
@@ -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"
+28
View File
@@ -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
+8
View File
@@ -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;
+33
View File
@@ -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
+4
View File
@@ -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
+2
View File
@@ -35,6 +35,8 @@ enum class TokenType {
KwPatches, KwPatches,
KwEnv, KwEnv,
KwService, KwService,
KwAssert,
KwImport,
KwPrepare, KwPrepare,
KwBuild, KwBuild,
KwCheck, KwCheck,
-23
View File
@@ -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
+2 -2
View File
@@ -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(); }
+62
View File
@@ -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
+4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"
]
} }