#include import cargoxx.codegen; import cargoxx.manifest; import cargoxx.layout; import cargoxx.lockfile; import cargoxx.linkdb; import std; using cargoxx::codegen::flake_nix; using cargoxx::codegen::GenerateInputs; 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; namespace { auto pkg(std::string name) -> Package { return Package{ .name = std::move(name), .version = "0.1.0", .edition = Edition::Cpp23, .authors = {}, .license = std::nullopt, }; } 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), .find_package = "", .targets = {}, .source = "curated", }; } auto root_pkg(std::string name, std::string version, std::optional rev = std::nullopt) -> 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, }; } auto dep_pkg(std::string name, std::string version, std::optional 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, }; } } // namespace TEST_CASE("flake_nix always emits the shared nixos-unstable nixpkgs input", "[codegen][flake]") { Manifest m{pkg("hello"), {}, {}}; DiscoveredLayout layout{}; Lockfile lock{1, {root_pkg("hello", "0.1.0")}}; GenerateInputs in{m, layout, lock, {}, {}, "/tmp/hello"}; auto out = flake_nix(in); 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 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_ 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 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 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", "[codegen][flake]") { Manifest m{pkg("hello"), {}, {}}; DiscoveredLayout layout{}; Lockfile lock{1, {root_pkg("hello", "0.1.0")}}; GenerateInputs in{m, layout, lock, {}, {}, "/tmp/hello"}; auto out = flake_nix(in); REQUIRE(out.find("buildInputs = [\n ];") != std::string::npos); } TEST_CASE("flake_nix dedupes deps that share input + attr", "[codegen][flake]") { 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"), dep_pkg("boost", "1.84.0", "rev42"), }}; std::vector recipes = {recipe("boost"), recipe("boost")}; GenerateInputs in{m, layout, lock, recipes, {}, "/tmp/app"}; auto out = flake_nix(in); auto first = out.find("pkgs_nixpkgs_boost_1_84_0.boost"); REQUIRE(first != 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"), {dep("fmt", "10.2.1"), dep("spdlog", "*")}, {}}; DiscoveredLayout layout{}; Lockfile lock{1, { root_pkg("app", "0.1.0"), dep_pkg("fmt", "10.2.1", "abc"), dep_pkg("spdlog", "*", std::nullopt), }}; std::vector recipes = {recipe("fmt_10"), recipe("spdlog")}; GenerateInputs in{m, layout, lock, recipes, {}, "/tmp/app"}; REQUIRE(flake_nix(in) == flake_nix(in)); } TEST_CASE("flake_nix output ends with a newline", "[codegen][flake]") { Manifest m{pkg("hello"), {}, {}}; DiscoveredLayout layout{}; Lockfile lock{1, {root_pkg("hello", "0.1.0")}}; GenerateInputs in{m, layout, lock, {}, {}, "/tmp/hello"}; auto out = flake_nix(in); 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 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); }