A flake-parts module carrying the parts of a dendritic Rust project flake that are the same in every project, so they can be bumped in one place instead of nine files per repository.
A project flake in this style ends up with rust-overlay.nix, treefmt.nix,
hooks.nix, devshell.nix, cargo-readme.nix, systems.nix, packages.nix,
_package.nix and overlay.nix. Across projects those files are near-identical,
but they are copies, so every change to the shared parts is a commit per
repository, and the copies fall out of step: a project keeps a workaround for an
upstream bug that has since been fixed, or a comment describing a mechanism that
was refactored away.
Here they are one input and a handful of options. Bumping the input bumps rust-overlay, treefmt-nix, devshell, hk-nix and cargo-readme together.
{
description = "...";
inputs = {
nixpkgs.url = "https://nixos.org/channels/nixpkgs-unstable/nixexprs.tar.xz";
flake-parts.url = "github:hercules-ci/flake-parts";
flake-parts.inputs.nixpkgs-lib.follows = "nixpkgs";
import-tree.url = "github:vic/import-tree";
dendritic-base = {
url = "github:nix-tools/dendritic-base";
inputs.nixpkgs.follows = "nixpkgs";
inputs.flake-parts.follows = "flake-parts";
inputs.import-tree.follows = "import-tree";
};
};
outputs =
{ flake-parts, import-tree, ... }@inputs:
flake-parts.lib.mkFlake { inherit inputs; } (import-tree ./nix);
}nix/base.nix:
{ inputs, ... }:
{
imports = [ inputs.dendritic-base.flakeModules.default ];
dendritic-base = {
root = ../.;
rust.readme.input = "src/main.rs";
};
}pkgs comes from the consumer's nixpkgs with the base's rust-overlay applied,
so there is one nixpkgs in the lock. Everything else -- rust-overlay,
treefmt-nix, devshell, hk-nix, cargo-readme -- comes from the base.
root is the one option every project sets. It has no default because a default
would have to be self, and a top-level flake output whose presence depends on
self is an infinite recursion: producing the output set means knowing its keys,
and whether the project is Rust decides whether there is an overlays output at
all. A path written in the consumer's own file also cannot accidentally name the
base's store path.
For a Cargo workspace, from Cargo.toml alone:
packages.<crate>for every member that produces a binary, pluspackages.defaultchecks.<crate>for each of those, so a crate nothing depends on still has to buildapps.defaultandoverlays.default- versions and descriptions, with
[workspace.package]inheritance resolved - the
cargo readmeinvocation, including how many../it takes to reach the template from the crate directory
Ambiguity is an error rather than a guess: with more than one binary crate and
none named after the repository, evaluation stops and asks for
dendritic-base.rust.defaultCrate.
A project's own nix/*.nix files are still flake-parts modules, and they merge
with the base's config. So most additions need no option at all:
# nix/demo.nix
{
perSystem =
{ pkgs, ... }:
{
devshells.default.packages = [ pkgs.vhs pkgs.gifsicle ];
};
}That works for devshells.default.packages and .env, treefmt.programs.*,
treefmt.settings.global.excludes, new keys under packages / checks /
apps, and flake.overlays.<name> for any name other than default.
Three things do need to go through an option, because defining them twice either conflicts or silently drops one definition:
flake.overlays.default, which is a unique option -- so remove a project's ownnix/overlay.nixwhen adopting the basehk-nix.settings, which merges shallowly -- add hook steps viadendritic-base.hooks.stepsinstead_module.args.pkgs, which is a single definition -- add overlays viadendritic-base.extraOverlaysinstead, and they apply after the base's own
VERSION holds a semver. The dev shell notices when a newer one is published and
prints a line saying so; nix run .#bump-base shows what changed between the
pinned version and the latest, then updates the input. The check never blocks
shell entry and does not run in CI.
Opt out with dendritic-base.updateCheck.enable = false, or per-shell with
DENDRITIC_BASE_NO_UPDATE_CHECK=1.