feat: add repos DSL, index format, and kappa index command
- Add repos { } block to system config with named repos, channels,
mirrors, and priority
- Add index "name" { } file format for package indexes
- Add kappa index <dir> subcommand that scans .kap files and
generates index.kap
- parse_index() and build_index() in DSL module
- format_index() roundtrip support in tools module
- validate and format subcommands handle all three formats
(package, config, index) with automatic detection
- Backward compatible: remotes = [...] still works unchanged
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
index "local" {
|
||||
foo { version = "1.2.3" }
|
||||
postgresql { version = "16.3" }
|
||||
make { version = "4.4.1" }
|
||||
}
|
||||
@@ -93,4 +93,14 @@ struct PackageDef {
|
||||
Phase uninstall;
|
||||
};
|
||||
|
||||
struct IndexEntry {
|
||||
std::string name;
|
||||
std::string version;
|
||||
};
|
||||
|
||||
struct IndexDef {
|
||||
std::string name; // "kappa-os/stable"
|
||||
std::vector<IndexEntry> packages;
|
||||
};
|
||||
|
||||
} // namespace kappa::dsl
|
||||
|
||||
@@ -57,9 +57,18 @@ struct ServiceRef {
|
||||
std::unordered_map<std::string, std::string> config;
|
||||
};
|
||||
|
||||
struct RepoDef {
|
||||
std::string name;
|
||||
std::string url;
|
||||
std::vector<std::string> channels;
|
||||
std::vector<std::string> mirrors;
|
||||
int priority = 50;
|
||||
};
|
||||
|
||||
struct SystemConfig {
|
||||
std::vector<std::string> imports;
|
||||
std::vector<std::string> remotes;
|
||||
std::vector<std::string> remotes; // legacy — flat URL list
|
||||
std::vector<RepoDef> repos; // named repos with channels/mirrors
|
||||
SystemBlock system;
|
||||
std::vector<PackageRef> packages;
|
||||
std::vector<ServiceRef> services;
|
||||
@@ -72,4 +81,7 @@ struct SystemConfig {
|
||||
SystemConfig parse_system_config(std::string_view source);
|
||||
SystemConfig resolve_imports(const SystemConfig& cfg, const std::string& base_dir);
|
||||
|
||||
IndexDef parse_index(std::string_view source);
|
||||
IndexDef build_index(const std::string& directory);
|
||||
|
||||
} // namespace kappa::dsl
|
||||
|
||||
@@ -49,6 +49,10 @@ enum class TokenType {
|
||||
KwUninstall,
|
||||
KwTrue,
|
||||
KwFalse,
|
||||
|
||||
// Repo / index
|
||||
KwIndex,
|
||||
KwRepos,
|
||||
};
|
||||
|
||||
struct Token {
|
||||
|
||||
@@ -9,5 +9,6 @@ namespace kappa::tools {
|
||||
|
||||
void format_package(std::ostream& os, const dsl::PackageDef& pkg);
|
||||
void format_config(std::ostream& os, const dsl::SystemConfig& cfg);
|
||||
void format_index(std::ostream& os, const dsl::IndexDef& idx);
|
||||
|
||||
} // namespace kappa::tools
|
||||
|
||||
@@ -33,6 +33,8 @@ static const std::unordered_map<std::string_view, TokenType> keywords = {
|
||||
{"uninstall", TokenType::KwUninstall},
|
||||
{"true", TokenType::KwTrue},
|
||||
{"false", TokenType::KwFalse},
|
||||
{"index", TokenType::KwIndex},
|
||||
{"repos", TokenType::KwRepos},
|
||||
};
|
||||
|
||||
std::string_view token_name(TokenType type) {
|
||||
@@ -74,6 +76,8 @@ std::string_view token_name(TokenType type) {
|
||||
case TokenType::KwUninstall: return "uninstall";
|
||||
case TokenType::KwTrue: return "true";
|
||||
case TokenType::KwFalse: return "false";
|
||||
case TokenType::KwIndex: return "index";
|
||||
case TokenType::KwRepos: return "repos";
|
||||
}
|
||||
return "?";
|
||||
}
|
||||
|
||||
+157
-1
@@ -1,6 +1,7 @@
|
||||
#include "kappa/dsl/system.hpp"
|
||||
#include "kappa/dsl/error.hpp"
|
||||
#include "kappa/dsl/lexer.hpp"
|
||||
#include "kappa/dsl/parser.hpp"
|
||||
|
||||
#include <filesystem>
|
||||
#include <format>
|
||||
@@ -29,6 +30,7 @@ private:
|
||||
void parse_boot_block(SystemConfig& cfg);
|
||||
void parse_users_block(SystemConfig& cfg);
|
||||
void parse_groups_block(SystemConfig& cfg);
|
||||
void parse_repos_block(SystemConfig& cfg);
|
||||
|
||||
std::string consume_ident();
|
||||
std::string consume_string();
|
||||
@@ -85,7 +87,9 @@ std::string SysParser::consume_ident() {
|
||||
at(TokenType::KwCheck) ||
|
||||
at(TokenType::KwInstall) ||
|
||||
at(TokenType::KwTrue) ||
|
||||
at(TokenType::KwFalse)) {
|
||||
at(TokenType::KwFalse) ||
|
||||
at(TokenType::KwIndex) ||
|
||||
at(TokenType::KwRepos)) {
|
||||
auto lexeme = current_.lexeme;
|
||||
advance();
|
||||
return lexeme;
|
||||
@@ -184,6 +188,7 @@ SystemConfig SysParser::parse() {
|
||||
else if (kw == "boot") { parse_boot_block(cfg); }
|
||||
else if (kw == "users") { parse_users_block(cfg); }
|
||||
else if (kw == "groups") { parse_groups_block(cfg); }
|
||||
else if (kw == "repos") { parse_repos_block(cfg); }
|
||||
else {
|
||||
throw ParseError(current_.line, current_.col,
|
||||
std::format("unknown section '{}'", kw));
|
||||
@@ -456,6 +461,63 @@ void SysParser::parse_groups_block(SystemConfig& cfg) {
|
||||
consume(TokenType::Rbrace);
|
||||
}
|
||||
|
||||
void SysParser::parse_repos_block(SystemConfig& cfg) {
|
||||
consume(TokenType::Lbrace);
|
||||
skip_newlines();
|
||||
|
||||
while (!at(TokenType::Rbrace) && !at(TokenType::Eof)) {
|
||||
skip_newlines();
|
||||
if (at(TokenType::Rbrace)) break;
|
||||
|
||||
RepoDef repo;
|
||||
repo.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 == "url") {
|
||||
consume(TokenType::Equals);
|
||||
repo.url = consume_string();
|
||||
} else if (key == "channels") {
|
||||
consume(TokenType::Equals);
|
||||
consume(TokenType::Lbracket);
|
||||
skip_newlines();
|
||||
while (!at(TokenType::Rbracket) && !at(TokenType::Eof)) {
|
||||
repo.channels.push_back(consume_string());
|
||||
skip_newlines();
|
||||
if (at(TokenType::Comma)) { consume(TokenType::Comma); }
|
||||
skip_newlines();
|
||||
}
|
||||
consume(TokenType::Rbracket);
|
||||
} else if (key == "mirrors") {
|
||||
consume(TokenType::Equals);
|
||||
consume(TokenType::Lbracket);
|
||||
skip_newlines();
|
||||
while (!at(TokenType::Rbracket) && !at(TokenType::Eof)) {
|
||||
repo.mirrors.push_back(consume_string());
|
||||
skip_newlines();
|
||||
if (at(TokenType::Comma)) { consume(TokenType::Comma); }
|
||||
skip_newlines();
|
||||
}
|
||||
consume(TokenType::Rbracket);
|
||||
} else if (key == "priority") {
|
||||
consume(TokenType::Equals);
|
||||
repo.priority = parse_int(current_.line, current_.col,
|
||||
current_.lexeme);
|
||||
advance();
|
||||
}
|
||||
skip_newlines();
|
||||
}
|
||||
consume(TokenType::Rbrace);
|
||||
}
|
||||
cfg.repos.push_back(std::move(repo));
|
||||
}
|
||||
consume(TokenType::Rbrace);
|
||||
}
|
||||
|
||||
SystemConfig parse_system_config(std::string_view source) {
|
||||
SysParser p(source);
|
||||
return p.parse();
|
||||
@@ -466,6 +528,7 @@ static void merge_config(SystemConfig& base, SystemConfig&& imported) {
|
||||
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& r : imported.remotes) { base.remotes.push_back(std::move(r)); }
|
||||
for (auto& r : imported.repos) { base.repos.push_back(std::move(r)); }
|
||||
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; }
|
||||
@@ -497,4 +560,97 @@ SystemConfig resolve_imports(const SystemConfig& cfg, const std::string& base_di
|
||||
return resolved;
|
||||
}
|
||||
|
||||
IndexDef parse_index(std::string_view source) {
|
||||
Lexer lexer(source);
|
||||
Token current;
|
||||
auto advance = [&] { current = lexer.next(); };
|
||||
advance();
|
||||
|
||||
auto skip_newlines = [&] {
|
||||
while (current.type == TokenType::Newline) { advance(); }
|
||||
};
|
||||
|
||||
auto consume = [&](TokenType type) -> Token {
|
||||
if (current.type != type) {
|
||||
throw ParseError(current.line, current.col,
|
||||
msg_expected(token_name(type),
|
||||
token_name(current.type)));
|
||||
}
|
||||
Token t = std::move(current);
|
||||
advance();
|
||||
return t;
|
||||
};
|
||||
|
||||
skip_newlines();
|
||||
consume(TokenType::KwIndex);
|
||||
|
||||
// index "kappa-os/stable" { ... }
|
||||
auto name_tok = consume(TokenType::String);
|
||||
consume(TokenType::Lbrace);
|
||||
|
||||
IndexDef idx;
|
||||
idx.name = std::move(name_tok.lexeme);
|
||||
|
||||
skip_newlines();
|
||||
while (current.type != TokenType::Rbrace && current.type != TokenType::Eof) {
|
||||
skip_newlines();
|
||||
if (current.type == TokenType::Rbrace) break;
|
||||
|
||||
IndexEntry entry;
|
||||
entry.name = current.lexeme;
|
||||
advance();
|
||||
|
||||
if (current.type == TokenType::Lbrace) {
|
||||
advance();
|
||||
skip_newlines();
|
||||
while (current.type != TokenType::Rbrace && current.type != TokenType::Eof) {
|
||||
if (current.type == TokenType::Newline) { advance(); continue; }
|
||||
auto key = current.lexeme;
|
||||
advance();
|
||||
consume(TokenType::Equals);
|
||||
if (key == "version") {
|
||||
entry.version = consume(TokenType::String).lexeme;
|
||||
}
|
||||
skip_newlines();
|
||||
}
|
||||
consume(TokenType::Rbrace);
|
||||
}
|
||||
idx.packages.push_back(std::move(entry));
|
||||
skip_newlines();
|
||||
}
|
||||
consume(TokenType::Rbrace);
|
||||
return idx;
|
||||
}
|
||||
|
||||
IndexDef build_index(const std::string& directory) {
|
||||
IndexDef idx;
|
||||
auto dirname = std::filesystem::path(directory).filename().string();
|
||||
// If the directory is empty, drop the "."
|
||||
if (dirname.empty() || dirname == ".") {
|
||||
dirname = "local";
|
||||
}
|
||||
idx.name = dirname;
|
||||
|
||||
std::error_code ec;
|
||||
for (auto& entry : std::filesystem::directory_iterator(directory, ec)) {
|
||||
if (ec) break;
|
||||
if (!entry.is_regular_file()) continue;
|
||||
auto ext = entry.path().extension().string();
|
||||
if (ext != ".kap") continue;
|
||||
|
||||
std::ifstream in(entry.path());
|
||||
if (!in) continue;
|
||||
std::ostringstream buf;
|
||||
buf << in.rdbuf();
|
||||
|
||||
try {
|
||||
auto pkg = parse(buf.str());
|
||||
idx.packages.push_back({pkg.name, pkg.version});
|
||||
} catch (...) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
|
||||
} // namespace kappa::dsl
|
||||
|
||||
+55
-9
@@ -52,6 +52,7 @@ Subcommands:
|
||||
rebuild <config> Compare config to installed state, rebuild changed
|
||||
list List installed packages
|
||||
rollback Show available generations
|
||||
index <dir> Build an index.kap from .kap files in a directory
|
||||
|
||||
Options:
|
||||
-h, --help Show this help message
|
||||
@@ -196,8 +197,9 @@ int main(int argc, char* argv[]) {
|
||||
|| (subcommand == "build")
|
||||
|| (subcommand == "rebuild")
|
||||
|| (subcommand == "list")
|
||||
|| (subcommand == "fetch-package")
|
||||
|| (subcommand == "rollback");
|
||||
|| (subcommand == "fetch-package")
|
||||
|| (subcommand == "rollback")
|
||||
|| (subcommand == "index");
|
||||
|
||||
if (!valid_subcommand) {
|
||||
std::cerr << "error: unknown subcommand '" << subcommand << "'\n\n";
|
||||
@@ -276,6 +278,29 @@ int main(int argc, char* argv[]) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (subcommand == "index") {
|
||||
if (file_arg == nullptr) {
|
||||
std::cerr << "error: no directory specified\n";
|
||||
return 1;
|
||||
}
|
||||
try {
|
||||
auto idx = dsl::build_index(file_arg);
|
||||
auto out_path = std::filesystem::path(file_arg) / "index.kap";
|
||||
std::ofstream out(out_path);
|
||||
if (!out) {
|
||||
std::cerr << "error: cannot write " << out_path.string() << "\n";
|
||||
return 1;
|
||||
}
|
||||
tools::format_index(out, idx);
|
||||
std::cout << idx.packages.size() << " packages indexed → "
|
||||
<< out_path.string() << "\n";
|
||||
return 0;
|
||||
} catch (const std::exception& e) {
|
||||
std::cerr << "index error: " << e.what() << "\n";
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (subcommand == "fetch-package") {
|
||||
if (file_arg == nullptr) {
|
||||
std::cerr << "error: no package name specified\n";
|
||||
@@ -331,7 +356,8 @@ int main(int argc, char* argv[]) {
|
||||
|
||||
if (file_arg == nullptr
|
||||
&& subcommand != "list"
|
||||
&& subcommand != "rollback") {
|
||||
&& subcommand != "rollback"
|
||||
&& subcommand != "index") {
|
||||
std::cerr << "error: no input file specified\n";
|
||||
return 1;
|
||||
}
|
||||
@@ -378,6 +404,12 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
std::cout << "\n";
|
||||
}
|
||||
if (!cfg.repos.empty()) {
|
||||
int total_channels = 0;
|
||||
for (auto& r : cfg.repos) total_channels += r.channels.size();
|
||||
std::cout << " repos: " << cfg.repos.size()
|
||||
<< " (" << total_channels << " channels)\n";
|
||||
}
|
||||
return 0;
|
||||
} catch (const std::runtime_error& e) {
|
||||
handle_parse_error(file_arg, source, e);
|
||||
@@ -414,9 +446,17 @@ int main(int argc, char* argv[]) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
} catch (const std::runtime_error& e) {
|
||||
handle_parse_error(file_arg, source, e);
|
||||
return 1;
|
||||
} catch (const std::runtime_error&) {
|
||||
try {
|
||||
auto idx = dsl::parse_index(source);
|
||||
std::cout << file_arg << ": valid index ("
|
||||
<< idx.name << ", "
|
||||
<< idx.packages.size() << " packages)\n";
|
||||
return 0;
|
||||
} catch (const std::runtime_error& e) {
|
||||
handle_parse_error(file_arg, source, e);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -472,9 +512,15 @@ int main(int argc, char* argv[]) {
|
||||
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;
|
||||
} catch (const std::runtime_error&) {
|
||||
try {
|
||||
auto idx = dsl::parse_index(source);
|
||||
tools::format_index(std::cout, idx);
|
||||
return 0;
|
||||
} catch (const std::runtime_error& e) {
|
||||
handle_parse_error(file_arg, source, e);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,6 +237,34 @@ void format_config(std::ostream& os, const dsl::SystemConfig& cfg) {
|
||||
os << "]\n\n";
|
||||
}
|
||||
|
||||
if (!cfg.repos.empty()) {
|
||||
os << "repos {\n";
|
||||
for (auto& repo : cfg.repos) {
|
||||
os << " " << repo.name << " {\n";
|
||||
os << " url = \"" << repo.url << "\"\n";
|
||||
if (!repo.channels.empty()) {
|
||||
os << " channels = [";
|
||||
for (std::size_t i = 0; i < repo.channels.size(); ++i) {
|
||||
if (i > 0) { os << ", "; }
|
||||
os << '"' << repo.channels[i] << '"';
|
||||
}
|
||||
os << "]\n";
|
||||
}
|
||||
if (!repo.mirrors.empty()) {
|
||||
os << " mirrors = [\n";
|
||||
for (auto& m : repo.mirrors) {
|
||||
os << " \"" << m << "\",\n";
|
||||
}
|
||||
os << " ]\n";
|
||||
}
|
||||
if (repo.priority != 50) {
|
||||
os << " priority = " << repo.priority << "\n";
|
||||
}
|
||||
os << " }\n";
|
||||
}
|
||||
os << "}\n\n";
|
||||
}
|
||||
|
||||
if (!cfg.assertions.empty()) {
|
||||
os << "assert {\n";
|
||||
for (auto& a : cfg.assertions) {
|
||||
@@ -345,4 +373,12 @@ void format_config(std::ostream& os, const dsl::SystemConfig& cfg) {
|
||||
}
|
||||
}
|
||||
|
||||
void format_index(std::ostream& os, const dsl::IndexDef& idx) {
|
||||
os << "index \"" << idx.name << "\" {\n";
|
||||
for (auto& pkg : idx.packages) {
|
||||
os << " " << pkg.name << " { version = \"" << pkg.version << "\" }\n";
|
||||
}
|
||||
os << "}\n";
|
||||
}
|
||||
|
||||
} // namespace kappa::tools
|
||||
|
||||
Reference in New Issue
Block a user