220 lines
7.5 KiB
C++
220 lines
7.5 KiB
C++
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<std::string> 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.<attr>) — 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<unsigned char>(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<std::string> rev;
|
|
std::optional<std::string> 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<DepBinding> {
|
|
std::vector<DepBinding> 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<DepBinding>& bindings)
|
|
-> std::vector<const DepBinding*> {
|
|
std::vector<const DepBinding*> out;
|
|
std::set<std::string> 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<const DepBinding*>& 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"
|
|
+ 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 += std::format(" flake-utils.url = \"{}\";\n", flake_utils_url);
|
|
out += " };\n";
|
|
return out;
|
|
}
|
|
|
|
auto emit_outputs_params(const std::vector<const DepBinding*>& 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<const DepBinding*>& 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<DepBinding>& bindings) -> std::string {
|
|
std::set<std::string> 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, in.lock);
|
|
|
|
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
|