[M3] add codegen::flake_nix
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<linkdb::Recipe> recipes; // one per manifest dep, same order
|
||||
std::filesystem::path project_root;
|
||||
};
|
||||
|
||||
auto flake_nix(const GenerateInputs& in) -> std::string;
|
||||
|
||||
} // namespace cargoxx::codegen
|
||||
|
||||
108
src/codegen/flake.cpp
Normal file
108
src/codegen/flake.cpp
Normal file
@@ -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<std::string>& xs) -> std::vector<std::string> {
|
||||
std::vector<std::string> out;
|
||||
std::set<std::string> 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<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 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
|
||||
@@ -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)
|
||||
|
||||
138
tests/codegen_flake.cpp
Normal file
138
tests/codegen_flake.cpp
Normal file
@@ -0,0 +1,138 @@
|
||||
#include <catch2/catch_test_macros.hpp>
|
||||
|
||||
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<std::string> 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.<attr> line per dep",
|
||||
"[codegen][flake]") {
|
||||
Manifest m{pkg("app"), {}, {}};
|
||||
DiscoveredLayout layout{};
|
||||
Lockfile lock{1, {root_pkg("app", "0.1.0", "rev42")}};
|
||||
std::vector<Recipe> 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<Recipe> 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<Recipe> 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');
|
||||
}
|
||||
Reference in New Issue
Block a user