diff --git a/CHANGELOG.md b/CHANGELOG.md index aa23ba0..6150690 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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; `tests/devbox_resolve_live.cpp` (gated by `CARGOXX_NETWORK_TESTS=1`) 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 shared `nixpkgs` input always tracks `nixos-unstable` and provides the toolchain. Each pinned manifest dep diff --git a/src/cli/cmd_add.cpp b/src/cli/cmd_add.cpp index ba6b461..80bbf60 100644 --- a/src/cli/cmd_add.cpp +++ b/src/cli/cmd_add.cpp @@ -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 // lockfile entries (root package, sibling deps) are preserved verbatim. 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 { const auto lock_path = project_root / "Cargoxx.lock"; lockfile::Lockfile lock; @@ -56,7 +57,8 @@ auto record_lockfile_rev(const fs::path& project_root, const std::string& name, bool replaced = false; for (auto& p : lock.packages) { 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; break; } @@ -66,8 +68,8 @@ auto record_lockfile_rev(const fs::path& project_root, const std::string& name, .name = name, .version = version, .dependencies = {}, - .nixpkgs_attr = std::nullopt, - .nixpkgs_rev = rev, + .nixpkgs_attr = resolved.nixpkgs_attr, + .nixpkgs_rev = resolved.nixpkgs_rev, .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 == "*"; auto* env = std::getenv("CARGOXX_NO_AUTORESOLVE"); const bool autoresolve_disabled = env != nullptr && *env != 0; - std::optional resolved_rev; + std::optional resolved; if (!wildcard && !autoresolve_disabled) { - auto rev = resolver::resolve_version(name, effective_version); - if (!rev) { - return std::unexpected(rev.error()); + auto r = resolver::resolve_version(name, effective_version); + if (!r) { + return std::unexpected(r.error()); } - resolved_rev = std::move(*rev); + resolved = std::move(*r); } 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()); } - if (resolved_rev) { + if (resolved) { if (auto r = record_lockfile_rev(project_root, name, effective_version, - *resolved_rev); + *resolved); !r) { return std::unexpected(r.error()); } diff --git a/src/cli/cmd_build.cpp b/src/cli/cmd_build.cpp index bfa100a..07cb1cf 100644 --- a/src/cli/cmd_build.cpp +++ b/src/cli/cmd_build.cpp @@ -75,14 +75,23 @@ auto merge_lockfile(const manifest::Manifest& m, const auto& dep = m.dependencies[i]; const auto& rec = recipes[i]; std::optional 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 @` + // 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) { rev = p->nixpkgs_rev; + if (p->nixpkgs_attr && !p->nixpkgs_attr->empty()) { + attr = *p->nixpkgs_attr; + } } lock.packages.push_back(lockfile::LockfilePackage{ .name = dep.name, .version = dep.version_spec, .dependencies = {}, - .nixpkgs_attr = rec.nixpkgs_attr, + .nixpkgs_attr = std::move(attr), .nixpkgs_rev = std::move(rev), .linkdb_source = rec.source, }); diff --git a/src/codegen/flake.cpp b/src/codegen/flake.cpp index ff11dc2..473c1c5 100644 --- a/src/codegen/flake.cpp +++ b/src/codegen/flake.cpp @@ -42,14 +42,19 @@ auto sanitize_input_attr(std::string_view name, std::string_view version) return std::format("nixpkgs_{}_{}", sanitize(name), sanitize(version)); } -auto find_lockfile_rev(const lockfile::Lockfile& lock, const std::string& name, - const std::string& version) -> std::optional { +struct LockfileRef { + std::optional rev; + std::optional attr; +}; + +auto find_lockfile_ref(const lockfile::Lockfile& lock, const std::string& name, + const std::string& version) -> LockfileRef { for (const auto& p : lock.packages) { 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 { @@ -58,12 +63,19 @@ auto build_bindings(const GenerateInputs& in) -> std::vector { for (std::size_t i = 0; i < in.manifest.dependencies.size(); ++i) { const auto& dep = in.manifest.dependencies[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{ .name = dep.name, .version = dep.version_spec, - .nixpkgs_attr = rec.nixpkgs_attr, + .nixpkgs_attr = std::move(attr), .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)); } diff --git a/src/resolver/resolver.cppm b/src/resolver/resolver.cppm index 955d524..3c0c04b 100644 --- a/src/resolver/resolver.cppm +++ b/src/resolver/resolver.cppm @@ -174,15 +174,25 @@ auto nixpkgs_git_resolve(const std::string& name, const std::string& version, std::optional repo_path = std::nullopt) -> util::Result; +// 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 -// `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. // -// Use this from `cargoxx add @` to capture the rev -// that lockfile/codegen will pin. Wildcards (`*`, empty) should NOT be -// passed — they are not concrete versions and the resolver returns -// `ResolutionVersionNotFound` for them by design. +// Use this from `cargoxx add @` to capture the pin +// that lockfile/codegen will use. Wildcards (`*`, empty) should NOT +// be passed — they are not concrete versions and the resolver +// returns `ResolutionVersionNotFound` for them by design. auto resolve_version(const std::string& name, const std::string& version) - -> util::Result; + -> util::Result; } // namespace cargoxx::resolver diff --git a/src/resolver/version_resolve.cpp b/src/resolver/version_resolve.cpp index a24e6d5..d9eef59 100644 --- a/src/resolver/version_resolve.cpp +++ b/src/resolver/version_resolve.cpp @@ -14,7 +14,7 @@ auto error(util::ErrorCode code, std::string msg) -> util::Error { } // namespace auto resolve_version(const std::string& name, const std::string& version) - -> util::Result { + -> util::Result { if (name.empty()) { return std::unexpected(error(util::ErrorCode::ResolutionUnknownPackage, "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 (!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) { - 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( util::ErrorCode::ResolutionVersionNotFound,