[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;
|
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.
|
||||||
|
- `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 build` now **merges** rather than overwrites
|
||||||
`Cargoxx.lock`: when an existing entry's `(name, version)` still
|
`Cargoxx.lock`: when an existing entry's `(name, version)` still
|
||||||
matches the manifest, its `nixpkgs_rev` is preserved. New deps and
|
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`
|
## 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
|
```nix
|
||||||
{
|
{
|
||||||
description = "<<package.name>>";
|
description = "<<package.name>>";
|
||||||
|
|
||||||
inputs = {
|
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";
|
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:
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
let
|
let
|
||||||
pkgs = import nixpkgs { inherit system; };
|
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;
|
llvmPkgs = pkgs.llvmPackages;
|
||||||
in {
|
in {
|
||||||
devShell = llvmPkgs.libcxxStdenv.mkDerivation {
|
devShell = llvmPkgs.libcxxStdenv.mkDerivation {
|
||||||
@@ -302,6 +315,12 @@ cargoxx generates exactly this template. Fields in `<<...>>` are substituted fro
|
|||||||
pkgs.cmake
|
pkgs.cmake
|
||||||
pkgs.clang-tools
|
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 [
|
env.NIX_CFLAGS_COMPILE = toString [
|
||||||
"-stdlib=libc++"
|
"-stdlib=libc++"
|
||||||
"-Wno-unused-command-line-argument"
|
"-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
|
## 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.
|
`resolve_version(package, version) -> nixpkgs_rev`:
|
||||||
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)`.
|
|
||||||
|
|
||||||
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` |
|
| 1. devbox_resolve + parser | ✅ | `df2c25b` |
|
||||||
| 2. nixpkgs_git_resolve fallback | ✅ | `cb82e91` |
|
| 2. nixpkgs_git_resolve fallback | ✅ | `cb82e91` |
|
||||||
| 3. resolve_version + cmd_add wire-up | ✅ | `6f8e9c4` |
|
| 3. resolve_version + cmd_add wire-up | ✅ | `6f8e9c4` |
|
||||||
| 4. cmd_build lockfile merge | ✅ | (this commit) |
|
| 4. cmd_build lockfile merge | ✅ | `c4b2a1b` |
|
||||||
| 5. flake codegen for per-dep inputs | pending | — |
|
| 5. flake codegen for per-dep inputs | ✅ | (this commit) |
|
||||||
| 6. SPEC §7/§10 amendment + smoke | pending | — |
|
| 6. SPEC §7/§10 amendment + smoke | ✅ | (this commit) |
|
||||||
|
|
||||||
## End-to-end verification (Phase 6)
|
## End-to-end verification (Phase 6)
|
||||||
|
|
||||||
|
|||||||
@@ -9,71 +9,139 @@ namespace cargoxx::codegen {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// SPEC.md §7 plus a `buildInputs` slot for resolved dep attrs (TECH_SPEC §10).
|
// One pinned dep gets its own nixpkgs flake input. Unpinned deps stay
|
||||||
// `${...}` in the env.NIX_CFLAGS_COMPILE block is literal Nix, not a marker —
|
// on the shared `nixpkgs` input (which always tracks nixos-unstable).
|
||||||
// our markers use the @@MARKER@@ form.
|
struct DepBinding {
|
||||||
constexpr std::string_view FLAKE_TEMPLATE = R"({
|
std::string name; // manifest dep name
|
||||||
description = "@@DESCRIPTION@@";
|
std::string version; // resolved version
|
||||||
|
std::string nixpkgs_attr; // recipe.nixpkgs_attr (e.g. "fmt_10")
|
||||||
inputs = {
|
std::string sanitized; // "nixpkgs_fmt_10_2_1" — input attr,
|
||||||
nixpkgs.url = "github:NixOS/nixpkgs/@@NIXPKGS_REV@@";
|
// let-binding stem, lambda param
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
std::optional<std::string> rev; // pinned commit (null → unpinned)
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs, flake-utils }:
|
// Replaces every char outside [a-zA-Z0-9_] with '_'. The result is safe
|
||||||
flake-utils.lib.eachDefaultSystem (system:
|
// to use as a Nix identifier (let bindings, lambda destructure params)
|
||||||
let
|
// and as an attribute name (inputs.<attr>) — Nix permits underscores in
|
||||||
pkgs = import nixpkgs { inherit system; };
|
// both places, hyphens only in attribute names.
|
||||||
llvmPkgs = pkgs.llvmPackages;
|
auto sanitize(std::string_view s) -> std::string {
|
||||||
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 {
|
|
||||||
std::string out;
|
std::string out;
|
||||||
out.reserve(tmpl.size());
|
out.reserve(s.size());
|
||||||
std::size_t pos = 0;
|
for (char c : s) {
|
||||||
while (pos < tmpl.size()) {
|
if (std::isalnum(static_cast<unsigned char>(c)) || c == '_') {
|
||||||
auto next = tmpl.find(marker, pos);
|
out += c;
|
||||||
if (next == std::string_view::npos) {
|
} else {
|
||||||
out.append(tmpl.substr(pos));
|
out += '_';
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
out.append(tmpl.substr(pos, next - pos));
|
|
||||||
out.append(value);
|
|
||||||
pos = next + marker.size();
|
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto stable_dedup(const std::vector<std::string>& xs) -> std::vector<std::string> {
|
auto sanitize_input_attr(std::string_view name, std::string_view version)
|
||||||
std::vector<std::string> out;
|
-> 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;
|
std::set<std::string> seen;
|
||||||
for (const auto& x : xs) {
|
for (const auto& b : bindings) {
|
||||||
if (seen.insert(x).second) {
|
if (!b.rev || b.rev->empty()) {
|
||||||
out.push_back(x);
|
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;
|
return out;
|
||||||
@@ -82,26 +150,48 @@ auto stable_dedup(const std::vector<std::string>& xs) -> std::vector<std::string
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
auto flake_nix(const GenerateInputs& in) -> std::string {
|
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;
|
std::string out;
|
||||||
attrs.reserve(in.recipes.size());
|
out += "{\n";
|
||||||
for (const auto& r : in.recipes) {
|
out += std::format(" description = \"{}\";\n\n", in.manifest.package.name);
|
||||||
attrs.push_back(r.nixpkgs_attr);
|
|
||||||
}
|
|
||||||
auto deduped = stable_dedup(attrs);
|
|
||||||
|
|
||||||
std::string dep_lines;
|
out += emit_inputs_block(pinned);
|
||||||
for (const auto& a : deduped) {
|
|
||||||
dep_lines += " pkgs.";
|
|
||||||
dep_lines += a;
|
|
||||||
dep_lines += '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
auto out = std::string{FLAKE_TEMPLATE};
|
out += "\n";
|
||||||
out = substitute(out, "@@DESCRIPTION@@", in.manifest.package.name);
|
out += " outputs = ";
|
||||||
out = substitute(out, "@@NIXPKGS_REV@@", rev);
|
out += emit_outputs_params(pinned);
|
||||||
out = substitute(out, "@@DEP_LINES@@", dep_lines);
|
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;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ using cargoxx::layout::DiscoveredLayout;
|
|||||||
using cargoxx::linkdb::Recipe;
|
using cargoxx::linkdb::Recipe;
|
||||||
using cargoxx::lockfile::Lockfile;
|
using cargoxx::lockfile::Lockfile;
|
||||||
using cargoxx::lockfile::LockfilePackage;
|
using cargoxx::lockfile::LockfilePackage;
|
||||||
|
using cargoxx::manifest::Dependency;
|
||||||
using cargoxx::manifest::Edition;
|
using cargoxx::manifest::Edition;
|
||||||
using cargoxx::manifest::Manifest;
|
using cargoxx::manifest::Manifest;
|
||||||
using cargoxx::manifest::Package;
|
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 {
|
auto recipe(std::string attr) -> Recipe {
|
||||||
return Recipe{
|
return Recipe{
|
||||||
.nixpkgs_attr = std::move(attr),
|
.nixpkgs_attr = std::move(attr),
|
||||||
@@ -50,21 +59,21 @@ auto root_pkg(std::string name, std::string version,
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
auto dep_pkg(std::string name, std::string version,
|
||||||
|
std::optional<std::string> rev) -> LockfilePackage {
|
||||||
TEST_CASE("flake_nix renders the package description and rev",
|
return LockfilePackage{
|
||||||
"[codegen][flake]") {
|
.name = std::move(name),
|
||||||
Manifest m{pkg("hello"), {}, {}};
|
.version = std::move(version),
|
||||||
DiscoveredLayout layout{};
|
.dependencies = {},
|
||||||
Lockfile lock{1, {root_pkg("hello", "0.1.0", "abc123def456")}};
|
.nixpkgs_attr = std::nullopt,
|
||||||
GenerateInputs in{m, layout, lock, {}, "/tmp/hello"};
|
.nixpkgs_rev = std::move(rev),
|
||||||
|
.linkdb_source = std::nullopt,
|
||||||
auto out = flake_nix(in);
|
};
|
||||||
REQUIRE(out.find("description = \"hello\";") != std::string::npos);
|
|
||||||
REQUIRE(out.find("nixpkgs/abc123def456") != std::string::npos);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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]") {
|
"[codegen][flake]") {
|
||||||
Manifest m{pkg("hello"), {}, {}};
|
Manifest m{pkg("hello"), {}, {}};
|
||||||
DiscoveredLayout layout{};
|
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"};
|
GenerateInputs in{m, layout, lock, {}, "/tmp/hello"};
|
||||||
|
|
||||||
auto out = flake_nix(in);
|
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",
|
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);
|
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]") {
|
"[codegen][flake]") {
|
||||||
Manifest m{pkg("app"), {}, {}};
|
Manifest m{pkg("app"),
|
||||||
|
{dep("boost", "1.84.0"), dep("boost", "1.84.0")},
|
||||||
|
{}};
|
||||||
DiscoveredLayout layout{};
|
DiscoveredLayout layout{};
|
||||||
Lockfile lock{1, {root_pkg("app", "0.1.0", "rev42")}};
|
Lockfile lock{1, {
|
||||||
std::vector<Recipe> recipes = {recipe("fmt_10"), recipe("spdlog")};
|
root_pkg("app", "0.1.0"),
|
||||||
GenerateInputs in{m, layout, lock, recipes, "/tmp/app"};
|
dep_pkg("boost", "1.84.0", "rev42"),
|
||||||
|
}};
|
||||||
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
|
|
||||||
std::vector<Recipe> recipes = {recipe("boost"), recipe("boost")};
|
std::vector<Recipe> recipes = {recipe("boost"), recipe("boost")};
|
||||||
GenerateInputs in{m, layout, lock, recipes, "/tmp/app"};
|
GenerateInputs in{m, layout, lock, recipes, "/tmp/app"};
|
||||||
|
|
||||||
auto out = flake_nix(in);
|
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(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]") {
|
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{};
|
DiscoveredLayout layout{};
|
||||||
Lockfile lock{1, {root_pkg("app", "0.1.0", "rev42")}};
|
Lockfile lock{1, {
|
||||||
std::vector<Recipe> recipes = {recipe("fmt_10"), recipe("spdlog"),
|
root_pkg("app", "0.1.0"),
|
||||||
recipe("nlohmann_json")};
|
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"};
|
GenerateInputs in{m, layout, lock, recipes, "/tmp/app"};
|
||||||
|
|
||||||
auto a = flake_nix(in);
|
REQUIRE(flake_nix(in) == flake_nix(in));
|
||||||
auto b = flake_nix(in);
|
|
||||||
REQUIRE(a == b);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("flake_nix output ends with a newline", "[codegen][flake]") {
|
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_FALSE(out.empty());
|
||||||
REQUIRE(out.back() == '\n');
|
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