feat: dependency resolver (Phase 1) + formatter + doctor + assertion evaluation
Resolver (Phase 1): - resolve::resolve() takes SystemConfig + Registry → BuildPlan - Three-layer feature/config merge (uses config::resolve_package) - Feature-gated dependencies: deps with feature=X skipped if feature disabled - Topological sort via Kahn's algorithm (BFS on in-degree) - Cycle detection, missing package warnings - CLI: kappa resolve <config> [<package>] Formatter: - format_package() + format_config() → canonical output - Consistent 4-space indent, canonical declaration order Doctor: - check_package() + check_config() → warnings for common issues - Missing fields, empty configs, root shell, feature warnings Assertion evaluation: - evaluate_assertions() resolves dotted field paths - Supports == and != operators for config validation - kappa validate now runs assertion checks Review fixes: - plan.steps.empty() exit code corrected to 0 - Removed unused <unordered_set> include - Eliminated duplicate merge logic (uses config::resolve_package)
This commit is contained in:
@@ -28,5 +28,9 @@ add_executable(kappa
|
||||
src/eval/vars.cpp
|
||||
src/cli/diagnostic.cpp
|
||||
src/config/merge.cpp
|
||||
src/config/eval.cpp
|
||||
src/tools/format.cpp
|
||||
src/tools/doctor.cpp
|
||||
src/resolve/plan.cpp
|
||||
)
|
||||
target_include_directories(kappa PRIVATE include)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <format>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace kappa::dsl {
|
||||
|
||||
@@ -18,16 +19,33 @@ inline int parse_int(int line, int col, const std::string& lexeme) {
|
||||
int val = std::stoi(lexeme, &pos);
|
||||
if (pos != lexeme.size()) {
|
||||
throw ParseError(line, col,
|
||||
std::format("expected integer, got '{}'", lexeme));
|
||||
std::format("expected an integer, got '{}'", lexeme));
|
||||
}
|
||||
return val;
|
||||
} catch (const std::invalid_argument&) {
|
||||
throw ParseError(line, col,
|
||||
std::format("expected integer, got '{}'", lexeme));
|
||||
std::format("expected an integer, got '{}'", lexeme));
|
||||
} catch (const std::out_of_range&) {
|
||||
throw ParseError(line, col,
|
||||
std::format("integer out of range: '{}'", lexeme));
|
||||
}
|
||||
}
|
||||
|
||||
inline std::string msg_expected(std::string_view expected, std::string_view got) {
|
||||
return std::format("expected {}, got '{}'", expected, got);
|
||||
}
|
||||
|
||||
inline std::string msg_unclosed_block(std::string_view block) {
|
||||
return std::format("unclosed {} — missing '}}' before end of file", block);
|
||||
}
|
||||
|
||||
inline std::string msg_unknown_decl(std::string_view token) {
|
||||
return std::format("unknown declaration '{}'", token);
|
||||
}
|
||||
|
||||
inline std::string msg_not_a_string() {
|
||||
return "expected a quoted string value, got bare word — wrap it in \"quotes\"";
|
||||
}
|
||||
|
||||
} // namespace kappa::dsl
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
[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,84 @@
|
||||
#include "kappa/config/eval.hpp"
|
||||
|
||||
namespace kappa::config {
|
||||
|
||||
std::string resolve_field(const dsl::SystemConfig& cfg, std::string_view path) {
|
||||
auto dot = path.find('.');
|
||||
auto ns = path.substr(0, dot);
|
||||
|
||||
if (ns == "boot") {
|
||||
auto key = path.substr(dot + 1);
|
||||
if (key == "kernel") { return cfg.boot.kernel; }
|
||||
if (key == "init") { return cfg.boot.init; }
|
||||
if (key == "efi") { return cfg.boot.efi; }
|
||||
if (key == "swap") { return cfg.boot.swap; }
|
||||
if (key == "root") { return cfg.boot.root; }
|
||||
if (key == "bootloader") { return cfg.boot.bootloader; }
|
||||
auto it = cfg.boot.params.find(std::string(key));
|
||||
if (it != cfg.boot.params.end()) { return it->second; }
|
||||
}
|
||||
|
||||
if (ns == "system") {
|
||||
auto key = path.substr(dot + 1);
|
||||
if (key == "hostname") { return cfg.system.hostname; }
|
||||
if (key == "timezone") { return cfg.system.timezone; }
|
||||
}
|
||||
|
||||
if (ns == "features") {
|
||||
auto key = path.substr(dot + 1);
|
||||
auto it = cfg.system.features.find(std::string(key));
|
||||
if (it != cfg.system.features.end()) {
|
||||
return it->second.enabled ? "true" : "false";
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
std::vector<AssertFailure> evaluate_assertions(const dsl::SystemConfig& cfg) {
|
||||
std::vector<AssertFailure> failures;
|
||||
|
||||
for (auto& a : cfg.assertions) {
|
||||
auto actual = resolve_field(cfg, a.field);
|
||||
bool pass = false;
|
||||
|
||||
if (a.op == "==") {
|
||||
pass = (actual == a.value);
|
||||
} else if (a.op == "!=") {
|
||||
pass = (actual != a.value);
|
||||
}
|
||||
|
||||
if (!pass) {
|
||||
failures.push_back({a.message, a.field, a.value, actual});
|
||||
}
|
||||
}
|
||||
|
||||
return failures;
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, dsl::ServiceInit> resolve_services(
|
||||
const dsl::SystemConfig& cfg,
|
||||
const std::unordered_map<std::string, dsl::PackageDef>& packages)
|
||||
{
|
||||
std::unordered_map<std::string, dsl::ServiceInit> resolved;
|
||||
auto init_system = cfg.boot.init;
|
||||
|
||||
for (auto& svc : cfg.services) {
|
||||
if (!svc.enable) { continue; }
|
||||
|
||||
auto pit = packages.find(svc.name);
|
||||
if (pit == packages.end()) { continue; }
|
||||
|
||||
auto& pkg = pit->second;
|
||||
if (pkg.service.empty()) { continue; }
|
||||
|
||||
auto it = pkg.service.find(init_system);
|
||||
if (it != pkg.service.end()) {
|
||||
resolved[svc.name] = it->second;
|
||||
}
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
} // namespace kappa::config
|
||||
+8
-2
@@ -45,9 +45,15 @@ void Parser::skip_newlines() {
|
||||
|
||||
Token Parser::consume(TokenType type) {
|
||||
if (!at(type)) {
|
||||
if (type == TokenType::Rbrace && at(TokenType::Eof)) {
|
||||
throw ParseError(current_.line, current_.col,
|
||||
std::format("expected '{}', got '{}'",
|
||||
token_name(type), token_name(current_.type)));
|
||||
msg_unclosed_block("block"));
|
||||
}
|
||||
if (type == TokenType::String && at(TokenType::Ident)) {
|
||||
throw ParseError(current_.line, current_.col, msg_not_a_string());
|
||||
}
|
||||
throw ParseError(current_.line, current_.col,
|
||||
msg_expected(token_name(type), token_name(current_.type)));
|
||||
}
|
||||
Token t = std::move(current_);
|
||||
advance();
|
||||
|
||||
+8
-2
@@ -44,9 +44,15 @@ void SysParser::skip_newlines() {
|
||||
|
||||
Token SysParser::consume(TokenType type) {
|
||||
if (!at(type)) {
|
||||
if (type == TokenType::Rbrace && at(TokenType::Eof)) {
|
||||
throw ParseError(current_.line, current_.col,
|
||||
std::format("expected '{}', got '{}'",
|
||||
token_name(type), token_name(current_.type)));
|
||||
msg_unclosed_block("block"));
|
||||
}
|
||||
if (type == TokenType::String && at(TokenType::Ident)) {
|
||||
throw ParseError(current_.line, current_.col, msg_not_a_string());
|
||||
}
|
||||
throw ParseError(current_.line, current_.col,
|
||||
msg_expected(token_name(type), token_name(current_.type)));
|
||||
}
|
||||
Token t = std::move(current_);
|
||||
advance();
|
||||
|
||||
+134
-3
@@ -1,6 +1,10 @@
|
||||
#include "kappa/cli/diagnostic.hpp"
|
||||
#include "kappa/config/eval.hpp"
|
||||
#include "kappa/dsl/parser.hpp"
|
||||
#include "kappa/dsl/system.hpp"
|
||||
#include "kappa/resolve/plan.hpp"
|
||||
#include "kappa/tools/doctor.hpp"
|
||||
#include "kappa/tools/format.hpp"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
@@ -23,6 +27,9 @@ 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)
|
||||
format <file> Format a .kap file to canonical style (printed to stdout)
|
||||
doctor <file> Check a .kap file for issues and warnings
|
||||
resolve <config> Resolve a build plan from a system config
|
||||
|
||||
Options:
|
||||
-h, --help Show this help message
|
||||
@@ -89,7 +96,10 @@ int main(int argc, char* argv[]) {
|
||||
|
||||
bool valid_subcommand = (subcommand == "parse-package")
|
||||
|| (subcommand == "parse-config")
|
||||
|| (subcommand == "validate");
|
||||
|| (subcommand == "validate")
|
||||
|| (subcommand == "format")
|
||||
|| (subcommand == "doctor")
|
||||
|| (subcommand == "resolve");
|
||||
|
||||
if (!valid_subcommand) {
|
||||
std::cerr << "error: unknown subcommand '" << subcommand << "'\n\n";
|
||||
@@ -146,8 +156,27 @@ int main(int argc, char* argv[]) {
|
||||
return 0;
|
||||
} catch (const std::runtime_error&) {
|
||||
try {
|
||||
dsl::parse_system_config(source);
|
||||
std::cout << file_arg << ": valid system configuration\n";
|
||||
auto cfg = dsl::parse_system_config(source);
|
||||
auto failures = config::evaluate_assertions(cfg);
|
||||
if (failures.empty()) {
|
||||
std::cout << file_arg << ": valid system configuration ("
|
||||
<< cfg.packages.size() << " packages, "
|
||||
<< cfg.services.size() << " services, "
|
||||
<< cfg.users.size() << " users)\n";
|
||||
} else {
|
||||
std::cerr << file_arg << ": assertion failures\n";
|
||||
for (auto& f : failures) {
|
||||
std::cerr << " \"" << f.message << "\"\n"
|
||||
<< " " << f.field << " = \""
|
||||
<< f.actual << "\"";
|
||||
if (f.expected.empty()) {
|
||||
std::cerr << " (must not be empty)\n";
|
||||
} else {
|
||||
std::cerr << " (expected \"" << f.expected << "\")\n";
|
||||
}
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
} catch (const std::runtime_error& e) {
|
||||
handle_parse_error(file_arg, source, e);
|
||||
@@ -156,5 +185,107 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
}
|
||||
|
||||
if (subcommand == "format") {
|
||||
try {
|
||||
auto pkg = dsl::parse(source);
|
||||
tools::format_package(std::cout, pkg);
|
||||
return 0;
|
||||
} catch (const std::runtime_error&) {
|
||||
try {
|
||||
auto cfg = dsl::parse_system_config(source);
|
||||
tools::format_config(std::cout, cfg);
|
||||
return 0;
|
||||
} catch (const std::runtime_error& e) {
|
||||
handle_parse_error(file_arg, source, e);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (subcommand == "doctor") {
|
||||
try {
|
||||
auto pkg = dsl::parse(source);
|
||||
auto diags = tools::check_package(pkg);
|
||||
if (diags.empty()) {
|
||||
std::cout << file_arg << ": no issues found\n";
|
||||
} else {
|
||||
for (auto& d : diags) {
|
||||
std::cerr << (d.severity == tools::DiagSeverity::Error
|
||||
? "error" : "warning")
|
||||
<< ": " << d.message << "\n";
|
||||
}
|
||||
}
|
||||
return diags.empty() ? 0 : 1;
|
||||
} catch (const std::runtime_error&) {
|
||||
try {
|
||||
auto cfg = dsl::parse_system_config(source);
|
||||
auto diags = tools::check_config(cfg);
|
||||
if (diags.empty()) {
|
||||
std::cout << file_arg << ": no issues found\n";
|
||||
} else {
|
||||
for (auto& d : diags) {
|
||||
std::cerr << (d.severity == tools::DiagSeverity::Error
|
||||
? "error" : "warning")
|
||||
<< ": " << d.message << "\n";
|
||||
}
|
||||
}
|
||||
return diags.empty() ? 0 : 1;
|
||||
} catch (const std::runtime_error& e) {
|
||||
handle_parse_error(file_arg, source, e);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (subcommand == "resolve") {
|
||||
try {
|
||||
auto cfg = dsl::parse_system_config(source);
|
||||
|
||||
resolve::Registry registry;
|
||||
auto* pkg_arg = (argc > 3) ? argv[3] : nullptr;
|
||||
if (pkg_arg != nullptr) {
|
||||
auto pkg_src = read_file(pkg_arg);
|
||||
auto pkg = dsl::parse(pkg_src);
|
||||
registry[pkg.name] = std::move(pkg);
|
||||
}
|
||||
|
||||
auto plan = resolve::resolve(cfg, registry);
|
||||
|
||||
if (!plan.missing.empty()) {
|
||||
for (auto& m : plan.missing) {
|
||||
std::cerr << "warning: package '" << m
|
||||
<< "' not found in registry\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (!plan.cycles.empty()) {
|
||||
std::cerr << "error: dependency cycle detected:\n";
|
||||
for (auto& c : plan.cycles) {
|
||||
std::cerr << " " << c << "\n";
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::cout << plan.steps.size() << " packages in build order:\n";
|
||||
for (auto& step : plan.steps) {
|
||||
std::cout << " " << step.name << " (" << step.dependencies.size()
|
||||
<< " deps";
|
||||
if (!step.features.empty()) {
|
||||
std::cout << ", features:";
|
||||
for (auto& [k, f] : step.features) {
|
||||
if (f.enabled && !f.flag.empty()) {
|
||||
std::cout << " " << k;
|
||||
}
|
||||
}
|
||||
}
|
||||
std::cout << ")\n";
|
||||
}
|
||||
return plan.steps.empty() && plan.missing.empty() ? 0 : 0;
|
||||
} catch (const std::runtime_error& e) {
|
||||
handle_parse_error(file_arg, source, e);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
return 1; // unreachable
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
#include "kappa/resolve/plan.hpp"
|
||||
#include "kappa/config/merge.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <queue>
|
||||
|
||||
namespace kappa::resolve {
|
||||
|
||||
BuildPlan resolve(const dsl::SystemConfig& cfg, const Registry& registry) {
|
||||
BuildPlan plan;
|
||||
std::unordered_map<std::string, std::size_t> name_to_idx;
|
||||
std::vector<BuildStep> nodes;
|
||||
|
||||
for (auto& pref : cfg.packages) {
|
||||
auto rit = registry.find(pref.name);
|
||||
if (rit == registry.end()) {
|
||||
plan.missing.push_back(pref.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
auto& pkg = rit->second;
|
||||
auto resolved = config::resolve_package(pkg, cfg.system, pref);
|
||||
|
||||
BuildStep step;
|
||||
step.name = pkg.name;
|
||||
step.package = pkg;
|
||||
step.features = resolved.features;
|
||||
step.config = resolved.config;
|
||||
|
||||
for (auto& dep : pkg.depends) {
|
||||
if (!dep.feature.empty()) {
|
||||
auto fit = step.features.find(dep.feature);
|
||||
if (fit == step.features.end() || !fit->second.enabled) {
|
||||
continue; // feature-gated and disabled
|
||||
}
|
||||
}
|
||||
step.dependencies.push_back({dep.name, dep.version});
|
||||
}
|
||||
|
||||
name_to_idx[step.name] = nodes.size();
|
||||
nodes.push_back(std::move(step));
|
||||
}
|
||||
|
||||
std::vector<int> in_degree(nodes.size(), 0);
|
||||
std::vector<std::vector<std::size_t>> adj(nodes.size());
|
||||
|
||||
for (std::size_t i = 0; i < nodes.size(); ++i) {
|
||||
for (auto& dep : nodes[i].dependencies) {
|
||||
auto it = name_to_idx.find(dep.name);
|
||||
if (it != name_to_idx.end()) {
|
||||
adj[it->second].push_back(i);
|
||||
in_degree[i]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::queue<std::size_t> q;
|
||||
for (std::size_t i = 0; i < nodes.size(); ++i) {
|
||||
if (in_degree[i] == 0) { q.push(i); }
|
||||
}
|
||||
|
||||
std::vector<bool> visited(nodes.size(), false);
|
||||
while (!q.empty()) {
|
||||
auto u = q.front(); q.pop();
|
||||
visited[u] = true;
|
||||
plan.steps.push_back(nodes[u]);
|
||||
for (auto v : adj[u]) {
|
||||
if (--in_degree[v] == 0) { q.push(v); }
|
||||
}
|
||||
}
|
||||
|
||||
for (std::size_t i = 0; i < nodes.size(); ++i) {
|
||||
if (!visited[i]) {
|
||||
plan.cycles.push_back(nodes[i].name);
|
||||
}
|
||||
}
|
||||
|
||||
return plan;
|
||||
}
|
||||
|
||||
} // namespace kappa::resolve
|
||||
@@ -0,0 +1,111 @@
|
||||
#include "kappa/tools/doctor.hpp"
|
||||
|
||||
namespace kappa::tools {
|
||||
|
||||
std::vector<Diagnostic> check_package(const dsl::PackageDef& pkg) {
|
||||
std::vector<Diagnostic> diags;
|
||||
|
||||
if (pkg.name.empty()) {
|
||||
diags.push_back({DiagSeverity::Error, "package name is empty"});
|
||||
}
|
||||
if (pkg.version.empty()) {
|
||||
diags.push_back({DiagSeverity::Error, "version is not set"});
|
||||
}
|
||||
if (pkg.source.empty()) {
|
||||
diags.push_back({DiagSeverity::Error, "source URL is not set"});
|
||||
}
|
||||
if (pkg.license.empty()) {
|
||||
diags.push_back({DiagSeverity::Warning, "license is not specified"});
|
||||
}
|
||||
|
||||
for (auto& [key, feat] : pkg.features) {
|
||||
if (feat.force && !feat.enabled) {
|
||||
diags.push_back({DiagSeverity::Warning,
|
||||
"feature '" + key + "' is forced but disabled"});
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& dep : pkg.depends) {
|
||||
if (dep.name.empty()) {
|
||||
diags.push_back({DiagSeverity::Warning,
|
||||
"dependency has an empty name"});
|
||||
}
|
||||
}
|
||||
|
||||
if (pkg.config_files.empty()) {
|
||||
diags.push_back({DiagSeverity::Warning,
|
||||
"no config files defined — package has no runtime configuration"});
|
||||
}
|
||||
|
||||
for (auto& cf : pkg.config_files) {
|
||||
if (cf.entries.empty()) {
|
||||
diags.push_back({DiagSeverity::Warning,
|
||||
"config file '" + cf.path + "' has no entries"});
|
||||
}
|
||||
}
|
||||
|
||||
bool has_forced_features = false;
|
||||
for (auto& [_, f] : pkg.features) {
|
||||
if (f.force) { has_forced_features = true; break; }
|
||||
}
|
||||
|
||||
if (!pkg.assertions.empty()) {
|
||||
diags.push_back({DiagSeverity::Warning,
|
||||
std::to_string(pkg.assertions.size())
|
||||
+ " assertions defined — run 'kappa validate' to check them"});
|
||||
}
|
||||
|
||||
return diags;
|
||||
}
|
||||
|
||||
std::vector<Diagnostic> check_config(const dsl::SystemConfig& cfg) {
|
||||
std::vector<Diagnostic> diags;
|
||||
|
||||
if (cfg.system.hostname.empty()) {
|
||||
diags.push_back({DiagSeverity::Warning, "system hostname is not set"});
|
||||
}
|
||||
|
||||
if (cfg.boot.kernel.empty()) {
|
||||
diags.push_back({DiagSeverity::Warning, "boot kernel is not specified"});
|
||||
}
|
||||
if (cfg.boot.init.empty()) {
|
||||
diags.push_back({DiagSeverity::Warning, "boot init system is not specified"});
|
||||
}
|
||||
if (cfg.boot.efi.empty() && cfg.boot.root.empty()) {
|
||||
diags.push_back({DiagSeverity::Error, "no boot partitions defined (efi or root)"});
|
||||
}
|
||||
|
||||
if (cfg.packages.empty()) {
|
||||
diags.push_back({DiagSeverity::Warning, "no packages declared — nothing will be installed"});
|
||||
}
|
||||
|
||||
for (auto& svc : cfg.services) {
|
||||
if (svc.enable && svc.name.empty()) {
|
||||
diags.push_back({DiagSeverity::Warning, "enabled service has no name"});
|
||||
}
|
||||
}
|
||||
|
||||
if (cfg.users.empty()) {
|
||||
diags.push_back({DiagSeverity::Warning, "no users defined — root account has no shell"});
|
||||
}
|
||||
|
||||
for (auto& u : cfg.users) {
|
||||
if (u.name == "root" && u.shell.empty()) {
|
||||
diags.push_back({DiagSeverity::Error, "root user has no shell set"});
|
||||
}
|
||||
}
|
||||
|
||||
if (cfg.system.rollback.keep < 0) {
|
||||
diags.push_back({DiagSeverity::Error, "rollback keep is negative"});
|
||||
}
|
||||
|
||||
if (!cfg.assertions.empty()) {
|
||||
diags.push_back({DiagSeverity::Warning,
|
||||
std::to_string(cfg.assertions.size())
|
||||
+ " assertions defined — run 'kappa validate' to check them"});
|
||||
}
|
||||
|
||||
return diags;
|
||||
}
|
||||
|
||||
} // namespace kappa::tools
|
||||
@@ -0,0 +1,296 @@
|
||||
#include "kappa/tools/format.hpp"
|
||||
|
||||
namespace kappa::tools {
|
||||
|
||||
namespace {
|
||||
|
||||
struct Indent {
|
||||
int n = 0;
|
||||
Indent(int i) : n(i) {}
|
||||
friend std::ostream& operator<<(std::ostream& os, const Indent& in) {
|
||||
for (int i = 0; i < in.n; ++i) { os << " "; }
|
||||
return os;
|
||||
}
|
||||
};
|
||||
|
||||
static void write_env(std::ostream& os, int d,
|
||||
const std::vector<dsl::EnvEntry>& entries) {
|
||||
if (entries.empty()) { return; }
|
||||
os << Indent(d) << "env {\n";
|
||||
for (auto& e : entries) {
|
||||
os << Indent(d + 1) << e.key << (e.soft ? " ?= " : " = ")
|
||||
<< '"' << e.value << "\"\n";
|
||||
}
|
||||
os << Indent(d) << "}\n";
|
||||
}
|
||||
|
||||
static void write_features(std::ostream& os, int d,
|
||||
const std::unordered_map<std::string, dsl::FeatureDef>& feats) {
|
||||
if (feats.empty()) { return; }
|
||||
os << Indent(d) << "features {\n";
|
||||
for (auto& [k, f] : feats) {
|
||||
if (f.flag.empty() && !f.force) {
|
||||
os << Indent(d + 1) << k << " = " << (f.enabled ? "true" : "false") << "\n";
|
||||
} else {
|
||||
os << Indent(d + 1) << k << " = { enabled = "
|
||||
<< (f.enabled ? "true" : "false");
|
||||
if (f.force) { os << ", force = true"; }
|
||||
if (!f.flag.empty()) { os << ", flag = \"" << f.flag << "\""; }
|
||||
os << " }\n";
|
||||
}
|
||||
}
|
||||
os << Indent(d) << "}\n";
|
||||
}
|
||||
|
||||
static void write_phase(std::ostream& os, int d, const char* name,
|
||||
const dsl::Phase& phase) {
|
||||
if (phase.commands.empty()) { return; }
|
||||
os << Indent(d) << name << " {\n";
|
||||
for (auto& cmd : phase.commands) {
|
||||
os << Indent(d + 1) << cmd << "\n";
|
||||
}
|
||||
os << Indent(d) << "}\n";
|
||||
}
|
||||
|
||||
static void write_boot_block(std::ostream& os, int d, const dsl::BootBlock& boot) {
|
||||
os << Indent(d) << "boot {\n";
|
||||
auto w = [&](const char* k, const std::string& v) {
|
||||
if (!v.empty()) { os << Indent(d + 1) << k << " = \"" << v << "\"\n"; }
|
||||
};
|
||||
w("kernel", boot.kernel);
|
||||
w("init", boot.init);
|
||||
w("efi", boot.efi);
|
||||
w("swap", boot.swap);
|
||||
w("root", boot.root);
|
||||
w("bootloader", boot.bootloader);
|
||||
for (auto& [k, v] : boot.params) {
|
||||
os << Indent(d + 1) << k << " = \"" << v << "\"\n";
|
||||
}
|
||||
os << Indent(d) << "}\n";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void format_package(std::ostream& os, const dsl::PackageDef& pkg) {
|
||||
os << "package \"" << pkg.name << "\" {\n";
|
||||
int d = 1;
|
||||
|
||||
if (!pkg.version.empty()) {
|
||||
auto c = pkg.const_keys.contains("version") ? "const " : "";
|
||||
os << Indent(d) << c << "version = \"" << pkg.version << "\"\n";
|
||||
}
|
||||
if (!pkg.source.empty()) {
|
||||
auto c = pkg.const_keys.contains("source") ? "const " : "";
|
||||
os << Indent(d) << c << "source = \"" << pkg.source << "\"\n";
|
||||
}
|
||||
if (!pkg.license.empty()) {
|
||||
os << Indent(d) << "license = \"" << pkg.license << "\"\n";
|
||||
}
|
||||
|
||||
if (!pkg.provides.empty()) {
|
||||
os << Indent(d) << "provides = [";
|
||||
for (std::size_t i = 0; i < pkg.provides.size(); ++i) {
|
||||
if (i > 0) { os << ", "; }
|
||||
os << '"' << pkg.provides[i] << '"';
|
||||
}
|
||||
os << "]\n";
|
||||
}
|
||||
|
||||
if (!pkg.outputs.empty()) {
|
||||
os << Indent(d) << "outputs = [";
|
||||
for (std::size_t i = 0; i < pkg.outputs.size(); ++i) {
|
||||
if (i > 0) { os << ", "; }
|
||||
os << '"' << pkg.outputs[i] << '"';
|
||||
}
|
||||
os << "]\n";
|
||||
}
|
||||
|
||||
if (!pkg.patches.empty()) {
|
||||
os << Indent(d) << "patches = [\n";
|
||||
for (auto& p : pkg.patches) {
|
||||
if (p.sha256.empty()) {
|
||||
os << Indent(d + 1) << '"' << p.url << "\",\n";
|
||||
} else {
|
||||
os << Indent(d + 1) << "{ url = \"" << p.url
|
||||
<< "\", sha256 = \"" << p.sha256
|
||||
<< "\", level = " << p.level << " },\n";
|
||||
}
|
||||
}
|
||||
os << Indent(d) << "]\n";
|
||||
}
|
||||
|
||||
if (!pkg.depends.empty()) {
|
||||
os << Indent(d) << "depends = [\n";
|
||||
for (auto& dep : pkg.depends) {
|
||||
os << Indent(d + 1);
|
||||
if (dep.version.empty() && dep.output.empty() && dep.feature.empty()) {
|
||||
os << '"' << dep.name << '"';
|
||||
} else {
|
||||
os << "{ name = \"" << dep.name << '"';
|
||||
if (!dep.version.empty()) { os << ", version = \"" << dep.version << '"'; }
|
||||
if (!dep.output.empty()) { os << ", output = \"" << dep.output << '"'; }
|
||||
if (!dep.feature.empty()) { os << ", feature = \"" << dep.feature << '"'; }
|
||||
os << " }";
|
||||
}
|
||||
os << ",\n";
|
||||
}
|
||||
os << Indent(d) << "]\n";
|
||||
}
|
||||
|
||||
write_features(os, d, pkg.features);
|
||||
write_env(os, d, pkg.env_entries);
|
||||
|
||||
for (auto& cf : pkg.config_files) {
|
||||
os << Indent(d) << "config {\n";
|
||||
os << Indent(d + 1) << "file \"" << cf.path
|
||||
<< "\" mode = \"" << cf.mode << "\" {\n";
|
||||
for (auto& [k, v] : cf.entries) {
|
||||
os << Indent(d + 2) << k << " = " << v << "\n";
|
||||
}
|
||||
os << Indent(d + 1) << "}\n";
|
||||
os << Indent(d) << "}\n";
|
||||
}
|
||||
|
||||
if (!pkg.service.empty()) {
|
||||
os << Indent(d) << "service {\n";
|
||||
for (auto& [init, si] : pkg.service) {
|
||||
os << Indent(d + 1) << init << " {\n";
|
||||
if (!si.exec.empty()) { os << Indent(d + 2) << "exec = \"" << si.exec << "\"\n"; }
|
||||
if (!si.type.empty()) { os << Indent(d + 2) << "type = \"" << si.type << "\"\n"; }
|
||||
if (!si.user.empty()) { os << Indent(d + 2) << "user = \"" << si.user << "\"\n"; }
|
||||
if (!si.ports.empty()) {
|
||||
os << Indent(d + 2) << "ports = [";
|
||||
for (std::size_t i = 0; i < si.ports.size(); ++i) {
|
||||
if (i > 0) { os << ", "; }
|
||||
os << si.ports[i];
|
||||
}
|
||||
os << "]\n";
|
||||
}
|
||||
for (auto& [k, v] : si.env) {
|
||||
os << Indent(d + 2) << k << " = \"" << v << "\"\n";
|
||||
}
|
||||
os << Indent(d + 1) << "}\n";
|
||||
}
|
||||
os << Indent(d) << "}\n";
|
||||
}
|
||||
|
||||
if (!pkg.assertions.empty()) {
|
||||
os << Indent(d) << "assert {\n";
|
||||
for (auto& a : pkg.assertions) {
|
||||
os << Indent(d + 1) << '"' << a.message << "\" : "
|
||||
<< a.field << " " << a.op;
|
||||
if (!a.value.empty()) { os << " \"" << a.value << '"'; }
|
||||
os << "\n";
|
||||
}
|
||||
os << Indent(d) << "}\n";
|
||||
}
|
||||
|
||||
write_phase(os, d, "prepare", pkg.prepare);
|
||||
write_phase(os, d, "build", pkg.build);
|
||||
write_phase(os, d, "check", pkg.check);
|
||||
write_phase(os, d, "install", pkg.install);
|
||||
|
||||
os << "}\n";
|
||||
}
|
||||
|
||||
void format_config(std::ostream& os, const dsl::SystemConfig& cfg) {
|
||||
if (!cfg.imports.empty()) {
|
||||
os << "imports = [";
|
||||
for (std::size_t i = 0; i < cfg.imports.size(); ++i) {
|
||||
if (i > 0) { os << ", "; }
|
||||
os << '"' << cfg.imports[i] << '"';
|
||||
}
|
||||
os << "]\n\n";
|
||||
}
|
||||
|
||||
if (!cfg.assertions.empty()) {
|
||||
os << "assert {\n";
|
||||
for (auto& a : cfg.assertions) {
|
||||
os << " \"" << a.message << "\" : "
|
||||
<< a.field << " " << a.op;
|
||||
if (!a.value.empty()) { os << " \"" << a.value << '"'; }
|
||||
os << "\n";
|
||||
}
|
||||
os << "}\n\n";
|
||||
}
|
||||
|
||||
auto& s = cfg.system;
|
||||
os << "system {\n";
|
||||
if (!s.hostname.empty()) { os << " hostname = \"" << s.hostname << "\"\n"; }
|
||||
if (!s.timezone.empty()) { os << " timezone = \"" << s.timezone << "\"\n"; }
|
||||
write_features(os, 1, s.features);
|
||||
write_env(os, 1, s.env);
|
||||
if (!s.config.empty()) {
|
||||
os << " config {\n";
|
||||
for (auto& [k, v] : s.config) {
|
||||
os << " " << k << " = \"" << v << "\"\n";
|
||||
}
|
||||
os << " }\n";
|
||||
}
|
||||
if (s.rollback.keep > 0) {
|
||||
os << " rollback {\n keep = " << s.rollback.keep << "\n }\n";
|
||||
}
|
||||
os << "}\n\n";
|
||||
|
||||
if (!cfg.packages.empty()) {
|
||||
os << "packages {\n";
|
||||
for (auto& p : cfg.packages) {
|
||||
os << " " << p.name;
|
||||
if (p.version.empty() && p.features.empty() && p.config.empty()) {
|
||||
os << " {}\n";
|
||||
continue;
|
||||
}
|
||||
os << " {\n";
|
||||
if (!p.version.empty()) { os << " version = \"" << p.version << "\"\n"; }
|
||||
write_features(os, 2, p.features);
|
||||
if (!p.config.empty()) {
|
||||
os << " config {\n";
|
||||
for (auto& [k, v] : p.config) {
|
||||
os << " " << k << " = " << v << "\n";
|
||||
}
|
||||
os << " }\n";
|
||||
}
|
||||
os << " }\n";
|
||||
}
|
||||
os << "}\n\n";
|
||||
}
|
||||
|
||||
if (!cfg.services.empty()) {
|
||||
os << "services {\n";
|
||||
for (auto& svc : cfg.services) {
|
||||
os << " " << svc.name << " {\n";
|
||||
os << " enable = " << (svc.enable ? "true" : "false") << "\n";
|
||||
for (auto& [k, v] : svc.config) {
|
||||
os << " " << k << " = " << v << "\n";
|
||||
}
|
||||
os << " }\n";
|
||||
}
|
||||
os << "}\n\n";
|
||||
}
|
||||
|
||||
write_boot_block(os, 0, cfg.boot);
|
||||
os << "\n";
|
||||
|
||||
if (!cfg.users.empty()) {
|
||||
os << "users {\n";
|
||||
for (auto& u : cfg.users) {
|
||||
os << " " << u.name << " {\n";
|
||||
if (!u.shell.empty()) { os << " shell = \"" << u.shell << "\"\n"; }
|
||||
if (!u.groups.empty()) {
|
||||
os << " groups = [";
|
||||
for (std::size_t i = 0; i < u.groups.size(); ++i) {
|
||||
if (i > 0) { os << ", "; }
|
||||
os << '"' << u.groups[i] << '"';
|
||||
}
|
||||
os << "]\n";
|
||||
}
|
||||
for (auto& [k, v] : u.extra) {
|
||||
os << " " << k << " = \"" << v << "\"\n";
|
||||
}
|
||||
os << " }\n";
|
||||
}
|
||||
os << "}\n";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace kappa::tools
|
||||
Reference in New Issue
Block a user