feat: Expose module for setting up the build and test environment.

This commit is contained in:
Robert Hensing 2025-10-27 01:37:18 +01:00
parent 62d4eb37a2
commit 26b7cb2116
4 changed files with 221 additions and 44 deletions

View file

@ -3,6 +3,39 @@
Use the Nix [C API] from Rust.
## Build with `nix-cargo-integration`
The development environment and building with Nix are taken care of by [nix-cargo-integration](https://github.com/90-008/nix-cargo-integration#readme) ([options](https://flake.parts/options/nix-cargo-integration.html)).
The dependency on Nix is taken care of with the [`nix-bindings-rust` flake-parts module]().
Example usage:
```nix
{
outputs =
inputs@{ self, flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; }
{
imports = [
inputs.nix-cargo-integration.flakeModule
inputs.nix-bindings-rust.modules.flake.default
];
perSystem = { config, pkgs, ... }: {
# optional:
nix-bindings-rust.nixPackage = pkgs.nix;
nci.projects."myproject" = {
depsDrvConfig = {
imports = [ config.nix-bindings-rust.nciBuildConfig ];
};
};
};
};
}
```
## Hacking
The following will open a shell with dependencies, and install pre-commit for automatic formatting.

View file

@ -10,8 +10,14 @@
inputs.hercules-ci-effects.flakeModule
];
perSystem =
{ config, pkgs, ... }:
{
config,
pkgs,
inputs',
...
}:
{
nix-bindings-rust.nixPackage = inputs'.nix.packages.default;
pre-commit.settings.hooks.nixfmt-rfc-style.enable = true;
# Temporarily disable rustfmt due to configuration issues

176
flake.nix
View file

@ -13,14 +13,158 @@
outputs =
inputs@{ self, flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } (
{
toplevel@{
lib,
withSystem,
...
}:
let
/**
Makes perSystem.nix-bindings-rust available.
*/
flake-parts-modules.basic =
{
config,
flake-parts-lib,
withSystem,
...
}:
{
options.perSystem = flake-parts-lib.mkPerSystemOption (
{ config, pkgs, ... }:
let
cfg = config.nix-bindings-rust;
in
{
options.nix-bindings-rust = {
nixPackage = lib.mkOption {
type = lib.types.package;
default = pkgs.nix;
defaultText = lib.literalMD "pkgs.nix";
description = ''
The Nix package to use when building the `nix-bindings-...` crates.
'';
};
nciBuildConfig = lib.mkOption {
type = lib.types.deferredModule;
description = ''
A module to load into your nix-cargo-integration
[`perSystem.nci.projects.<name>.depsDrvConfig`](https://flake.parts/options/nix-cargo-integration.html#opt-perSystem.nci.projects._name_.depsDrvConfig) or similar such options.
Example:
```nix
perSystem = perSystem@{ config, ... }: {
nci.projects."my_project".depsDrvConfig = perSystem.config.nix-bindings-rust.nciBuildConfig;
}
```
'';
};
};
config.nix-bindings-rust = {
nciBuildConfig = {
mkDerivation = {
buildInputs = [
# stdbool.h
pkgs.stdenv.cc
]
++ (
if cfg.nixPackage ? libs then
let
l = cfg.nixPackage.libs;
in
[
l.nix-expr-c
l.nix-store-c
l.nix-util-c
l.nix-fetchers-c or null # Nix >= 2.29
l.nix-flake-c
]
else
[ cfg.nixPackage ]
);
nativeBuildInputs = [
pkgs.pkg-config
];
# bindgen uses clang to generate bindings, but it doesn't know where to
# find our stdenv cc's headers, so when it's gcc, we need to tell it.
postConfigure = lib.optionalString pkgs.stdenv.cc.isGNU ''
source ${./bindgen-gcc.sh}
'';
};
# NOTE: duplicated in flake.nix devShell
env = {
LIBCLANG_PATH = lib.makeLibraryPath [ pkgs.buildPackages.llvmPackages.clang-unwrapped ];
BINDGEN_EXTRA_CLANG_ARGS =
# Work around missing [[deprecated]] in clang
"-x c++ -std=c++2a";
}
// lib.optionalAttrs pkgs.stdenv.cc.isGNU {
# Avoid cc wrapper, because we only need to add the compiler/"system" dirs
NIX_CC_UNWRAPPED = "${pkgs.stdenv.cc.cc}/bin/gcc";
};
};
};
}
);
};
/**
Adds flake checks that test the bindings with the provided nix package.
*/
flake-parts-modules.tested =
# Consumer toplevel
{ options, config, ... }:
{
imports = [ flake-parts-modules.basic ];
config.perSystem =
# Consumer perSystem
consumerPerSystem@{
lib,
config,
system,
pkgs,
...
}:
let
# nix-bindings-rust's perSystem, but with the consumer's `pkgs`
nix-bindings-rust-perSystemConfig =
# Extending our own perSystem, not the consumer's perSystem!
toplevel.config.partitions.testing-support.module.nix-bindings-rust.internalWithSystem system
({ extendModules, ... }: extendModules)
{
modules = [
{
config = {
# Overriding our `perSystem` to use the consumer's `pkgs`
_module.args.pkgs = lib.mkForce consumerPerSystem.pkgs;
# ... and `nixPackage`
nix-bindings-rust.nixPackage = lib.mkForce consumerPerSystem.config.nix-bindings-rust.nixPackage;
};
}
];
};
in
{
key = "nix-bindings-rust-add-checks";
config.checks = lib.concatMapAttrs (
k: v:
lib.optionalAttrs (lib.strings.hasPrefix "nix-bindings-" k && !lib.strings.hasSuffix "-clippy" k) {
"dependency-${k}" = v;
}
) nix-bindings-rust-perSystemConfig.config.checks;
};
};
flake-parts-modules.default = flake-parts-modules.tested;
in
{
imports = [
inputs.nix-cargo-integration.flakeModule
inputs.flake-parts.flakeModules.partitions
inputs.flake-parts.flakeModules.modules
# dogfood
flake-parts-modules.tested
./nci.nix
];
systems = [
@ -44,10 +188,40 @@
partitionedAttrs.devShells = "dev";
partitionedAttrs.checks = "dev";
partitionedAttrs.herculesCI = "dev";
# Packages are basically just checks in this project; a library by
# itself is not useful. That's just not how the Rust integration works.
# By taking `packages` from `dev` we benefit from this dev-only definition:
# nix-bindings-rust.nixPackage = inputs'.nix.packages.default;
partitionedAttrs.packages = "dev";
partitions.dev.extraInputsFlake = ./dev;
partitions.dev.module = {
imports = [ ./dev/flake-module.nix ];
};
# A partition that doesn't dogfood the flake-parts-modules.tested module
# so that we can actually retrieve `checks` without infinite recursions
# from trying to include the dogfooded attrs.
partitions.testing-support.module =
{ withSystem, ... }:
{
# Make a clean withSystem available for consumers
options.nix-bindings-rust.internalWithSystem = lib.mkOption { internal = true; };
config = {
nix-bindings-rust.internalWithSystem = withSystem;
perSystem = {
# Remove dogfooded checks. This configuration's checks are
# *consumed* by nix-bindings-rust-add-checks, so they should
# *NOT* also be *produced* by it.
disabledModules = [ { key = "nix-bindings-rust-add-checks"; } ];
};
};
};
# flake output attributes
flake = {
modules.flake = flake-parts-modules;
};
}
);
}

48
nci.nix
View file

@ -11,33 +11,12 @@
nci.projects.nix-bindings = {
path = ./.;
drvConfig = {
imports = [
# Downstream projects import this into depsDrvConfig instead
config.nix-bindings-rust.nciBuildConfig
];
# Extra settings for running the tests
mkDerivation = {
buildInputs = [
# stdbool.h
pkgs.stdenv.cc
]
++ (
if config.packages.nix ? libs then
let
l = config.packages.nix.libs;
in
[
l.nix-expr-c
l.nix-store-c
l.nix-util-c
l.nix-flake-c
]
else
[ config.packages.nix ]
);
nativeBuildInputs = [
pkgs.pkg-config
];
# bindgen uses clang to generate bindings, but it doesn't know where to
# find our stdenv cc's headers, so when it's gcc, we need to tell it.
postConfigure = lib.optionalString pkgs.stdenv.cc.isGNU ''
source ${./bindgen-gcc.sh}
'';
# Prepare the environment for Nix to work.
# Nix does not provide a suitable environment for running itself in
# the sandbox - not by default. We configure it to use a relocated store.
@ -61,26 +40,11 @@
echo "experimental-features = ca-derivations flakes" > "$NIX_CONF_DIR/nix.conf"
# Init ahead of time, because concurrent initialization is flaky
${
# Not using nativeBuildInputs because this should (hopefully) be
# the only place where we need a nix binary. Let's stay in control.
pkgs.buildPackages.nix
}/bin/nix-store --init
${config.nix-bindings-rust.nixPackage}/bin/nix-store --init
echo "Store initialized."
'';
};
# NOTE: duplicated in flake.nix devShell
env = {
LIBCLANG_PATH = lib.makeLibraryPath [ pkgs.buildPackages.llvmPackages.clang-unwrapped ];
BINDGEN_EXTRA_CLANG_ARGS =
# Work around missing [[deprecated]] in clang
"-x c++ -std=c++2a";
}
// lib.optionalAttrs pkgs.stdenv.cc.isGNU {
# Avoid cc wrapper, because we only need to add the compiler/"system" dirs
NIX_CC_UNWRAPPED = "${pkgs.stdenv.cc.cc}/bin/gcc";
};
};
};
};