Conflicts were detected in the resolver but silently ignored in both the resolve and rebuild subcommands. Both now print conflict errors and abort before any builds start.
890 lines
33 KiB
C++
890 lines
33 KiB
C++
#include "kappa/build/build.hpp"
|
|
#include "kappa/cli/diagnostic.hpp"
|
|
#include "kappa/config/eval.hpp"
|
|
#include "kappa/dsl/parser.hpp"
|
|
#include "kappa/dsl/system.hpp"
|
|
#include "kappa/fetch/fetch.hpp"
|
|
#include "kappa/fetch/recipe.hpp"
|
|
#include "kappa/install/install.hpp"
|
|
#include "kappa/paths.hpp"
|
|
#include "kappa/rebuild/rebuild.hpp"
|
|
#include "kappa/resolve/plan.hpp"
|
|
#include "kappa/boot/bootloader.hpp"
|
|
#include "kappa/boot/types.hpp"
|
|
#include "kappa/sched/scheduler.hpp"
|
|
#include "kappa/service/service.hpp"
|
|
#include "kappa/service/types.hpp"
|
|
#include "kappa/system/activate.hpp"
|
|
#include "kappa/tools/doctor.hpp"
|
|
#include "kappa/tools/format.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <format>
|
|
#include <fstream>
|
|
#include <iostream>
|
|
#include <sstream>
|
|
#include <stdexcept>
|
|
#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)
|
|
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
|
|
fetch <package> Download and verify source for a package
|
|
fetch-package <name> Fetch a package recipe from configured remotes
|
|
build <package> Build a package from its source directory
|
|
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
|
|
add <pkg> [v] Add a package to system config (optional version)
|
|
remove <pkg> Remove a package from system config
|
|
|
|
Options:
|
|
-h, --help Show this help message
|
|
-V, --version Show version information
|
|
-j, --jobs <n> Jobs per package (default: 1)
|
|
-w <n> Concurrent package builds (default: 1)
|
|
--root <path> Set kappa root directory (default: /usr/local/kappa)
|
|
--dry-run Report changes without building (rebuild only)
|
|
)";
|
|
}
|
|
|
|
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) {
|
|
throw std::runtime_error(std::format("cannot open '{}'", path));
|
|
}
|
|
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';
|
|
}
|
|
|
|
static resolve::Registry build_registry(const dsl::SystemConfig& cfg) {
|
|
resolve::Registry registry;
|
|
|
|
for (const auto& pref : cfg.packages) {
|
|
bool found = false;
|
|
|
|
// Check standard locations: local .kap, examples/, cache/packages/
|
|
std::vector<std::string> search_paths = {
|
|
std::string(pref.name) + ".kap",
|
|
std::string("examples/") + pref.name + ".kap",
|
|
(paths::packages_dir() / (pref.name + ".kap")).string(),
|
|
};
|
|
|
|
for (const auto& sp : search_paths) {
|
|
std::ifstream in(sp);
|
|
if (!in) continue;
|
|
std::ostringstream buf;
|
|
buf << in.rdbuf();
|
|
try {
|
|
auto pkg = dsl::parse(buf.str());
|
|
registry[pkg.name] = std::move(pkg);
|
|
found = true;
|
|
break;
|
|
} catch (const std::exception& e) {
|
|
std::cerr << "warning: parse error in " << sp << ": " << e.what() << "\n";
|
|
} catch (...) {
|
|
std::cerr << "warning: unknown parse error in " << sp << "\n";
|
|
}
|
|
}
|
|
|
|
// If not found locally, try repos (index-aware) first, then legacy remotes
|
|
if (!found && !cfg.repos.empty()) {
|
|
auto result = fetch::fetch_recipe_from_repos(pref.name, cfg.repos);
|
|
if (result.ok && !result.path.empty()) {
|
|
std::ifstream in(result.path);
|
|
if (in) {
|
|
std::ostringstream buf;
|
|
buf << in.rdbuf();
|
|
try {
|
|
auto pkg = dsl::parse(buf.str());
|
|
registry[pkg.name] = std::move(pkg);
|
|
found = true;
|
|
} catch (const std::exception& e) {
|
|
std::cerr << "warning: parse error in remote recipe " << pref.name << ": " << e.what() << "\n";
|
|
} catch (...) {
|
|
std::cerr << "warning: unknown parse error in remote recipe " << pref.name << "\n";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!found && !cfg.remotes.empty()) {
|
|
auto result = fetch::fetch_recipe(pref.name, cfg.remotes);
|
|
if (result.ok && !result.path.empty()) {
|
|
std::ifstream in(result.path);
|
|
if (in) {
|
|
std::ostringstream buf;
|
|
buf << in.rdbuf();
|
|
try {
|
|
auto pkg = dsl::parse(buf.str());
|
|
registry[pkg.name] = std::move(pkg);
|
|
found = true;
|
|
} catch (const std::exception& e) {
|
|
std::cerr << "warning: parse error in remote recipe " << pref.name << ": " << e.what() << "\n";
|
|
} catch (...) {
|
|
std::cerr << "warning: unknown parse error in remote recipe " << pref.name << "\n";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!found) {
|
|
std::cerr << "warning: package '" << pref.name
|
|
<< "' not found locally or in remotes\n";
|
|
}
|
|
}
|
|
|
|
return registry;
|
|
}
|
|
|
|
int main(int argc, char* argv[]) {
|
|
if (argc < 2) {
|
|
std::cerr << "kappa: missing subcommand\n\n";
|
|
print_usage();
|
|
return 1;
|
|
}
|
|
|
|
// Parse leading --root before subcommand
|
|
int arg_start = 1;
|
|
while (arg_start < argc && std::string_view(argv[arg_start]) == "--root" && arg_start + 1 < argc) {
|
|
paths::set_root(argv[arg_start + 1]);
|
|
arg_start += 2;
|
|
}
|
|
|
|
auto subcommand = std::string_view(argv[arg_start]);
|
|
|
|
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")
|
|
|| (subcommand == "format")
|
|
|| (subcommand == "doctor")
|
|
|| (subcommand == "resolve")
|
|
|| (subcommand == "fetch")
|
|
|| (subcommand == "build")
|
|
|| (subcommand == "rebuild")
|
|
|| (subcommand == "list")
|
|
|| (subcommand == "fetch-package")
|
|
|| (subcommand == "rollback")
|
|
|| (subcommand == "index")
|
|
|| (subcommand == "add")
|
|
|| (subcommand == "remove");
|
|
|
|
if (!valid_subcommand) {
|
|
std::cerr << "error: unknown subcommand '" << subcommand << "'\n\n";
|
|
print_usage();
|
|
return 1;
|
|
}
|
|
|
|
const char* file_arg = nullptr;
|
|
const char* version_arg = nullptr;
|
|
bool dry_run = false;
|
|
for (int i = arg_start + 1; i < argc; ++i) {
|
|
if (std::string_view(argv[i]) == "-h"
|
|
|| std::string_view(argv[i]) == "--help") {
|
|
print_usage();
|
|
return 0;
|
|
}
|
|
if (std::string_view(argv[i]) == "-V"
|
|
|| std::string_view(argv[i]) == "--version") {
|
|
std::cout << version << '\n';
|
|
return 0;
|
|
}
|
|
if (std::string_view(argv[i]) == "-j"
|
|
|| std::string_view(argv[i]) == "--jobs"
|
|
|| (std::string_view(argv[i]) == "-w" && i + 1 < argc)) {
|
|
++i;
|
|
continue;
|
|
}
|
|
if (std::string_view(argv[i]) == "--root" && i + 1 < argc) {
|
|
paths::set_root(argv[++i]);
|
|
continue;
|
|
}
|
|
if (std::string_view(argv[i]) == "--dry-run") {
|
|
dry_run = true;
|
|
continue;
|
|
}
|
|
if (!is_flag(argv[i])) {
|
|
if (file_arg == nullptr) {
|
|
file_arg = argv[i];
|
|
} else if (version_arg == nullptr) {
|
|
version_arg = argv[i];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (subcommand == "list") {
|
|
auto entries = install::read_installed();
|
|
if (entries.empty()) {
|
|
std::cout << "no packages installed\n";
|
|
} else {
|
|
std::cout << entries.size() << " packages installed:\n";
|
|
for (auto& e : entries) {
|
|
std::cout << " " << e.name << " " << e.version
|
|
<< " (" << e.hash << ")";
|
|
if (!e.provides.empty()) {
|
|
std::cout << " provides:";
|
|
for (auto& p : e.provides) { std::cout << " " << p; }
|
|
}
|
|
std::cout << "\n";
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
if (subcommand == "rollback") {
|
|
auto gen_dir = paths::db_dir() / "generations";
|
|
std::error_code ec;
|
|
if (!std::filesystem::exists(gen_dir)) {
|
|
std::cout << "no generations found\n";
|
|
return 0;
|
|
}
|
|
std::vector<std::string> gens;
|
|
for (auto& entry : std::filesystem::directory_iterator(gen_dir, ec)) {
|
|
gens.push_back(entry.path().filename().string());
|
|
}
|
|
std::sort(gens.begin(), gens.end());
|
|
std::cout << gens.size() << " generations:\n";
|
|
for (auto& g : gens) {
|
|
std::cout << " " << g << "\n";
|
|
}
|
|
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 == "add") {
|
|
if (file_arg == nullptr) {
|
|
std::cerr << "error: no package name specified\n";
|
|
return 1;
|
|
}
|
|
try {
|
|
auto config_path = std::filesystem::path(paths::system_dir()) / "config.kap";
|
|
dsl::SystemConfig cfg;
|
|
|
|
if (std::filesystem::exists(config_path)) {
|
|
auto src = read_file(config_path.c_str());
|
|
cfg = dsl::parse_system_config(src);
|
|
}
|
|
|
|
bool found = false;
|
|
for (auto& p : cfg.packages) {
|
|
if (p.name == file_arg) {
|
|
found = true;
|
|
if (version_arg != nullptr) {
|
|
p.version = version_arg;
|
|
std::cout << "updated " << file_arg
|
|
<< " → version " << version_arg << "\n";
|
|
} else {
|
|
std::cout << file_arg << " already in packages\n";
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found) {
|
|
dsl::PackageRef pref;
|
|
pref.name = file_arg;
|
|
if (version_arg != nullptr) {
|
|
pref.version = version_arg;
|
|
}
|
|
cfg.packages.push_back(std::move(pref));
|
|
std::cout << "added " << file_arg;
|
|
if (version_arg != nullptr) {
|
|
std::cout << " " << version_arg;
|
|
}
|
|
std::cout << "\n";
|
|
}
|
|
|
|
std::error_code ec;
|
|
std::filesystem::create_directories(paths::system_dir(), ec);
|
|
std::ofstream out(config_path);
|
|
if (!out) {
|
|
std::cerr << "error: cannot write " << config_path.string() << "\n";
|
|
return 1;
|
|
}
|
|
tools::format_config(out, cfg);
|
|
return 0;
|
|
} catch (const std::exception& e) {
|
|
std::cerr << "add error: " << e.what() << "\n";
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
if (subcommand == "remove") {
|
|
if (file_arg == nullptr) {
|
|
std::cerr << "error: no package name specified\n";
|
|
return 1;
|
|
}
|
|
try {
|
|
auto config_path = std::filesystem::path(paths::system_dir()) / "config.kap";
|
|
if (!std::filesystem::exists(config_path)) {
|
|
std::cout << file_arg << " not found (no config)\n";
|
|
return 0;
|
|
}
|
|
|
|
auto src = read_file(config_path.c_str());
|
|
auto cfg = dsl::parse_system_config(src);
|
|
|
|
auto it = std::remove_if(cfg.packages.begin(), cfg.packages.end(),
|
|
[&](const dsl::PackageRef& p) { return p.name == file_arg; });
|
|
|
|
if (it == cfg.packages.end()) {
|
|
std::cout << file_arg << " not in packages\n";
|
|
return 0;
|
|
}
|
|
cfg.packages.erase(it, cfg.packages.end());
|
|
std::cout << "removed " << file_arg << "\n";
|
|
|
|
std::ofstream out(config_path);
|
|
if (!out) {
|
|
std::cerr << "error: cannot write " << config_path.string() << "\n";
|
|
return 1;
|
|
}
|
|
tools::format_config(out, cfg);
|
|
return 0;
|
|
} catch (const std::exception& e) {
|
|
std::cerr << "remove error: " << e.what() << "\n";
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
if (subcommand == "fetch-package") {
|
|
if (file_arg == nullptr) {
|
|
std::cerr << "error: no package name specified\n";
|
|
return 1;
|
|
}
|
|
try {
|
|
std::vector<std::string> remotes;
|
|
std::vector<dsl::RepoDef> repos;
|
|
auto config_path = std::filesystem::path(paths::system_dir()) / "config.kap";
|
|
if (std::filesystem::exists(config_path)) {
|
|
auto cfg_src = read_file(config_path.c_str());
|
|
try {
|
|
auto cfg = dsl::parse_system_config(cfg_src);
|
|
remotes = cfg.remotes;
|
|
repos = std::move(cfg.repos);
|
|
} catch (const std::exception& e) {
|
|
std::cerr << "warning: config parse error: " << e.what() << "\n";
|
|
} catch (...) {
|
|
std::cerr << "warning: unknown config parse error\n";
|
|
}
|
|
}
|
|
fetch::RecipeResult result;
|
|
if (!repos.empty()) {
|
|
result = fetch::fetch_recipe_from_repos(file_arg, repos);
|
|
} else {
|
|
result = fetch::fetch_recipe(file_arg, remotes);
|
|
}
|
|
if (result.ok) {
|
|
if (result.updated) {
|
|
std::cout << "fetched " << file_arg << " " << result.version
|
|
<< " → " << result.path << "\n";
|
|
} else {
|
|
std::cout << file_arg << " " << result.version
|
|
<< " (cached, up to date)\n";
|
|
}
|
|
return 0;
|
|
}
|
|
std::cerr << "fetch failed: " << result.error << "\n";
|
|
return 1;
|
|
} catch (const std::exception& e) {
|
|
std::cerr << "fetch error: " << e.what() << "\n";
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
// Subcommands that MUST have writable directories to function.
|
|
bool needs_write = (subcommand == "build"
|
|
|| subcommand == "fetch" || subcommand == "fetch-package");
|
|
// rebuild benefits from cache dirs for registry lookups but
|
|
// degrades gracefully — it only reports, never writes.
|
|
bool needs_dirs = needs_write || subcommand == "rebuild" || subcommand == "resolve";
|
|
if (needs_dirs && !paths::directories_exist()) {
|
|
if (!paths::ensure_directories()) {
|
|
if (needs_write) {
|
|
std::cerr << "error: cannot create kappa directories (check permissions)\n";
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (file_arg == nullptr
|
|
&& subcommand != "list"
|
|
&& subcommand != "rollback"
|
|
&& subcommand != "index") {
|
|
std::cerr << "error: no input file specified\n";
|
|
return 1;
|
|
}
|
|
|
|
try {
|
|
auto source = file_arg ? 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";
|
|
if (!cfg.boot.init.empty()) {
|
|
auto is = kappa::service::parse_init_system(cfg.boot.init);
|
|
std::cout << " init: " << cfg.boot.init;
|
|
if (is != kappa::service::InitSystem::Unknown) {
|
|
std::cout << " (" << kappa::service::init_description(is) << ")";
|
|
} else {
|
|
std::cout << " (unrecognized)";
|
|
}
|
|
std::cout << "\n";
|
|
}
|
|
if (!cfg.boot.bootloader.empty()) {
|
|
auto bl = kappa::boot::parse_bootloader(cfg.boot.bootloader);
|
|
std::cout << " bootloader: " << cfg.boot.bootloader;
|
|
if (bl != kappa::boot::Bootloader::Unknown) {
|
|
std::cout << " (" << kappa::boot::bootloader_description(bl) << ")";
|
|
} else {
|
|
std::cout << " (unrecognized)";
|
|
}
|
|
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);
|
|
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 {
|
|
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&) {
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (subcommand == "build") {
|
|
try {
|
|
auto pkg = dsl::parse(source);
|
|
|
|
int jobs = 1;
|
|
for (int i = arg_start + 1; i < argc; ++i) {
|
|
auto arg = std::string_view(argv[i]);
|
|
if ((arg == "-j" || arg == "--jobs") && i + 1 < argc) {
|
|
jobs = std::stoi(argv[++i]);
|
|
}
|
|
}
|
|
|
|
resolve::Registry reg;
|
|
reg[pkg.name] = pkg;
|
|
|
|
resolve::BuildStep step;
|
|
step.name = pkg.name;
|
|
step.package = ®.at(pkg.name);
|
|
step.resolved = config::resolve_package(pkg, {}, {});
|
|
|
|
auto work_dir = paths::temp_dir() / "build";
|
|
auto result = build::build(step, work_dir, jobs);
|
|
if (result.ok) {
|
|
auto inst = install::install(step, work_dir);
|
|
if (inst.ok) {
|
|
std::cout << "build successful — installed to "
|
|
<< inst.store_path << "\n";
|
|
return 0;
|
|
}
|
|
std::cerr << "install failed: " << inst.error << "\n";
|
|
return 1;
|
|
}
|
|
std::cerr << "build failed in phase '" << result.phase
|
|
<< "': " << result.error << "\n";
|
|
return 1;
|
|
} catch (const std::exception& e) {
|
|
std::cerr << "build error: " << e.what() << "\n";
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
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&) {
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
auto registry = build_registry(cfg);
|
|
|
|
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;
|
|
}
|
|
|
|
if (!plan.conflicts.empty()) {
|
|
std::cerr << "error: package conflicts detected:\n";
|
|
for (auto& c : plan.conflicts) {
|
|
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.resolved.features.empty()) {
|
|
std::cout << ", features:";
|
|
for (auto& [k, f] : step.resolved.features) {
|
|
if (f.enabled && !f.flag.empty()) {
|
|
std::cout << " " << k;
|
|
}
|
|
}
|
|
}
|
|
std::cout << ")\n";
|
|
}
|
|
return plan.missing.empty() ? 0 : 1;
|
|
} catch (const std::runtime_error& e) {
|
|
handle_parse_error(file_arg, source, e);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
if (subcommand == "fetch") {
|
|
try {
|
|
auto pkg = dsl::parse(source);
|
|
auto result = fetch::fetch(pkg);
|
|
if (result.ok()) {
|
|
std::cout << "fetched to " << result.work_dir << "\n";
|
|
return 0;
|
|
}
|
|
std::cerr << "fetch failed: " << result.error << "\n";
|
|
return 1;
|
|
} catch (const std::runtime_error& e) {
|
|
handle_parse_error(file_arg, source, e);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
if (subcommand == "rebuild") {
|
|
try {
|
|
auto cfg = dsl::parse_system_config(source);
|
|
auto cs = rebuild::compute_changes(cfg);
|
|
|
|
if (cs.added.empty() && cs.changed.empty()
|
|
&& !cs.kernel_changed && !cs.init_changed
|
|
&& !cs.bootloader_changed && !cs.services_changed) {
|
|
std::cout << "nothing to rebuild\n";
|
|
return 0;
|
|
}
|
|
|
|
if (dry_run) {
|
|
// Dry run: report changes without executing
|
|
std::cout << cs.added.size() + cs.changed.size()
|
|
<< " packages to rebuild\n";
|
|
return 0;
|
|
}
|
|
auto registry = build_registry(cfg);
|
|
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;
|
|
}
|
|
if (!plan.conflicts.empty()) {
|
|
std::cerr << "error: package conflicts detected:\n";
|
|
for (auto& c : plan.conflicts) std::cerr << " " << c << "\n";
|
|
return 1;
|
|
}
|
|
if (plan.steps.empty()) {
|
|
std::cout << "nothing to build\n";
|
|
return 0;
|
|
}
|
|
|
|
// Parse -w/-j flags after subcommand
|
|
int workers = 1, jobs = 1;
|
|
for (int i = arg_start + 1; i < argc; ++i) {
|
|
auto arg = std::string_view(argv[i]);
|
|
if (arg == "-w" && i + 1 < argc) workers = std::stoi(argv[++i]);
|
|
if ((arg == "-j" || arg == "--jobs") && i + 1 < argc) jobs = std::stoi(argv[++i]);
|
|
}
|
|
|
|
// Build
|
|
std::cout << cs.added.size() + cs.changed.size()
|
|
<< " packages to rebuild\n";
|
|
|
|
auto work_root = paths::temp_dir();
|
|
auto sr = sched::run(plan, work_root.string(), workers, jobs);
|
|
|
|
if (!sr.ok) {
|
|
std::cerr << sr.failed.size() << " packages failed to build\n";
|
|
for (auto& f : sr.failed) std::cerr << " " << f << "\n";
|
|
return 1;
|
|
}
|
|
|
|
// Install built packages
|
|
for (auto& step : plan.steps) {
|
|
if (std::find(sr.built.begin(), sr.built.end(), step.name) == sr.built.end())
|
|
continue;
|
|
auto work_dir = work_root / step.name;
|
|
auto inst = install::install(step, work_dir);
|
|
if (!inst.ok) {
|
|
std::cerr << "install failed for " << step.name << ": " << inst.error << "\n";
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
// Generate service files for enabled services
|
|
auto is = kappa::service::parse_init_system(cfg.boot.init);
|
|
if (is != kappa::service::InitSystem::Unknown && !cfg.services.empty()) {
|
|
auto resolved = config::resolve_services(cfg, registry);
|
|
for (auto& sref : cfg.services) {
|
|
if (!sref.enable) continue;
|
|
auto it = resolved.find(sref.name);
|
|
if (it == resolved.end()) {
|
|
std::cerr << "warning: service '" << sref.name << "' not found in any package\n";
|
|
continue;
|
|
}
|
|
auto spec = service::ServiceSpec::from_service_init(it->second);
|
|
auto svc_result = service::install_service(is, spec);
|
|
if (!svc_result.ok)
|
|
std::cerr << "service generation failed for " << sref.name << ": " << svc_result.error << "\n";
|
|
}
|
|
}
|
|
|
|
// Generate bootloader config
|
|
if (cs.bootloader_changed) {
|
|
auto bl = kappa::boot::parse_bootloader(cfg.boot.bootloader);
|
|
if (bl != kappa::boot::Bootloader::Unknown) {
|
|
boot::BootSpec boot_spec;
|
|
boot_spec.kernel_path = cfg.boot.kernel;
|
|
boot_spec.init_path = cfg.boot.init;
|
|
boot_spec.root = cfg.boot.root;
|
|
for (auto& [k, v] : cfg.boot.params)
|
|
boot_spec.kernel_params += k + "=" + v + " ";
|
|
auto boot_result = boot::install_bootloader_config(bl, boot_spec);
|
|
if (!boot_result.ok)
|
|
std::cerr << "bootloader config failed: " << boot_result.error << "\n";
|
|
}
|
|
}
|
|
|
|
// Activate system configuration
|
|
if (!cfg.system.hostname.empty())
|
|
system::write_hostname(cfg.system.hostname);
|
|
if (!cfg.system.timezone.empty())
|
|
system::write_timezone(cfg.system.timezone);
|
|
|
|
std::cout << "rebuild complete — " << sr.built.size() << " packages built\n";
|
|
return 0;
|
|
} catch (const std::runtime_error& e) {
|
|
handle_parse_error(file_arg, source, e);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
} catch (const std::exception& e) {
|
|
std::cerr << "error: " << e.what() << "\n";
|
|
return 1;
|
|
}
|
|
|
|
return 1; // unreachable
|
|
}
|