module cargoxx.codegen; import std; import cargoxx.manifest; import cargoxx.linkdb; import cargoxx.lockfile; namespace cargoxx::codegen { namespace { // 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 rev; // pinned commit (null → unpinned) }; // 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.) — Nix permits underscores in // both places, hyphens only in attribute names. auto sanitize(std::string_view s) -> std::string { std::string out; out.reserve(s.size()); for (char c : s) { if (std::isalnum(static_cast(c)) || c == '_') { out += c; } else { out += '_'; } } return out; } auto sanitize_input_attr(std::string_view name, std::string_view version) -> std::string { return std::format("nixpkgs_{}_{}", sanitize(name), sanitize(version)); } 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 LockfileRef{.rev = p.nixpkgs_rev, .attr = p.nixpkgs_attr}; } } return LockfileRef{}; } auto build_bindings(const GenerateInputs& in) -> std::vector { std::vector out; out.reserve(in.manifest.dependencies.size() + in.manifest.dev_dependencies.size()); auto push = [&](const manifest::Dependency& dep, const linkdb::Recipe& rec) { auto ref = find_lockfile_ref(in.lock, dep.name, dep.version_spec); std::string attr = (ref.attr && !ref.attr->empty()) ? *ref.attr : rec.nixpkgs_attr; out.push_back(DepBinding{ .name = dep.name, .version = dep.version_spec, .nixpkgs_attr = std::move(attr), .sanitized = sanitize_input_attr(dep.name, dep.version_spec), .rev = std::move(ref.rev), }); }; for (std::size_t i = 0; i < in.manifest.dependencies.size(); ++i) { push(in.manifest.dependencies[i], in.recipes[i]); } for (std::size_t i = 0; i < in.manifest.dev_dependencies.size(); ++i) { push(in.manifest.dev_dependencies[i], in.dev_recipes[i]); } 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& bindings) -> std::vector { std::vector out; std::set seen; 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& 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& 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& 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 base_expr(const DepBinding& b) -> std::string { return b.rev && !b.rev->empty() ? std::format("pkgs_{}.{}", b.sanitized, b.nixpkgs_attr) : std::format("pkgs.{}", b.nixpkgs_attr); } auto emit_build_inputs(const std::vector& bindings) -> std::string { std::set seen; std::string out; for (const auto& b : bindings) { auto expr = base_expr(b); if (seen.insert(expr).second) { out += std::format(" {}\n", expr); } } return out; } } // namespace auto flake_nix(const GenerateInputs& in) -> std::string { auto bindings = build_bindings(in); auto pinned = pinned_inputs_dedup(bindings); std::string out; out += "{\n"; out += std::format(" description = \"{}\";\n\n", in.manifest.package.name); out += emit_inputs_block(pinned); const bool any_pkg_config = std::ranges::any_of(in.recipes, [](const linkdb::Recipe& r) { return r.pkg_config_module && !r.pkg_config_module->empty(); }) || std::ranges::any_of(in.dev_recipes, [](const linkdb::Recipe& r) { return r.pkg_config_module && !r.pkg_config_module->empty(); }); 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 += " in {\n" " devShell = pkgs.gcc15Stdenv.mkDerivation {\n" " name = \"shell\";\n" " version = \"1.0\";\n" " nativeBuildInputs = [\n" " pkgs.ninja\n" " pkgs.cmake\n"; if (any_pkg_config) { out += " pkgs.pkg-config\n"; } out += " ];\n" " buildInputs = [\n"; out += emit_build_inputs(bindings); out += " ];\n" " hardeningDisable = [\n" " \"all\"\n" " ];\n" " };\n" " });\n" "}\n"; return out; } } // namespace cargoxx::codegen