Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70d59a3f9c | ||
|
|
d0ff52a5d7 | ||
|
|
1f61f116f8 | ||
|
|
d918f7b6fc | ||
|
|
01fcb20a57 | ||
|
|
33ff453cd1 | ||
|
|
b0b54be70e | ||
|
|
e69ebf220d |
+3
-1
@@ -8,7 +8,7 @@ project, keep reading.
|
|||||||
|
|
||||||
These aren't guidelines. They're the deal.
|
These aren't guidelines. They're the deal.
|
||||||
|
|
||||||
### 1. C++23 or don't bother
|
### 1. C++23, or don't bother
|
||||||
|
|
||||||
We compile with Clang, `-std=c++23`, and zero warnings. If your code needs a
|
We compile with Clang, `-std=c++23`, and zero warnings. If your code needs a
|
||||||
polyfill for `std::format` or can't handle designated initializers, it
|
polyfill for `std::format` or can't handle designated initializers, it
|
||||||
@@ -88,6 +88,7 @@ change means someone's `config.kap` stops parsing, it doesn't ship.
|
|||||||
### Pick something
|
### Pick something
|
||||||
|
|
||||||
Good first issues:
|
Good first issues:
|
||||||
|
|
||||||
- Adding a 6th init system backend (we have 5: systemd, openrc, s6, runit, dinit)
|
- Adding a 6th init system backend (we have 5: systemd, openrc, s6, runit, dinit)
|
||||||
- Adding a 3rd bootloader backend (we have 2: grub, limine)
|
- Adding a 3rd bootloader backend (we have 2: grub, limine)
|
||||||
- Improving the `kappa doctor` diagnostics for package recipes
|
- Improving the `kappa doctor` diagnostics for package recipes
|
||||||
@@ -95,6 +96,7 @@ Good first issues:
|
|||||||
- Shell completion scripts (bash, zsh, fish)
|
- Shell completion scripts (bash, zsh, fish)
|
||||||
|
|
||||||
Ambitious issues:
|
Ambitious issues:
|
||||||
|
|
||||||
- Binary package support (pre-built caches)
|
- Binary package support (pre-built caches)
|
||||||
- Remote build farm (distcc-style)
|
- Remote build farm (distcc-style)
|
||||||
- Signed package verification with index signing
|
- Signed package verification with index signing
|
||||||
|
|||||||
+462
-6
@@ -19,7 +19,9 @@
|
|||||||
#include "kappa/tools/format.hpp"
|
#include "kappa/tools/format.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <fcntl.h>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <format>
|
#include <format>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
@@ -27,6 +29,8 @@
|
|||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
using namespace std::string_view_literals;
|
using namespace std::string_view_literals;
|
||||||
using namespace kappa;
|
using namespace kappa;
|
||||||
@@ -53,8 +57,14 @@ Subcommands:
|
|||||||
list List installed packages
|
list List installed packages
|
||||||
rollback Show available generations
|
rollback Show available generations
|
||||||
index <dir> Build an index.kap from .kap files in a directory
|
index <dir> Build an index.kap from .kap files in a directory
|
||||||
|
search <query> Search cached indexes for packages
|
||||||
add <pkg> [v] Add a package to system config (optional version)
|
add <pkg> [v] Add a package to system config (optional version)
|
||||||
|
--features "key=on,key2=off" Set feature flags
|
||||||
|
--config "key=val,key2=val2" Set config values
|
||||||
remove <pkg> Remove a package from system config
|
remove <pkg> Remove a package from system config
|
||||||
|
upgrade Check indexes for newer versions of installed packages
|
||||||
|
bump <file> <v> Update a .kap package to a new version (auto-download, rehash)
|
||||||
|
bump-all <dir> Scan directory and bump all .kap files to latest available
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-h, --help Show this help message
|
-h, --help Show this help message
|
||||||
@@ -107,6 +117,24 @@ static void handle_parse_error(const char* path,
|
|||||||
static resolve::Registry build_registry(const dsl::SystemConfig& cfg) {
|
static resolve::Registry build_registry(const dsl::SystemConfig& cfg) {
|
||||||
resolve::Registry registry;
|
resolve::Registry registry;
|
||||||
|
|
||||||
|
// Pre-populate from cached packages so transitive deps can be resolved
|
||||||
|
auto pkg_dir = paths::packages_dir();
|
||||||
|
std::error_code ec;
|
||||||
|
if (std::filesystem::exists(pkg_dir)) {
|
||||||
|
for (auto& entry : std::filesystem::directory_iterator(pkg_dir, ec)) {
|
||||||
|
if (ec) break;
|
||||||
|
if (entry.path().extension() != ".kap") continue;
|
||||||
|
std::ifstream in(entry.path());
|
||||||
|
if (!in) continue;
|
||||||
|
std::ostringstream buf;
|
||||||
|
buf << in.rdbuf();
|
||||||
|
try {
|
||||||
|
auto pkg = dsl::parse(buf.str());
|
||||||
|
registry.try_emplace(pkg.name, std::move(pkg));
|
||||||
|
} catch (...) { continue; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const auto& pref : cfg.packages) {
|
for (const auto& pref : cfg.packages) {
|
||||||
bool found = false;
|
bool found = false;
|
||||||
|
|
||||||
@@ -221,8 +249,12 @@ int main(int argc, char* argv[]) {
|
|||||||
|| (subcommand == "fetch-package")
|
|| (subcommand == "fetch-package")
|
||||||
|| (subcommand == "rollback")
|
|| (subcommand == "rollback")
|
||||||
|| (subcommand == "index")
|
|| (subcommand == "index")
|
||||||
|
|| (subcommand == "search")
|
||||||
|| (subcommand == "add")
|
|| (subcommand == "add")
|
||||||
|| (subcommand == "remove");
|
|| (subcommand == "remove")
|
||||||
|
|| (subcommand == "upgrade")
|
||||||
|
|| (subcommand == "bump")
|
||||||
|
|| (subcommand == "bump-all");
|
||||||
|
|
||||||
if (!valid_subcommand) {
|
if (!valid_subcommand) {
|
||||||
std::cerr << "error: unknown subcommand '" << subcommand << "'\n\n";
|
std::cerr << "error: unknown subcommand '" << subcommand << "'\n\n";
|
||||||
@@ -232,6 +264,8 @@ int main(int argc, char* argv[]) {
|
|||||||
|
|
||||||
const char* file_arg = nullptr;
|
const char* file_arg = nullptr;
|
||||||
const char* version_arg = nullptr;
|
const char* version_arg = nullptr;
|
||||||
|
const char* features_arg = nullptr;
|
||||||
|
const char* config_arg = nullptr;
|
||||||
bool dry_run = false;
|
bool dry_run = false;
|
||||||
for (int i = arg_start + 1; i < argc; ++i) {
|
for (int i = arg_start + 1; i < argc; ++i) {
|
||||||
if (std::string_view(argv[i]) == "-h"
|
if (std::string_view(argv[i]) == "-h"
|
||||||
@@ -254,6 +288,14 @@ int main(int argc, char* argv[]) {
|
|||||||
paths::set_root(argv[++i]);
|
paths::set_root(argv[++i]);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (std::string_view(argv[i]) == "--features" && i + 1 < argc) {
|
||||||
|
features_arg = argv[++i];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (std::string_view(argv[i]) == "--config" && i + 1 < argc) {
|
||||||
|
config_arg = argv[++i];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (std::string_view(argv[i]) == "--dry-run") {
|
if (std::string_view(argv[i]) == "--dry-run") {
|
||||||
dry_run = true;
|
dry_run = true;
|
||||||
continue;
|
continue;
|
||||||
@@ -329,11 +371,101 @@ int main(int argc, char* argv[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (subcommand == "search") {
|
||||||
|
if (file_arg == nullptr) {
|
||||||
|
std::cerr << "error: no search query specified\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
auto idx_dir = paths::cache_dir() / "indexes";
|
||||||
|
std::error_code ec;
|
||||||
|
if (!std::filesystem::exists(idx_dir)) {
|
||||||
|
std::cout << "no cached indexes — run kappa fetch-package <name> first\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view query(file_arg);
|
||||||
|
int found = 0;
|
||||||
|
for (auto& entry : std::filesystem::directory_iterator(idx_dir, ec)) {
|
||||||
|
if (ec) break;
|
||||||
|
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 idx = dsl::parse_index(buf.str());
|
||||||
|
for (auto& pkg : idx.packages) {
|
||||||
|
if (pkg.name.find(query) != std::string::npos) {
|
||||||
|
std::cout << pkg.name << " " << pkg.version
|
||||||
|
<< " @ " << idx.name << "\n";
|
||||||
|
found++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (...) { continue; }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (found == 0) {
|
||||||
|
std::cout << "no packages matching '" << query << "'\n";
|
||||||
|
} else {
|
||||||
|
std::cout << found << " result(s)\n";
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (subcommand == "add") {
|
if (subcommand == "add") {
|
||||||
if (file_arg == nullptr) {
|
if (file_arg == nullptr) {
|
||||||
std::cerr << "error: no package name specified\n";
|
std::cerr << "error: no package name specified\n";
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse --features key=on,key2=off
|
||||||
|
std::unordered_map<std::string, dsl::FeatureDef> features;
|
||||||
|
if (features_arg != nullptr) {
|
||||||
|
std::string_view fsv(features_arg);
|
||||||
|
std::size_t pos = 0;
|
||||||
|
while (pos < fsv.size()) {
|
||||||
|
auto comma = fsv.find(',', pos);
|
||||||
|
auto pair = fsv.substr(pos, comma == std::string_view::npos
|
||||||
|
? std::string_view::npos
|
||||||
|
: comma - pos);
|
||||||
|
auto eq = pair.find('=');
|
||||||
|
if (eq != std::string_view::npos) {
|
||||||
|
auto key = std::string(pair.substr(0, eq));
|
||||||
|
auto val = pair.substr(eq + 1);
|
||||||
|
while (!key.empty() && key.back() == ' ') key.pop_back();
|
||||||
|
while (!val.empty() && val.front() == ' ') val.remove_prefix(1);
|
||||||
|
features[key] = {val == "on" || val == "true" || val == "1",
|
||||||
|
false, ""};
|
||||||
|
}
|
||||||
|
pos = comma == std::string_view::npos ? fsv.size() : comma + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse --config key=val,key2=val2
|
||||||
|
std::unordered_map<std::string, std::string> config;
|
||||||
|
if (config_arg != nullptr) {
|
||||||
|
std::string_view csv(config_arg);
|
||||||
|
std::size_t pos = 0;
|
||||||
|
while (pos < csv.size()) {
|
||||||
|
auto comma = csv.find(',', pos);
|
||||||
|
auto pair = csv.substr(pos, comma == std::string_view::npos
|
||||||
|
? std::string_view::npos
|
||||||
|
: comma - pos);
|
||||||
|
auto eq = pair.find('=');
|
||||||
|
if (eq != std::string_view::npos) {
|
||||||
|
auto key = std::string(pair.substr(0, eq));
|
||||||
|
auto val = std::string(pair.substr(eq + 1));
|
||||||
|
while (!key.empty() && key.back() == ' ') key.pop_back();
|
||||||
|
while (!val.empty() && val.front() == ' ') val.erase(0, 1);
|
||||||
|
config[key] = val;
|
||||||
|
}
|
||||||
|
pos = comma == std::string_view::npos ? csv.size() : comma + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
auto config_path = std::filesystem::path(paths::system_dir()) / "config.kap";
|
auto config_path = std::filesystem::path(paths::system_dir()) / "config.kap";
|
||||||
dsl::SystemConfig cfg;
|
dsl::SystemConfig cfg;
|
||||||
@@ -347,10 +479,21 @@ int main(int argc, char* argv[]) {
|
|||||||
for (auto& p : cfg.packages) {
|
for (auto& p : cfg.packages) {
|
||||||
if (p.name == file_arg) {
|
if (p.name == file_arg) {
|
||||||
found = true;
|
found = true;
|
||||||
|
bool changed = false;
|
||||||
if (version_arg != nullptr) {
|
if (version_arg != nullptr) {
|
||||||
p.version = version_arg;
|
p.version = version_arg;
|
||||||
std::cout << "updated " << file_arg
|
changed = true;
|
||||||
<< " → version " << version_arg << "\n";
|
}
|
||||||
|
if (!features.empty()) {
|
||||||
|
for (auto& [k, v] : features) { p.features[k] = v; changed = true; }
|
||||||
|
}
|
||||||
|
if (!config.empty()) {
|
||||||
|
for (auto& [k, v] : config) { p.config[k] = v; changed = true; }
|
||||||
|
}
|
||||||
|
if (changed) {
|
||||||
|
std::cout << "updated " << file_arg;
|
||||||
|
if (version_arg != nullptr) std::cout << " → " << version_arg;
|
||||||
|
std::cout << "\n";
|
||||||
} else {
|
} else {
|
||||||
std::cout << file_arg << " already in packages\n";
|
std::cout << file_arg << " already in packages\n";
|
||||||
}
|
}
|
||||||
@@ -364,11 +507,11 @@ int main(int argc, char* argv[]) {
|
|||||||
if (version_arg != nullptr) {
|
if (version_arg != nullptr) {
|
||||||
pref.version = version_arg;
|
pref.version = version_arg;
|
||||||
}
|
}
|
||||||
|
pref.features = std::move(features);
|
||||||
|
pref.config = std::move(config);
|
||||||
cfg.packages.push_back(std::move(pref));
|
cfg.packages.push_back(std::move(pref));
|
||||||
std::cout << "added " << file_arg;
|
std::cout << "added " << file_arg;
|
||||||
if (version_arg != nullptr) {
|
if (version_arg != nullptr) std::cout << " " << version_arg;
|
||||||
std::cout << " " << version_arg;
|
|
||||||
}
|
|
||||||
std::cout << "\n";
|
std::cout << "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,6 +568,319 @@ int main(int argc, char* argv[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (subcommand == "upgrade") {
|
||||||
|
auto installed = install::read_installed();
|
||||||
|
if (installed.empty()) {
|
||||||
|
std::cout << "no packages installed\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a map of latest versions from cached indexes
|
||||||
|
std::unordered_map<std::string, std::string> latest;
|
||||||
|
auto idx_dir = paths::cache_dir() / "indexes";
|
||||||
|
std::error_code ec;
|
||||||
|
if (std::filesystem::exists(idx_dir)) {
|
||||||
|
for (auto& entry : std::filesystem::directory_iterator(idx_dir, ec)) {
|
||||||
|
if (ec) break;
|
||||||
|
if (entry.path().extension() != ".kap") continue;
|
||||||
|
std::ifstream in(entry.path());
|
||||||
|
if (!in) continue;
|
||||||
|
std::ostringstream buf;
|
||||||
|
buf << in.rdbuf();
|
||||||
|
try {
|
||||||
|
auto idx = dsl::parse_index(buf.str());
|
||||||
|
for (auto& pkg : idx.packages) {
|
||||||
|
auto it = latest.find(pkg.name);
|
||||||
|
if (it == latest.end() || pkg.version > it->second) {
|
||||||
|
latest[pkg.name] = pkg.version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (...) { continue; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (latest.empty()) {
|
||||||
|
std::cout << "no cached indexes — run kappa fetch-package <name> first\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::pair<std::string, std::string>> upgrades;
|
||||||
|
for (auto& e : installed) {
|
||||||
|
auto it = latest.find(e.name);
|
||||||
|
if (it != latest.end() && it->second > e.version) {
|
||||||
|
upgrades.emplace_back(e.name, it->second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (upgrades.empty()) {
|
||||||
|
std::cout << "all " << installed.size() << " packages up to date\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << upgrades.size() << " upgrade(s) available:\n";
|
||||||
|
for (auto& [name, ver] : upgrades) {
|
||||||
|
// Find old version
|
||||||
|
std::string old_ver;
|
||||||
|
for (auto& e : installed) {
|
||||||
|
if (e.name == name) { old_ver = e.version; break; }
|
||||||
|
}
|
||||||
|
std::cout << " " << name << " " << old_ver << " → " << ver << "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dry_run) {
|
||||||
|
std::cout << "run kappa add <pkg> <version> for each to apply\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply: update system config with new versions
|
||||||
|
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());
|
||||||
|
try {
|
||||||
|
cfg = dsl::parse_system_config(src);
|
||||||
|
} catch (...) { cfg = {}; }
|
||||||
|
}
|
||||||
|
|
||||||
|
for (auto& [name, ver] : upgrades) {
|
||||||
|
bool found = false;
|
||||||
|
for (auto& p : cfg.packages) {
|
||||||
|
if (p.name == name) {
|
||||||
|
p.version = ver;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
dsl::PackageRef pref;
|
||||||
|
pref.name = name;
|
||||||
|
pref.version = ver;
|
||||||
|
cfg.packages.push_back(std::move(pref));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::create_directories(paths::system_dir(), ec);
|
||||||
|
std::ofstream out(config_path);
|
||||||
|
if (!out) {
|
||||||
|
std::cerr << "error: cannot write config\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
tools::format_config(out, cfg);
|
||||||
|
|
||||||
|
std::cout << "config updated — run kappa rebuild to apply\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand == "bump") {
|
||||||
|
if (file_arg == nullptr || version_arg == nullptr) {
|
||||||
|
std::cerr << "error: bump requires <file> <new-version>\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
auto src = read_file(file_arg);
|
||||||
|
auto pkg = dsl::parse(src);
|
||||||
|
|
||||||
|
auto old_version = pkg.version;
|
||||||
|
auto old_sha = pkg.sha256;
|
||||||
|
|
||||||
|
// Construct new source URL with the new version
|
||||||
|
auto new_source = pkg.source;
|
||||||
|
std::string_view new_ver(version_arg);
|
||||||
|
for (auto& [from, to] : {
|
||||||
|
std::pair{"${version}"sv, new_ver},
|
||||||
|
std::pair{"${name}"sv, std::string_view(pkg.name)}}) {
|
||||||
|
std::size_t pos = 0;
|
||||||
|
while ((pos = new_source.find(from, pos)) != std::string::npos) {
|
||||||
|
new_source.replace(pos, from.size(), to);
|
||||||
|
pos += to.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "downloading " << new_source << "...\n";
|
||||||
|
auto temp_tarball = paths::temp_dir() / "bump-download.tar.gz";
|
||||||
|
std::filesystem::create_directories(paths::temp_dir());
|
||||||
|
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
execlp("curl", "curl", "-Lsf", "-o", temp_tarball.c_str(),
|
||||||
|
new_source.c_str(), nullptr);
|
||||||
|
_exit(1);
|
||||||
|
}
|
||||||
|
int status = 0;
|
||||||
|
while (waitpid(pid, &status, 0) == -1 && errno == EINTR) {}
|
||||||
|
if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) {
|
||||||
|
std::cerr << "download failed\n";
|
||||||
|
std::filesystem::remove(temp_tarball);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute new hash
|
||||||
|
int pipefd[2];
|
||||||
|
if (pipe(pipefd) != 0) { return 1; }
|
||||||
|
pid_t pid2 = fork();
|
||||||
|
if (pid2 == 0) {
|
||||||
|
close(pipefd[0]);
|
||||||
|
dup2(pipefd[1], STDOUT_FILENO);
|
||||||
|
close(pipefd[1]);
|
||||||
|
execlp("sha256sum", "sha256sum", temp_tarball.c_str(), nullptr);
|
||||||
|
_exit(1);
|
||||||
|
}
|
||||||
|
close(pipefd[1]);
|
||||||
|
std::array<char, 128> buf;
|
||||||
|
std::string hash_out;
|
||||||
|
ssize_t n;
|
||||||
|
while ((n = read(pipefd[0], buf.data(), buf.size() - 1)) > 0) {
|
||||||
|
buf[static_cast<std::size_t>(n)] = '\0';
|
||||||
|
hash_out += buf.data();
|
||||||
|
}
|
||||||
|
close(pipefd[0]);
|
||||||
|
while (waitpid(pid2, nullptr, 0) == -1 && errno == EINTR) {}
|
||||||
|
|
||||||
|
auto space = hash_out.find(' ');
|
||||||
|
auto new_sha = (space != std::string::npos)
|
||||||
|
? hash_out.substr(0, space) : hash_out;
|
||||||
|
|
||||||
|
std::filesystem::remove(temp_tarball);
|
||||||
|
|
||||||
|
// Update the .kap file
|
||||||
|
pkg.version = version_arg;
|
||||||
|
pkg.sha256 = new_sha;
|
||||||
|
|
||||||
|
std::ofstream out(file_arg);
|
||||||
|
if (!out) {
|
||||||
|
std::cerr << "error: cannot write " << file_arg << "\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
tools::format_package(out, pkg);
|
||||||
|
|
||||||
|
std::cout << "bumped " << pkg.name << " " << old_version
|
||||||
|
<< " → " << version_arg << " sha256: " << new_sha << "\n";
|
||||||
|
return 0;
|
||||||
|
} catch (const std::exception& e) {
|
||||||
|
std::cerr << "bump error: " << e.what() << "\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subcommand == "bump-all") {
|
||||||
|
if (file_arg == nullptr) {
|
||||||
|
std::cerr << "error: bump-all requires a directory\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
std::string dir(file_arg);
|
||||||
|
int bumped = 0;
|
||||||
|
int skipped = 0;
|
||||||
|
std::error_code ec;
|
||||||
|
|
||||||
|
for (auto& entry : std::filesystem::directory_iterator(dir, ec)) {
|
||||||
|
if (ec) break;
|
||||||
|
if (entry.path().extension() != ".kap") continue;
|
||||||
|
|
||||||
|
std::ifstream in(entry.path());
|
||||||
|
if (!in) continue;
|
||||||
|
std::ostringstream buf;
|
||||||
|
buf << in.rdbuf();
|
||||||
|
in.close();
|
||||||
|
|
||||||
|
dsl::PackageDef pkg;
|
||||||
|
try { pkg = dsl::parse(buf.str()); }
|
||||||
|
catch (...) { skipped++; continue; }
|
||||||
|
|
||||||
|
// Try to auto-detect next version: increment patch number
|
||||||
|
auto dot1 = pkg.version.rfind('.');
|
||||||
|
if (dot1 == std::string::npos) { skipped++; continue; }
|
||||||
|
|
||||||
|
auto base = pkg.version.substr(0, dot1 + 1);
|
||||||
|
auto patch_str = pkg.version.substr(dot1 + 1);
|
||||||
|
|
||||||
|
int patch = 0;
|
||||||
|
try { patch = std::stoi(patch_str); }
|
||||||
|
catch (...) { skipped++; continue; }
|
||||||
|
|
||||||
|
// Try next 15 patch versions
|
||||||
|
bool found = false;
|
||||||
|
for (int next = patch + 1; next <= patch + 15; ++next) {
|
||||||
|
auto candidate = base + std::to_string(next);
|
||||||
|
auto url = pkg.source;
|
||||||
|
std::string_view cand_ver(candidate);
|
||||||
|
for (auto& [from, to] : {
|
||||||
|
std::pair{"${version}"sv, cand_ver},
|
||||||
|
std::pair{"${name}"sv, std::string_view(pkg.name)}}) {
|
||||||
|
std::size_t pos = 0;
|
||||||
|
while ((pos = url.find(from, pos)) != std::string::npos) {
|
||||||
|
url.replace(pos, from.size(), to);
|
||||||
|
pos += to.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try downloading the candidate URL
|
||||||
|
auto temp = paths::temp_dir() / "bumpall-download.tar.gz";
|
||||||
|
std::filesystem::create_directories(paths::temp_dir());
|
||||||
|
pid_t dl_pid = fork();
|
||||||
|
if (dl_pid == 0) {
|
||||||
|
// Redirect stdout/stderr to /dev/null for quiet operation
|
||||||
|
int devnull = open("/dev/null", O_WRONLY);
|
||||||
|
if (devnull >= 0) {
|
||||||
|
dup2(devnull, STDOUT_FILENO);
|
||||||
|
dup2(devnull, STDERR_FILENO);
|
||||||
|
close(devnull);
|
||||||
|
}
|
||||||
|
execlp("curl", "curl", "-Lsf", "-o", temp.c_str(), url.c_str(), nullptr);
|
||||||
|
_exit(1);
|
||||||
|
}
|
||||||
|
int dl_status = 0;
|
||||||
|
while (waitpid(dl_pid, &dl_status, 0) == -1 && errno == EINTR) {}
|
||||||
|
if (!WIFEXITED(dl_status) || WEXITSTATUS(dl_status) != 0) {
|
||||||
|
std::filesystem::remove(temp);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash
|
||||||
|
int hpipe[2];
|
||||||
|
if (pipe(hpipe) != 0) continue;
|
||||||
|
pid_t hpid = fork();
|
||||||
|
if (hpid == 0) {
|
||||||
|
close(hpipe[0]); dup2(hpipe[1], STDOUT_FILENO); close(hpipe[1]);
|
||||||
|
execlp("sha256sum", "sha256sum", temp.c_str(), nullptr);
|
||||||
|
_exit(1);
|
||||||
|
}
|
||||||
|
close(hpipe[1]);
|
||||||
|
std::array<char, 128> hbuf;
|
||||||
|
std::string hout;
|
||||||
|
ssize_t hn;
|
||||||
|
while ((hn = read(hpipe[0], hbuf.data(), hbuf.size() - 1)) > 0) {
|
||||||
|
hbuf[static_cast<std::size_t>(hn)] = '\0';
|
||||||
|
hout += hbuf.data();
|
||||||
|
}
|
||||||
|
close(hpipe[0]);
|
||||||
|
while (waitpid(hpid, nullptr, 0) == -1 && errno == EINTR) {}
|
||||||
|
std::filesystem::remove(temp);
|
||||||
|
|
||||||
|
auto sp = hout.find(' ');
|
||||||
|
auto new_sha = (sp != std::string::npos) ? hout.substr(0, sp) : hout;
|
||||||
|
|
||||||
|
// Update file
|
||||||
|
auto old_ver = pkg.version;
|
||||||
|
pkg.version = candidate;
|
||||||
|
pkg.sha256 = new_sha;
|
||||||
|
|
||||||
|
std::ofstream out(entry.path());
|
||||||
|
if (!out) continue;
|
||||||
|
tools::format_package(out, pkg);
|
||||||
|
|
||||||
|
std::cout << pkg.name << " " << old_ver
|
||||||
|
<< " → " << candidate << "\n";
|
||||||
|
found = true;
|
||||||
|
bumped++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!found) skipped++;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << bumped << " bumped, " << skipped << " skipped\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (subcommand == "fetch-package") {
|
if (subcommand == "fetch-package") {
|
||||||
if (file_arg == nullptr) {
|
if (file_arg == nullptr) {
|
||||||
std::cerr << "error: no package name specified\n";
|
std::cerr << "error: no package name specified\n";
|
||||||
|
|||||||
+52
-1
@@ -42,7 +42,58 @@ BuildPlan resolve(const dsl::SystemConfig& cfg, const Registry& registry) {
|
|||||||
nodes.push_back(std::move(step));
|
nodes.push_back(std::move(step));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect conflicts between selected packages
|
// Transitive dependency discovery: pull in deps that are in the
|
||||||
|
// registry but not yet in the plan. Continue until no new deps
|
||||||
|
// are discovered — handles arbitrarily deep dependency chains.
|
||||||
|
bool changed = true;
|
||||||
|
while (changed) {
|
||||||
|
changed = false;
|
||||||
|
std::size_t current_size = nodes.size();
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < current_size; ++i) {
|
||||||
|
if (!nodes[i].package) continue;
|
||||||
|
|
||||||
|
for (auto& dep : nodes[i].package->depends) {
|
||||||
|
if (name_to_idx.contains(dep.name)) continue;
|
||||||
|
|
||||||
|
// Skip feature-conditional deps that aren't enabled
|
||||||
|
if (!dep.feature.empty()) {
|
||||||
|
auto fit = nodes[i].resolved.features.find(dep.feature);
|
||||||
|
if (fit == nodes[i].resolved.features.end() || !fit->second.enabled) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto rit = registry.find(dep.name);
|
||||||
|
if (rit == registry.end()) continue;
|
||||||
|
|
||||||
|
auto& pkg = rit->second;
|
||||||
|
auto resolved = config::resolve_package(pkg, cfg.system, {});
|
||||||
|
|
||||||
|
BuildStep step;
|
||||||
|
step.name = pkg.name;
|
||||||
|
step.package = &pkg;
|
||||||
|
step.resolved = std::move(resolved);
|
||||||
|
step.enabled_init = cfg.boot.init;
|
||||||
|
|
||||||
|
for (auto& ddep : pkg.depends) {
|
||||||
|
if (!ddep.feature.empty()) {
|
||||||
|
auto fit = step.resolved.features.find(ddep.feature);
|
||||||
|
if (fit == step.resolved.features.end() || !fit->second.enabled) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
step.dependencies.push_back({ddep.name, ddep.version});
|
||||||
|
}
|
||||||
|
|
||||||
|
name_to_idx[step.name] = nodes.size();
|
||||||
|
nodes.push_back(std::move(step));
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect conflicts between all selected packages (including transitive)
|
||||||
std::unordered_set<std::string> selected;
|
std::unordered_set<std::string> selected;
|
||||||
for (auto& node : nodes) { selected.insert(node.name); }
|
for (auto& node : nodes) { selected.insert(node.name); }
|
||||||
|
|
||||||
|
|||||||
+209
@@ -521,6 +521,39 @@ check_fail "remove needs name" \
|
|||||||
"$KAPPA remove" \
|
"$KAPPA remove" \
|
||||||
'no package name'
|
'no package name'
|
||||||
|
|
||||||
|
|
||||||
|
# --features and --config flags
|
||||||
|
check "add with --features" \
|
||||||
|
"$KAPPA add featpkg --features 'ssl=on,gzip=off'" \
|
||||||
|
'added featpkg'
|
||||||
|
|
||||||
|
check "add features present in config" \
|
||||||
|
"grep -c 'ssl = true' $KAPPA_ROOT/system/config.kap" \
|
||||||
|
'1'
|
||||||
|
|
||||||
|
check "update existing with --features" \
|
||||||
|
"$KAPPA add featpkg --features 'http2=on'" \
|
||||||
|
'updated featpkg'
|
||||||
|
|
||||||
|
check "add with --config" \
|
||||||
|
"$KAPPA add cfgpkg --config 'port=9090,worker=4'" \
|
||||||
|
'added cfgpkg'
|
||||||
|
|
||||||
|
check "config values written to file" \
|
||||||
|
"grep 'port = 9090' $KAPPA_ROOT/system/config.kap" \
|
||||||
|
'port = 9090'
|
||||||
|
|
||||||
|
check "add with features and config together" \
|
||||||
|
"$KAPPA add combopkg --features 'ssl=on' --config 'host=localhost'" \
|
||||||
|
'added combopkg'
|
||||||
|
|
||||||
|
check "features and config both in file" \
|
||||||
|
"grep -c ssl $KAPPA_ROOT/system/config.kap" \
|
||||||
|
'2'
|
||||||
|
|
||||||
|
check "add idempotent with no flags" \
|
||||||
|
"$KAPPA add cfgpkg" \
|
||||||
|
'already in packages'
|
||||||
# ═══════════════════════════════════════════
|
# ═══════════════════════════════════════════
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Install: list and rollback ==="
|
echo "=== Install: list and rollback ==="
|
||||||
@@ -670,6 +703,64 @@ check "resolve deptree (c has dep)" \
|
|||||||
"$KAPPA resolve $TMPDIR/deptree-config.kap 2>&1" \
|
"$KAPPA resolve $TMPDIR/deptree-config.kap 2>&1" \
|
||||||
'dep-c (1 deps)'
|
'dep-c (1 deps)'
|
||||||
|
|
||||||
|
# Transitive dependency resolution
|
||||||
|
cat > "$KAPPA_ROOT/cache/packages/trans-a.kap" << 'TRANSEOF'
|
||||||
|
package "trans-a" {
|
||||||
|
version = "1.0"
|
||||||
|
source = "a.tar.gz"
|
||||||
|
sha256 = "abc"
|
||||||
|
license = "MIT"
|
||||||
|
build { echo a }
|
||||||
|
install { echo a }
|
||||||
|
}
|
||||||
|
TRANSEOF
|
||||||
|
cat > "$KAPPA_ROOT/cache/packages/trans-b.kap" << 'TRANSEOF'
|
||||||
|
package "trans-b" {
|
||||||
|
version = "1.0"
|
||||||
|
source = "b.tar.gz"
|
||||||
|
sha256 = "abc"
|
||||||
|
license = "MIT"
|
||||||
|
depends = [{ name = "trans-a" }]
|
||||||
|
build { echo b }
|
||||||
|
install { echo b }
|
||||||
|
}
|
||||||
|
TRANSEOF
|
||||||
|
cat > "$KAPPA_ROOT/cache/packages/trans-c.kap" << 'TRANSEOF'
|
||||||
|
package "trans-c" {
|
||||||
|
version = "1.0"
|
||||||
|
source = "c.tar.gz"
|
||||||
|
sha256 = "abc"
|
||||||
|
license = "MIT"
|
||||||
|
depends = [{ name = "trans-b" }]
|
||||||
|
build { echo c }
|
||||||
|
install { echo c }
|
||||||
|
}
|
||||||
|
TRANSEOF
|
||||||
|
|
||||||
|
cat > "$TMPDIR/transitive-config.kap" << 'TRANSEOF'
|
||||||
|
system { hostname = "test" }
|
||||||
|
packages { trans-c {} }
|
||||||
|
services {}
|
||||||
|
boot {
|
||||||
|
kernel = "linux"
|
||||||
|
init = "s6"
|
||||||
|
root = "/dev/sda1"
|
||||||
|
bootloader = "limine"
|
||||||
|
}
|
||||||
|
TRANSEOF
|
||||||
|
|
||||||
|
check "transitive deps auto-resolve (only c declared)" \
|
||||||
|
"$KAPPA resolve $TMPDIR/transitive-config.kap 2>&1" \
|
||||||
|
'trans-a (0 deps)'
|
||||||
|
|
||||||
|
check "transitive deps include intermediate" \
|
||||||
|
"$KAPPA resolve $TMPDIR/transitive-config.kap 2>&1" \
|
||||||
|
'trans-b (1 deps)'
|
||||||
|
|
||||||
|
check "transitive deps resolve root last" \
|
||||||
|
"$KAPPA resolve $TMPDIR/transitive-config.kap 2>&1" \
|
||||||
|
'trans-c (1 deps)'
|
||||||
|
|
||||||
# ═══════════════════════════════════════════
|
# ═══════════════════════════════════════════
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Build: simple execution ==="
|
echo "=== Build: simple execution ==="
|
||||||
@@ -955,6 +1046,124 @@ check "parse-config with features" \
|
|||||||
|
|
||||||
# ═══════════════════════════════════════════
|
# ═══════════════════════════════════════════
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Search: cached index queries ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
mkdir -p "$KAPPA_ROOT/cache/indexes"
|
||||||
|
cat > "$KAPPA_ROOT/cache/indexes/test-repo-stable.kap" << 'SRCHEOF'
|
||||||
|
index "test-repo/stable" {
|
||||||
|
trans-a { version = "1.0" }
|
||||||
|
trans-b { version = "1.0" }
|
||||||
|
trans-c { version = "1.0" }
|
||||||
|
testpkg { version = "1.0" }
|
||||||
|
}
|
||||||
|
SRCHEOF
|
||||||
|
|
||||||
|
check "search finds package by name" \
|
||||||
|
"$KAPPA search trans-a" \
|
||||||
|
'trans-a'
|
||||||
|
|
||||||
|
check "search finds by substring" \
|
||||||
|
"$KAPPA search ans" \
|
||||||
|
'trans-a'
|
||||||
|
|
||||||
|
check "search shows repo source" \
|
||||||
|
"$KAPPA search trans-a" \
|
||||||
|
'test-repo/stable'
|
||||||
|
|
||||||
|
check "search no match" \
|
||||||
|
"$KAPPA search zzz999" \
|
||||||
|
'no packages matching'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Upgrade: version comparison ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
mkdir -p "$KAPPA_ROOT/db"
|
||||||
|
cat > "$KAPPA_ROOT/db/installed" << 'UPGEOF'
|
||||||
|
trans-a 0.9 a1b2c3d4e5f6a7b8
|
||||||
|
trans-b 0.9 b2c3d4e5f6a7b8c9
|
||||||
|
trans-c 1.0 c3d4e5f6a7b8c9d0
|
||||||
|
UPGEOF
|
||||||
|
|
||||||
|
check "upgrade detects newer versions" \
|
||||||
|
"$KAPPA upgrade 2>&1" \
|
||||||
|
'trans-a'
|
||||||
|
|
||||||
|
check "upgrade shows old version" \
|
||||||
|
"$KAPPA upgrade 2>&1" \
|
||||||
|
'0.9'
|
||||||
|
|
||||||
|
check "upgrade dry-run does not apply" \
|
||||||
|
"$KAPPA upgrade --dry-run 2>&1" \
|
||||||
|
'run kappa add'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Bump: explicit version update ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
cat > "$TMPDIR/bump-test.kap" << 'BMPEOF'
|
||||||
|
package "bumpme" {
|
||||||
|
const version = "4.4.0"
|
||||||
|
const source = "https://ftp.gnu.org/gnu/make/make-${version}.tar.gz"
|
||||||
|
sha256 = "oldhash"
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
build { make }
|
||||||
|
install { make install }
|
||||||
|
}
|
||||||
|
BMPEOF
|
||||||
|
|
||||||
|
check "bump updates version" \
|
||||||
|
"$KAPPA bump $TMPDIR/bump-test.kap 4.4.1" \
|
||||||
|
'bumped bumpme'
|
||||||
|
|
||||||
|
check "bump writes correct version" \
|
||||||
|
"grep -c '4.4.1' $TMPDIR/bump-test.kap" \
|
||||||
|
'1'
|
||||||
|
|
||||||
|
check "bump preserves source URL" \
|
||||||
|
"grep -c 'make-' $TMPDIR/bump-test.kap" \
|
||||||
|
'1'
|
||||||
|
|
||||||
|
check_fail "bump requires version arg" \
|
||||||
|
"$KAPPA bump $TMPDIR/bump-test.kap" \
|
||||||
|
'requires'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Bump-all: directory scan ==="
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
BPALLDIR="$TMPDIR/bumpall-pkgs"
|
||||||
|
mkdir -p "$BPALLDIR"
|
||||||
|
|
||||||
|
cat > "$BPALLDIR/make.kap" << 'BMPEOF'
|
||||||
|
package "make" {
|
||||||
|
const version = "4.4.0"
|
||||||
|
const source = "https://ftp.gnu.org/gnu/make/make-${version}.tar.gz"
|
||||||
|
sha256 = "old"
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
build { make }
|
||||||
|
install { make install }
|
||||||
|
}
|
||||||
|
BMPEOF
|
||||||
|
|
||||||
|
check "bump-all finds newer version" \
|
||||||
|
"$KAPPA bump-all $BPALLDIR" \
|
||||||
|
'4.4.0 → 4.4.1'
|
||||||
|
|
||||||
|
check "bump-all reports count" \
|
||||||
|
"$KAPPA bump-all $BPALLDIR" \
|
||||||
|
'bumped'
|
||||||
|
|
||||||
|
check_fail "bump-all needs directory" \
|
||||||
|
"$KAPPA bump-all" \
|
||||||
|
'requires a directory'
|
||||||
|
rm -f "$KAPPA_ROOT/db/installed"
|
||||||
echo "=== Rebuild: dry-run detection ==="
|
echo "=== Rebuild: dry-run detection ==="
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user