diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..e8ea98a --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,15 @@ +Checks: > + -*, + bugprone-*, + modernize-*, + performance-*, + portability-*, + readability-*, + -modernize-use-trailing-return-type, + -readability-identifier-length, + -readability-magic-numbers, + -readability-identifier-naming + +CheckOptions: + - key: modernize-use-nullptr.NullptrMacros + value: 'NULL' diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..4d5d0dd --- /dev/null +++ b/.clangd @@ -0,0 +1,5 @@ +CompileFlags: + CompilationDatabase: build +Diagnostics: + UnusedIncludes: Strict + MissingIncludes: Strict diff --git a/.gitignore b/.gitignore index e257658..18bf9d3 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,7 @@ *.out *.app +# Build +build/ +compile_commands.json + diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..043d788 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,33 @@ +cmake_minimum_required(VERSION 3.20) + +# Enforce Clang +set(CMAKE_C_COMPILER clang) +set(CMAKE_CXX_COMPILER clang++) + +project(kappa VERSION 0.1.0 LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 23) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +# Generate compile_commands.json for clangd +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# --- vcpkg integration --- +if(DEFINED ENV{VCPKG_ROOT}) + set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" + 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/eval/vars.cpp +) +target_include_directories(kappa PRIVATE include) +target_link_libraries(kappa PRIVATE tomlplusplus::tomlplusplus) diff --git a/examples/foo.kap b/examples/foo.kap new file mode 100644 index 0000000..aaaf1b8 --- /dev/null +++ b/examples/foo.kap @@ -0,0 +1,78 @@ +/* + * foo — a web server with optional SSL and GUI support. + * Demonstrates the full kappa DSL surface. + */ +package "foo" { + const version = "1.2.3" + const source = "https://example.com/foo-${version}.tar.gz" + license = "MIT" + + provides = ["libfoo.so.1", "foo"] + + patches = [ + { + url = "https://example.com/fix-build.patch" + sha256 = "abc123def456" + level = 1 + }, + "local-fix.patch" // local shorthand, no hash + ] + outputs = ["bin", "lib", "dev"] + + depends = [ + { name = "zlib", version = ">=1.2,<2.0" }, + { name = "openssl", feature = "ssl" }, + { name = "gtk", feature = "gui", version = ">=3" }, + "gettext:lib" // name:output shorthand + ] + + features { + ssl = { enabled = true, flag = "--with-ssl-dir=${cfg.ssl_dir}" } + gui = { enabled = false, flag = "--enable-gui" } + drivers = { enabled = true, force = true } + debug = false + } + + config { + // Written once, left alone on rebuild if the user edits it. + file "etc/foo.conf" mode = "default" { + hostname = ${cfg.hostname !} // required — user must set + listen_port = ${cfg.port ? 8080} // optional, default 8080 + ssl_enabled = ${cfg.ssl ? true} // optional, default true + } + + // Always overwritten on rebuild. + file "etc/log.conf" mode = "replace" { + log_level = ${cfg.log_level ? info} + log_path = ${cfg.log_path ? /var/log/foo} + } + + // Three-way diff on rebuild. + file "etc/limits.conf" mode = "merge" { + max_connections = ${cfg.max_conn ? 1024} + } + } + + env { + CFLAGS = "-O2 -march=native" + LDFLAGS = "-Wl,--as-needed" + CFLAGS ?= "-g" + } + + prepare { + patch "fix-build.patch" + } + + build { + ./configure --prefix=${prefix} ${feature.ssl} ${feature.gui} + make -j${jobs} + } + + check { + make check + } + + install { + make DESTDIR=${destdir} install + } +} diff --git a/include/kappa/dsl/ast.hpp b/include/kappa/dsl/ast.hpp new file mode 100644 index 0000000..7b1ad78 --- /dev/null +++ b/include/kappa/dsl/ast.hpp @@ -0,0 +1,66 @@ +#pragma once + +#include +#include +#include +#include + +namespace kappa::dsl { + +using Command = std::string; + +struct Phase { + std::vector commands; +}; + +struct Dependency { + std::string name; + std::string version; + std::string output; + std::string feature; // empty = unconditional +}; + +struct ConfigFile { + std::string path; + std::string mode; + std::unordered_map entries; +}; + +struct FeatureDef { + bool enabled = false; + bool force = false; + std::string flag; +}; + +struct Patch { + std::string url; + std::string sha256; + int level = 1; +}; + +struct EnvEntry { + std::string key; + std::string value; + bool soft = false; +}; + +struct PackageDef { + std::string name; + std::string version; + std::string source; + std::string license; + std::vector depends; + std::vector provides; + std::vector outputs; + std::unordered_map features; + std::vector config_files; + std::vector patches; + std::vector env_entries; + std::unordered_set const_keys; + Phase prepare; + Phase build; + Phase check; + Phase install; +}; + +} // namespace kappa::dsl diff --git a/include/kappa/dsl/lexer.hpp b/include/kappa/dsl/lexer.hpp new file mode 100644 index 0000000..20386b1 --- /dev/null +++ b/include/kappa/dsl/lexer.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include "kappa/dsl/token.hpp" + +#include + +namespace kappa::dsl { + +class Lexer { +public: + explicit Lexer(std::string_view source); + + Token next(); + +private: + Token scan_ident(); + Token scan_string(); + Token scan_symbol(); + void skip_whitespace(); + bool skip_comment(); + + char peek() const; + char advance(); + bool match(char c); + + std::string_view source_; + std::size_t pos_ = 0; + int line_ = 1; + int col_ = 1; + int token_start_line_ = 0; + int token_start_col_ = 0; +}; + +} // namespace kappa::dsl diff --git a/include/kappa/dsl/parser.hpp b/include/kappa/dsl/parser.hpp new file mode 100644 index 0000000..802bcd9 --- /dev/null +++ b/include/kappa/dsl/parser.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include "kappa/dsl/ast.hpp" + +#include +#include + +namespace kappa::dsl { + +PackageDef parse(std::string_view source); + +} // namespace kappa::dsl diff --git a/include/kappa/dsl/token.hpp b/include/kappa/dsl/token.hpp new file mode 100644 index 0000000..c72bda5 --- /dev/null +++ b/include/kappa/dsl/token.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include +#include + +namespace kappa::dsl { + +enum class TokenType { + Eof, + Newline, + + // Symbols + Lbrace, // { + Rbrace, // } + Equals, // = + Lbracket, // [ + Rbracket, // ] + Comma, // , + + // Literals + String, // "..." + Ident, // bare word (keyword, command, variable ref) + + // Keywords (tokenised as Ident, then classified) + KwPackage, + KwVersion, + KwSource, + KwDepends, + KwProvides, + KwOutputs, + KwFeatures, + KwConfig, + KwConst, + KwLicense, + KwPatches, + KwEnv, + KwPrepare, + KwBuild, + KwCheck, + KwInstall, + KwTrue, + KwFalse, +}; + +struct Token { + TokenType type = TokenType::Eof; + std::string lexeme; + int line = 0; + int col = 0; +}; + +std::string_view token_name(TokenType type); + +} // namespace kappa::dsl diff --git a/include/kappa/eval/vars.hpp b/include/kappa/eval/vars.hpp new file mode 100644 index 0000000..e1acace --- /dev/null +++ b/include/kappa/eval/vars.hpp @@ -0,0 +1,37 @@ +#pragma once + +#include "kappa/dsl/ast.hpp" + +#include +#include +#include + +namespace kappa::eval { + +enum class VarKind { + Unknown, + Builtin, // ${prefix}, ${jobs}, ${jobopts}, ${userargs} + Package, // ${version}, ${name}, ${source} + Config, // ${cfg.key} + Feature, // ${feature.key} → expanded flag or "" +}; + +struct ResolvedVar { + std::string value; + VarKind kind = VarKind::Unknown; +}; + +struct Scope { + std::unordered_map builtins; + std::unordered_map config; + std::unordered_map features; + std::unordered_map package; +}; + +Scope make_default_scope(); + +ResolvedVar resolve(std::string_view name, const Scope& scope); + +std::string interpolate(std::string_view input, const Scope& scope); + +} // namespace kappa::eval diff --git a/include/kappa/package.hpp b/include/kappa/package.hpp new file mode 100644 index 0000000..3aa3d74 --- /dev/null +++ b/include/kappa/package.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include +#include +#include + +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 dependencies; + std::string build_system; +}; + +PackageConfig parse_package(const std::string& toml_path); +void print_package(std::ostream& os, const PackageConfig& pkg); + +} // namespace kappa diff --git a/include/kappa/paths.hpp b/include/kappa/paths.hpp new file mode 100644 index 0000000..a9dd30a --- /dev/null +++ b/include/kappa/paths.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +namespace kappa::paths { + +inline const std::filesystem::path root{"/kappa"}; +inline const auto bin_dir = root / "bin"; +inline const auto temp_dir = root / "temp"; +inline const auto db_dir = root / "db"; +inline const auto system_dir = root / "system"; +inline const auto builds_dir = system_dir / "builds"; + +void ensure_directories(); + +} // namespace kappa::paths diff --git a/package.toml b/package.toml new file mode 100644 index 0000000..3fa0713 --- /dev/null +++ b/package.toml @@ -0,0 +1,14 @@ +[package] +name = "binutils" +version = "2.46.1" +description = "GNU Binary Utilities (ld, as, objdump, readelf)" + +[source] +url = "https://ftp.gnu.org/gnu/${name}/${name}-${version}.tar.xz" # ${} is string substitution +sha256 = "e127a709cba24c76de8936cb7083dd768f28cd37eb010492e2f19b71eb1294e4" # or sha512, or md5 + +[dependencies] +deps = ["zlib", "gettext"] + +[build] +system = "autotools" # or cargo, make, cmake, or meson diff --git a/src/dsl/lexer.cpp b/src/dsl/lexer.cpp new file mode 100644 index 0000000..2e769e8 --- /dev/null +++ b/src/dsl/lexer.cpp @@ -0,0 +1,191 @@ +#include "kappa/dsl/lexer.hpp" + +#include +#include +#include + +namespace kappa::dsl { + +static const std::unordered_map 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(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(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 diff --git a/src/dsl/parser.cpp b/src/dsl/parser.cpp new file mode 100644 index 0000000..bd1c602 --- /dev/null +++ b/src/dsl/parser.cpp @@ -0,0 +1,416 @@ +#include "kappa/dsl/parser.hpp" +#include "kappa/dsl/lexer.hpp" + +#include +#include +#include +#include + +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 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 Parser::parse_string_list() { + consume(TokenType::Lbracket); + skip_newlines(); + std::vector 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 diff --git a/src/eval/vars.cpp b/src/eval/vars.cpp new file mode 100644 index 0000000..9286fee --- /dev/null +++ b/src/eval/vars.cpp @@ -0,0 +1,82 @@ +#include "kappa/eval/vars.hpp" + +#include + +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 diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..42311b4 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,28 @@ +#include "kappa/package.hpp" + +#include +#include +#include +#include +#include + +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; + } +} diff --git a/src/package.cpp b/src/package.cpp new file mode 100644 index 0000000..c4c7fe2 --- /dev/null +++ b/src/package.cpp @@ -0,0 +1,150 @@ +#include "kappa/package.hpp" + +#include +#include +#include +#include +#include +#include + +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 diff --git a/src/paths.cpp b/src/paths.cpp new file mode 100644 index 0000000..c217c98 --- /dev/null +++ b/src/paths.cpp @@ -0,0 +1,16 @@ +#include "kappa/paths.hpp" + +#include + +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 diff --git a/vcpkg.json b/vcpkg.json new file mode 100644 index 0000000..834b1b4 --- /dev/null +++ b/vcpkg.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", + "name": "kappa", + "version": "0.1.0", + "dependencies": [ + "tomlplusplus" + ] +}