[M5+] flake codegen + SPEC §7/§10 amendment for per-dep nixpkgs pins
This commit is contained in:
@@ -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@@";
|
||||
// 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)
|
||||
};
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/@@NIXPKGS_REV@@";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user