[M5+] flake codegen + SPEC §7/§10 amendment for per-dep nixpkgs pins
This commit is contained in:
20
CHANGELOG.md
20
CHANGELOG.md
@@ -91,6 +91,26 @@ 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.
|
||||
- `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
|
||||
(`<pkg>@<concrete-version>` whose lockfile entry has a non-null
|
||||
`nixpkgs_rev`) gets its own `nixpkgs_<sanitized>` input, a
|
||||
matching outputs-lambda parameter, a `pkgs_nixpkgs_<sanitized>`
|
||||
`let` binding, and a `pkgs_nixpkgs_<sanitized>.<attr>` line in
|
||||
`buildInputs`. Wildcard / unpinned deps stay on the shared
|
||||
`pkgs.<attr>`. Sanitization replaces every char outside
|
||||
`[a-zA-Z0-9_]` with `_`, giving a single identifier-safe form for
|
||||
all three Nix contexts. Two pinned deps that share the same
|
||||
`(name, version)` are deduplicated. Live verified:
|
||||
`cargoxx new app && cargoxx add fmt@10.2.1 && cargoxx add zlib &&
|
||||
cargoxx build` produces a binary that calls fmt 10.2.1 + zlib from
|
||||
the toolchain nixpkgs; a second `cargoxx build` regenerates
|
||||
byte-identical `flake.nix` + `Cargoxx.lock`.
|
||||
- SPEC.md §7 (flake template) and §10 (version resolution) amended
|
||||
to describe the per-dep model. The previous single-shared-rev
|
||||
whole-project SAT was retired (user-directed; documented ABI
|
||||
trade-off).
|
||||
- `cargoxx build` now **merges** rather than overwrites
|
||||
`Cargoxx.lock`: when an existing entry's `(name, version)` still
|
||||
matches the manifest, its `nixpkgs_rev` is preserved. New deps and
|
||||
|
||||
60
SPEC.md
60
SPEC.md
@@ -277,21 +277,34 @@ Stubs in v0.1 — accepted but only print a deprecation-style message: `cargoxx
|
||||
|
||||
## 7. Generated `flake.nix`
|
||||
|
||||
cargoxx generates exactly this template. Fields in `<<...>>` are substituted from the manifest.
|
||||
The shared `nixpkgs` input always tracks `nixos-unstable` and provides the
|
||||
toolchain (clang, cmake, ninja, libc++). Each *pinned* manifest dep
|
||||
(`<pkg>@<concrete-version>`) gets its own additional `nixpkgs_<...>`
|
||||
input pointing at a specific commit; the dep is built from that
|
||||
flake's `pkgs` set instead of the shared one. Wildcard deps stay on
|
||||
the shared `nixpkgs`.
|
||||
|
||||
```nix
|
||||
{
|
||||
description = "<<package.name>>";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/<<resolved-rev>>";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
# one line per pinned dep, in manifest order:
|
||||
nixpkgs_<<sanitized_dep_a>>.url = "github:NixOS/nixpkgs/<<rev_a>>";
|
||||
nixpkgs_<<sanitized_dep_b>>.url = "github:NixOS/nixpkgs/<<rev_b>>";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
outputs = { self, nixpkgs, nixpkgs_<<sanitized_dep_a>>,
|
||||
nixpkgs_<<sanitized_dep_b>>, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
pkgs_nixpkgs_<<sanitized_dep_a>> =
|
||||
import nixpkgs_<<sanitized_dep_a>> { inherit system; };
|
||||
pkgs_nixpkgs_<<sanitized_dep_b>> =
|
||||
import nixpkgs_<<sanitized_dep_b>> { inherit system; };
|
||||
llvmPkgs = pkgs.llvmPackages;
|
||||
in {
|
||||
devShell = llvmPkgs.libcxxStdenv.mkDerivation {
|
||||
@@ -302,6 +315,12 @@ cargoxx generates exactly this template. Fields in `<<...>>` are substituted fro
|
||||
pkgs.cmake
|
||||
pkgs.clang-tools
|
||||
];
|
||||
buildInputs = [
|
||||
# pinned dep: from the per-dep nixpkgs set
|
||||
pkgs_nixpkgs_<<sanitized_dep_a>>.<<dep_a_attr>>
|
||||
# wildcard / unpinned dep: from the shared `pkgs`
|
||||
pkgs.<<dep_c_attr>>
|
||||
];
|
||||
env.NIX_CFLAGS_COMPILE = toString [
|
||||
"-stdlib=libc++"
|
||||
"-Wno-unused-command-line-argument"
|
||||
@@ -316,9 +335,21 @@ cargoxx generates exactly this template. Fields in `<<...>>` are substituted fro
|
||||
}
|
||||
```
|
||||
|
||||
`<<resolved-rev>>` is the Nixpkgs commit hash from `Cargoxx.lock`. The toolchain (`clang_21`, `cmake`, `ninja`) is fixed in v0.1 — Clang because module support is most mature there.
|
||||
`<<sanitized_dep>>` is `<dep-name>_<dep-version>` with every char
|
||||
outside `[a-zA-Z0-9_]` replaced by `_`. The same identifier is reused
|
||||
in three places: the input attribute, the outputs lambda parameter,
|
||||
and the `pkgs_...` `let` binding — keeping it a valid Nix identifier
|
||||
in all three contexts.
|
||||
|
||||
Regeneration is idempotent: cargoxx writes the file only if its content would change. This avoids spurious `flake.lock` updates.
|
||||
`<<rev_a>>` etc. come from `Cargoxx.lock`'s per-dep `nixpkgs_rev`
|
||||
field. Wildcard pins (`pkg = "*"`) leave the field null and emit no
|
||||
extra input.
|
||||
|
||||
The toolchain (`clang_21`, `cmake`, `ninja`) is fixed in v0.1 — Clang
|
||||
because module support is most mature there.
|
||||
|
||||
Regeneration is idempotent: cargoxx writes the file only if its
|
||||
content would change. This avoids spurious `flake.lock` updates.
|
||||
|
||||
---
|
||||
|
||||
@@ -515,17 +546,20 @@ Each successful step writes the result to the user overlay before returning.
|
||||
|
||||
## 10. Version resolution algorithm
|
||||
|
||||
`resolve_version(package, version_spec) -> (version, nixpkgs_rev)`:
|
||||
cargoxx pins **per dep**: each `<pkg>@<concrete-version>` resolves to
|
||||
its own nixpkgs revision and lands as a separate flake input. The
|
||||
shared `nixpkgs` input always tracks `nixos-unstable` and provides
|
||||
the toolchain.
|
||||
|
||||
1. If `Cargoxx.lock` already pins this package and the spec is satisfied, return the lockfile entry.
|
||||
2. Query nixhub.io: `https://www.nixhub.io/packages/<package>?_data=routes%2F_nixhub.packages.%24pkg._index`. Parse JSON for available versions and their commits.
|
||||
3. If nixhub.io is unreachable, fall back to lazamar: `https://lazamar.co.uk/nix-versions/?package=<package>&channel=nixpkgs-unstable`. Parse HTML (well-formed table).
|
||||
4. If both fail, fall back to a local Nixpkgs git clone at `~/.cache/cargoxx/nixpkgs/`. Run `git log --all -S 'version = "<version>"' -- pkgs/`.
|
||||
5. Filter the candidate list by the version spec, choose the highest match, return `(version, rev)`.
|
||||
`resolve_version(package, version) -> nixpkgs_rev`:
|
||||
|
||||
For the *whole-project* resolution (multiple deps), cargoxx picks one revision: the latest revision that contains acceptable versions of every dependency. This is brute-force in v0.1: for each candidate revision (newest first, capped at 50 attempts), check whether all deps resolve. Take the first hit.
|
||||
1. **Primary — devbox API.** `GET https://search.devbox.sh/v1/resolve?name=<package>&version=<version>`. The JSON response contains `commit_hash`. Same Jetify backend that powers nixhub.io, but the API is documented and returns the commit directly. (See `devbox/internal/searcher/client.go`.) 10-second timeout.
|
||||
2. **Fallback — local nixpkgs git clone.** Lazy clone of `https://github.com/NixOS/nixpkgs.git` at `~/.cache/cargoxx/nixpkgs/` (or `$XDG_CACHE_HOME/cargoxx/nixpkgs/`). On a miss, run `git -C <repo> log --all -S 'version = "<version>"' --pretty='%H %ct' -- pkgs/` and pick the youngest matching commit.
|
||||
3. If both fail, return `ResolutionVersionNotFound`.
|
||||
|
||||
If no revision satisfies all constraints simultaneously, fail with a list of conflicting deps. The user resolves manually by relaxing version specs.
|
||||
When `cargoxx add <pkg>@<ver>` succeeds, the rev is written to `Cargoxx.lock` next to the dep entry (`nixpkgs_rev`). `cargoxx build` then merges the lockfile rather than overwriting it — pins survive arbitrary rebuilds. Wildcard pins (`<pkg>` with no `@<ver>`, or `<pkg>@*`) skip resolution entirely; the dep tracks the shared `nixos-unstable` input.
|
||||
|
||||
There is no whole-project rev solver. Two pinned deps may pull from different nixpkgs commits, with the ABI-mismatch risk that implies (different glibc / libc++ majors). cargoxx accepts that trade-off in exchange for fine-grained control; a future `cargoxx update` may surface compatibility warnings.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -309,9 +309,9 @@ mitigation is to append `-<short-sha>` to the input attr.
|
||||
| 1. devbox_resolve + parser | ✅ | `df2c25b` |
|
||||
| 2. nixpkgs_git_resolve fallback | ✅ | `cb82e91` |
|
||||
| 3. resolve_version + cmd_add wire-up | ✅ | `6f8e9c4` |
|
||||
| 4. cmd_build lockfile merge | ✅ | (this commit) |
|
||||
| 5. flake codegen for per-dep inputs | pending | — |
|
||||
| 6. SPEC §7/§10 amendment + smoke | pending | — |
|
||||
| 4. cmd_build lockfile merge | ✅ | `c4b2a1b` |
|
||||
| 5. flake codegen for per-dep inputs | ✅ | (this commit) |
|
||||
| 6. SPEC §7/§10 amendment + smoke | ✅ | (this commit) |
|
||||
|
||||
## End-to-end verification (Phase 6)
|
||||
|
||||
|
||||
@@ -9,71 +9,139 @@ namespace cargoxx::codegen {
|
||||
|
||||
namespace {
|
||||
|
||||
// SPEC.md §7 plus a `buildInputs` slot for resolved dep attrs (TECH_SPEC §10).
|
||||
// `${...}` in the env.NIX_CFLAGS_COMPILE block is literal Nix, not a marker —
|
||||
// our markers use the @@MARKER@@ form.
|
||||
constexpr std::string_view FLAKE_TEMPLATE = R"({
|
||||
description = "@@DESCRIPTION@@";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/@@NIXPKGS_REV@@";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
// One pinned dep gets its own nixpkgs flake input. Unpinned deps stay
|
||||
// on the shared `nixpkgs` input (which always tracks nixos-unstable).
|
||||
struct DepBinding {
|
||||
std::string name; // manifest dep name
|
||||
std::string version; // resolved version
|
||||
std::string nixpkgs_attr; // recipe.nixpkgs_attr (e.g. "fmt_10")
|
||||
std::string sanitized; // "nixpkgs_fmt_10_2_1" — input attr,
|
||||
// let-binding stem, lambda param
|
||||
std::optional<std::string> rev; // pinned commit (null → unpinned)
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
llvmPkgs = pkgs.llvmPackages;
|
||||
in {
|
||||
devShell = llvmPkgs.libcxxStdenv.mkDerivation {
|
||||
name = "shell";
|
||||
version = "1.0";
|
||||
nativeBuildInputs = [
|
||||
pkgs.ninja
|
||||
pkgs.cmake
|
||||
pkgs.clang-tools
|
||||
];
|
||||
buildInputs = [
|
||||
@@DEP_LINES@@ ];
|
||||
env.NIX_CFLAGS_COMPILE = toString [
|
||||
"-stdlib=libc++"
|
||||
"-Wno-unused-command-line-argument"
|
||||
"-B${pkgs.lib.getLib pkgs.libcxx}/lib"
|
||||
"-isystem ${pkgs.lib.getDev pkgs.libcxx}/include/c++/v1"
|
||||
];
|
||||
hardeningDisable = [
|
||||
"all"
|
||||
];
|
||||
};
|
||||
});
|
||||
}
|
||||
)";
|
||||
|
||||
auto substitute(std::string_view tmpl, std::string_view marker, std::string_view value)
|
||||
-> std::string {
|
||||
// Replaces every char outside [a-zA-Z0-9_] with '_'. The result is safe
|
||||
// to use as a Nix identifier (let bindings, lambda destructure params)
|
||||
// and as an attribute name (inputs.<attr>) — Nix permits underscores in
|
||||
// both places, hyphens only in attribute names.
|
||||
auto sanitize(std::string_view s) -> std::string {
|
||||
std::string out;
|
||||
out.reserve(tmpl.size());
|
||||
std::size_t pos = 0;
|
||||
while (pos < tmpl.size()) {
|
||||
auto next = tmpl.find(marker, pos);
|
||||
if (next == std::string_view::npos) {
|
||||
out.append(tmpl.substr(pos));
|
||||
break;
|
||||
out.reserve(s.size());
|
||||
for (char c : s) {
|
||||
if (std::isalnum(static_cast<unsigned char>(c)) || c == '_') {
|
||||
out += c;
|
||||
} else {
|
||||
out += '_';
|
||||
}
|
||||
out.append(tmpl.substr(pos, next - pos));
|
||||
out.append(value);
|
||||
pos = next + marker.size();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
auto stable_dedup(const std::vector<std::string>& xs) -> std::vector<std::string> {
|
||||
std::vector<std::string> out;
|
||||
auto sanitize_input_attr(std::string_view name, std::string_view version)
|
||||
-> std::string {
|
||||
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<std::string> {
|
||||
for (const auto& p : lock.packages) {
|
||||
if (p.name == name && p.version == version) {
|
||||
return p.nixpkgs_rev;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto build_bindings(const GenerateInputs& in) -> std::vector<DepBinding> {
|
||||
std::vector<DepBinding> out;
|
||||
out.reserve(in.manifest.dependencies.size());
|
||||
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];
|
||||
DepBinding b{
|
||||
.name = dep.name,
|
||||
.version = dep.version_spec,
|
||||
.nixpkgs_attr = rec.nixpkgs_attr,
|
||||
.sanitized = sanitize_input_attr(dep.name, dep.version_spec),
|
||||
.rev = find_lockfile_rev(in.lock, dep.name, dep.version_spec),
|
||||
};
|
||||
out.push_back(std::move(b));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Pinned deps share a `(name, version, rev)` identity — dedupe so two
|
||||
// deps that happen to land on the same nixpkgs revision don't generate
|
||||
// duplicate input attributes.
|
||||
auto pinned_inputs_dedup(const std::vector<DepBinding>& bindings)
|
||||
-> std::vector<const DepBinding*> {
|
||||
std::vector<const DepBinding*> out;
|
||||
std::set<std::string> seen;
|
||||
for (const auto& x : xs) {
|
||||
if (seen.insert(x).second) {
|
||||
out.push_back(x);
|
||||
for (const auto& b : bindings) {
|
||||
if (!b.rev || b.rev->empty()) {
|
||||
continue;
|
||||
}
|
||||
if (seen.insert(b.sanitized).second) {
|
||||
out.push_back(&b);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
auto emit_inputs_block(const std::vector<const DepBinding*>& pinned)
|
||||
-> std::string {
|
||||
// Always emit the shared toolchain `nixpkgs` and `flake-utils`
|
||||
// inputs. Per-pinned-dep inputs land between them so the output
|
||||
// diff stays stable across reruns.
|
||||
std::string out =
|
||||
" inputs = {\n"
|
||||
" nixpkgs.url = \"github:NixOS/nixpkgs/nixos-unstable\";\n";
|
||||
for (const auto* b : pinned) {
|
||||
out += std::format(" {}.url = \"github:NixOS/nixpkgs/{}\";\n",
|
||||
b->sanitized, *b->rev);
|
||||
}
|
||||
out += " flake-utils.url = \"github:numtide/flake-utils\";\n"
|
||||
" };\n";
|
||||
return out;
|
||||
}
|
||||
|
||||
auto emit_outputs_params(const std::vector<const DepBinding*>& pinned)
|
||||
-> std::string {
|
||||
std::string out = "{ self, nixpkgs";
|
||||
for (const auto* b : pinned) {
|
||||
out += ", ";
|
||||
out += b->sanitized;
|
||||
}
|
||||
out += ", flake-utils }";
|
||||
return out;
|
||||
}
|
||||
|
||||
auto emit_let_bindings(const std::vector<const DepBinding*>& pinned)
|
||||
-> std::string {
|
||||
std::string out;
|
||||
for (const auto* b : pinned) {
|
||||
out += std::format(" pkgs_{} = import {} {{ inherit system; }};\n",
|
||||
b->sanitized, b->sanitized);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
auto emit_build_input_line(const DepBinding& b) -> std::string {
|
||||
if (b.rev && !b.rev->empty()) {
|
||||
return std::format(" pkgs_{}.{}\n", b.sanitized, b.nixpkgs_attr);
|
||||
}
|
||||
return std::format(" pkgs.{}\n", b.nixpkgs_attr);
|
||||
}
|
||||
|
||||
auto emit_build_inputs(const std::vector<DepBinding>& bindings) -> std::string {
|
||||
std::set<std::string> seen;
|
||||
std::string out;
|
||||
for (const auto& b : bindings) {
|
||||
auto key = b.rev && !b.rev->empty()
|
||||
? std::format("pkgs_{}.{}", b.sanitized, b.nixpkgs_attr)
|
||||
: std::format("pkgs.{}", b.nixpkgs_attr);
|
||||
if (seen.insert(key).second) {
|
||||
out += emit_build_input_line(b);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
@@ -82,26 +150,48 @@ auto stable_dedup(const std::vector<std::string>& xs) -> std::vector<std::string
|
||||
} // namespace
|
||||
|
||||
auto flake_nix(const GenerateInputs& in) -> std::string {
|
||||
auto rev = in.lock.nixpkgs_rev().value_or("nixos-unstable");
|
||||
auto bindings = build_bindings(in);
|
||||
auto pinned = pinned_inputs_dedup(bindings);
|
||||
|
||||
std::vector<std::string> attrs;
|
||||
attrs.reserve(in.recipes.size());
|
||||
for (const auto& r : in.recipes) {
|
||||
attrs.push_back(r.nixpkgs_attr);
|
||||
}
|
||||
auto deduped = stable_dedup(attrs);
|
||||
std::string out;
|
||||
out += "{\n";
|
||||
out += std::format(" description = \"{}\";\n\n", in.manifest.package.name);
|
||||
|
||||
std::string dep_lines;
|
||||
for (const auto& a : deduped) {
|
||||
dep_lines += " pkgs.";
|
||||
dep_lines += a;
|
||||
dep_lines += '\n';
|
||||
}
|
||||
out += emit_inputs_block(pinned);
|
||||
|
||||
auto out = std::string{FLAKE_TEMPLATE};
|
||||
out = substitute(out, "@@DESCRIPTION@@", in.manifest.package.name);
|
||||
out = substitute(out, "@@NIXPKGS_REV@@", rev);
|
||||
out = substitute(out, "@@DEP_LINES@@", dep_lines);
|
||||
out += "\n";
|
||||
out += " outputs = ";
|
||||
out += emit_outputs_params(pinned);
|
||||
out += ":\n"
|
||||
" flake-utils.lib.eachDefaultSystem (system:\n"
|
||||
" let\n"
|
||||
" pkgs = import nixpkgs { inherit system; };\n";
|
||||
out += emit_let_bindings(pinned);
|
||||
out += " llvmPkgs = pkgs.llvmPackages;\n"
|
||||
" in {\n"
|
||||
" devShell = llvmPkgs.libcxxStdenv.mkDerivation {\n"
|
||||
" name = \"shell\";\n"
|
||||
" version = \"1.0\";\n"
|
||||
" nativeBuildInputs = [\n"
|
||||
" pkgs.ninja\n"
|
||||
" pkgs.cmake\n"
|
||||
" pkgs.clang-tools\n"
|
||||
" ];\n"
|
||||
" buildInputs = [\n";
|
||||
out += emit_build_inputs(bindings);
|
||||
out += " ];\n"
|
||||
" env.NIX_CFLAGS_COMPILE = toString [\n"
|
||||
" \"-stdlib=libc++\"\n"
|
||||
" \"-Wno-unused-command-line-argument\"\n"
|
||||
" \"-B${pkgs.lib.getLib pkgs.libcxx}/lib\"\n"
|
||||
" \"-isystem ${pkgs.lib.getDev pkgs.libcxx}/include/c++/v1\"\n"
|
||||
" ];\n"
|
||||
" hardeningDisable = [\n"
|
||||
" \"all\"\n"
|
||||
" ];\n"
|
||||
" };\n"
|
||||
" });\n"
|
||||
"}\n";
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ using cargoxx::layout::DiscoveredLayout;
|
||||
using cargoxx::linkdb::Recipe;
|
||||
using cargoxx::lockfile::Lockfile;
|
||||
using cargoxx::lockfile::LockfilePackage;
|
||||
using cargoxx::manifest::Dependency;
|
||||
using cargoxx::manifest::Edition;
|
||||
using cargoxx::manifest::Manifest;
|
||||
using cargoxx::manifest::Package;
|
||||
@@ -29,6 +30,14 @@ auto pkg(std::string name) -> Package {
|
||||
};
|
||||
}
|
||||
|
||||
auto dep(std::string name, std::string version) -> Dependency {
|
||||
return Dependency{
|
||||
.name = std::move(name),
|
||||
.version_spec = std::move(version),
|
||||
.components = {},
|
||||
};
|
||||
}
|
||||
|
||||
auto recipe(std::string attr) -> Recipe {
|
||||
return Recipe{
|
||||
.nixpkgs_attr = std::move(attr),
|
||||
@@ -50,21 +59,21 @@ auto root_pkg(std::string name, std::string version,
|
||||
};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("flake_nix renders the package description and rev",
|
||||
"[codegen][flake]") {
|
||||
Manifest m{pkg("hello"), {}, {}};
|
||||
DiscoveredLayout layout{};
|
||||
Lockfile lock{1, {root_pkg("hello", "0.1.0", "abc123def456")}};
|
||||
GenerateInputs in{m, layout, lock, {}, "/tmp/hello"};
|
||||
|
||||
auto out = flake_nix(in);
|
||||
REQUIRE(out.find("description = \"hello\";") != std::string::npos);
|
||||
REQUIRE(out.find("nixpkgs/abc123def456") != std::string::npos);
|
||||
auto dep_pkg(std::string name, std::string version,
|
||||
std::optional<std::string> rev) -> LockfilePackage {
|
||||
return LockfilePackage{
|
||||
.name = std::move(name),
|
||||
.version = std::move(version),
|
||||
.dependencies = {},
|
||||
.nixpkgs_attr = std::nullopt,
|
||||
.nixpkgs_rev = std::move(rev),
|
||||
.linkdb_source = std::nullopt,
|
||||
};
|
||||
}
|
||||
|
||||
TEST_CASE("flake_nix uses 'nixos-unstable' when no rev is pinned",
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("flake_nix always emits the shared nixos-unstable nixpkgs input",
|
||||
"[codegen][flake]") {
|
||||
Manifest m{pkg("hello"), {}, {}};
|
||||
DiscoveredLayout layout{};
|
||||
@@ -72,7 +81,63 @@ TEST_CASE("flake_nix uses 'nixos-unstable' when no rev is pinned",
|
||||
GenerateInputs in{m, layout, lock, {}, "/tmp/hello"};
|
||||
|
||||
auto out = flake_nix(in);
|
||||
REQUIRE(out.find("nixpkgs/nixos-unstable") != std::string::npos);
|
||||
REQUIRE(out.find("description = \"hello\";") != std::string::npos);
|
||||
REQUIRE(out.find("nixpkgs.url = \"github:NixOS/nixpkgs/nixos-unstable\";") !=
|
||||
std::string::npos);
|
||||
// No pinned deps → no extra `nixpkgs_*` inputs.
|
||||
REQUIRE(out.find("nixpkgs_") == std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("flake_nix emits a per-pinned-dep nixpkgs input", "[codegen][flake]") {
|
||||
Manifest m{pkg("app"), {dep("fmt", "10.2.1")}, {}};
|
||||
DiscoveredLayout layout{};
|
||||
Lockfile lock{1, {
|
||||
root_pkg("app", "0.1.0"),
|
||||
dep_pkg("fmt", "10.2.1", "abc123def456"),
|
||||
}};
|
||||
std::vector<Recipe> recipes = {recipe("fmt_10")};
|
||||
GenerateInputs in{m, layout, lock, recipes, "/tmp/app"};
|
||||
|
||||
auto out = flake_nix(in);
|
||||
// Per-dep input attribute
|
||||
REQUIRE(out.find("nixpkgs_fmt_10_2_1.url = \"github:NixOS/nixpkgs/abc123def456\";") !=
|
||||
std::string::npos);
|
||||
// Outputs lambda binds it
|
||||
REQUIRE(out.find(", nixpkgs_fmt_10_2_1,") != std::string::npos);
|
||||
// let-binding constructs pkgs_<sanitized>
|
||||
REQUIRE(out.find("pkgs_nixpkgs_fmt_10_2_1 = import nixpkgs_fmt_10_2_1") !=
|
||||
std::string::npos);
|
||||
// buildInputs uses the pinned set, not the shared `pkgs`
|
||||
REQUIRE(out.find("pkgs_nixpkgs_fmt_10_2_1.fmt_10") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("flake_nix uses shared `pkgs` for unpinned deps",
|
||||
"[codegen][flake]") {
|
||||
Manifest m{pkg("app"), {dep("fmt", "*")}, {}};
|
||||
DiscoveredLayout layout{};
|
||||
Lockfile lock{1, {root_pkg("app", "0.1.0"), dep_pkg("fmt", "*", std::nullopt)}};
|
||||
std::vector<Recipe> recipes = {recipe("fmt_10")};
|
||||
GenerateInputs in{m, layout, lock, recipes, "/tmp/app"};
|
||||
|
||||
auto out = flake_nix(in);
|
||||
REQUIRE(out.find("pkgs.fmt_10") != std::string::npos);
|
||||
REQUIRE(out.find("nixpkgs_fmt_") == std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("flake_nix mixes pinned and unpinned deps", "[codegen][flake]") {
|
||||
Manifest m{pkg("app"), {dep("fmt", "10.2.1"), dep("zlib", "*")}, {}};
|
||||
DiscoveredLayout layout{};
|
||||
Lockfile lock{1, {
|
||||
root_pkg("app", "0.1.0"),
|
||||
dep_pkg("fmt", "10.2.1", "abc"),
|
||||
dep_pkg("zlib", "*", std::nullopt),
|
||||
}};
|
||||
std::vector<Recipe> recipes = {recipe("fmt_10"), recipe("zlib")};
|
||||
GenerateInputs in{m, layout, lock, recipes, "/tmp/app"};
|
||||
|
||||
auto out = flake_nix(in);
|
||||
REQUIRE(out.find("pkgs_nixpkgs_fmt_10_2_1.fmt_10") != std::string::npos);
|
||||
REQUIRE(out.find("pkgs.zlib") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("flake_nix emits an empty buildInputs list when there are no deps",
|
||||
@@ -86,44 +151,38 @@ TEST_CASE("flake_nix emits an empty buildInputs list when there are no deps",
|
||||
REQUIRE(out.find("buildInputs = [\n ];") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("flake_nix emits one pkgs.<attr> line per dep",
|
||||
TEST_CASE("flake_nix dedupes deps that share input + attr",
|
||||
"[codegen][flake]") {
|
||||
Manifest m{pkg("app"), {}, {}};
|
||||
Manifest m{pkg("app"),
|
||||
{dep("boost", "1.84.0"), dep("boost", "1.84.0")},
|
||||
{}};
|
||||
DiscoveredLayout layout{};
|
||||
Lockfile lock{1, {root_pkg("app", "0.1.0", "rev42")}};
|
||||
std::vector<Recipe> recipes = {recipe("fmt_10"), recipe("spdlog")};
|
||||
GenerateInputs in{m, layout, lock, recipes, "/tmp/app"};
|
||||
|
||||
auto out = flake_nix(in);
|
||||
REQUIRE(out.find("pkgs.fmt_10") != std::string::npos);
|
||||
REQUIRE(out.find("pkgs.spdlog") != std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("flake_nix dedupes duplicate nixpkgs_attrs", "[codegen][flake]") {
|
||||
Manifest m{pkg("app"), {}, {}};
|
||||
DiscoveredLayout layout{};
|
||||
Lockfile lock{1, {root_pkg("app", "0.1.0", "rev42")}};
|
||||
// boost appears twice — same nixpkgs_attr from two component-bearing entries
|
||||
Lockfile lock{1, {
|
||||
root_pkg("app", "0.1.0"),
|
||||
dep_pkg("boost", "1.84.0", "rev42"),
|
||||
}};
|
||||
std::vector<Recipe> recipes = {recipe("boost"), recipe("boost")};
|
||||
GenerateInputs in{m, layout, lock, recipes, "/tmp/app"};
|
||||
|
||||
auto out = flake_nix(in);
|
||||
auto first = out.find("pkgs.boost");
|
||||
auto first = out.find("pkgs_nixpkgs_boost_1_84_0.boost");
|
||||
REQUIRE(first != std::string::npos);
|
||||
REQUIRE(out.find("pkgs.boost", first + 1) == std::string::npos);
|
||||
REQUIRE(out.find("pkgs_nixpkgs_boost_1_84_0.boost", first + 1) ==
|
||||
std::string::npos);
|
||||
}
|
||||
|
||||
TEST_CASE("flake_nix produces deterministic output", "[codegen][flake]") {
|
||||
Manifest m{pkg("app"), {}, {}};
|
||||
Manifest m{pkg("app"), {dep("fmt", "10.2.1"), dep("spdlog", "*")}, {}};
|
||||
DiscoveredLayout layout{};
|
||||
Lockfile lock{1, {root_pkg("app", "0.1.0", "rev42")}};
|
||||
std::vector<Recipe> recipes = {recipe("fmt_10"), recipe("spdlog"),
|
||||
recipe("nlohmann_json")};
|
||||
Lockfile lock{1, {
|
||||
root_pkg("app", "0.1.0"),
|
||||
dep_pkg("fmt", "10.2.1", "abc"),
|
||||
dep_pkg("spdlog", "*", std::nullopt),
|
||||
}};
|
||||
std::vector<Recipe> recipes = {recipe("fmt_10"), recipe("spdlog")};
|
||||
GenerateInputs in{m, layout, lock, recipes, "/tmp/app"};
|
||||
|
||||
auto a = flake_nix(in);
|
||||
auto b = flake_nix(in);
|
||||
REQUIRE(a == b);
|
||||
REQUIRE(flake_nix(in) == flake_nix(in));
|
||||
}
|
||||
|
||||
TEST_CASE("flake_nix output ends with a newline", "[codegen][flake]") {
|
||||
@@ -136,3 +195,19 @@ TEST_CASE("flake_nix output ends with a newline", "[codegen][flake]") {
|
||||
REQUIRE_FALSE(out.empty());
|
||||
REQUIRE(out.back() == '\n');
|
||||
}
|
||||
|
||||
TEST_CASE("flake_nix sanitizes hyphens and dots in dep names",
|
||||
"[codegen][flake]") {
|
||||
Manifest m{pkg("app"), {dep("range-v3", "0.12.0")}, {}};
|
||||
DiscoveredLayout layout{};
|
||||
Lockfile lock{1, {
|
||||
root_pkg("app", "0.1.0"),
|
||||
dep_pkg("range-v3", "0.12.0", "rev123"),
|
||||
}};
|
||||
std::vector<Recipe> recipes = {recipe("range-v3")};
|
||||
GenerateInputs in{m, layout, lock, recipes, "/tmp/app"};
|
||||
|
||||
auto out = flake_nix(in);
|
||||
REQUIRE(out.find("nixpkgs_range_v3_0_12_0.url = "
|
||||
"\"github:NixOS/nixpkgs/rev123\";") != std::string::npos);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user