[M5+] use devbox attr_paths for pinned-dep nixpkgs attr

This commit is contained in:
2026-05-10 13:06:35 +00:00
parent 935e8d5f79
commit 1604b1d5a8
6 changed files with 86 additions and 27 deletions

View File

@@ -91,6 +91,19 @@ All notable changes to cargoxx will be documented in this file.
6 cases against fixtures derived from a real fmt 10.2.1 response; 6 cases against fixtures derived from a real fmt 10.2.1 response;
`tests/devbox_resolve_live.cpp` (gated by `CARGOXX_NETWORK_TESTS=1`) `tests/devbox_resolve_live.cpp` (gated by `CARGOXX_NETWORK_TESTS=1`)
hits the live API. hits the live API.
- Fix: pinned-dep `buildInputs` line used the curated linkdb's
`nixpkgs_attr` (e.g. `fmt_10`) regardless of the pinned rev, so
`cargoxx add fmt@12.1.0` emitted `pkgs_nixpkgs_fmt_12_1_0.fmt_10`
even though that rev only exposes `fmt`. `resolve_version` now
returns `ResolvedVersion { nixpkgs_rev, nixpkgs_attr }` (the attr
comes from devbox's `attr_paths` for the resolved rev, with the
package name as a best-effort fallback for the git path).
`cmd_add` writes the attr into the lockfile and `cmd_build`'s
`merge_lockfile` now also preserves it. Codegen prefers the
lockfile's attr over the linkdb recipe's whenever it's set, so
pinned deps reach the right attribute on their per-dep nixpkgs.
Live verified: `cargoxx add fmt@12.1.0 && cargoxx build` now emits
`pkgs_nixpkgs_fmt_12_1_0.fmt` and the binary builds and runs.
- `flake.nix` codegen rewritten for **per-dep nixpkgs revisions**. The - `flake.nix` codegen rewritten for **per-dep nixpkgs revisions**. The
shared `nixpkgs` input always tracks `nixos-unstable` and provides shared `nixpkgs` input always tracks `nixos-unstable` and provides
the toolchain. Each pinned manifest dep the toolchain. Each pinned manifest dep

View File

@@ -38,7 +38,8 @@ auto recipe_already_known(const std::string& name, const std::string& version,
// Overwrites any existing entry for the same (name, version). Other // Overwrites any existing entry for the same (name, version). Other
// lockfile entries (root package, sibling deps) are preserved verbatim. // lockfile entries (root package, sibling deps) are preserved verbatim.
auto record_lockfile_rev(const fs::path& project_root, const std::string& name, auto record_lockfile_rev(const fs::path& project_root, const std::string& name,
const std::string& version, const std::string& rev) const std::string& version,
const resolver::ResolvedVersion& resolved)
-> util::Result<void> { -> util::Result<void> {
const auto lock_path = project_root / "Cargoxx.lock"; const auto lock_path = project_root / "Cargoxx.lock";
lockfile::Lockfile lock; lockfile::Lockfile lock;
@@ -56,7 +57,8 @@ auto record_lockfile_rev(const fs::path& project_root, const std::string& name,
bool replaced = false; bool replaced = false;
for (auto& p : lock.packages) { for (auto& p : lock.packages) {
if (p.name == name && p.version == version) { if (p.name == name && p.version == version) {
p.nixpkgs_rev = rev; p.nixpkgs_rev = resolved.nixpkgs_rev;
p.nixpkgs_attr = resolved.nixpkgs_attr;
replaced = true; replaced = true;
break; break;
} }
@@ -66,8 +68,8 @@ auto record_lockfile_rev(const fs::path& project_root, const std::string& name,
.name = name, .name = name,
.version = version, .version = version,
.dependencies = {}, .dependencies = {},
.nixpkgs_attr = std::nullopt, .nixpkgs_attr = resolved.nixpkgs_attr,
.nixpkgs_rev = rev, .nixpkgs_rev = resolved.nixpkgs_rev,
.linkdb_source = std::nullopt, .linkdb_source = std::nullopt,
}); });
} }
@@ -169,13 +171,13 @@ auto cmd_add(const fs::path& project_root, const std::string& name,
const bool wildcard = effective_version == "*"; const bool wildcard = effective_version == "*";
auto* env = std::getenv("CARGOXX_NO_AUTORESOLVE"); auto* env = std::getenv("CARGOXX_NO_AUTORESOLVE");
const bool autoresolve_disabled = env != nullptr && *env != 0; const bool autoresolve_disabled = env != nullptr && *env != 0;
std::optional<std::string> resolved_rev; std::optional<resolver::ResolvedVersion> resolved;
if (!wildcard && !autoresolve_disabled) { if (!wildcard && !autoresolve_disabled) {
auto rev = resolver::resolve_version(name, effective_version); auto r = resolver::resolve_version(name, effective_version);
if (!rev) { if (!r) {
return std::unexpected(rev.error()); return std::unexpected(r.error());
} }
resolved_rev = std::move(*rev); resolved = std::move(*r);
} }
m->dependencies.push_back(manifest::Dependency{ m->dependencies.push_back(manifest::Dependency{
@@ -188,9 +190,9 @@ auto cmd_add(const fs::path& project_root, const std::string& name,
return std::unexpected(r.error()); return std::unexpected(r.error());
} }
if (resolved_rev) { if (resolved) {
if (auto r = record_lockfile_rev(project_root, name, effective_version, if (auto r = record_lockfile_rev(project_root, name, effective_version,
*resolved_rev); *resolved);
!r) { !r) {
return std::unexpected(r.error()); return std::unexpected(r.error());
} }

View File

@@ -75,14 +75,23 @@ auto merge_lockfile(const manifest::Manifest& m,
const auto& dep = m.dependencies[i]; const auto& dep = m.dependencies[i];
const auto& rec = recipes[i]; const auto& rec = recipes[i];
std::optional<std::string> rev; std::optional<std::string> rev;
// The recipe's nixpkgs_attr is correct for unpinned deps (it's
// curated against nixos-unstable). When the prior lockfile
// already carries an attr — written by `cargoxx add <pkg>@<v>`
// from devbox's authoritative attr_paths for the pinned rev —
// that one wins.
std::string attr = rec.nixpkgs_attr;
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()) {
attr = *p->nixpkgs_attr;
}
} }
lock.packages.push_back(lockfile::LockfilePackage{ lock.packages.push_back(lockfile::LockfilePackage{
.name = dep.name, .name = dep.name,
.version = dep.version_spec, .version = dep.version_spec,
.dependencies = {}, .dependencies = {},
.nixpkgs_attr = rec.nixpkgs_attr, .nixpkgs_attr = std::move(attr),
.nixpkgs_rev = std::move(rev), .nixpkgs_rev = std::move(rev),
.linkdb_source = rec.source, .linkdb_source = rec.source,
}); });

View File

@@ -42,14 +42,19 @@ 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));
} }
auto find_lockfile_rev(const lockfile::Lockfile& lock, const std::string& name, struct LockfileRef {
const std::string& version) -> std::optional<std::string> { std::optional<std::string> rev;
std::optional<std::string> attr;
};
auto find_lockfile_ref(const lockfile::Lockfile& lock, const std::string& name,
const std::string& version) -> LockfileRef {
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 p.nixpkgs_rev; return LockfileRef{.rev = p.nixpkgs_rev, .attr = p.nixpkgs_attr};
} }
} }
return std::nullopt; return LockfileRef{};
} }
auto build_bindings(const GenerateInputs& in) -> std::vector<DepBinding> { auto build_bindings(const GenerateInputs& in) -> std::vector<DepBinding> {
@@ -58,12 +63,19 @@ auto build_bindings(const GenerateInputs& in) -> std::vector<DepBinding> {
for (std::size_t i = 0; i < in.manifest.dependencies.size(); ++i) { for (std::size_t i = 0; i < in.manifest.dependencies.size(); ++i) {
const auto& dep = in.manifest.dependencies[i]; const auto& dep = in.manifest.dependencies[i];
const auto& rec = in.recipes[i]; const auto& rec = in.recipes[i];
auto ref = find_lockfile_ref(in.lock, dep.name, dep.version_spec);
// For pinned deps the lockfile's nixpkgs_attr is authoritative
// (it came from devbox's attr_paths for this specific rev). The
// curated recipe's attr only applies to nixos-unstable, so it's
// wrong when the dep pulls from a different rev.
std::string attr = (ref.attr && !ref.attr->empty()) ? *ref.attr
: rec.nixpkgs_attr;
DepBinding b{ DepBinding b{
.name = dep.name, .name = dep.name,
.version = dep.version_spec, .version = dep.version_spec,
.nixpkgs_attr = rec.nixpkgs_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 = find_lockfile_rev(in.lock, dep.name, dep.version_spec), .rev = std::move(ref.rev),
}; };
out.push_back(std::move(b)); out.push_back(std::move(b));
} }

View File

@@ -174,15 +174,25 @@ auto nixpkgs_git_resolve(const std::string& name, const std::string& version,
std::optional<std::filesystem::path> repo_path = std::nullopt) std::optional<std::filesystem::path> repo_path = std::nullopt)
-> util::Result<std::string>; -> util::Result<std::string>;
// What `resolve_version` returns: a nixpkgs commit and the attribute
// path under which the package lives at that commit. The attr is
// authoritative *for that rev* — the cargoxx-curated linkdb attrs
// (e.g. `fmt_10`) only apply to the unpinned `nixos-unstable` set.
// When devbox supplies multiple attr paths the first is canonical.
struct ResolvedVersion {
std::string nixpkgs_rev;
std::string nixpkgs_attr;
};
// Top-level orchestrator: try `devbox_resolve` first, then // Top-level orchestrator: try `devbox_resolve` first, then
// `nixpkgs_git_resolve` as fallback. Returns a 40-char nixpkgs SHA, or // `nixpkgs_git_resolve` as fallback. Returns the rev + attr or
// `ResolutionVersionNotFound` when both probes come back empty. // `ResolutionVersionNotFound` when both probes come back empty.
// //
// Use this from `cargoxx add <pkg>@<concrete-ver>` to capture the rev // Use this from `cargoxx add <pkg>@<concrete-ver>` to capture the pin
// that lockfile/codegen will pin. Wildcards (`*`, empty) should NOT be // that lockfile/codegen will use. Wildcards (`*`, empty) should NOT
// passed — they are not concrete versions and the resolver returns // be passed — they are not concrete versions and the resolver
// `ResolutionVersionNotFound` for them by design. // returns `ResolutionVersionNotFound` for them by design.
auto resolve_version(const std::string& name, const std::string& version) auto resolve_version(const std::string& name, const std::string& version)
-> util::Result<std::string>; -> util::Result<ResolvedVersion>;
} // namespace cargoxx::resolver } // namespace cargoxx::resolver

View File

@@ -14,7 +14,7 @@ auto error(util::ErrorCode code, std::string msg) -> util::Error {
} // namespace } // namespace
auto resolve_version(const std::string& name, const std::string& version) auto resolve_version(const std::string& name, const std::string& version)
-> util::Result<std::string> { -> util::Result<ResolvedVersion> {
if (name.empty()) { if (name.empty()) {
return std::unexpected(error(util::ErrorCode::ResolutionUnknownPackage, return std::unexpected(error(util::ErrorCode::ResolutionUnknownPackage,
"resolve_version: name is empty")); "resolve_version: name is empty"));
@@ -28,11 +28,24 @@ auto resolve_version(const std::string& name, const std::string& version)
if (auto r = devbox_resolve(name, version); r) { if (auto r = devbox_resolve(name, version); r) {
if (!r->commit_hash.empty()) { if (!r->commit_hash.empty()) {
return r->commit_hash; // attr_paths is authoritative for the rev devbox pointed at.
// The first entry is the canonical path; fall back to the
// package name itself only when devbox didn't surface any.
std::string attr = r->attr_paths.empty() ? name : r->attr_paths.front();
return ResolvedVersion{
.nixpkgs_rev = r->commit_hash,
.nixpkgs_attr = std::move(attr),
};
} }
} }
if (auto r = nixpkgs_git_resolve(name, version, std::nullopt); r) { if (auto r = nixpkgs_git_resolve(name, version, std::nullopt); r) {
return *r; // The git fallback can't tell us the attr path. Best-effort:
// assume the package name itself is the attr (true for the
// vast majority of nixpkgs derivations).
return ResolvedVersion{
.nixpkgs_rev = *r,
.nixpkgs_attr = name,
};
} }
return std::unexpected(error( return std::unexpected(error(
util::ErrorCode::ResolutionVersionNotFound, util::ErrorCode::ResolutionVersionNotFound,