feat: DSL parser, eval system, and project structure
- Hand-written lexer + recursive-descent parser for .kap package definitions
- Package constructs: const, license, provides, outputs, patches, depends
(version-constrained, feature-gated, output-targeted), features (enabled/
forced/flag), config files (default/replace/merge with cfg interpolation),
env (?= soft-set), prepare/build/check/install phases
- Evaluator: recursive resolver for prefix, jobopts, destdir, userargs,
cfg.*, feature.*, package vars
- Comments: // single-line, /* */ multi-line
- /kappa/{bin,temp,db,system,system/builds} path layout
- Clang 22, C++23, CMake + vcpkg (tomlplusplus)
- .clangd + .clang-tidy configured
This commit is contained in:
@@ -0,0 +1,191 @@
|
||||
#include "kappa/dsl/lexer.hpp"
|
||||
|
||||
#include <cctype>
|
||||
#include <stdexcept>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace kappa::dsl {
|
||||
|
||||
static const std::unordered_map<std::string_view, TokenType> keywords = {
|
||||
{"package", TokenType::KwPackage},
|
||||
{"version", TokenType::KwVersion},
|
||||
{"source", TokenType::KwSource},
|
||||
{"depends", TokenType::KwDepends},
|
||||
{"provides", TokenType::KwProvides},
|
||||
{"outputs", TokenType::KwOutputs},
|
||||
{"features", TokenType::KwFeatures},
|
||||
{"config", TokenType::KwConfig},
|
||||
{"const", TokenType::KwConst},
|
||||
{"license", TokenType::KwLicense},
|
||||
{"patches", TokenType::KwPatches},
|
||||
{"env", TokenType::KwEnv},
|
||||
{"prepare", TokenType::KwPrepare},
|
||||
{"build", TokenType::KwBuild},
|
||||
{"check", TokenType::KwCheck},
|
||||
{"install", TokenType::KwInstall},
|
||||
{"true", TokenType::KwTrue},
|
||||
{"false", TokenType::KwFalse},
|
||||
};
|
||||
|
||||
std::string_view token_name(TokenType type) {
|
||||
switch (type) {
|
||||
case TokenType::Eof: return "EOF";
|
||||
case TokenType::Newline: return "newline";
|
||||
case TokenType::Lbrace: return "{";
|
||||
case TokenType::Rbrace: return "}";
|
||||
case TokenType::Equals: return "=";
|
||||
case TokenType::Lbracket: return "[";
|
||||
case TokenType::Rbracket: return "]";
|
||||
case TokenType::Comma: return ",";
|
||||
case TokenType::String: return "string";
|
||||
case TokenType::Ident: return "ident";
|
||||
case TokenType::KwPackage: return "package";
|
||||
case TokenType::KwVersion: return "version";
|
||||
case TokenType::KwSource: return "source";
|
||||
case TokenType::KwDepends: return "depends";
|
||||
case TokenType::KwProvides: return "provides";
|
||||
case TokenType::KwOutputs: return "outputs";
|
||||
case TokenType::KwFeatures: return "features";
|
||||
case TokenType::KwConfig: return "config";
|
||||
case TokenType::KwConst: return "const";
|
||||
case TokenType::KwLicense: return "license";
|
||||
case TokenType::KwPatches: return "patches";
|
||||
case TokenType::KwEnv: return "env";
|
||||
case TokenType::KwPrepare: return "prepare";
|
||||
case TokenType::KwBuild: return "build";
|
||||
case TokenType::KwCheck: return "check";
|
||||
case TokenType::KwInstall: return "install";
|
||||
case TokenType::KwTrue: return "true";
|
||||
case TokenType::KwFalse: return "false";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
Lexer::Lexer(std::string_view source) : source_(source) {}
|
||||
|
||||
char Lexer::peek() const {
|
||||
return pos_ < source_.size() ? source_[pos_] : '\0';
|
||||
}
|
||||
|
||||
char Lexer::advance() {
|
||||
if (pos_ >= source_.size()) { return '\0'; }
|
||||
char c = source_[pos_++];
|
||||
if (c == '\n') { ++line_; col_ = 1; }
|
||||
else { ++col_; }
|
||||
return c;
|
||||
}
|
||||
|
||||
bool Lexer::match(char c) {
|
||||
if (peek() == c) { advance(); return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
void Lexer::skip_whitespace() {
|
||||
while (std::isspace(static_cast<unsigned char>(peek())) && peek() != '\n') {
|
||||
advance();
|
||||
}
|
||||
}
|
||||
|
||||
bool Lexer::skip_comment() {
|
||||
if (peek() != '/') { return false; }
|
||||
if (pos_ + 1 >= source_.size()) { return false; }
|
||||
|
||||
char next = source_[pos_ + 1];
|
||||
if (next == '/') {
|
||||
advance(); advance(); // skip //
|
||||
while (pos_ < source_.size() && peek() != '\n') { advance(); }
|
||||
return true;
|
||||
}
|
||||
if (next == '*') {
|
||||
advance(); advance(); // skip /*
|
||||
while (pos_ + 1 < source_.size()) {
|
||||
if (peek() == '*' && source_[pos_ + 1] == '/') {
|
||||
advance(); advance(); // skip */
|
||||
return true;
|
||||
}
|
||||
advance();
|
||||
}
|
||||
return true; // unterminated — consume to EOF
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
Token Lexer::next() {
|
||||
while (true) {
|
||||
skip_whitespace();
|
||||
if (skip_comment()) { continue; }
|
||||
|
||||
token_start_line_ = line_;
|
||||
token_start_col_ = col_;
|
||||
|
||||
if (pos_ >= source_.size()) { return {TokenType::Eof, "", line_, col_}; }
|
||||
|
||||
char c = peek();
|
||||
|
||||
if (c == '\n') {
|
||||
advance();
|
||||
return {TokenType::Newline, "\n", token_start_line_, token_start_col_};
|
||||
}
|
||||
if (c == '"') { return scan_string(); }
|
||||
if (c == '{' || c == '}' || c == '=' || c == '[' || c == ']' || c == ',') {
|
||||
return scan_symbol();
|
||||
}
|
||||
return scan_ident();
|
||||
}
|
||||
}
|
||||
|
||||
Token Lexer::scan_ident() {
|
||||
std::string lexeme;
|
||||
while (pos_ < source_.size()) {
|
||||
char c = peek();
|
||||
if (std::isspace(static_cast<unsigned char>(c))) { break; }
|
||||
if (c == '"' || c == '[' || c == ']' || c == ',') { break; }
|
||||
lexeme += advance();
|
||||
}
|
||||
|
||||
auto it = keywords.find(lexeme);
|
||||
if (it != keywords.end()) {
|
||||
return {it->second, lexeme, token_start_line_, token_start_col_};
|
||||
}
|
||||
return {TokenType::Ident, lexeme, token_start_line_, token_start_col_};
|
||||
}
|
||||
|
||||
Token Lexer::scan_string() {
|
||||
advance(); // opening "
|
||||
std::string lexeme;
|
||||
while (pos_ < source_.size()) {
|
||||
char c = peek();
|
||||
if (c == '"') { advance(); break; }
|
||||
if (c == '\\') {
|
||||
advance();
|
||||
if (pos_ < source_.size()) {
|
||||
char esc = advance();
|
||||
switch (esc) {
|
||||
case 'n': lexeme += '\n'; break;
|
||||
case 't': lexeme += '\t'; break;
|
||||
case '\\': lexeme += '\\'; break;
|
||||
case '"': lexeme += '"'; break;
|
||||
default: lexeme += esc; break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
lexeme += advance();
|
||||
}
|
||||
}
|
||||
return {TokenType::String, lexeme, token_start_line_, token_start_col_};
|
||||
}
|
||||
|
||||
Token Lexer::scan_symbol() {
|
||||
char c = advance();
|
||||
switch (c) {
|
||||
case '{': return {TokenType::Lbrace, "{", token_start_line_, token_start_col_};
|
||||
case '}': return {TokenType::Rbrace, "}", token_start_line_, token_start_col_};
|
||||
case '=': return {TokenType::Equals, "=", token_start_line_, token_start_col_};
|
||||
case '[': return {TokenType::Lbracket, "[", token_start_line_, token_start_col_};
|
||||
case ']': return {TokenType::Rbracket, "]", token_start_line_, token_start_col_};
|
||||
case ',': return {TokenType::Comma, ",", token_start_line_, token_start_col_};
|
||||
default: return {TokenType::Eof, "", token_start_line_, token_start_col_};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace kappa::dsl
|
||||
@@ -0,0 +1,416 @@
|
||||
#include "kappa/dsl/parser.hpp"
|
||||
#include "kappa/dsl/lexer.hpp"
|
||||
|
||||
#include <format>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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(); }
|
||||
|
||||
PackageDef parse();
|
||||
|
||||
private:
|
||||
void advance();
|
||||
Token consume(TokenType type);
|
||||
void skip_newlines();
|
||||
bool at(TokenType type) const { return current_.type == type; }
|
||||
|
||||
PackageDef parse_package_def();
|
||||
void parse_body(PackageDef& pkg);
|
||||
Phase parse_phase();
|
||||
Command parse_command_line();
|
||||
bool parse_bool();
|
||||
|
||||
std::vector<std::string> parse_string_list();
|
||||
Dependency parse_dependency_item();
|
||||
ConfigFile parse_config_file();
|
||||
FeatureDef parse_feature_def();
|
||||
Patch parse_patch_item();
|
||||
|
||||
Token current_;
|
||||
Lexer lexer_;
|
||||
};
|
||||
|
||||
void Parser::advance() { current_ = lexer_.next(); }
|
||||
|
||||
void Parser::skip_newlines() {
|
||||
while (at(TokenType::Newline)) { advance(); }
|
||||
}
|
||||
|
||||
Token Parser::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;
|
||||
}
|
||||
|
||||
PackageDef Parser::parse() {
|
||||
skip_newlines();
|
||||
return parse_package_def();
|
||||
}
|
||||
|
||||
PackageDef Parser::parse_package_def() {
|
||||
consume(TokenType::KwPackage);
|
||||
auto name_tok = consume(TokenType::String);
|
||||
consume(TokenType::Lbrace);
|
||||
|
||||
PackageDef pkg;
|
||||
pkg.name = std::move(name_tok.lexeme);
|
||||
|
||||
parse_body(pkg);
|
||||
|
||||
consume(TokenType::Rbrace);
|
||||
return pkg;
|
||||
}
|
||||
|
||||
void Parser::parse_body(PackageDef& pkg) {
|
||||
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||
skip_newlines();
|
||||
if (at(TokenType::Rbrace) || at(TokenType::Eof)) { break; }
|
||||
|
||||
switch (current_.type) {
|
||||
case TokenType::KwConst: {
|
||||
consume(TokenType::KwConst);
|
||||
if (at(TokenType::KwVersion)) {
|
||||
consume(TokenType::KwVersion);
|
||||
consume(TokenType::Equals);
|
||||
pkg.version = consume(TokenType::String).lexeme;
|
||||
pkg.const_keys.insert("version");
|
||||
} else if (at(TokenType::KwSource)) {
|
||||
consume(TokenType::KwSource);
|
||||
consume(TokenType::Equals);
|
||||
pkg.source = consume(TokenType::String).lexeme;
|
||||
pkg.const_keys.insert("source");
|
||||
} else {
|
||||
throw ParseError(current_.line, current_.col,
|
||||
std::format("unexpected token '{}' after const",
|
||||
token_name(current_.type)));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case TokenType::KwVersion:
|
||||
consume(TokenType::KwVersion);
|
||||
consume(TokenType::Equals);
|
||||
pkg.version = consume(TokenType::String).lexeme;
|
||||
break;
|
||||
|
||||
case TokenType::KwSource:
|
||||
consume(TokenType::KwSource);
|
||||
consume(TokenType::Equals);
|
||||
pkg.source = consume(TokenType::String).lexeme;
|
||||
break;
|
||||
|
||||
case TokenType::KwLicense:
|
||||
consume(TokenType::KwLicense);
|
||||
consume(TokenType::Equals);
|
||||
pkg.license = consume(TokenType::String).lexeme;
|
||||
break;
|
||||
|
||||
case TokenType::KwProvides:
|
||||
consume(TokenType::KwProvides);
|
||||
consume(TokenType::Equals);
|
||||
pkg.provides = parse_string_list();
|
||||
break;
|
||||
|
||||
case TokenType::KwOutputs:
|
||||
consume(TokenType::KwOutputs);
|
||||
consume(TokenType::Equals);
|
||||
pkg.outputs = parse_string_list();
|
||||
break;
|
||||
|
||||
case TokenType::KwDepends:
|
||||
consume(TokenType::KwDepends);
|
||||
consume(TokenType::Equals);
|
||||
consume(TokenType::Lbracket);
|
||||
skip_newlines();
|
||||
while (!at(TokenType::Rbracket) && !at(TokenType::Eof)) {
|
||||
pkg.depends.push_back(parse_dependency_item());
|
||||
skip_newlines();
|
||||
if (at(TokenType::Comma)) { consume(TokenType::Comma); }
|
||||
skip_newlines();
|
||||
}
|
||||
consume(TokenType::Rbracket);
|
||||
break;
|
||||
|
||||
case TokenType::KwFeatures:
|
||||
consume(TokenType::KwFeatures);
|
||||
consume(TokenType::Lbrace);
|
||||
skip_newlines();
|
||||
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||
if (at(TokenType::Newline)) { advance(); continue; }
|
||||
auto key = consume(TokenType::Ident).lexeme;
|
||||
consume(TokenType::Equals);
|
||||
pkg.features[key] = parse_feature_def();
|
||||
skip_newlines();
|
||||
}
|
||||
consume(TokenType::Rbrace);
|
||||
break;
|
||||
|
||||
case TokenType::KwConfig:
|
||||
consume(TokenType::KwConfig);
|
||||
consume(TokenType::Lbrace);
|
||||
skip_newlines();
|
||||
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||
if (at(TokenType::Newline)) { advance(); continue; }
|
||||
pkg.config_files.push_back(parse_config_file());
|
||||
skip_newlines();
|
||||
}
|
||||
consume(TokenType::Rbrace);
|
||||
break;
|
||||
|
||||
case TokenType::KwPatches:
|
||||
consume(TokenType::KwPatches);
|
||||
consume(TokenType::Equals);
|
||||
consume(TokenType::Lbracket);
|
||||
skip_newlines();
|
||||
while (!at(TokenType::Rbracket) && !at(TokenType::Eof)) {
|
||||
pkg.patches.push_back(parse_patch_item());
|
||||
skip_newlines();
|
||||
if (at(TokenType::Comma)) { consume(TokenType::Comma); }
|
||||
skip_newlines();
|
||||
}
|
||||
consume(TokenType::Rbracket);
|
||||
break;
|
||||
|
||||
case TokenType::KwEnv:
|
||||
consume(TokenType::KwEnv);
|
||||
consume(TokenType::Lbrace);
|
||||
skip_newlines();
|
||||
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||
if (at(TokenType::Newline)) { advance(); continue; }
|
||||
auto key = consume(TokenType::Ident).lexeme;
|
||||
bool soft = false;
|
||||
if (at(TokenType::Ident) && current_.lexeme == "?=") {
|
||||
soft = true;
|
||||
advance();
|
||||
} else {
|
||||
consume(TokenType::Equals);
|
||||
}
|
||||
pkg.env_entries.push_back(
|
||||
{std::move(key), consume(TokenType::String).lexeme, soft});
|
||||
skip_newlines();
|
||||
}
|
||||
consume(TokenType::Rbrace);
|
||||
break;
|
||||
|
||||
case TokenType::KwPrepare:
|
||||
consume(TokenType::KwPrepare);
|
||||
pkg.prepare = parse_phase();
|
||||
break;
|
||||
|
||||
case TokenType::KwBuild:
|
||||
consume(TokenType::KwBuild);
|
||||
pkg.build = parse_phase();
|
||||
break;
|
||||
|
||||
case TokenType::KwCheck:
|
||||
consume(TokenType::KwCheck);
|
||||
pkg.check = parse_phase();
|
||||
break;
|
||||
|
||||
case TokenType::KwInstall:
|
||||
consume(TokenType::KwInstall);
|
||||
pkg.install = parse_phase();
|
||||
break;
|
||||
|
||||
default:
|
||||
throw ParseError(current_.line, current_.col,
|
||||
std::format("unexpected token '{}' in package body",
|
||||
token_name(current_.type)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> Parser::parse_string_list() {
|
||||
consume(TokenType::Lbracket);
|
||||
skip_newlines();
|
||||
std::vector<std::string> items;
|
||||
while (!at(TokenType::Rbracket) && !at(TokenType::Eof)) {
|
||||
items.push_back(consume(TokenType::String).lexeme);
|
||||
skip_newlines();
|
||||
if (at(TokenType::Comma)) { consume(TokenType::Comma); }
|
||||
skip_newlines();
|
||||
}
|
||||
consume(TokenType::Rbracket);
|
||||
return items;
|
||||
}
|
||||
|
||||
Dependency Parser::parse_dependency_item() {
|
||||
if (at(TokenType::String)) {
|
||||
auto raw = consume(TokenType::String).lexeme;
|
||||
auto colon = raw.find(':');
|
||||
if (colon != std::string::npos) {
|
||||
return {raw.substr(0, colon), "", raw.substr(colon + 1)};
|
||||
}
|
||||
return {std::move(raw), "", ""};
|
||||
}
|
||||
|
||||
consume(TokenType::Lbrace);
|
||||
skip_newlines();
|
||||
Dependency dep;
|
||||
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||
if (at(TokenType::Newline)) { advance(); continue; }
|
||||
auto key = current_.lexeme; // accept Ident or keyword as object key
|
||||
advance();
|
||||
consume(TokenType::Equals);
|
||||
if (key == "name") { dep.name = consume(TokenType::String).lexeme; }
|
||||
else if (key == "version") { dep.version = consume(TokenType::String).lexeme; }
|
||||
else if (key == "output") { dep.output = consume(TokenType::String).lexeme; }
|
||||
else if (key == "feature") { dep.feature = consume(TokenType::String).lexeme; }
|
||||
else { throw ParseError(current_.line, current_.col,
|
||||
std::format("unknown dependency key '{}'", key)); }
|
||||
skip_newlines();
|
||||
if (at(TokenType::Comma)) { consume(TokenType::Comma); }
|
||||
skip_newlines();
|
||||
}
|
||||
consume(TokenType::Rbrace);
|
||||
return dep;
|
||||
}
|
||||
|
||||
ConfigFile Parser::parse_config_file() {
|
||||
consume(TokenType::Ident); // "file"
|
||||
ConfigFile cf;
|
||||
cf.path = consume(TokenType::String).lexeme;
|
||||
|
||||
if (at(TokenType::Ident) && current_.lexeme == "mode") {
|
||||
advance();
|
||||
consume(TokenType::Equals);
|
||||
cf.mode = consume(TokenType::String).lexeme;
|
||||
}
|
||||
|
||||
consume(TokenType::Lbrace);
|
||||
skip_newlines();
|
||||
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||
if (at(TokenType::Newline)) { advance(); continue; }
|
||||
auto key = current_.lexeme;
|
||||
advance();
|
||||
consume(TokenType::Equals);
|
||||
std::string val;
|
||||
while (!at(TokenType::Newline) && !at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||
if (!val.empty() && !at(TokenType::Comma)) { val += ' '; }
|
||||
val += current_.lexeme;
|
||||
advance();
|
||||
}
|
||||
cf.entries[key] = std::move(val);
|
||||
skip_newlines();
|
||||
}
|
||||
consume(TokenType::Rbrace);
|
||||
return cf;
|
||||
}
|
||||
|
||||
bool Parser::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)));
|
||||
}
|
||||
|
||||
FeatureDef Parser::parse_feature_def() {
|
||||
if (at(TokenType::KwTrue) || at(TokenType::KwFalse)) {
|
||||
return {parse_bool(), false, ""};
|
||||
}
|
||||
|
||||
consume(TokenType::Lbrace);
|
||||
skip_newlines();
|
||||
FeatureDef f;
|
||||
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||
if (at(TokenType::Newline)) { advance(); continue; }
|
||||
auto key = current_.lexeme;
|
||||
advance();
|
||||
consume(TokenType::Equals);
|
||||
if (key == "enabled") {
|
||||
f.enabled = parse_bool();
|
||||
} else if (key == "force") {
|
||||
f.force = parse_bool();
|
||||
} else if (key == "flag") {
|
||||
std::string val;
|
||||
while (!at(TokenType::Newline) && !at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||
if (!val.empty() && !at(TokenType::Comma)) { val += ' '; }
|
||||
val += current_.lexeme;
|
||||
advance();
|
||||
}
|
||||
f.flag = std::move(val);
|
||||
}
|
||||
skip_newlines();
|
||||
if (at(TokenType::Comma)) { consume(TokenType::Comma); }
|
||||
skip_newlines();
|
||||
}
|
||||
consume(TokenType::Rbrace);
|
||||
return f;
|
||||
}
|
||||
|
||||
Patch Parser::parse_patch_item() {
|
||||
if (at(TokenType::String)) {
|
||||
return {consume(TokenType::String).lexeme, "", 1};
|
||||
}
|
||||
|
||||
consume(TokenType::Lbrace);
|
||||
skip_newlines();
|
||||
Patch p;
|
||||
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||
if (at(TokenType::Newline)) { advance(); continue; }
|
||||
auto key = current_.lexeme;
|
||||
advance();
|
||||
consume(TokenType::Equals);
|
||||
if (key == "url") {
|
||||
p.url = consume(TokenType::String).lexeme;
|
||||
} else if (key == "sha256") {
|
||||
p.sha256 = consume(TokenType::String).lexeme;
|
||||
} else if (key == "level") {
|
||||
p.level = std::stoi(std::string(current_.lexeme));
|
||||
advance();
|
||||
}
|
||||
skip_newlines();
|
||||
if (at(TokenType::Comma)) { consume(TokenType::Comma); }
|
||||
skip_newlines();
|
||||
}
|
||||
consume(TokenType::Rbrace);
|
||||
return p;
|
||||
}
|
||||
|
||||
Phase Parser::parse_phase() {
|
||||
consume(TokenType::Lbrace);
|
||||
skip_newlines();
|
||||
|
||||
Phase phase;
|
||||
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||
if (at(TokenType::Newline)) { advance(); continue; }
|
||||
phase.commands.push_back(parse_command_line());
|
||||
}
|
||||
consume(TokenType::Rbrace);
|
||||
return phase;
|
||||
}
|
||||
|
||||
Command Parser::parse_command_line() {
|
||||
std::string cmd;
|
||||
while (!at(TokenType::Newline) && !at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||
if (!cmd.empty() && !at(TokenType::Comma)) { cmd += ' '; }
|
||||
cmd += current_.lexeme;
|
||||
advance();
|
||||
}
|
||||
return cmd;
|
||||
}
|
||||
|
||||
PackageDef parse(std::string_view source) {
|
||||
Parser p(source);
|
||||
return p.parse();
|
||||
}
|
||||
|
||||
} // namespace kappa::dsl
|
||||
@@ -0,0 +1,82 @@
|
||||
#include "kappa/eval/vars.hpp"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace kappa::eval {
|
||||
|
||||
Scope make_default_scope() {
|
||||
Scope s;
|
||||
s.builtins["prefix"] = "/usr";
|
||||
s.builtins["jobs"] = "1";
|
||||
s.builtins["jobopts"] = "-j1";
|
||||
s.builtins["destdir"] = "/kappa/temp/destdir";
|
||||
s.builtins["userargs"] = "";
|
||||
return s;
|
||||
}
|
||||
|
||||
ResolvedVar resolve(std::string_view name, const Scope& scope) {
|
||||
auto dot = name.find('.');
|
||||
if (dot == std::string_view::npos) {
|
||||
if (auto it = scope.builtins.find(std::string(name)); it != scope.builtins.end()) {
|
||||
return {it->second, VarKind::Builtin};
|
||||
}
|
||||
if (auto it = scope.package.find(std::string(name)); it != scope.package.end()) {
|
||||
return {it->second, VarKind::Package};
|
||||
}
|
||||
return {{}, VarKind::Unknown};
|
||||
}
|
||||
|
||||
auto ns = name.substr(0, dot);
|
||||
auto key = name.substr(dot + 1);
|
||||
|
||||
if (ns == "cfg") {
|
||||
if (auto it = scope.config.find(std::string(key)); it != scope.config.end()) {
|
||||
return {it->second, VarKind::Config};
|
||||
}
|
||||
return {{}, VarKind::Unknown};
|
||||
}
|
||||
|
||||
if (ns == "feature") {
|
||||
if (auto it = scope.features.find(std::string(key)); it != scope.features.end()) {
|
||||
const auto& f = it->second;
|
||||
if (!f.enabled) { return {"", VarKind::Feature}; }
|
||||
return {f.flag, VarKind::Feature};
|
||||
}
|
||||
return {{}, VarKind::Unknown};
|
||||
}
|
||||
|
||||
return {{}, VarKind::Unknown};
|
||||
}
|
||||
|
||||
std::string interpolate(std::string_view input, const Scope& scope) {
|
||||
std::string result;
|
||||
result.reserve(input.size());
|
||||
std::size_t i = 0;
|
||||
bool had_substitution = false;
|
||||
|
||||
while (i < input.size()) {
|
||||
if (input[i] == '$' && i + 1 < input.size() && input[i + 1] == '{') {
|
||||
auto end = input.find('}', i + 2);
|
||||
if (end == std::string_view::npos) {
|
||||
result += input.substr(i);
|
||||
break;
|
||||
}
|
||||
auto var_name = input.substr(i + 2, end - (i + 2));
|
||||
auto rv = resolve(var_name, scope);
|
||||
result += rv.value;
|
||||
had_substitution = true;
|
||||
i = end + 1;
|
||||
} else {
|
||||
result += input[i];
|
||||
++i;
|
||||
}
|
||||
}
|
||||
|
||||
if (had_substitution && result.contains("${")) {
|
||||
return interpolate(result, scope);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace kappa::eval
|
||||
@@ -0,0 +1,28 @@
|
||||
#include "kappa/package.hpp"
|
||||
|
||||
#include <toml++/toml.hpp>
|
||||
#include <cstdlib>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
|
||||
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";
|
||||
return 1;
|
||||
}
|
||||
|
||||
try {
|
||||
auto pkg = kappa::parse_package(toml_path.native());
|
||||
kappa::print_package(std::cout, pkg);
|
||||
return 0;
|
||||
} catch (const toml::parse_error& e) {
|
||||
std::cerr << "Parse error:\n" << e << '\n';
|
||||
return 1;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "Error: " << e.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
#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
|
||||
@@ -0,0 +1,16 @@
|
||||
#include "kappa/paths.hpp"
|
||||
|
||||
#include <system_error>
|
||||
|
||||
namespace kappa::paths {
|
||||
|
||||
void ensure_directories() {
|
||||
std::error_code ec;
|
||||
|
||||
std::filesystem::create_directories(bin_dir, ec);
|
||||
std::filesystem::create_directories(temp_dir, ec);
|
||||
std::filesystem::create_directories(db_dir, ec);
|
||||
std::filesystem::create_directories(builds_dir, ec);
|
||||
}
|
||||
|
||||
} // namespace kappa::paths
|
||||
Reference in New Issue
Block a user