diff --git a/CHANGELOG.md b/CHANGELOG.md index 9371195..a2d2311 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,15 @@ All notable changes to cargoxx will be documented in this file. `write(lock, path)` matching the format in `SPEC.md` §5. Also `Lockfile::nixpkgs_rev()` returns the shared revision (codegen will consume this in M3). `tests/lockfile_round_trip.cpp` covers 9 cases. +- `cargoxx.codegen`: `GenerateInputs` plus the pure function + `flake_nix(in) -> std::string`. Substitutes the package name, the + resolved nixpkgs revision (defaulting to `nixos-unstable` when the + lockfile pins none), and a deduplicated list of dep `nixpkgs_attr` + entries into `buildInputs`. Output is byte-deterministic. + `tests/codegen_flake.cpp` covers 7 cases. Note: SPEC §7's template did + not show `buildInputs`; we add one between `nativeBuildInputs` and the + `env.NIX_CFLAGS_COMPILE` block as the natural slot for the deps that + TECH_SPEC §10 says we splice in. - SQLite overlay: `Database::open(overlay_path)` now opens (and creates, if missing) a per-user `linkdb.sqlite` cache, applying the schema from `SPEC.md` §9 idempotently. Default path is diff --git a/CMakeLists.txt b/CMakeLists.txt index 7a7e3fb..c130a21 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,6 +48,7 @@ target_sources(cargoxx src/linkdb/recipe.cpp src/linkdb/curated.cpp src/linkdb/overlay.cpp + src/codegen/flake.cpp src/cli/cmd_new.cpp src/cli/run.cpp PUBLIC diff --git a/src/codegen/codegen.cppm b/src/codegen/codegen.cppm index 6329337..4932f06 100644 --- a/src/codegen/codegen.cppm +++ b/src/codegen/codegen.cppm @@ -1,7 +1,24 @@ export module cargoxx.codegen; +import std; import cargoxx.util; import cargoxx.manifest; -import cargoxx.lockfile; import cargoxx.layout; import cargoxx.linkdb; +import cargoxx.lockfile; + +export namespace cargoxx::codegen { + +// All inputs the generators need. Held by const reference; the caller owns +// the underlying objects. Not copyable. +struct GenerateInputs { + const manifest::Manifest& manifest; + const layout::DiscoveredLayout& layout; + const lockfile::Lockfile& lock; + std::vector recipes; // one per manifest dep, same order + std::filesystem::path project_root; +}; + +auto flake_nix(const GenerateInputs& in) -> std::string; + +} // namespace cargoxx::codegen diff --git a/src/codegen/flake.cpp b/src/codegen/flake.cpp new file mode 100644 index 0000000..7b3f3ba --- /dev/null +++ b/src/codegen/flake.cpp @@ -0,0 +1,108 @@ +module cargoxx.codegen; + +import std; +import cargoxx.manifest; +import cargoxx.linkdb; +import cargoxx.lockfile; + +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@@"; + + 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 { + 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.append(tmpl.substr(pos, next - pos)); + out.append(value); + pos = next + marker.size(); + } + return out; +} + +auto stable_dedup(const std::vector& xs) -> std::vector { + std::vector out; + std::set seen; + for (const auto& x : xs) { + if (seen.insert(x).second) { + out.push_back(x); + } + } + return out; +} + +} // namespace + +auto flake_nix(const GenerateInputs& in) -> std::string { + auto rev = in.lock.nixpkgs_rev().value_or("nixos-unstable"); + + std::vector 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 dep_lines; + for (const auto& a : deduped) { + dep_lines += " pkgs."; + dep_lines += a; + dep_lines += '\n'; + } + + 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); + return out; +} + +} // namespace cargoxx::codegen diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 741b17b..9cdc10a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -15,4 +15,5 @@ cargoxx_add_test(layout_discovery) cargoxx_add_test(lockfile_round_trip) cargoxx_add_test(linkdb_lookup) cargoxx_add_test(linkdb_overlay) +cargoxx_add_test(codegen_flake) cargoxx_add_test(cmd_new) diff --git a/tests/codegen_flake.cpp b/tests/codegen_flake.cpp new file mode 100644 index 0000000..a61dccb --- /dev/null +++ b/tests/codegen_flake.cpp @@ -0,0 +1,138 @@ +#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::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 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, + }; +} + +} // namespace + +TEST_CASE("flake_nix renders the package description and rev", + "[codegen][flake]") { + Manifest m{pkg("hello"), {}, {}}; + DiscoveredLayout layout{}; + Lockfile lock{1, {root_pkg("hello", "0.1.0", "abc123def456")}}; + GenerateInputs in{m, layout, lock, {}, "/tmp/hello"}; + + 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", + "[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("nixpkgs/nixos-unstable") != 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 emits one pkgs. line per dep", + "[codegen][flake]") { + Manifest m{pkg("app"), {}, {}}; + DiscoveredLayout layout{}; + Lockfile lock{1, {root_pkg("app", "0.1.0", "rev42")}}; + std::vector recipes = {recipe("fmt_10"), recipe("spdlog")}; + 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("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 recipes = {recipe("boost"), recipe("boost")}; + GenerateInputs in{m, layout, lock, recipes, "/tmp/app"}; + + auto out = flake_nix(in); + auto first = out.find("pkgs.boost"); + REQUIRE(first != std::string::npos); + REQUIRE(out.find("pkgs.boost", first + 1) == std::string::npos); +} + +TEST_CASE("flake_nix produces deterministic output", "[codegen][flake]") { + Manifest m{pkg("app"), {}, {}}; + DiscoveredLayout layout{}; + Lockfile lock{1, {root_pkg("app", "0.1.0", "rev42")}}; + std::vector recipes = {recipe("fmt_10"), recipe("spdlog"), + recipe("nlohmann_json")}; + GenerateInputs in{m, layout, lock, recipes, "/tmp/app"}; + + auto a = flake_nix(in); + auto b = flake_nix(in); + REQUIRE(a == b); +} + +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'); +}