diff --git a/examples/index.kap b/examples/index.kap new file mode 100644 index 0000000..357c2e6 --- /dev/null +++ b/examples/index.kap @@ -0,0 +1,5 @@ +index "local" { + foo { version = "1.2.3" } + postgresql { version = "16.3" } + make { version = "4.4.1" } +} diff --git a/include/kappa/dsl/ast.hpp b/include/kappa/dsl/ast.hpp index 8035c2c..5ff2138 100644 --- a/include/kappa/dsl/ast.hpp +++ b/include/kappa/dsl/ast.hpp @@ -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 packages; +}; + } // namespace kappa::dsl diff --git a/include/kappa/dsl/system.hpp b/include/kappa/dsl/system.hpp index 2632479..b770d3c 100644 --- a/include/kappa/dsl/system.hpp +++ b/include/kappa/dsl/system.hpp @@ -57,9 +57,18 @@ struct ServiceRef { std::unordered_map config; }; +struct RepoDef { + std::string name; + std::string url; + std::vector channels; + std::vector mirrors; + int priority = 50; +}; + struct SystemConfig { std::vector imports; - std::vector remotes; + std::vector remotes; // legacy — flat URL list + std::vector repos; // named repos with channels/mirrors SystemBlock system; std::vector packages; std::vector 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 diff --git a/include/kappa/dsl/token.hpp b/include/kappa/dsl/token.hpp index b25f8f6..9c3819d 100644 --- a/include/kappa/dsl/token.hpp +++ b/include/kappa/dsl/token.hpp @@ -49,6 +49,10 @@ enum class TokenType { KwUninstall, KwTrue, KwFalse, + + // Repo / index + KwIndex, + KwRepos, }; struct Token { diff --git a/include/kappa/tools/format.hpp b/include/kappa/tools/format.hpp index ebf0b84..02823ee 100644 --- a/include/kappa/tools/format.hpp +++ b/include/kappa/tools/format.hpp @@ -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 diff --git a/src/dsl/lexer.cpp b/src/dsl/lexer.cpp index 5949de1..4b4a203 100644 --- a/src/dsl/lexer.cpp +++ b/src/dsl/lexer.cpp @@ -33,6 +33,8 @@ static const std::unordered_map 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 "?"; } diff --git a/src/dsl/system.cpp b/src/dsl/system.cpp index d4d22ec..7823c99 100644 --- a/src/dsl/system.cpp +++ b/src/dsl/system.cpp @@ -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 #include @@ -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 diff --git a/src/main.cpp b/src/main.cpp index 4cc6223..ad7d271 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -52,6 +52,7 @@ Subcommands: rebuild Compare config to installed state, rebuild changed list List installed packages rollback Show available generations + index 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; + } } } } diff --git a/src/tools/format.cpp b/src/tools/format.cpp index 6785ffa..bd9fcd3 100644 --- a/src/tools/format.cpp +++ b/src/tools/format.cpp @@ -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