[M8] cargoxx-pkgs as a flake: cargoxx add → string-form dep
Wrapper: fix cache.nixos.org-1 key; drop AppImage; pin publish to mozart/cargoxx-pkgs.
This commit is contained in:
@@ -63,6 +63,7 @@ target_sources(cargoxx
|
|||||||
../src/manifest/parser.cpp
|
../src/manifest/parser.cpp
|
||||||
../src/manifest/writer.cpp
|
../src/manifest/writer.cpp
|
||||||
../src/resolver/brute_scan.cpp
|
../src/resolver/brute_scan.cpp
|
||||||
|
../src/resolver/cargoxx_pkgs_probe.cpp
|
||||||
../src/resolver/conan_probe.cpp
|
../src/resolver/conan_probe.cpp
|
||||||
../src/resolver/discover.cpp
|
../src/resolver/discover.cpp
|
||||||
../src/resolver/findmodule_scan.cpp
|
../src/resolver/findmodule_scan.cpp
|
||||||
@@ -201,6 +202,15 @@ target_link_libraries(test_cmd_new PRIVATE
|
|||||||
Catch2::Catch2WithMain
|
Catch2::Catch2WithMain
|
||||||
)
|
)
|
||||||
catch_discover_tests(test_cmd_new)
|
catch_discover_tests(test_cmd_new)
|
||||||
|
add_executable(test_cmd_publish_validation ../tests/cmd_publish_validation.cpp)
|
||||||
|
target_link_libraries(test_cmd_publish_validation PRIVATE
|
||||||
|
cargoxx
|
||||||
|
reproc
|
||||||
|
SQLite::SQLite3
|
||||||
|
Catch2::Catch2
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
catch_discover_tests(test_cmd_publish_validation)
|
||||||
add_executable(test_cmd_remove ../tests/cmd_remove.cpp)
|
add_executable(test_cmd_remove ../tests/cmd_remove.cpp)
|
||||||
target_link_libraries(test_cmd_remove PRIVATE
|
target_link_libraries(test_cmd_remove PRIVATE
|
||||||
cargoxx
|
cargoxx
|
||||||
|
|||||||
@@ -35,7 +35,7 @@
|
|||||||
experimental-features = nix-command flakes
|
experimental-features = nix-command flakes
|
||||||
build-users-group =
|
build-users-group =
|
||||||
substituters = https://cache.cargoxx.amadey.xyz https://cache.nixos.org
|
substituters = https://cache.cargoxx.amadey.xyz https://cache.nixos.org
|
||||||
trusted-public-keys = cache.cargoxx.amadey.xyz:HQNcKDh9lufWm0M32a06AEiLf1Hr0WoRY3Bp5NnWZxs= cache.nixos.org:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=
|
trusted-public-keys = cache.cargoxx.amadey.xyz:HQNcKDh9lufWm0M32a06AEiLf1Hr0WoRY3Bp5NnWZxs= cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=
|
||||||
'';
|
'';
|
||||||
|
|
||||||
cargoxx-bin = pkgs.gcc15Stdenv.mkDerivation {
|
cargoxx-bin = pkgs.gcc15Stdenv.mkDerivation {
|
||||||
@@ -53,9 +53,6 @@
|
|||||||
'';
|
'';
|
||||||
installPhase = ''
|
installPhase = ''
|
||||||
mkdir -p $out/bin
|
mkdir -p $out/bin
|
||||||
# Codegen since Phase 1a routes every executable through
|
|
||||||
# RUNTIME_OUTPUT_DIRECTORY=${"$"}{CMAKE_BINARY_DIR}/bin, so
|
|
||||||
# the binary lives at build/release/bin/cargoxx.
|
|
||||||
cp build/release/bin/cargoxx $out/bin/
|
cp build/release/bin/cargoxx $out/bin/
|
||||||
wrapProgram $out/bin/cargoxx \
|
wrapProgram $out/bin/cargoxx \
|
||||||
--prefix PATH : ${cargoxxRuntimePath} \
|
--prefix PATH : ${cargoxxRuntimePath} \
|
||||||
@@ -189,7 +186,6 @@
|
|||||||
'';
|
'';
|
||||||
in {
|
in {
|
||||||
packages.default = cargoxx-bin;
|
packages.default = cargoxx-bin;
|
||||||
packages.appimage = bundlers-sys.toAppImage cargoxx-bin;
|
|
||||||
packages.dockerImage = bundlers-sys.toDockerImage cargoxx-bin;
|
packages.dockerImage = bundlers-sys.toDockerImage cargoxx-bin;
|
||||||
packages.deb = bundlers-sys.toDEB cargoxx-bin;
|
packages.deb = bundlers-sys.toDEB cargoxx-bin;
|
||||||
packages.rpm = bundlers-sys.toRPM cargoxx-bin;
|
packages.rpm = bundlers-sys.toRPM cargoxx-bin;
|
||||||
@@ -197,7 +193,6 @@
|
|||||||
|
|
||||||
# Reusable packaging functions, all of the shape `drv -> drv`.
|
# Reusable packaging functions, all of the shape `drv -> drv`.
|
||||||
# Mirror the `to*` naming used by github:NixOS/bundlers.
|
# Mirror the `to*` naming used by github:NixOS/bundlers.
|
||||||
lib.toAppImage = bundlers-sys.toAppImage;
|
|
||||||
lib.toDockerImage = bundlers-sys.toDockerImage;
|
lib.toDockerImage = bundlers-sys.toDockerImage;
|
||||||
lib.toDEB = bundlers-sys.toDEB;
|
lib.toDEB = bundlers-sys.toDEB;
|
||||||
lib.toRPM = bundlers-sys.toRPM;
|
lib.toRPM = bundlers-sys.toRPM;
|
||||||
|
|||||||
@@ -30,17 +30,14 @@ auto cmd_vendor(const std::filesystem::path& project_root,
|
|||||||
-> util::Result<void>;
|
-> util::Result<void>;
|
||||||
|
|
||||||
// Publish the project's current HEAD as a new version recipe in the
|
// Publish the project's current HEAD as a new version recipe in the
|
||||||
// cargoxx-pkgs registry. Validates manifest + lockfile, computes the
|
// cargoxx-pkgs repo (mozart/cargoxx-pkgs). Validates manifest + lockfile,
|
||||||
// source sha256 via `nix flake prefetch`, writes
|
// computes the source sha256 via `nix flake prefetch`, writes
|
||||||
// `recipes/<name>/versions/<version>.toml` (and `maintainers.txt` for
|
// `recipes/<name>/versions/<version>.toml` (and `maintainers.txt` for
|
||||||
// new packages) into a `publish/<name>-<version>` branch via the
|
// new packages) into a `publish/<name>-<version>` branch via the
|
||||||
// Gitea contents API, opens a PR via `tea api`. With `dry_run=true`,
|
// Gitea contents API, opens a PR via `tea api`. With `dry_run=true`,
|
||||||
// prints the recipe TOML and skips all network operations.
|
// prints the recipe TOML and skips all network operations.
|
||||||
//
|
// Authentication comes from `tea login`.
|
||||||
// `registry_slug` is "<owner>/<repo>" (default: $CARGOXX_REGISTRY or
|
auto cmd_publish(const std::filesystem::path& project_root, bool dry_run)
|
||||||
// "mozart/cargoxx-pkgs"). Authentication comes from `tea login`.
|
|
||||||
auto cmd_publish(const std::filesystem::path& project_root, bool dry_run,
|
|
||||||
std::optional<std::string> registry_slug = std::nullopt)
|
|
||||||
-> util::Result<void>;
|
-> util::Result<void>;
|
||||||
|
|
||||||
// Builds the project, picks a binary target, and execs it with `args`.
|
// Builds the project, picks a binary target, and execs it with `args`.
|
||||||
|
|||||||
@@ -141,6 +141,81 @@ auto cmd_add(const fs::path& project_root, const std::string& name,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Probe cargoxx-pkgs (the project's own recipe flake) before falling
|
||||||
|
// through to the nixpkgs/conan/vcpkg chain. If the name has a recipe
|
||||||
|
// there, the dep is identical in shape to a nixpkgs-resolved one —
|
||||||
|
// string-form spec in Cargoxx.toml, lockfile entry pinning the attr
|
||||||
|
// + cargoxx-pkgs repo rev, generated build/flake.nix gets
|
||||||
|
// `inputs.cargoxx-pkgs` and `cargoxx-pkgs.packages.${system}.<attr>`
|
||||||
|
// as a buildInput. Disabled in tests via CARGOXX_NO_AUTORESOLVE.
|
||||||
|
{
|
||||||
|
auto* env = std::getenv("CARGOXX_NO_AUTORESOLVE");
|
||||||
|
const bool autoresolve_disabled = env != nullptr && *env != 0;
|
||||||
|
if (!autoresolve_disabled) {
|
||||||
|
auto hit = resolver::try_cargoxx_pkgs(name, effective_version);
|
||||||
|
if (hit) {
|
||||||
|
// cargoxx-pkgs's flake exposes per-version attrs as
|
||||||
|
// `<n>_<safe>` (e.g. greeter_0_1_1) plus a bare `<n>`
|
||||||
|
// pointing at the latest. Pin the concrete attr so the
|
||||||
|
// lock is hermetic.
|
||||||
|
std::string safe;
|
||||||
|
safe.reserve(hit->version.size());
|
||||||
|
for (char c : hit->version) {
|
||||||
|
safe.push_back((c == '.' || c == '-' || c == '+') ? '_' : c);
|
||||||
|
}
|
||||||
|
auto attr = std::format("{}_{}", name, safe);
|
||||||
|
|
||||||
|
m->dependencies.push_back(manifest::Dependency{
|
||||||
|
.name = name,
|
||||||
|
.version_spec = hit->version,
|
||||||
|
.components = std::move(components),
|
||||||
|
});
|
||||||
|
if (auto r = manifest::write(*m, manifest_path); !r) {
|
||||||
|
return std::unexpected(r.error());
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto lock_path = project_root / "Cargoxx.lock";
|
||||||
|
lockfile::Lockfile lock;
|
||||||
|
if (std::error_code ec; std::filesystem::exists(lock_path, ec)) {
|
||||||
|
if (auto parsed = lockfile::parse(lock_path); parsed) {
|
||||||
|
lock = std::move(*parsed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (lock.version == 0) {
|
||||||
|
lock.version = 1;
|
||||||
|
}
|
||||||
|
lockfile::LockfilePackage entry{
|
||||||
|
.name = name,
|
||||||
|
.version = hit->version,
|
||||||
|
.cargoxx_pkgs_attr = attr,
|
||||||
|
.cargoxx_pkgs_rev = hit->repo_rev,
|
||||||
|
.linkdb_source = "cargoxx-pkgs",
|
||||||
|
.find_package = std::format("{} CONFIG REQUIRED", name),
|
||||||
|
.targets = {std::format("{}::{}", name, name)},
|
||||||
|
};
|
||||||
|
bool replaced = false;
|
||||||
|
for (auto& p : lock.packages) {
|
||||||
|
if (p.name == name && p.version == hit->version) {
|
||||||
|
p = entry;
|
||||||
|
replaced = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!replaced) {
|
||||||
|
lock.packages.push_back(std::move(entry));
|
||||||
|
}
|
||||||
|
if (auto r = lockfile::write(lock, lock_path); !r) {
|
||||||
|
return std::unexpected(r.error());
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (hit.error().code != util::ErrorCode::ResolutionUnknownPackage) {
|
||||||
|
return std::unexpected(hit.error());
|
||||||
|
}
|
||||||
|
// Fall through to the linkdb chain.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const auto effective_overlay = overlay_path.value_or(linkdb::default_overlay_path());
|
const auto effective_overlay = overlay_path.value_or(linkdb::default_overlay_path());
|
||||||
|
|
||||||
// Drop any auto-discovered overlay rows for this package before
|
// Drop any auto-discovered overlay rows for this package before
|
||||||
|
|||||||
@@ -116,11 +116,25 @@ auto merge_lockfile(const manifest::Manifest& m,
|
|||||||
auto emit_dep = [&](const manifest::Dependency& dep, const linkdb::Recipe& rec) {
|
auto emit_dep = [&](const manifest::Dependency& dep, const linkdb::Recipe& rec) {
|
||||||
std::optional<std::string> rev;
|
std::optional<std::string> rev;
|
||||||
std::string attr = rec.nixpkgs_attr;
|
std::string attr = rec.nixpkgs_attr;
|
||||||
|
std::optional<std::string> cxx_pkgs_attr;
|
||||||
|
std::optional<std::string> cxx_pkgs_rev;
|
||||||
if (auto p = find_prior(dep.name, dep.version_spec); p) {
|
if (auto p = find_prior(dep.name, dep.version_spec); p) {
|
||||||
rev = p->nixpkgs_rev;
|
rev = p->nixpkgs_rev;
|
||||||
if (p->nixpkgs_attr && !p->nixpkgs_attr->empty()) {
|
if (p->nixpkgs_attr && !p->nixpkgs_attr->empty()) {
|
||||||
attr = *p->nixpkgs_attr;
|
attr = *p->nixpkgs_attr;
|
||||||
}
|
}
|
||||||
|
cxx_pkgs_attr = p->cargoxx_pkgs_attr;
|
||||||
|
cxx_pkgs_rev = p->cargoxx_pkgs_rev;
|
||||||
|
}
|
||||||
|
// For cargoxx-pkgs-resolved deps, drop the nixpkgs fields — the
|
||||||
|
// recipe synthesized at `cargoxx add` time left them empty and
|
||||||
|
// the codegen layer prefers cargoxx_pkgs_attr anyway.
|
||||||
|
std::optional<std::string> nix_attr_opt;
|
||||||
|
if (cxx_pkgs_attr && !cxx_pkgs_attr->empty()) {
|
||||||
|
attr.clear();
|
||||||
|
rev.reset();
|
||||||
|
} else if (!attr.empty()) {
|
||||||
|
nix_attr_opt = attr;
|
||||||
}
|
}
|
||||||
std::optional<std::string> source_kind;
|
std::optional<std::string> source_kind;
|
||||||
std::optional<std::string> source_path;
|
std::optional<std::string> source_path;
|
||||||
@@ -142,8 +156,10 @@ auto merge_lockfile(const manifest::Manifest& m,
|
|||||||
.name = dep.name,
|
.name = dep.name,
|
||||||
.version = dep.version_spec,
|
.version = dep.version_spec,
|
||||||
.dependencies = {},
|
.dependencies = {},
|
||||||
.nixpkgs_attr = std::move(attr),
|
.nixpkgs_attr = std::move(nix_attr_opt),
|
||||||
.nixpkgs_rev = std::move(rev),
|
.nixpkgs_rev = std::move(rev),
|
||||||
|
.cargoxx_pkgs_attr = std::move(cxx_pkgs_attr),
|
||||||
|
.cargoxx_pkgs_rev = std::move(cxx_pkgs_rev),
|
||||||
.linkdb_source = rec.source,
|
.linkdb_source = rec.source,
|
||||||
.find_package = rec.find_package,
|
.find_package = rec.find_package,
|
||||||
.targets = rec.targets,
|
.targets = rec.targets,
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ namespace fs = std::filesystem;
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr std::string_view DEFAULT_REGISTRY = "mozart/cargoxx-pkgs";
|
// The one and only publish destination.
|
||||||
|
constexpr std::string_view CARGOXX_PKGS_REPO = "mozart/cargoxx-pkgs";
|
||||||
|
|
||||||
auto err(util::ErrorCode code, std::string msg, std::string hint = "")
|
auto err(util::ErrorCode code, std::string msg, std::string hint = "")
|
||||||
-> util::Error {
|
-> util::Error {
|
||||||
@@ -232,8 +233,8 @@ auto path_exists_remote(const std::string& registry, const std::string& path)
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
auto cmd_publish(const fs::path& project_root, bool dry_run,
|
auto cmd_publish(const fs::path& project_root, bool dry_run)
|
||||||
std::optional<std::string> registry_slug_arg) -> util::Result<void> {
|
-> util::Result<void> {
|
||||||
// 1. Read manifest + lockfile
|
// 1. Read manifest + lockfile
|
||||||
auto m = manifest::parse(project_root / "Cargoxx.toml");
|
auto m = manifest::parse(project_root / "Cargoxx.toml");
|
||||||
if (!m) {
|
if (!m) {
|
||||||
@@ -322,11 +323,8 @@ auto cmd_publish(const fs::path& project_root, bool dry_run,
|
|||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Registry slug + auth.
|
// 6. Auth.
|
||||||
auto registry = registry_slug_arg.value_or([]() -> std::string {
|
const std::string registry{CARGOXX_PKGS_REPO};
|
||||||
auto* env = std::getenv("CARGOXX_REGISTRY");
|
|
||||||
return env && *env ? env : std::string{DEFAULT_REGISTRY};
|
|
||||||
}());
|
|
||||||
|
|
||||||
auto publisher = tea_whoami();
|
auto publisher = tea_whoami();
|
||||||
if (!publisher) {
|
if (!publisher) {
|
||||||
|
|||||||
@@ -44,14 +44,10 @@ auto run(int argc, char** argv) -> int {
|
|||||||
"Path to write vendor.toml (default ./vendor.toml)");
|
"Path to write vendor.toml (default ./vendor.toml)");
|
||||||
|
|
||||||
auto* publish_cmd = app.add_subcommand(
|
auto* publish_cmd = app.add_subcommand(
|
||||||
"publish", "Publish the current HEAD as a new recipe in the cargoxx registry");
|
"publish", "Publish the current HEAD as a new recipe in cargoxx-pkgs");
|
||||||
bool publish_dry_run = false;
|
bool publish_dry_run = false;
|
||||||
std::string publish_registry;
|
|
||||||
publish_cmd->add_flag("--dry-run", publish_dry_run,
|
publish_cmd->add_flag("--dry-run", publish_dry_run,
|
||||||
"Print the recipe TOML; skip all network ops");
|
"Print the recipe TOML; skip all network ops");
|
||||||
publish_cmd->add_option("--registry", publish_registry,
|
|
||||||
"Registry repo slug <owner>/<repo> "
|
|
||||||
"(default $CARGOXX_REGISTRY or mozart/cargoxx-pkgs)");
|
|
||||||
|
|
||||||
auto* run_cmd = app.add_subcommand("run", "Build and run a binary target");
|
auto* run_cmd = app.add_subcommand("run", "Build and run a binary target");
|
||||||
bool run_release = false;
|
bool run_release = false;
|
||||||
@@ -163,11 +159,7 @@ auto run(int argc, char** argv) -> int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (*publish_cmd) {
|
if (*publish_cmd) {
|
||||||
std::optional<std::string> registry;
|
auto r = cmd_publish(cwd, publish_dry_run);
|
||||||
if (!publish_registry.empty()) {
|
|
||||||
registry = publish_registry;
|
|
||||||
}
|
|
||||||
auto r = cmd_publish(cwd, publish_dry_run, registry);
|
|
||||||
if (!r) {
|
if (!r) {
|
||||||
std::cerr << util::format(r.error());
|
std::cerr << util::format(r.error());
|
||||||
return 1;
|
return 1;
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ namespace cargoxx::codegen {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
constexpr std::string_view CARGOXX_PKGS_URL =
|
||||||
|
"git+https://git.amadey.xyz/mozart/cargoxx-pkgs";
|
||||||
|
|
||||||
// One pinned dep gets its own nixpkgs flake input. Unpinned deps stay
|
// One pinned dep gets its own nixpkgs flake input. Unpinned deps stay
|
||||||
// on the shared `nixpkgs` input (which always tracks nixos-unstable).
|
// on the shared `nixpkgs` input (which always tracks nixos-unstable).
|
||||||
struct DepBinding {
|
struct DepBinding {
|
||||||
@@ -20,6 +23,15 @@ struct DepBinding {
|
|||||||
std::optional<std::string> rev; // pinned commit (null → unpinned)
|
std::optional<std::string> rev; // pinned commit (null → unpinned)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Parallel to DepBinding for cargoxx-pkgs-resolved deps. All deps from
|
||||||
|
// cargoxx-pkgs share a single `cargoxx-pkgs` flake input pinned at
|
||||||
|
// `rev` — if a project ever needs multiple revs simultaneously, this
|
||||||
|
// can grow per-rev inputs the way DepBinding does for nixpkgs.
|
||||||
|
struct CargoxxPkgsBinding {
|
||||||
|
std::string attr; // e.g. "greeter_0_1_1"
|
||||||
|
std::string rev; // cargoxx-pkgs repo HEAD at lock time
|
||||||
|
};
|
||||||
|
|
||||||
// Replaces every char outside [a-zA-Z0-9_] with '_'. The result is safe
|
// Replaces every char outside [a-zA-Z0-9_] with '_'. The result is safe
|
||||||
// to use as a Nix identifier (let bindings, lambda destructure params)
|
// to use as a Nix identifier (let bindings, lambda destructure params)
|
||||||
// and as an attribute name (inputs.<attr>) — Nix permits underscores in
|
// and as an attribute name (inputs.<attr>) — Nix permits underscores in
|
||||||
@@ -42,34 +54,66 @@ auto sanitize_input_attr(std::string_view name, std::string_view version)
|
|||||||
return std::format("nixpkgs_{}_{}", sanitize(name), sanitize(version));
|
return std::format("nixpkgs_{}_{}", sanitize(name), sanitize(version));
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LockfileRef {
|
struct LockfileEntry {
|
||||||
std::optional<std::string> rev;
|
std::optional<std::string> nixpkgs_attr;
|
||||||
std::optional<std::string> attr;
|
std::optional<std::string> nixpkgs_rev;
|
||||||
|
std::optional<std::string> cargoxx_pkgs_attr;
|
||||||
|
std::optional<std::string> cargoxx_pkgs_rev;
|
||||||
};
|
};
|
||||||
|
|
||||||
auto find_lockfile_ref(const lockfile::Lockfile& lock, const std::string& name,
|
auto find_lockfile_entry(const lockfile::Lockfile& lock, const std::string& name,
|
||||||
const std::string& version) -> LockfileRef {
|
const std::string& version) -> LockfileEntry {
|
||||||
for (const auto& p : lock.packages) {
|
for (const auto& p : lock.packages) {
|
||||||
if (p.name == name && p.version == version) {
|
if (p.name == name && p.version == version) {
|
||||||
return LockfileRef{.rev = p.nixpkgs_rev, .attr = p.nixpkgs_attr};
|
return LockfileEntry{
|
||||||
|
.nixpkgs_attr = p.nixpkgs_attr,
|
||||||
|
.nixpkgs_rev = p.nixpkgs_rev,
|
||||||
|
.cargoxx_pkgs_attr = p.cargoxx_pkgs_attr,
|
||||||
|
.cargoxx_pkgs_rev = p.cargoxx_pkgs_rev,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return LockfileRef{};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto build_bindings(const GenerateInputs& in) -> std::vector<DepBinding> {
|
struct Bindings {
|
||||||
std::vector<DepBinding> out;
|
std::vector<DepBinding> nixpkgs;
|
||||||
out.reserve(in.manifest.dependencies.size() + in.manifest.dev_dependencies.size());
|
std::vector<CargoxxPkgsBinding> cargoxx_pkgs;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto build_bindings(const GenerateInputs& in) -> Bindings {
|
||||||
|
Bindings out;
|
||||||
auto push = [&](const manifest::Dependency& dep, const linkdb::Recipe& rec) {
|
auto push = [&](const manifest::Dependency& dep, const linkdb::Recipe& rec) {
|
||||||
auto ref = find_lockfile_ref(in.lock, dep.name, dep.version_spec);
|
// cargoxx-source deps (path/git) don't live in nixpkgs — they're
|
||||||
std::string attr = (ref.attr && !ref.attr->empty()) ? *ref.attr
|
// produced by a recursive buildCppPackage invocation when the
|
||||||
: rec.nixpkgs_attr;
|
// consumer is built via `nix build`. Emitting them here would
|
||||||
out.push_back(DepBinding{
|
// generate `pkgs.` (empty attr) in the devshell flake. Skip them;
|
||||||
|
// the cargoxx-build-direct path will pick them up via a separate
|
||||||
|
// pre-build resolution step in a follow-up.
|
||||||
|
if (dep.source == manifest::DepSource::CargoxxPath
|
||||||
|
|| dep.source == manifest::DepSource::CargoxxGit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto entry = find_lockfile_entry(in.lock, dep.name, dep.version_spec);
|
||||||
|
// cargoxx-pkgs resolution wins if the lockfile records it.
|
||||||
|
if (entry.cargoxx_pkgs_attr && !entry.cargoxx_pkgs_attr->empty()
|
||||||
|
&& entry.cargoxx_pkgs_rev && !entry.cargoxx_pkgs_rev->empty()) {
|
||||||
|
out.cargoxx_pkgs.push_back(CargoxxPkgsBinding{
|
||||||
|
.attr = *entry.cargoxx_pkgs_attr,
|
||||||
|
.rev = *entry.cargoxx_pkgs_rev,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::string attr =
|
||||||
|
(entry.nixpkgs_attr && !entry.nixpkgs_attr->empty())
|
||||||
|
? *entry.nixpkgs_attr
|
||||||
|
: rec.nixpkgs_attr;
|
||||||
|
out.nixpkgs.push_back(DepBinding{
|
||||||
.name = dep.name,
|
.name = dep.name,
|
||||||
.version = dep.version_spec,
|
.version = dep.version_spec,
|
||||||
.nixpkgs_attr = std::move(attr),
|
.nixpkgs_attr = std::move(attr),
|
||||||
.sanitized = sanitize_input_attr(dep.name, dep.version_spec),
|
.sanitized = sanitize_input_attr(dep.name, dep.version_spec),
|
||||||
.rev = std::move(ref.rev),
|
.rev = entry.nixpkgs_rev,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
for (std::size_t i = 0; i < in.manifest.dependencies.size(); ++i) {
|
for (std::size_t i = 0; i < in.manifest.dependencies.size(); ++i) {
|
||||||
@@ -99,7 +143,20 @@ auto pinned_inputs_dedup(const std::vector<DepBinding>& bindings)
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// All cargoxx-pkgs deps share a single `cargoxx-pkgs` flake input.
|
||||||
|
// Picks the first rev seen; if revs diverge across deps the project
|
||||||
|
// is mid-migration and the user should re-run `cargoxx add` for each
|
||||||
|
// stale dep to align them.
|
||||||
|
auto cargoxx_pkgs_rev(const std::vector<CargoxxPkgsBinding>& bs)
|
||||||
|
-> std::optional<std::string> {
|
||||||
|
if (bs.empty()) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return bs.front().rev;
|
||||||
|
}
|
||||||
|
|
||||||
auto emit_inputs_block(const std::vector<const DepBinding*>& pinned,
|
auto emit_inputs_block(const std::vector<const DepBinding*>& pinned,
|
||||||
|
const std::optional<std::string>& cargoxx_pkgs_rev,
|
||||||
const lockfile::Lockfile& lock,
|
const lockfile::Lockfile& lock,
|
||||||
const std::optional<VendorIndex>& vendor)
|
const std::optional<VendorIndex>& vendor)
|
||||||
-> std::string {
|
-> std::string {
|
||||||
@@ -131,18 +188,25 @@ auto emit_inputs_block(const std::vector<const DepBinding*>& pinned,
|
|||||||
b->sanitized, *b->rev);
|
b->sanitized, *b->rev);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (cargoxx_pkgs_rev) {
|
||||||
|
out += std::format(" cargoxx-pkgs.url = \"{}?rev={}\";\n",
|
||||||
|
CARGOXX_PKGS_URL, *cargoxx_pkgs_rev);
|
||||||
|
}
|
||||||
out += std::format(" flake-utils.url = \"{}\";\n", flake_utils_url);
|
out += std::format(" flake-utils.url = \"{}\";\n", flake_utils_url);
|
||||||
out += " };\n";
|
out += " };\n";
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto emit_outputs_params(const std::vector<const DepBinding*>& pinned)
|
auto emit_outputs_params(const std::vector<const DepBinding*>& pinned,
|
||||||
-> std::string {
|
bool any_cargoxx_pkgs) -> std::string {
|
||||||
std::string out = "{ self, nixpkgs";
|
std::string out = "{ self, nixpkgs";
|
||||||
for (const auto* b : pinned) {
|
for (const auto* b : pinned) {
|
||||||
out += ", ";
|
out += ", ";
|
||||||
out += b->sanitized;
|
out += b->sanitized;
|
||||||
}
|
}
|
||||||
|
if (any_cargoxx_pkgs) {
|
||||||
|
out += ", cargoxx-pkgs";
|
||||||
|
}
|
||||||
out += ", flake-utils }";
|
out += ", flake-utils }";
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@@ -163,15 +227,23 @@ auto base_expr(const DepBinding& b) -> std::string {
|
|||||||
: std::format("pkgs.{}", b.nixpkgs_attr);
|
: std::format("pkgs.{}", b.nixpkgs_attr);
|
||||||
}
|
}
|
||||||
|
|
||||||
auto emit_build_inputs(const std::vector<DepBinding>& bindings) -> std::string {
|
auto emit_build_inputs(const std::vector<DepBinding>& nixpkgs_bs,
|
||||||
|
const std::vector<CargoxxPkgsBinding>& cxx_bs)
|
||||||
|
-> std::string {
|
||||||
std::set<std::string> seen;
|
std::set<std::string> seen;
|
||||||
std::string out;
|
std::string out;
|
||||||
for (const auto& b : bindings) {
|
for (const auto& b : nixpkgs_bs) {
|
||||||
auto expr = base_expr(b);
|
auto expr = base_expr(b);
|
||||||
if (seen.insert(expr).second) {
|
if (seen.insert(expr).second) {
|
||||||
out += std::format(" {}\n", expr);
|
out += std::format(" {}\n", expr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (const auto& b : cxx_bs) {
|
||||||
|
auto expr = std::format("cargoxx-pkgs.packages.${{system}}.{}", b.attr);
|
||||||
|
if (seen.insert(expr).second) {
|
||||||
|
out += std::format(" {}\n", expr);
|
||||||
|
}
|
||||||
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,13 +251,14 @@ auto emit_build_inputs(const std::vector<DepBinding>& bindings) -> std::string {
|
|||||||
|
|
||||||
auto flake_nix(const GenerateInputs& in) -> std::string {
|
auto flake_nix(const GenerateInputs& in) -> std::string {
|
||||||
auto bindings = build_bindings(in);
|
auto bindings = build_bindings(in);
|
||||||
auto pinned = pinned_inputs_dedup(bindings);
|
auto pinned = pinned_inputs_dedup(bindings.nixpkgs);
|
||||||
|
auto cxx_rev = cargoxx_pkgs_rev(bindings.cargoxx_pkgs);
|
||||||
|
|
||||||
std::string out;
|
std::string out;
|
||||||
out += "{\n";
|
out += "{\n";
|
||||||
out += std::format(" description = \"{}\";\n\n", in.manifest.package.name);
|
out += std::format(" description = \"{}\";\n\n", in.manifest.package.name);
|
||||||
|
|
||||||
out += emit_inputs_block(pinned, in.lock, in.vendor);
|
out += emit_inputs_block(pinned, cxx_rev, in.lock, in.vendor);
|
||||||
|
|
||||||
const bool any_pkg_config =
|
const bool any_pkg_config =
|
||||||
std::ranges::any_of(in.recipes,
|
std::ranges::any_of(in.recipes,
|
||||||
@@ -201,7 +274,7 @@ auto flake_nix(const GenerateInputs& in) -> std::string {
|
|||||||
|
|
||||||
out += "\n";
|
out += "\n";
|
||||||
out += " outputs = ";
|
out += " outputs = ";
|
||||||
out += emit_outputs_params(pinned);
|
out += emit_outputs_params(pinned, cxx_rev.has_value());
|
||||||
out += ":\n"
|
out += ":\n"
|
||||||
" flake-utils.lib.eachDefaultSystem (system:\n"
|
" flake-utils.lib.eachDefaultSystem (system:\n"
|
||||||
" let\n"
|
" let\n"
|
||||||
@@ -219,7 +292,7 @@ auto flake_nix(const GenerateInputs& in) -> std::string {
|
|||||||
}
|
}
|
||||||
out += " ];\n"
|
out += " ];\n"
|
||||||
" buildInputs = [\n";
|
" buildInputs = [\n";
|
||||||
out += emit_build_inputs(bindings);
|
out += emit_build_inputs(bindings.nixpkgs, bindings.cargoxx_pkgs);
|
||||||
out += " ];\n"
|
out += " ];\n"
|
||||||
" hardeningDisable = [\n"
|
" hardeningDisable = [\n"
|
||||||
" \"all\"\n"
|
" \"all\"\n"
|
||||||
|
|||||||
@@ -76,6 +76,12 @@ auto parse_package(const toml::table& tbl, const std::filesystem::path& path)
|
|||||||
if (auto v = tbl["nixpkgs_rev"].value<std::string>()) {
|
if (auto v = tbl["nixpkgs_rev"].value<std::string>()) {
|
||||||
pkg.nixpkgs_rev = *v;
|
pkg.nixpkgs_rev = *v;
|
||||||
}
|
}
|
||||||
|
if (auto v = tbl["cargoxx_pkgs_attr"].value<std::string>()) {
|
||||||
|
pkg.cargoxx_pkgs_attr = *v;
|
||||||
|
}
|
||||||
|
if (auto v = tbl["cargoxx_pkgs_rev"].value<std::string>()) {
|
||||||
|
pkg.cargoxx_pkgs_rev = *v;
|
||||||
|
}
|
||||||
if (auto v = tbl["linkdb_source"].value<std::string>()) {
|
if (auto v = tbl["linkdb_source"].value<std::string>()) {
|
||||||
pkg.linkdb_source = *v;
|
pkg.linkdb_source = *v;
|
||||||
}
|
}
|
||||||
@@ -200,6 +206,12 @@ auto write(const Lockfile& lock, const std::filesystem::path& path) -> util::Res
|
|||||||
if (p.nixpkgs_rev) {
|
if (p.nixpkgs_rev) {
|
||||||
tbl.insert_or_assign("nixpkgs_rev", *p.nixpkgs_rev);
|
tbl.insert_or_assign("nixpkgs_rev", *p.nixpkgs_rev);
|
||||||
}
|
}
|
||||||
|
if (p.cargoxx_pkgs_attr) {
|
||||||
|
tbl.insert_or_assign("cargoxx_pkgs_attr", *p.cargoxx_pkgs_attr);
|
||||||
|
}
|
||||||
|
if (p.cargoxx_pkgs_rev) {
|
||||||
|
tbl.insert_or_assign("cargoxx_pkgs_rev", *p.cargoxx_pkgs_rev);
|
||||||
|
}
|
||||||
if (p.linkdb_source) {
|
if (p.linkdb_source) {
|
||||||
tbl.insert_or_assign("linkdb_source", *p.linkdb_source);
|
tbl.insert_or_assign("linkdb_source", *p.linkdb_source);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,13 @@ struct LockfilePackage {
|
|||||||
std::vector<std::string> dependencies;
|
std::vector<std::string> dependencies;
|
||||||
std::optional<std::string> nixpkgs_attr;
|
std::optional<std::string> nixpkgs_attr;
|
||||||
std::optional<std::string> nixpkgs_rev;
|
std::optional<std::string> nixpkgs_rev;
|
||||||
|
// Parallel to nixpkgs_attr/rev: the dep was resolved through the
|
||||||
|
// cargoxx-pkgs flake (https://git.amadey.xyz/mozart/cargoxx-pkgs).
|
||||||
|
// `cargoxx_pkgs_attr` is the attribute name in
|
||||||
|
// `cargoxx-pkgs.packages.<system>` (e.g. "greeter_0_1_1"), and
|
||||||
|
// `cargoxx_pkgs_rev` pins the cargoxx-pkgs repo itself.
|
||||||
|
std::optional<std::string> cargoxx_pkgs_attr;
|
||||||
|
std::optional<std::string> cargoxx_pkgs_rev;
|
||||||
std::optional<std::string> linkdb_source;
|
std::optional<std::string> linkdb_source;
|
||||||
std::optional<std::string> find_package;
|
std::optional<std::string> find_package;
|
||||||
std::vector<std::string> targets;
|
std::vector<std::string> targets;
|
||||||
@@ -19,9 +26,8 @@ struct LockfilePackage {
|
|||||||
std::vector<std::string> brute_force_libs;
|
std::vector<std::string> brute_force_libs;
|
||||||
std::vector<std::string> brute_force_includes;
|
std::vector<std::string> brute_force_includes;
|
||||||
// For cargoxx-source deps (not nixpkgs/linkdb-resolved).
|
// For cargoxx-source deps (not nixpkgs/linkdb-resolved).
|
||||||
// "cargoxx-path" → source_path only
|
// "cargoxx-path" → source_path only
|
||||||
// "cargoxx-git" → source_git_url + source_git_commit + source_git_sha256
|
// "cargoxx-git" → source_git_url + source_git_commit + source_git_sha256
|
||||||
// "cargoxx-registry" → (1d)
|
|
||||||
std::optional<std::string> source_kind;
|
std::optional<std::string> source_kind;
|
||||||
std::optional<std::string> source_path;
|
std::optional<std::string> source_path;
|
||||||
std::optional<std::string> source_git_url;
|
std::optional<std::string> source_git_url;
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ auto extract_string_array(const toml::array& arr, std::string_view field,
|
|||||||
|
|
||||||
constexpr std::array PACKAGE_KNOWN_KEYS = {
|
constexpr std::array PACKAGE_KNOWN_KEYS = {
|
||||||
"name", "version", "edition", "authors", "license", "description", "repository",
|
"name", "version", "edition", "authors", "license", "description", "repository",
|
||||||
|
"homepage",
|
||||||
};
|
};
|
||||||
|
|
||||||
constexpr std::array BUILD_KNOWN_KEYS = {
|
constexpr std::array BUILD_KNOWN_KEYS = {
|
||||||
|
|||||||
233
src/resolver/cargoxx_pkgs_probe.cpp
Normal file
233
src/resolver/cargoxx_pkgs_probe.cpp
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
module;
|
||||||
|
|
||||||
|
#include <toml.hpp>
|
||||||
|
|
||||||
|
module cargoxx.resolver;
|
||||||
|
|
||||||
|
import std;
|
||||||
|
import cargoxx.util;
|
||||||
|
import cargoxx.exec;
|
||||||
|
|
||||||
|
namespace cargoxx::resolver {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr std::string_view CARGOXX_PKGS_OWNER = "mozart";
|
||||||
|
constexpr std::string_view CARGOXX_PKGS_REPO = "cargoxx-pkgs";
|
||||||
|
constexpr std::string_view CARGOXX_PKGS_HOST = "https://git.amadey.xyz";
|
||||||
|
|
||||||
|
auto error(util::ErrorCode code, std::string msg) -> util::Error {
|
||||||
|
return util::Error{code, std::move(msg), "", std::nullopt, std::nullopt};
|
||||||
|
}
|
||||||
|
|
||||||
|
auto curl_get(const std::string& url) -> util::Result<std::string> {
|
||||||
|
auto r = exec::run("curl", {"-fsSL", "--max-time", "30", url},
|
||||||
|
exec::ExecOptions{
|
||||||
|
.cwd = {},
|
||||||
|
.env_overrides = {},
|
||||||
|
.timeout = std::chrono::seconds{40},
|
||||||
|
.inherit_stdio = false,
|
||||||
|
});
|
||||||
|
if (!r) {
|
||||||
|
return std::unexpected(r.error());
|
||||||
|
}
|
||||||
|
if (r->exit_code == 22) {
|
||||||
|
return std::unexpected(error(util::ErrorCode::ResolutionUnknownPackage,
|
||||||
|
std::format("HTTP 4xx for {}", url)));
|
||||||
|
}
|
||||||
|
if (r->exit_code != 0) {
|
||||||
|
return std::unexpected(error(
|
||||||
|
util::ErrorCode::ResolutionNetworkError,
|
||||||
|
std::format("curl failed (exit {}) fetching {}: {}",
|
||||||
|
r->exit_code, url, r->stderr_text)));
|
||||||
|
}
|
||||||
|
return std::move(r->stdout_text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the recipe basenames in `versions/` for `<name>`. Hits the
|
||||||
|
// Gitea contents API which returns a JSON array; we don't pull in a
|
||||||
|
// JSON library for this tiny shape — instead we scrape `"name":"X"`
|
||||||
|
// occurrences. The shape is documented at:
|
||||||
|
// /api/swagger#/repository/repoGetContentsList
|
||||||
|
auto extract_filenames(std::string_view json) -> std::vector<std::string> {
|
||||||
|
std::vector<std::string> out;
|
||||||
|
constexpr std::string_view key = R"("name":")";
|
||||||
|
std::size_t pos = 0;
|
||||||
|
while ((pos = json.find(key, pos)) != std::string_view::npos) {
|
||||||
|
pos += key.size();
|
||||||
|
auto end = json.find('"', pos);
|
||||||
|
if (end == std::string_view::npos) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
out.emplace_back(json.substr(pos, end - pos));
|
||||||
|
pos = end + 1;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto pick_best_version(const std::vector<std::string>& versions,
|
||||||
|
std::string_view spec) -> std::optional<std::string> {
|
||||||
|
std::optional<std::string> best;
|
||||||
|
for (const auto& v : versions) {
|
||||||
|
if (spec != "*" && !util::satisfies(v, spec)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!best) {
|
||||||
|
best = v;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Both v and *best satisfy; pick the higher. `satisfies(v, "> best")`
|
||||||
|
// is the cheapest precedence test using only the existing helper.
|
||||||
|
if (util::satisfies(v, std::format("> {}", *best))) {
|
||||||
|
best = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
auto parse_cargoxx_pkgs_versions(std::string_view contents_json)
|
||||||
|
-> util::Result<std::vector<std::string>> {
|
||||||
|
auto names = extract_filenames(contents_json);
|
||||||
|
std::vector<std::string> out;
|
||||||
|
out.reserve(names.size());
|
||||||
|
for (const auto& n : names) {
|
||||||
|
constexpr std::string_view suffix = ".toml";
|
||||||
|
if (n.size() <= suffix.size() || !n.ends_with(suffix)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.push_back(n.substr(0, n.size() - suffix.size()));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto parse_cargoxx_pkgs_recipe(std::string_view body) -> util::Result<PkgsHit> {
|
||||||
|
toml::table root;
|
||||||
|
try {
|
||||||
|
root = toml::parse(std::string{body});
|
||||||
|
} catch (const toml::parse_error& e) {
|
||||||
|
return std::unexpected(error(util::ErrorCode::ResolutionNetworkError,
|
||||||
|
std::format("cargoxx-pkgs recipe parse error: {}",
|
||||||
|
e.description())));
|
||||||
|
}
|
||||||
|
PkgsHit hit;
|
||||||
|
if (auto v = root["version"].value<std::string>()) {
|
||||||
|
hit.version = *v;
|
||||||
|
} else {
|
||||||
|
return std::unexpected(error(util::ErrorCode::ResolutionNetworkError,
|
||||||
|
"cargoxx-pkgs recipe missing 'version'"));
|
||||||
|
}
|
||||||
|
const auto* src = root["source"].as_table();
|
||||||
|
if (!src) {
|
||||||
|
return std::unexpected(error(util::ErrorCode::ResolutionNetworkError,
|
||||||
|
"cargoxx-pkgs recipe missing [source] table"));
|
||||||
|
}
|
||||||
|
if (auto v = (*src)["type"].value<std::string>(); !v || *v != "git") {
|
||||||
|
return std::unexpected(error(
|
||||||
|
util::ErrorCode::ResolutionNetworkError,
|
||||||
|
"cargoxx-pkgs recipe [source].type must be 'git'"));
|
||||||
|
}
|
||||||
|
auto url = (*src)["url"].value<std::string>();
|
||||||
|
auto commit = (*src)["commit"].value<std::string>();
|
||||||
|
auto sha = (*src)["sha256"].value<std::string>();
|
||||||
|
if (!url || !commit || !sha) {
|
||||||
|
return std::unexpected(error(
|
||||||
|
util::ErrorCode::ResolutionNetworkError,
|
||||||
|
"cargoxx-pkgs recipe [source] needs url + commit + sha256"));
|
||||||
|
}
|
||||||
|
hit.source_url = *url;
|
||||||
|
hit.source_commit = *commit;
|
||||||
|
hit.source_sha256 = *sha;
|
||||||
|
return hit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the HEAD rev of cargoxx-pkgs's `master` branch. Used to pin
|
||||||
|
// `inputs.cargoxx-pkgs.url = ".../?rev=<rev>"` in the consumer's flake.
|
||||||
|
auto query_cargoxx_pkgs_head() -> util::Result<std::string> {
|
||||||
|
auto url = std::format("{}/api/v1/repos/{}/{}/branches/master",
|
||||||
|
CARGOXX_PKGS_HOST, CARGOXX_PKGS_OWNER,
|
||||||
|
CARGOXX_PKGS_REPO);
|
||||||
|
auto body = curl_get(url);
|
||||||
|
if (!body) {
|
||||||
|
return std::unexpected(body.error());
|
||||||
|
}
|
||||||
|
// Tiny ad-hoc extraction; Gitea returns `"commit":{"id":"<sha>", ...}`.
|
||||||
|
constexpr std::string_view key = R"("id":")";
|
||||||
|
auto pos = body->find(key);
|
||||||
|
if (pos == std::string::npos) {
|
||||||
|
return std::unexpected(error(
|
||||||
|
util::ErrorCode::ResolutionNetworkError,
|
||||||
|
std::format("cargoxx-pkgs branches response from {} has no commit id",
|
||||||
|
url)));
|
||||||
|
}
|
||||||
|
pos += key.size();
|
||||||
|
auto end = body->find('"', pos);
|
||||||
|
if (end == std::string::npos) {
|
||||||
|
return std::unexpected(error(
|
||||||
|
util::ErrorCode::ResolutionNetworkError,
|
||||||
|
std::format("cargoxx-pkgs branches response from {} is truncated",
|
||||||
|
url)));
|
||||||
|
}
|
||||||
|
return body->substr(pos, end - pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto try_cargoxx_pkgs(const std::string& name, const std::string& version_spec)
|
||||||
|
-> util::Result<PkgsHit> {
|
||||||
|
if (name.empty()) {
|
||||||
|
return std::unexpected(error(util::ErrorCode::ResolutionUnknownPackage,
|
||||||
|
"package name is empty"));
|
||||||
|
}
|
||||||
|
// Step 1: list version recipes via the Gitea contents API.
|
||||||
|
auto list_url = std::format(
|
||||||
|
"{}/api/v1/repos/{}/{}/contents/recipes/{}/versions",
|
||||||
|
CARGOXX_PKGS_HOST, CARGOXX_PKGS_OWNER, CARGOXX_PKGS_REPO, name);
|
||||||
|
auto listing = curl_get(list_url);
|
||||||
|
if (!listing) {
|
||||||
|
return std::unexpected(listing.error());
|
||||||
|
}
|
||||||
|
auto versions_r = parse_cargoxx_pkgs_versions(*listing);
|
||||||
|
if (!versions_r) {
|
||||||
|
return std::unexpected(versions_r.error());
|
||||||
|
}
|
||||||
|
if (versions_r->empty()) {
|
||||||
|
return std::unexpected(error(util::ErrorCode::ResolutionUnknownPackage,
|
||||||
|
std::format("cargoxx-pkgs has no versions for '{}'",
|
||||||
|
name)));
|
||||||
|
}
|
||||||
|
auto chosen = pick_best_version(*versions_r, version_spec);
|
||||||
|
if (!chosen) {
|
||||||
|
return std::unexpected(error(
|
||||||
|
util::ErrorCode::ResolutionUnknownPackage,
|
||||||
|
std::format("cargoxx-pkgs has no version of '{}' satisfying '{}'",
|
||||||
|
name, version_spec)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: pull the recipe TOML.
|
||||||
|
auto recipe_url = std::format(
|
||||||
|
"{}/{}/{}/raw/branch/master/recipes/{}/versions/{}.toml",
|
||||||
|
CARGOXX_PKGS_HOST, CARGOXX_PKGS_OWNER, CARGOXX_PKGS_REPO, name, *chosen);
|
||||||
|
auto body = curl_get(recipe_url);
|
||||||
|
if (!body) {
|
||||||
|
return std::unexpected(body.error());
|
||||||
|
}
|
||||||
|
auto hit = parse_cargoxx_pkgs_recipe(*body);
|
||||||
|
if (!hit) {
|
||||||
|
return std::unexpected(hit.error());
|
||||||
|
}
|
||||||
|
if (hit->version != *chosen) {
|
||||||
|
return std::unexpected(error(
|
||||||
|
util::ErrorCode::ResolutionNetworkError,
|
||||||
|
std::format("cargoxx-pkgs recipe at {} declares version '{}' "
|
||||||
|
"but the path implies '{}'",
|
||||||
|
recipe_url, hit->version, *chosen)));
|
||||||
|
}
|
||||||
|
auto rev = query_cargoxx_pkgs_head();
|
||||||
|
if (!rev) {
|
||||||
|
return std::unexpected(rev.error());
|
||||||
|
}
|
||||||
|
hit->repo_rev = *rev;
|
||||||
|
return hit;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace cargoxx::resolver
|
||||||
@@ -64,6 +64,42 @@ struct PrefetchedSource {
|
|||||||
std::string hash; // SRI form, e.g. "sha256-<base64>"
|
std::string hash; // SRI form, e.g. "sha256-<base64>"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Result of looking a package up in the cargoxx-pkgs Gitea repo
|
||||||
|
// (https://git.amadey.xyz/mozart/cargoxx-pkgs). Mirrors what
|
||||||
|
// `nixpkgs_probe` returns for the nixpkgs path: a confirmed (name,
|
||||||
|
// version) pair plus the source-of-truth flake rev so cargoxx build
|
||||||
|
// can pin `inputs.cargoxx-pkgs.url = ".../?rev=<repo_rev>"`. The
|
||||||
|
// recipe-source fields (url/commit/sha256) are surfaced for
|
||||||
|
// diagnostics; consumers pull the dep transitively through the
|
||||||
|
// cargoxx-pkgs flake, not by re-fetching directly.
|
||||||
|
struct PkgsHit {
|
||||||
|
std::string version; // the concrete version that matched (e.g. "0.1.1")
|
||||||
|
std::string repo_rev; // HEAD rev of cargoxx-pkgs at lookup time
|
||||||
|
std::string source_url; // [source].url from the recipe TOML (diag)
|
||||||
|
std::string source_commit; // [source].commit (diag)
|
||||||
|
std::string source_sha256; // [source].sha256 (SRI form) (diag)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pure: parse a Gitea `/contents/<dir>` JSON listing and extract the
|
||||||
|
// versions (basenames stripped of `.toml`). Returns the versions in
|
||||||
|
// the order they appear in the JSON.
|
||||||
|
auto parse_cargoxx_pkgs_versions(std::string_view contents_json)
|
||||||
|
-> util::Result<std::vector<std::string>>;
|
||||||
|
|
||||||
|
// Pure: parse a single recipe TOML (the file at
|
||||||
|
// `recipes/<name>/versions/<v>.toml`) into a PkgsHit. The caller fills
|
||||||
|
// in `version` from the URL it asked for; `source_*` come from the
|
||||||
|
// recipe's `[source]` table.
|
||||||
|
auto parse_cargoxx_pkgs_recipe(std::string_view body) -> util::Result<PkgsHit>;
|
||||||
|
|
||||||
|
// Hits the cargoxx-pkgs Gitea repo for `<name>`'s recipe list, picks
|
||||||
|
// the highest version satisfying `version_spec` (`*` → highest), fetches
|
||||||
|
// the recipe TOML, and returns a PkgsHit. Returns
|
||||||
|
// `ResolutionUnknownPackage` when the package directory or no version
|
||||||
|
// matching the spec exists.
|
||||||
|
auto try_cargoxx_pkgs(const std::string& name, const std::string& version_spec)
|
||||||
|
-> util::Result<PkgsHit>;
|
||||||
|
|
||||||
// Same as realize_flake_source but also returns the SRI hash so the
|
// Same as realize_flake_source but also returns the SRI hash so the
|
||||||
// caller can persist it in a lockfile and feed it to `pkgs.fetchgit`
|
// caller can persist it in a lockfile and feed it to `pkgs.fetchgit`
|
||||||
// as a fixed-output derivation pin. Used by cargoxx-git deps.
|
// as a fixed-output derivation pin. Used by cargoxx-git deps.
|
||||||
|
|||||||
93
tests/cmd_publish_validation.cpp
Normal file
93
tests/cmd_publish_validation.cpp
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
// Validation gates for `cargoxx publish` that fail BEFORE any network
|
||||||
|
// I/O. The publish flow performs `nix flake prefetch` + tea API calls,
|
||||||
|
// both of which need live infra. Everything tested here happens earlier
|
||||||
|
// — schema checks of Cargoxx.toml + Cargoxx.lock + git state.
|
||||||
|
|
||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
|
||||||
|
import cargoxx.cli;
|
||||||
|
import cargoxx.manifest;
|
||||||
|
import cargoxx.lockfile;
|
||||||
|
import cargoxx.util;
|
||||||
|
import std;
|
||||||
|
|
||||||
|
using cargoxx::cli::cmd_publish;
|
||||||
|
using cargoxx::util::ErrorCode;
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
namespace manifest = cargoxx::manifest;
|
||||||
|
namespace lockfile = cargoxx::lockfile;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
auto fresh_dir() -> fs::path {
|
||||||
|
auto d = fs::temp_directory_path() /
|
||||||
|
std::format("cargoxx-publish-test-{}", std::random_device{}());
|
||||||
|
fs::create_directories(d);
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto write_manifest(const fs::path& dir, const manifest::Manifest& m) {
|
||||||
|
REQUIRE(manifest::write(m, dir / "Cargoxx.toml").has_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto write_lock(const fs::path& dir) {
|
||||||
|
lockfile::Lockfile lock{
|
||||||
|
.version = 1,
|
||||||
|
.packages = {lockfile::LockfilePackage{.name = "foo", .version = "0.1.0"}},
|
||||||
|
};
|
||||||
|
REQUIRE(lockfile::write(lock, dir / "Cargoxx.lock").has_value());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto minimal_pkg() -> manifest::Package {
|
||||||
|
return manifest::Package{
|
||||||
|
.name = "foo",
|
||||||
|
.version = "0.1.0",
|
||||||
|
.edition = manifest::Edition::Cpp23,
|
||||||
|
.license = "MIT",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("publish rejects a manifest missing [package].license",
|
||||||
|
"[cli][publish]") {
|
||||||
|
auto root = fresh_dir();
|
||||||
|
auto pkg = minimal_pkg();
|
||||||
|
pkg.license = std::nullopt;
|
||||||
|
write_manifest(root, manifest::Manifest{pkg, {}, {}});
|
||||||
|
write_lock(root);
|
||||||
|
|
||||||
|
auto r = cmd_publish(root, /*dry_run=*/true);
|
||||||
|
REQUIRE_FALSE(r.has_value());
|
||||||
|
REQUIRE(r.error().code == ErrorCode::ManifestInvalidField);
|
||||||
|
REQUIRE(r.error().message.find("license") != std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("publish rejects a project without Cargoxx.lock", "[cli][publish]") {
|
||||||
|
auto root = fresh_dir();
|
||||||
|
write_manifest(root, manifest::Manifest{minimal_pkg(), {}, {}});
|
||||||
|
// No lockfile.
|
||||||
|
|
||||||
|
auto r = cmd_publish(root, /*dry_run=*/true);
|
||||||
|
REQUIRE_FALSE(r.has_value());
|
||||||
|
REQUIRE(r.error().code == ErrorCode::ManifestNotFound);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("publish rejects path-form dependencies", "[cli][publish]") {
|
||||||
|
auto root = fresh_dir();
|
||||||
|
auto pkg = minimal_pkg();
|
||||||
|
manifest::Dependency dep{
|
||||||
|
.name = "sibling",
|
||||||
|
.version_spec = "*",
|
||||||
|
.source = manifest::DepSource::CargoxxPath,
|
||||||
|
.path = "../sibling",
|
||||||
|
};
|
||||||
|
write_manifest(root, manifest::Manifest{pkg, {dep}, {}});
|
||||||
|
write_lock(root);
|
||||||
|
|
||||||
|
auto r = cmd_publish(root, /*dry_run=*/true);
|
||||||
|
REQUIRE_FALSE(r.has_value());
|
||||||
|
REQUIRE(r.error().code == ErrorCode::ManifestInvalidField);
|
||||||
|
REQUIRE(r.error().message.find("path dep") != std::string::npos);
|
||||||
|
}
|
||||||
@@ -181,16 +181,21 @@ optimize = "max"
|
|||||||
REQUIRE(r.error().code == ErrorCode::ManifestUnknownField);
|
REQUIRE(r.error().code == ErrorCode::ManifestUnknownField);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("parse accepts reserved [package] fields", "[manifest][parse]") {
|
TEST_CASE("parse stores description / repository / homepage on Package",
|
||||||
|
"[manifest][parse]") {
|
||||||
auto p = write_manifest(R"(
|
auto p = write_manifest(R"(
|
||||||
[package]
|
[package]
|
||||||
name = "foo"
|
name = "foo"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "demo"
|
description = "demo"
|
||||||
repository = "https://example.com/foo"
|
repository = "https://example.com/foo"
|
||||||
|
homepage = "https://example.com"
|
||||||
)");
|
)");
|
||||||
auto r = parse(p);
|
auto r = parse(p);
|
||||||
REQUIRE(r.has_value());
|
REQUIRE(r.has_value());
|
||||||
|
REQUIRE(r->package.description == "demo");
|
||||||
|
REQUIRE(r->package.repository == "https://example.com/foo");
|
||||||
|
REQUIRE(r->package.homepage == "https://example.com");
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("parse accepts reserved top-level tables", "[manifest][parse]") {
|
TEST_CASE("parse accepts reserved top-level tables", "[manifest][parse]") {
|
||||||
|
|||||||
@@ -138,3 +138,44 @@ TEST_CASE("write fails when the target directory does not exist",
|
|||||||
auto r = write(m, "/nonexistent/dir/Cargoxx.toml");
|
auto r = write(m, "/nonexistent/dir/Cargoxx.toml");
|
||||||
REQUIRE_FALSE(r.has_value());
|
REQUIRE_FALSE(r.has_value());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("write round-trips a path-form dependency", "[manifest][write]") {
|
||||||
|
Manifest m{
|
||||||
|
pkg("consumer", "0.1.0"),
|
||||||
|
{Dependency{
|
||||||
|
.name = "mylib",
|
||||||
|
.version_spec = "*",
|
||||||
|
.components = {},
|
||||||
|
.source = cargoxx::manifest::DepSource::CargoxxPath,
|
||||||
|
.path = "../mylib",
|
||||||
|
}},
|
||||||
|
{},
|
||||||
|
};
|
||||||
|
REQUIRE(round_trip(m) == m);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("write round-trips a git-form dependency", "[manifest][write]") {
|
||||||
|
Manifest m{
|
||||||
|
pkg("consumer", "0.1.0"),
|
||||||
|
{Dependency{
|
||||||
|
.name = "mylib",
|
||||||
|
.version_spec = "*",
|
||||||
|
.components = {},
|
||||||
|
.source = cargoxx::manifest::DepSource::CargoxxGit,
|
||||||
|
.git_url = "https://gitea.example/me/mylib",
|
||||||
|
.git_rev = "0123456789012345678901234567890123456789",
|
||||||
|
}},
|
||||||
|
{},
|
||||||
|
};
|
||||||
|
REQUIRE(round_trip(m) == m);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("write round-trips description/repository/homepage",
|
||||||
|
"[manifest][write]") {
|
||||||
|
auto p = pkg("foo", "0.1.0");
|
||||||
|
p.description = "demo library";
|
||||||
|
p.repository = "https://gitea.example/me/foo";
|
||||||
|
p.homepage = "https://example.com/foo";
|
||||||
|
Manifest m{p, {}, {}};
|
||||||
|
REQUIRE(round_trip(m) == m);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user