Compare commits
3
Commits
6fd31b694b
...
0c1e82d66b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c1e82d66b | ||
|
|
d5f7d397ed | ||
|
|
a8b53681fc |
+15
@@ -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'
|
||||
@@ -0,0 +1,5 @@
|
||||
CompileFlags:
|
||||
CompilationDatabase: build
|
||||
Diagnostics:
|
||||
UnusedIncludes: Strict
|
||||
MissingIncludes: Strict
|
||||
@@ -32,3 +32,9 @@
|
||||
*.out
|
||||
*.app
|
||||
|
||||
# Build
|
||||
build/
|
||||
compile_commands.json
|
||||
vcpkg_installed/
|
||||
kappa
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
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()
|
||||
|
||||
add_executable(kappa
|
||||
src/main.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)
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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"
|
||||
|
||||
features {
|
||||
ssl = true
|
||||
wayland = false
|
||||
pulseaudio = false
|
||||
}
|
||||
|
||||
env {
|
||||
CFLAGS = "-O2 -march=native"
|
||||
LDFLAGS = "-Wl,--as-needed"
|
||||
MAKEFLAGS = "-j8"
|
||||
}
|
||||
|
||||
config {
|
||||
prefix = "/usr"
|
||||
log_dir = "/var/log"
|
||||
}
|
||||
|
||||
rollback {
|
||||
keep = 5
|
||||
}
|
||||
}
|
||||
|
||||
packages {
|
||||
foo {
|
||||
version = ">=1.2"
|
||||
features {
|
||||
gui = true
|
||||
}
|
||||
config {
|
||||
port = "9090"
|
||||
ssl = true
|
||||
}
|
||||
}
|
||||
bar {}
|
||||
baz {
|
||||
features {
|
||||
ssl = false
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
// Per-package overrides can also live in
|
||||
// /kappa/system/builds/<name>.kap
|
||||
*/
|
||||
}
|
||||
|
||||
services {
|
||||
nginx {
|
||||
enable = true
|
||||
port = 80
|
||||
ssl = true
|
||||
}
|
||||
sshd {
|
||||
enable = true
|
||||
port = 22
|
||||
}
|
||||
cron {
|
||||
enable = false
|
||||
}
|
||||
}
|
||||
|
||||
boot {
|
||||
kernel = "linux"
|
||||
init = "s6"
|
||||
efi = "/dev/sda2"
|
||||
swap = "/dev/sda3"
|
||||
bootloader = "limine"
|
||||
root = "/dev/sda1"
|
||||
}
|
||||
|
||||
users {
|
||||
specter {
|
||||
shell = "/bin/zsh"
|
||||
groups = ["wheel", "audio", "docker"]
|
||||
}
|
||||
root {
|
||||
shell = "/bin/zsh"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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"
|
||||
}
|
||||
|
||||
service {
|
||||
runit {
|
||||
exec = "/usr/bin/foo --daemon"
|
||||
type = "forking"
|
||||
user = "foo"
|
||||
}
|
||||
s6 {
|
||||
exec = "/usr/bin/foo"
|
||||
type = "longrun"
|
||||
ports = [80, 443]
|
||||
user = "foo"
|
||||
}
|
||||
}
|
||||
|
||||
prepare {
|
||||
patch "fix-build.patch"
|
||||
}
|
||||
|
||||
build {
|
||||
./configure --prefix=${prefix} ${feature.ssl} ${feature.gui}
|
||||
make -j${jobs}
|
||||
}
|
||||
|
||||
check {
|
||||
make check
|
||||
}
|
||||
|
||||
install {
|
||||
make DESTDIR=${destdir} install
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace kappa::cli {
|
||||
|
||||
struct SourceLocation {
|
||||
std::string_view file;
|
||||
int line = 0;
|
||||
int col = 0;
|
||||
};
|
||||
|
||||
void print_error(std::ostream& os,
|
||||
std::string_view source,
|
||||
SourceLocation loc,
|
||||
std::string_view message);
|
||||
|
||||
void print_error(std::ostream& os,
|
||||
std::string_view source,
|
||||
SourceLocation loc,
|
||||
std::string_view message,
|
||||
std::string_view help);
|
||||
|
||||
void print_note(std::ostream& os,
|
||||
std::string_view source,
|
||||
SourceLocation loc,
|
||||
std::string_view message);
|
||||
|
||||
} // namespace kappa::cli
|
||||
@@ -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
|
||||
@@ -0,0 +1,83 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace kappa::dsl {
|
||||
|
||||
using Command = std::string;
|
||||
|
||||
struct Phase {
|
||||
std::vector<Command> 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<std::string, std::string> 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 ServiceInit {
|
||||
std::string exec;
|
||||
std::string type;
|
||||
std::string user;
|
||||
std::vector<int> ports;
|
||||
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;
|
||||
std::string source;
|
||||
std::string license;
|
||||
std::vector<Dependency> depends;
|
||||
std::vector<std::string> provides;
|
||||
std::vector<std::string> outputs;
|
||||
std::unordered_map<std::string, FeatureDef> features;
|
||||
std::vector<ConfigFile> config_files;
|
||||
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;
|
||||
Phase check;
|
||||
Phase install;
|
||||
};
|
||||
|
||||
} // namespace kappa::dsl
|
||||
@@ -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
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include "kappa/dsl/token.hpp"
|
||||
|
||||
#include <string_view>
|
||||
|
||||
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
|
||||
@@ -0,0 +1,12 @@
|
||||
#pragma once
|
||||
|
||||
#include "kappa/dsl/ast.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace kappa::dsl {
|
||||
|
||||
PackageDef parse(std::string_view source);
|
||||
|
||||
} // namespace kappa::dsl
|
||||
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#include "kappa/dsl/ast.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace kappa::dsl {
|
||||
|
||||
struct RollbackConfig {
|
||||
int keep = 0;
|
||||
};
|
||||
|
||||
struct SystemBlock {
|
||||
std::string hostname;
|
||||
std::string timezone;
|
||||
std::vector<EnvEntry> env;
|
||||
std::unordered_map<std::string, std::string> config;
|
||||
std::unordered_map<std::string, FeatureDef> features;
|
||||
RollbackConfig rollback;
|
||||
};
|
||||
|
||||
struct PackageRef {
|
||||
std::string name;
|
||||
std::string version;
|
||||
std::unordered_map<std::string, FeatureDef> features;
|
||||
std::unordered_map<std::string, std::string> config;
|
||||
};
|
||||
|
||||
struct BootBlock {
|
||||
std::string kernel;
|
||||
std::string init;
|
||||
std::string efi;
|
||||
std::string swap;
|
||||
std::string root;
|
||||
std::string bootloader;
|
||||
std::unordered_map<std::string, std::string> params;
|
||||
};
|
||||
|
||||
struct UserRef {
|
||||
std::string name;
|
||||
std::string shell;
|
||||
std::vector<std::string> groups;
|
||||
std::unordered_map<std::string, std::string> extra;
|
||||
};
|
||||
|
||||
struct ServiceRef {
|
||||
std::string name;
|
||||
bool enable = false;
|
||||
std::unordered_map<std::string, std::string> config;
|
||||
};
|
||||
|
||||
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
|
||||
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
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,
|
||||
KwService,
|
||||
KwAssert,
|
||||
KwImport,
|
||||
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
|
||||
@@ -0,0 +1,37 @@
|
||||
#pragma once
|
||||
|
||||
#include "kappa/dsl/ast.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
|
||||
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<std::string, std::string> builtins;
|
||||
std::unordered_map<std::string, std::string> config;
|
||||
std::unordered_map<std::string, dsl::FeatureDef> features;
|
||||
std::unordered_map<std::string, std::string> 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
|
||||
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
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
|
||||
@@ -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
|
||||
@@ -0,0 +1,94 @@
|
||||
#include "kappa/cli/diagnostic.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <format>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
namespace kappa::cli {
|
||||
|
||||
using namespace std::string_view_literals;
|
||||
|
||||
static constexpr auto bold = "\033[1m"sv;
|
||||
static constexpr auto red = "\033[31m"sv;
|
||||
static constexpr auto cyan = "\033[36m"sv;
|
||||
static constexpr auto yellow = "\033[33m"sv;
|
||||
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 = 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 (start == std::string_view::npos) { return ""sv; }
|
||||
|
||||
auto end = source.find('\n', start);
|
||||
if (end == std::string_view::npos) { end = source.size(); }
|
||||
return source.substr(start, end - start);
|
||||
}
|
||||
|
||||
static void print_header(std::ostream& os,
|
||||
std::string_view severity,
|
||||
std::string_view color,
|
||||
std::string_view file,
|
||||
int line, int col,
|
||||
std::string_view message) {
|
||||
os << color << bold << severity << ": " << reset
|
||||
<< color << message << reset << '\n'
|
||||
<< " " << cyan << "\u250C\u2500 " << file << ':' << line << ':' << col
|
||||
<< reset << '\n';
|
||||
}
|
||||
|
||||
void print_error(std::ostream& os,
|
||||
std::string_view source,
|
||||
SourceLocation loc,
|
||||
std::string_view message) {
|
||||
print_header(os, "error", red, loc.file, loc.line, loc.col, message);
|
||||
|
||||
auto line_src = line_at(source, loc.line);
|
||||
if (!line_src.empty()) {
|
||||
auto line_num = std::format("{}", loc.line);
|
||||
os << " " << cyan << "\u2502" << reset << '\n'
|
||||
<< ' ' << line_num << " " << cyan << "\u2502 " << reset
|
||||
<< line_src << '\n';
|
||||
|
||||
os << " " << std::string(line_num.size(), ' ')
|
||||
<< ' ' << cyan << "\u2502 " << reset;
|
||||
for (int i = 0; i < loc.col - 1; ++i) { os << ' '; }
|
||||
os << red << bold << '^' << reset << '\n';
|
||||
}
|
||||
|
||||
os << " " << cyan << "\u2502" << reset << '\n';
|
||||
}
|
||||
|
||||
void print_error(std::ostream& os,
|
||||
std::string_view source,
|
||||
SourceLocation loc,
|
||||
std::string_view message,
|
||||
std::string_view help) {
|
||||
print_error(os, source, loc, message);
|
||||
os << " " << cyan << "\u2570\u2500\u2500 " << reset
|
||||
<< yellow << "help: " << reset << help << '\n';
|
||||
}
|
||||
|
||||
void print_note(std::ostream& os,
|
||||
std::string_view source,
|
||||
SourceLocation loc,
|
||||
std::string_view message) {
|
||||
print_header(os, "note", cyan, loc.file, loc.line, loc.col, message);
|
||||
auto line_src = line_at(source, loc.line);
|
||||
if (!line_src.empty()) {
|
||||
auto line_num = std::format("{}", loc.line);
|
||||
os << " " << cyan << "\u2502" << reset << '\n'
|
||||
<< ' ' << line_num << " " << cyan << "\u2502 " << reset
|
||||
<< line_src << '\n'
|
||||
<< " " << std::string(line_num.size(), ' ')
|
||||
<< ' ' << cyan << "\u2502" << reset << '\n';
|
||||
}
|
||||
os << " " << cyan << "\u2570\u2500\u2500 " << reset
|
||||
<< cyan << "note: " << reset << message << '\n';
|
||||
}
|
||||
|
||||
} // namespace kappa::cli
|
||||
@@ -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
|
||||
@@ -0,0 +1,197 @@
|
||||
#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},
|
||||
{"service", TokenType::KwService},
|
||||
{"assert", TokenType::KwAssert},
|
||||
{"import", TokenType::KwImport},
|
||||
{"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::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";
|
||||
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,495 @@
|
||||
#include "kappa/dsl/parser.hpp"
|
||||
#include "kappa/dsl/error.hpp"
|
||||
#include "kappa/dsl/lexer.hpp"
|
||||
|
||||
#include <format>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace kappa::dsl {
|
||||
|
||||
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::KwService:
|
||||
consume(TokenType::KwService);
|
||||
consume(TokenType::Lbrace);
|
||||
skip_newlines();
|
||||
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||
if (at(TokenType::Newline)) { advance(); continue; }
|
||||
auto init_name = current_.lexeme;
|
||||
advance();
|
||||
consume(TokenType::Lbrace);
|
||||
skip_newlines();
|
||||
ServiceInit si;
|
||||
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||
if (at(TokenType::Newline)) { advance(); continue; }
|
||||
auto key = current_.lexeme;
|
||||
advance();
|
||||
consume(TokenType::Equals);
|
||||
if (key == "exec") {
|
||||
si.exec = consume(TokenType::String).lexeme;
|
||||
} else if (key == "type") {
|
||||
si.type = consume(TokenType::String).lexeme;
|
||||
} else if (key == "user") {
|
||||
si.user = consume(TokenType::String).lexeme;
|
||||
} else if (key == "ports") {
|
||||
consume(TokenType::Lbracket);
|
||||
skip_newlines();
|
||||
while (!at(TokenType::Rbracket) && !at(TokenType::Eof)) {
|
||||
si.ports.push_back(
|
||||
parse_int(current_.line, current_.col,
|
||||
current_.lexeme));
|
||||
advance();
|
||||
skip_newlines();
|
||||
if (at(TokenType::Comma)) { consume(TokenType::Comma); }
|
||||
skip_newlines();
|
||||
}
|
||||
consume(TokenType::Rbracket);
|
||||
} else {
|
||||
si.env[key] = consume(TokenType::String).lexeme;
|
||||
}
|
||||
skip_newlines();
|
||||
}
|
||||
consume(TokenType::Rbrace);
|
||||
pkg.service[std::string(init_name)] = std::move(si);
|
||||
skip_newlines();
|
||||
}
|
||||
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();
|
||||
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 = parse_int(current_.line, current_.col,
|
||||
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,439 @@
|
||||
#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 SysParser {
|
||||
public:
|
||||
explicit SysParser(std::string_view source) : lexer_(source) { advance(); }
|
||||
|
||||
SystemConfig parse();
|
||||
|
||||
private:
|
||||
void advance();
|
||||
Token consume(TokenType type);
|
||||
void skip_newlines();
|
||||
bool at(TokenType type) const { return current_.type == type; }
|
||||
|
||||
bool parse_bool();
|
||||
void parse_system_block(SystemConfig& cfg);
|
||||
void parse_packages_block(SystemConfig& cfg);
|
||||
void parse_services_block(SystemConfig& cfg);
|
||||
void parse_boot_block(SystemConfig& cfg);
|
||||
void parse_users_block(SystemConfig& cfg);
|
||||
|
||||
std::string consume_ident();
|
||||
std::string consume_string();
|
||||
|
||||
Token current_;
|
||||
Lexer lexer_;
|
||||
};
|
||||
|
||||
void SysParser::advance() { current_ = lexer_.next(); }
|
||||
|
||||
void SysParser::skip_newlines() {
|
||||
while (at(TokenType::Newline)) { advance(); }
|
||||
}
|
||||
|
||||
Token SysParser::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;
|
||||
}
|
||||
|
||||
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) ||
|
||||
at(TokenType::KwSource) ||
|
||||
at(TokenType::KwLicense) ||
|
||||
at(TokenType::KwPatches) ||
|
||||
at(TokenType::KwConst) ||
|
||||
at(TokenType::KwPackage) ||
|
||||
at(TokenType::KwDepends) ||
|
||||
at(TokenType::KwProvides) ||
|
||||
at(TokenType::KwOutputs) ||
|
||||
at(TokenType::KwPrepare) ||
|
||||
at(TokenType::KwBuild) ||
|
||||
at(TokenType::KwCheck) ||
|
||||
at(TokenType::KwInstall) ||
|
||||
at(TokenType::KwTrue) ||
|
||||
at(TokenType::KwFalse)) {
|
||||
auto lexeme = current_.lexeme;
|
||||
advance();
|
||||
return lexeme;
|
||||
}
|
||||
throw ParseError(current_.line, current_.col,
|
||||
std::format("expected identifier, got '{}'",
|
||||
token_name(current_.type)));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
bool SysParser::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)));
|
||||
}
|
||||
|
||||
SystemConfig SysParser::parse() {
|
||||
skip_newlines();
|
||||
SystemConfig cfg;
|
||||
|
||||
while (!at(TokenType::Eof)) {
|
||||
skip_newlines();
|
||||
if (at(TokenType::Eof)) { break; }
|
||||
|
||||
auto kw = consume_ident();
|
||||
|
||||
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); }
|
||||
else if (kw == "users") { parse_users_block(cfg); }
|
||||
else {
|
||||
throw ParseError(current_.line, current_.col,
|
||||
std::format("unknown section '{}'", kw));
|
||||
}
|
||||
}
|
||||
|
||||
return cfg;
|
||||
}
|
||||
|
||||
void SysParser::parse_system_block(SystemConfig& cfg) {
|
||||
consume(TokenType::Lbrace);
|
||||
skip_newlines();
|
||||
|
||||
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||
skip_newlines();
|
||||
if (at(TokenType::Rbrace)) { break; }
|
||||
|
||||
auto key = consume_ident();
|
||||
if (key == "hostname") {
|
||||
consume(TokenType::Equals);
|
||||
cfg.system.hostname = consume_string();
|
||||
} else if (key == "timezone") {
|
||||
consume(TokenType::Equals);
|
||||
cfg.system.timezone = consume_string();
|
||||
} else if (key == "env") {
|
||||
consume(TokenType::Lbrace);
|
||||
skip_newlines();
|
||||
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||
if (at(TokenType::Newline)) { advance(); continue; }
|
||||
auto k = consume_ident();
|
||||
bool soft = false;
|
||||
if (at(TokenType::Ident) && current_.lexeme == "?=") {
|
||||
soft = true;
|
||||
advance();
|
||||
} else {
|
||||
consume(TokenType::Equals);
|
||||
}
|
||||
cfg.system.env.push_back({std::move(k), consume_string(), soft});
|
||||
skip_newlines();
|
||||
}
|
||||
consume(TokenType::Rbrace);
|
||||
} else if (key == "config") {
|
||||
consume(TokenType::Lbrace);
|
||||
skip_newlines();
|
||||
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||
if (at(TokenType::Newline)) { advance(); continue; }
|
||||
auto k = consume_ident();
|
||||
consume(TokenType::Equals);
|
||||
cfg.system.config[k] = consume_string();
|
||||
skip_newlines();
|
||||
}
|
||||
consume(TokenType::Rbrace);
|
||||
} else if (key == "features") {
|
||||
consume(TokenType::Lbrace);
|
||||
skip_newlines();
|
||||
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||
if (at(TokenType::Newline)) { advance(); continue; }
|
||||
auto k = consume_ident();
|
||||
consume(TokenType::Equals);
|
||||
cfg.system.features[k] = FeatureDef{parse_bool(), false, ""};
|
||||
skip_newlines();
|
||||
}
|
||||
consume(TokenType::Rbrace);
|
||||
} else if (key == "rollback") {
|
||||
consume(TokenType::Lbrace);
|
||||
skip_newlines();
|
||||
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||
if (at(TokenType::Newline)) { advance(); continue; }
|
||||
auto k = consume_ident();
|
||||
consume(TokenType::Equals);
|
||||
if (k == "keep") {
|
||||
cfg.system.rollback.keep = parse_int(current_.line, current_.col,
|
||||
current_.lexeme);
|
||||
advance();
|
||||
}
|
||||
skip_newlines();
|
||||
}
|
||||
consume(TokenType::Rbrace);
|
||||
} else {
|
||||
throw ParseError(current_.line, current_.col,
|
||||
std::format("unknown system key '{}'", key));
|
||||
}
|
||||
}
|
||||
consume(TokenType::Rbrace);
|
||||
}
|
||||
|
||||
void SysParser::parse_packages_block(SystemConfig& cfg) {
|
||||
consume(TokenType::Lbrace);
|
||||
skip_newlines();
|
||||
|
||||
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||
skip_newlines();
|
||||
if (at(TokenType::Rbrace)) { break; }
|
||||
|
||||
PackageRef pkg;
|
||||
pkg.name = consume_ident();
|
||||
|
||||
if (at(TokenType::Lbrace)) {
|
||||
consume(TokenType::Lbrace);
|
||||
skip_newlines();
|
||||
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||
skip_newlines();
|
||||
if (at(TokenType::Rbrace)) { break; }
|
||||
auto key = consume_ident();
|
||||
if (key == "version") {
|
||||
consume(TokenType::Equals);
|
||||
pkg.version = consume_string();
|
||||
} else if (key == "features") {
|
||||
consume(TokenType::Lbrace);
|
||||
skip_newlines();
|
||||
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||
if (at(TokenType::Newline)) { advance(); continue; }
|
||||
auto k = consume_ident();
|
||||
consume(TokenType::Equals);
|
||||
pkg.features[k] = FeatureDef{parse_bool(), false, ""};
|
||||
skip_newlines();
|
||||
}
|
||||
consume(TokenType::Rbrace);
|
||||
} else if (key == "config") {
|
||||
consume(TokenType::Lbrace);
|
||||
skip_newlines();
|
||||
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||
if (at(TokenType::Newline)) { advance(); continue; }
|
||||
auto k = consume_ident();
|
||||
consume(TokenType::Equals);
|
||||
pkg.config[k] = current_.lexeme;
|
||||
advance();
|
||||
skip_newlines();
|
||||
}
|
||||
consume(TokenType::Rbrace);
|
||||
}
|
||||
}
|
||||
consume(TokenType::Rbrace);
|
||||
}
|
||||
cfg.packages.push_back(std::move(pkg));
|
||||
}
|
||||
consume(TokenType::Rbrace);
|
||||
}
|
||||
|
||||
void SysParser::parse_services_block(SystemConfig& cfg) {
|
||||
consume(TokenType::Lbrace);
|
||||
skip_newlines();
|
||||
|
||||
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||
skip_newlines();
|
||||
if (at(TokenType::Rbrace)) { break; }
|
||||
|
||||
ServiceRef svc;
|
||||
svc.name = consume_ident();
|
||||
|
||||
if (at(TokenType::Lbrace)) {
|
||||
consume(TokenType::Lbrace);
|
||||
skip_newlines();
|
||||
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||
if (at(TokenType::Newline)) { advance(); continue; }
|
||||
auto key = consume_ident();
|
||||
if (key == "enable") {
|
||||
consume(TokenType::Equals);
|
||||
svc.enable = parse_bool();
|
||||
} else {
|
||||
consume(TokenType::Equals);
|
||||
svc.config[key] = current_.lexeme;
|
||||
advance();
|
||||
}
|
||||
skip_newlines();
|
||||
}
|
||||
consume(TokenType::Rbrace);
|
||||
}
|
||||
cfg.services.push_back(std::move(svc));
|
||||
}
|
||||
consume(TokenType::Rbrace);
|
||||
}
|
||||
|
||||
void SysParser::parse_boot_block(SystemConfig& cfg) {
|
||||
consume(TokenType::Lbrace);
|
||||
skip_newlines();
|
||||
|
||||
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||
skip_newlines();
|
||||
if (at(TokenType::Rbrace)) { break; }
|
||||
auto key = consume_ident();
|
||||
consume(TokenType::Equals);
|
||||
auto val = consume_string();
|
||||
if (key == "kernel") { cfg.boot.kernel = 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 == "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();
|
||||
}
|
||||
consume(TokenType::Rbrace);
|
||||
}
|
||||
|
||||
void SysParser::parse_users_block(SystemConfig& cfg) {
|
||||
consume(TokenType::Lbrace);
|
||||
skip_newlines();
|
||||
|
||||
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||
skip_newlines();
|
||||
if (at(TokenType::Rbrace)) { break; }
|
||||
|
||||
UserRef u;
|
||||
u.name = consume_ident();
|
||||
|
||||
if (at(TokenType::Lbrace)) {
|
||||
consume(TokenType::Lbrace);
|
||||
skip_newlines();
|
||||
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||
if (at(TokenType::Newline)) { advance(); continue; }
|
||||
auto key = consume_ident();
|
||||
if (key == "shell") {
|
||||
consume(TokenType::Equals);
|
||||
u.shell = consume_string();
|
||||
} else if (key == "groups") {
|
||||
consume(TokenType::Equals);
|
||||
consume(TokenType::Lbracket);
|
||||
skip_newlines();
|
||||
while (!at(TokenType::Rbracket) && !at(TokenType::Eof)) {
|
||||
u.groups.push_back(consume_string());
|
||||
skip_newlines();
|
||||
if (at(TokenType::Comma)) { consume(TokenType::Comma); }
|
||||
skip_newlines();
|
||||
}
|
||||
consume(TokenType::Rbracket);
|
||||
} else {
|
||||
consume(TokenType::Equals);
|
||||
u.extra[key] = consume_string();
|
||||
}
|
||||
skip_newlines();
|
||||
}
|
||||
consume(TokenType::Rbrace);
|
||||
}
|
||||
cfg.users.push_back(std::move(u));
|
||||
}
|
||||
consume(TokenType::Rbrace);
|
||||
}
|
||||
|
||||
SystemConfig parse_system_config(std::string_view source) {
|
||||
SysParser p(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
|
||||
@@ -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
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
#include "kappa/cli/diagnostic.hpp"
|
||||
#include "kappa/dsl/parser.hpp"
|
||||
#include "kappa/dsl/system.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string_view>
|
||||
|
||||
using namespace std::string_view_literals;
|
||||
using namespace kappa;
|
||||
|
||||
static constexpr auto version = "kappa 0.1.0"sv;
|
||||
|
||||
static void print_usage() {
|
||||
std::cout << R"(kappa — a declarative source-based package manager
|
||||
|
||||
Usage:
|
||||
kappa <subcommand> [options] <file>
|
||||
|
||||
Subcommands:
|
||||
parse-package <file> Parse and validate a package definition (.kap)
|
||||
parse-config <file> Parse and validate a system configuration
|
||||
validate <file> Validate any kappa file (package or config)
|
||||
|
||||
Options:
|
||||
-h, --help Show this help message
|
||||
-V, --version Show version information
|
||||
)";
|
||||
}
|
||||
|
||||
static bool is_flag(std::string_view arg) {
|
||||
return arg == "-h" || arg == "--help" || arg == "-V" || arg == "--version";
|
||||
}
|
||||
|
||||
static std::string read_file(const char* path) {
|
||||
std::ifstream in(path);
|
||||
if (!in) {
|
||||
std::cerr << "error: cannot open '" << path << "'\n";
|
||||
std::exit(1);
|
||||
}
|
||||
std::ostringstream buf;
|
||||
buf << in.rdbuf();
|
||||
return buf.str();
|
||||
}
|
||||
|
||||
static void handle_parse_error(const char* path,
|
||||
std::string_view source,
|
||||
const std::runtime_error& e) {
|
||||
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) {
|
||||
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");
|
||||
return;
|
||||
} catch (const std::exception&) {}
|
||||
}
|
||||
std::cerr << "error: " << e.what() << '\n';
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
if (argc < 2) {
|
||||
std::cerr << "kappa: missing subcommand\n\n";
|
||||
print_usage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto subcommand = std::string_view(argv[1]);
|
||||
|
||||
if (subcommand == "-h" || subcommand == "--help") {
|
||||
print_usage();
|
||||
return 0;
|
||||
}
|
||||
if (subcommand == "-V" || subcommand == "--version") {
|
||||
std::cout << version << '\n';
|
||||
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) {
|
||||
if (!is_flag(argv[i])) {
|
||||
file_arg = argv[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (file_arg == nullptr) {
|
||||
std::cerr << "error: no input file specified\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto source = read_file(file_arg);
|
||||
|
||||
if (subcommand == "parse-package") {
|
||||
try {
|
||||
auto pkg = dsl::parse(source);
|
||||
std::cout << "package \"" << pkg.name << "\" " << pkg.version
|
||||
<< " — valid\n";
|
||||
return 0;
|
||||
} catch (const std::runtime_error& e) {
|
||||
handle_parse_error(file_arg, source, e);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (subcommand == "parse-config") {
|
||||
try {
|
||||
auto cfg = dsl::parse_system_config(source);
|
||||
std::cout << "system config — valid ("
|
||||
<< cfg.packages.size() << " packages, "
|
||||
<< cfg.services.size() << " services, "
|
||||
<< cfg.users.size() << " users)\n";
|
||||
return 0;
|
||||
} catch (const std::runtime_error& e) {
|
||||
handle_parse_error(file_arg, source, e);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (subcommand == "validate") {
|
||||
try {
|
||||
dsl::parse(source);
|
||||
std::cout << file_arg << ": valid package definition\n";
|
||||
return 0;
|
||||
} catch (const std::runtime_error&) {
|
||||
try {
|
||||
dsl::parse_system_config(source);
|
||||
std::cout << file_arg << ": valid system configuration\n";
|
||||
return 0;
|
||||
} catch (const std::runtime_error& e) {
|
||||
handle_parse_error(file_arg, source, e);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 1; // unreachable
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json",
|
||||
"name": "kappa",
|
||||
"version": "0.1.0",
|
||||
"dependencies": []
|
||||
}
|
||||
Reference in New Issue
Block a user