diff --git a/flake.nix b/flake.nix index 7d75c10..7fc072d 100644 --- a/flake.nix +++ b/flake.nix @@ -57,7 +57,7 @@ cp build/release/${pname}_bin $out/bin/${pname} ''; hardeningDisable = [ "all" ]; - __noChroot = true; + __noChroot = false; }; in { packages.default = cargoxx-bin; diff --git a/src/cli/cmd_build.cpp b/src/cli/cmd_build.cpp index c5e4d0a..9294cba 100644 --- a/src/cli/cmd_build.cpp +++ b/src/cli/cmd_build.cpp @@ -35,6 +35,34 @@ auto write_text(const fs::path& path, std::string_view content) -> util::Result< return {}; } +auto query_flake_rev(std::string_view flake_ref) -> std::optional { + auto r = exec::run("nix", + {"--extra-experimental-features", + "nix-command flakes", "flake", "metadata", "--json", + std::string{flake_ref}}, + exec::ExecOptions{ + .cwd = fs::current_path(), + .env_overrides = {}, + .timeout = std::chrono::seconds{30}, + .inherit_stdio = false, + }); + if (!r || r->exit_code != 0) { + return std::nullopt; + } + std::string_view body = r->stdout_text; + constexpr std::string_view key = "\"rev\":\""; + auto pos = body.find(key); + if (pos == std::string_view::npos) { + return std::nullopt; + } + pos += key.size(); + auto end = body.find('"', pos); + if (end == std::string_view::npos) { + return std::nullopt; + } + return std::string{body.substr(pos, end - pos)}; +} + // Builds the lockfile from the manifest + resolved recipes, **preserving** // `nixpkgs_rev` for any (name, version) entry that already exists in // `prior` with a matching key. This is what makes `cargoxx build` @@ -59,6 +87,13 @@ auto merge_lockfile(const manifest::Manifest& m, lockfile::Lockfile lock; lock.version = 1; + lock.nixpkgs_rev_pin = prior.nixpkgs_rev_pin.has_value() + ? prior.nixpkgs_rev_pin + : query_flake_rev("github:NixOS/nixpkgs/nixos-unstable"); + lock.flake_utils_rev_pin = + prior.flake_utils_rev_pin.has_value() + ? prior.flake_utils_rev_pin + : query_flake_rev("github:numtide/flake-utils"); lockfile::LockfilePackage root{ .name = m.package.name, @@ -114,7 +149,9 @@ namespace { auto run_nix_cmake(const fs::path& project_root, const std::vector& cmake_args, std::string_view phase) -> util::Result { - std::vector args{"develop", "path:./build", "--command", "cmake"}; + std::vector args{"--extra-experimental-features", + "nix-command flakes", "develop", + "path:./build", "--command", "cmake"}; args.insert(args.end(), cmake_args.begin(), cmake_args.end()); auto r = exec::run("nix", args, exec::ExecOptions{ diff --git a/src/cli/cmd_test.cpp b/src/cli/cmd_test.cpp index a2fd8d7..f659f32 100644 --- a/src/cli/cmd_test.cpp +++ b/src/cli/cmd_test.cpp @@ -19,7 +19,8 @@ auto cmd_test(const fs::path& project_root, bool release, const auto build_dir = std::format("build/{}", profile); auto r = exec::run("nix", - {"develop", "path:./build", "--command", "ctest", + {"--extra-experimental-features", "nix-command flakes", + "develop", "path:./build", "--command", "ctest", "--test-dir", build_dir, "--output-on-failure"}, exec::ExecOptions{ .cwd = project_root, diff --git a/src/codegen/flake.cpp b/src/codegen/flake.cpp index 09ecd73..3b07657 100644 --- a/src/codegen/flake.cpp +++ b/src/codegen/flake.cpp @@ -99,20 +99,26 @@ auto pinned_inputs_dedup(const std::vector& bindings) 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. +auto emit_inputs_block(const std::vector& pinned, + const lockfile::Lockfile& lock) -> std::string { + auto nixpkgs_url = + lock.nixpkgs_rev_pin && !lock.nixpkgs_rev_pin->empty() + ? std::format("github:NixOS/nixpkgs/{}", *lock.nixpkgs_rev_pin) + : std::string{"github:NixOS/nixpkgs/nixos-unstable"}; + auto flake_utils_url = + lock.flake_utils_rev_pin && !lock.flake_utils_rev_pin->empty() + ? std::format("github:numtide/flake-utils/{}", + *lock.flake_utils_rev_pin) + : std::string{"github:numtide/flake-utils"}; std::string out = " inputs = {\n" - " nixpkgs.url = \"github:NixOS/nixpkgs/nixos-unstable\";\n"; + + std::format(" nixpkgs.url = \"{}\";\n", nixpkgs_url); 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"; + out += std::format(" flake-utils.url = \"{}\";\n", flake_utils_url); + out += " };\n"; return out; } @@ -165,7 +171,7 @@ auto flake_nix(const GenerateInputs& in) -> std::string { out += "{\n"; out += std::format(" description = \"{}\";\n\n", in.manifest.package.name); - out += emit_inputs_block(pinned); + out += emit_inputs_block(pinned, in.lock); const bool any_pkg_config = std::ranges::any_of(in.recipes, diff --git a/src/lockfile/lockfile.cpp b/src/lockfile/lockfile.cpp index 889c67d..6bfce0d 100644 --- a/src/lockfile/lockfile.cpp +++ b/src/lockfile/lockfile.cpp @@ -131,6 +131,12 @@ auto parse(const std::filesystem::path& path) -> util::Result { if (auto v = root["version"].value()) { lock.version = *v; } + if (auto v = root["nixpkgs_rev"].value()) { + lock.nixpkgs_rev_pin = *v; + } + if (auto v = root["flake_utils_rev"].value()) { + lock.flake_utils_rev_pin = *v; + } if (const auto* arr = root["package"].as_array()) { lock.packages.reserve(arr->size()); @@ -154,6 +160,12 @@ auto parse(const std::filesystem::path& path) -> util::Result { auto write(const Lockfile& lock, const std::filesystem::path& path) -> util::Result { toml::table root; root.insert_or_assign("version", lock.version); + if (lock.nixpkgs_rev_pin) { + root.insert_or_assign("nixpkgs_rev", *lock.nixpkgs_rev_pin); + } + if (lock.flake_utils_rev_pin) { + root.insert_or_assign("flake_utils_rev", *lock.flake_utils_rev_pin); + } toml::array packages; for (const auto& p : lock.packages) { diff --git a/src/lockfile/lockfile.cppm b/src/lockfile/lockfile.cppm index 863000c..e2b1f49 100644 --- a/src/lockfile/lockfile.cppm +++ b/src/lockfile/lockfile.cppm @@ -24,12 +24,12 @@ struct LockfilePackage { struct Lockfile { int version = 1; + std::optional nixpkgs_rev_pin; + std::optional flake_utils_rev_pin; std::vector packages; bool operator==(const Lockfile&) const = default; - // The nixpkgs revision is shared across every dep package per SPEC ยง5. - // Returns the first non-empty rev seen, or nullopt if no deps are pinned. [[nodiscard]] auto nixpkgs_rev() const -> std::optional; }; diff --git a/tests/cmd_build.cpp b/tests/cmd_build.cpp index e975ad1..c002171 100644 --- a/tests/cmd_build.cpp +++ b/tests/cmd_build.cpp @@ -75,7 +75,7 @@ TEST_CASE("cmd_build generates files for a no-deps binary project", auto flake_text = read_file(root / "build" / "flake.nix"); REQUIRE(flake_text.find("description = \"hello\";") != std::string::npos); - REQUIRE(flake_text.find("github:NixOS/nixpkgs/nixos-unstable") != std::string::npos); + REQUIRE(flake_text.find("github:NixOS/nixpkgs/") != std::string::npos); } TEST_CASE("cmd_build generates files for a library project", "[cli][build]") { diff --git a/tests/codegen_flake.cpp b/tests/codegen_flake.cpp index 3e21fe7..34f70f7 100644 --- a/tests/codegen_flake.cpp +++ b/tests/codegen_flake.cpp @@ -77,7 +77,7 @@ TEST_CASE("flake_nix adds pkgs.pkg-config to nativeBuildInputs only when needed" "[codegen][flake]") { Manifest m{pkg("app"), {dep("sqlite", "*")}, {}}; DiscoveredLayout layout{}; - Lockfile lock{1, {root_pkg("app", "0.1.0")}}; + Lockfile lock{.version = 1, .packages = {root_pkg("app", "0.1.0")}}; std::vector recipes = {Recipe{ .nixpkgs_attr = "sqlite", .find_package = "PkgConfig REQUIRED", @@ -96,7 +96,7 @@ TEST_CASE("flake_nix omits pkgs.pkg-config when no recipe needs it", "[codegen][flake]") { Manifest m{pkg("hello"), {dep("fmt", "*")}, {}}; DiscoveredLayout layout{}; - Lockfile lock{1, {root_pkg("hello", "0.1.0")}}; + Lockfile lock{.version = 1, .packages = {root_pkg("hello", "0.1.0")}}; std::vector recipes = {recipe("fmt_10")}; GenerateInputs in{m, layout, lock, recipes, {}, "/tmp/hello"}; @@ -108,7 +108,7 @@ 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")}}; + Lockfile lock{.version = 1, .packages = {root_pkg("hello", "0.1.0")}}; GenerateInputs in{m, layout, lock, {}, {}, "/tmp/hello"}; auto out = flake_nix(in); @@ -122,7 +122,7 @@ TEST_CASE("flake_nix always emits the shared nixos-unstable nixpkgs input", 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, { + Lockfile lock{.version = 1, .packages = { root_pkg("app", "0.1.0"), dep_pkg("fmt", "10.2.1", "abc123def456"), }}; @@ -146,7 +146,7 @@ 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)}}; + Lockfile lock{.version = 1, .packages = {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"}; @@ -158,7 +158,7 @@ TEST_CASE("flake_nix uses shared `pkgs` for unpinned deps", 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, { + Lockfile lock{.version = 1, .packages = { root_pkg("app", "0.1.0"), dep_pkg("fmt", "10.2.1", "abc"), dep_pkg("zlib", "*", std::nullopt), @@ -175,7 +175,7 @@ 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")}}; + Lockfile lock{.version = 1, .packages = {root_pkg("hello", "0.1.0")}}; GenerateInputs in{m, layout, lock, {}, {}, "/tmp/hello"}; auto out = flake_nix(in); @@ -188,7 +188,7 @@ TEST_CASE("flake_nix dedupes deps that share input + attr", {dep("boost", "1.84.0"), dep("boost", "1.84.0")}, {}}; DiscoveredLayout layout{}; - Lockfile lock{1, { + Lockfile lock{.version = 1, .packages = { root_pkg("app", "0.1.0"), dep_pkg("boost", "1.84.0", "rev42"), }}; @@ -205,7 +205,7 @@ TEST_CASE("flake_nix dedupes deps that share input + attr", 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, { + Lockfile lock{.version = 1, .packages = { root_pkg("app", "0.1.0"), dep_pkg("fmt", "10.2.1", "abc"), dep_pkg("spdlog", "*", std::nullopt), @@ -219,7 +219,7 @@ TEST_CASE("flake_nix produces deterministic output", "[codegen][flake]") { 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")}}; + Lockfile lock{.version = 1, .packages = {root_pkg("hello", "0.1.0")}}; GenerateInputs in{m, layout, lock, {}, {}, "/tmp/hello"}; auto out = flake_nix(in); @@ -231,7 +231,7 @@ 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, { + Lockfile lock{.version = 1, .packages = { root_pkg("app", "0.1.0"), dep_pkg("range-v3", "0.12.0", "rev123"), }}; diff --git a/tests/lockfile_round_trip.cpp b/tests/lockfile_round_trip.cpp index 1de3335..1555483 100644 --- a/tests/lockfile_round_trip.cpp +++ b/tests/lockfile_round_trip.cpp @@ -54,14 +54,14 @@ auto round_trip(const Lockfile& l) -> Lockfile { } // namespace TEST_CASE("write round-trips a minimal lockfile", "[lockfile]") { - Lockfile l{1, {root_pkg("my-project", "0.1.0")}}; + Lockfile l{.version = 1, .packages = {root_pkg("my-project", "0.1.0")}}; REQUIRE(round_trip(l) == l); } TEST_CASE("write round-trips a lockfile with deps", "[lockfile]") { Lockfile l{ - 1, - { + .version = 1, + .packages = { root_pkg("my-project", "0.1.0", {"fmt 10.2.1", "spdlog 1.13.0"}), dep_pkg("fmt", "10.2.1", "fmt_10", "8a3f...c2d1"), dep_pkg("spdlog", "1.13.0", "spdlog", "8a3f...c2d1"), @@ -72,8 +72,8 @@ TEST_CASE("write round-trips a lockfile with deps", "[lockfile]") { TEST_CASE("write round-trips lockfile recipe fields", "[lockfile]") { Lockfile l{ - 1, - { + .version = 1, + .packages = { LockfilePackage{ .name = "fmt", .version = "10.2.1", @@ -120,8 +120,8 @@ TEST_CASE("write round-trips lockfile recipe fields", "[lockfile]") { TEST_CASE("Lockfile::nixpkgs_rev returns the shared rev", "[lockfile]") { Lockfile l{ - 1, - { + .version = 1, + .packages = { root_pkg("p", "0.1.0", {"fmt 10.2.1"}), dep_pkg("fmt", "10.2.1", "fmt_10", "abc123"), }, @@ -129,8 +129,19 @@ TEST_CASE("Lockfile::nixpkgs_rev returns the shared rev", "[lockfile]") { REQUIRE(l.nixpkgs_rev() == "abc123"); } +TEST_CASE("write round-trips top-level nixpkgs_rev + flake_utils_rev pins", + "[lockfile]") { + Lockfile l{ + .version = 1, + .nixpkgs_rev_pin = "549bd84d6279f9852cae6225e372cc67fb91a4c1", + .flake_utils_rev_pin = "11707dc2f618dd54ca8739b309ec4fc024de578b", + .packages = {root_pkg("p", "0.1.0")}, + }; + REQUIRE(round_trip(l) == l); +} + TEST_CASE("Lockfile::nixpkgs_rev is nullopt when no deps", "[lockfile]") { - Lockfile l{1, {root_pkg("p", "0.1.0")}}; + Lockfile l{.version = 1, .packages = {root_pkg("p", "0.1.0")}}; REQUIRE_FALSE(l.nixpkgs_rev().has_value()); }