I’ve been trying to write a small nix flake for a rust project and am struggling getting it to a point where I’m happy with it. I’m aware I’m procrastinating by writing a flake instead of code, but I find it to be an interesting sport.
The problem I’m facing is in trying to make the flake the “central hub” of the project’s development. The package definition itself, devShell, checks, formatting and pre-commit hooks are all things I generally use, and I’m trying to put it all into my flake.
I would like the ability to choose a specific rust toolchain, meaning I want to use something like fenix (rust-overlay would work as well, but I’ve decided on fenix for no particular reason).
I started using crane to avoid issues with nix flake check’s sandboxed environment, particularly because I require pkg-config and some other libs. My understanding is that I should do something like this (note that I use flake-parts):
flake.nix
{
inputs = {
flake-parts.url = "github:hercules-ci/flake-parts";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
fenix = {
url = "github:nix-community/fenix";
inputs.nixpkgs.follows = "nixpkgs";
};
crane.url = "github:ipetkov/crane";
};
outputs =
inputs@{ flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
systems = [
"x86_64-linux"
"aarch64-linux"
];
perSystem = { inputs', config, pkgs, lib, ... }:
let
nativeBuildInputs = [ pkgs.git pkgs.pkg-config ];
buildInputs = [ pkgs.wayland pkgs.libGL pkgs.vulkan-loader ];
libraryPath = lib.makeLibraryPath buildInputs;
rustToolchain = inputs'.fenix.packages.stable.toolchain;
craneLib = (inputs.crane.mkLib pkgs).overrideToolchain rustToolchain;
root = ./.;
src = lib.fileset.toSource {
inherit root;
fileset = lib.fileset.unions [
(craneLib.fileset.commonCargoSources root)
(pkgs.lib.fileset.fileFilter (file: file.hasExt "wgsl") root)
];
};
commonArgs = {
inherit src;
strictDeps = true;
inherit nativeBuildInputs buildInputs;
};
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
myapp = craneLib.buildPackage (
commonArgs
// {
inherit cargoArtifacts;
postFixup = "patchelf --add-rpath ${libraryPath} $out/bin/myapp";
}
);
in
{
checks = {
inherit myapp;
myapp-doc = craneLib.cargoDoc (commonArgs // { inherit cargoArtifacts; });
};
packages = {
inherit myapp;
default = myapp;
};
devShells.default = craneLib.devShell {
checks = config.checks;
env.LD_LIBRARY_PATH = libraryPath;
};
};
};
}
If I now want to add formatting and pre-commit hooks into the mix, things get confusing. A format check for nix flake check could be added by crane (using craneLib.cargoFmt), by treefmt-nix (using treefmt.programs.rustfmt.enable) or by git-hooks-nix (using pre-commit.settings.hooks.rustfmt).
But if I wanted this formatter to also work with nix fmt, that leaves only treefmt-nix. That will also allow me to add other formatters for markdown etc., if I want. Thankfully, it seems I can integrate treefmt-nix into git-hooks-nix with pre-commit.settings.hooks.treefmt, except now I do duplicate work with nix flake check, as the formatting gets checked twice (once through treefmt, and once through git-hooks) so I have to disable the flake check from git-hooks using pre-commit.check.enable = false;. I believe I also have to care that my chosen toolchain is used using treefmt.programs.rustfmt.package.
updated flake.nix
{
inputs = {
flake-parts.url = "github:hercules-ci/flake-parts";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
fenix = {
url = "github:nix-community/fenix";
inputs.nixpkgs.follows = "nixpkgs";
};
crane.url = "github:ipetkov/crane";
treefmt = {
url = "github:numtide/treefmt-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
git-hooks = {
url = "github:cachix/git-hooks.nix";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs =
inputs@{ flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
imports = [
inputs.treefmt.flakeModule
inputs.git-hooks.flakeModule
];
systems = [
"x86_64-linux"
"aarch64-linux"
];
perSystem =
{
inputs',
config,
pkgs,
lib,
...
}:
let
nativeBuildInputs = [
pkgs.git
pkgs.pkg-config
];
buildInputs = [
pkgs.wayland
pkgs.libGL
pkgs.vulkan-loader
];
libraryPath = lib.makeLibraryPath buildInputs;
rustToolchain = inputs'.fenix.packages.stable.toolchain;
craneLib = (inputs.crane.mkLib pkgs).overrideToolchain rustToolchain;
root = ./.;
src = lib.fileset.toSource {
inherit root;
fileset = lib.fileset.unions [
(craneLib.fileset.commonCargoSources root)
(pkgs.lib.fileset.fileFilter (file: file.hasExt "wgsl") root)
];
};
commonArgs = {
inherit src;
strictDeps = true;
inherit nativeBuildInputs buildInputs;
};
cargoArtifacts = craneLib.buildDepsOnly commonArgs;
myapp = craneLib.buildPackage (
commonArgs
// {
inherit cargoArtifacts;
postFixup = "patchelf --add-rpath ${libraryPath} $out/bin/myapp";
}
);
in
{
treefmt = {
programs = {
nixfmt.enable = true;
rustfmt = {
enable = true;
package = rustToolchain;
};
taplo.enable = true;
};
};
pre-commit = {
check.enable = false;
settings.hooks = {
treefmt.enable = true;
};
};
checks = {
inherit myapp;
myapp-doc = craneLib.cargoDoc (commonArgs // { inherit cargoArtifacts; });
};
packages = {
inherit myapp;
default = myapp;
};
devShells.default = craneLib.devShell {
checks = config.checks;
shellHook = config.pre-commit.shellHook;
env.LD_LIBRARY_PATH = libraryPath;
};
};
};
}
Lastly I wanted to add a simple cargo-check pre-commit run, and this is where I kind of got stuck. The issue is that I have defined how my app is supposed to be built with crane and set the check to use that package. But now I want a pre-commit cargo check run. If I only integrate pre-commit into the devShell, it works by virtue of cargo check being run inside the devShell which sets up the build environment. But it seems strange to me. I often use Sublime Merge which doesn’t integrate with devShell whatsoever from what I can tell (so direnv is no solution either) and I kind of hoped that I could make use of what I defined with crane from within a pre-commit hook. I.e. that the hook itself gets the correct environment to run the check.
Am I just trying to do too much with my flake here, or am I missing some magical connection between these tools? I’ve had some tough time with pre-commit in another project, because using it directly has it hard-coding its own nix store path, which had me having to reinstall the hooks every time I update my system.