Nix: structuring Flakes with Blueprint
Table of Contents

I’m sitting here, in an Airbnb in Brussels, after a long weekend of talking tech and drinking beer during my first FOSSDEM. Seems like as good a time as any then to try and restart my regular posting schedule 😂.
That being said, given that my physical, mental and spiritual battery is running low 😫, I’m going to keep this one simple and to the point. Much like the topic of today’s blog post 😉.
Blueprint
From the mind behind flake-utils, blueprint is yet another way to organize your flake, with a heavy emphasis on convention and keeping things simple.
It isn’t intended to compete with the flexibility of solutions like flake-parts.
Instead, it sets out to restrict how you should do things, with a small set of conventions and minimal plumbing, to keep it easy to understand and reason about.
There’s no module system here, just a small library of vanilla Nix.
Getting started
At its core, everything is based around a relatively flat directory structure:
checks/
for flake checks.devshells/
for devshells.hosts/
for machine configurations.hosts/*/users/
for Home Manager configurations.lib/
for Nix functions.modules/
for NixOS and other modules.packages/
for packages.templates/
for flake templates.devshell.nix
for the default devshellformatter.nix
for the default formatterpackage.nix
for the default package
We initialise blueprint as follows:
{
inputs = {
blueprint.url = "github:numtide/blueprint";
};
outputs = inputs:
inputs.blueprint {
inherit inputs;
};
}
Your flake will be configured with a standard list of systems, but you can override that:
{
outputs = inputs:
inputs.blueprint {
inherit inputs;
systems = ["x86_64-linux" "aarch64-linux"];
};
}
In addition, I tend to put everything in a nix/
folder, which can be done by specifying the prefix
attribute:
{
outputs = inputs:
inputs.blueprint {
inherit inputs;
prefix = "nix/";
systems = ["x86_64-linux" "aarch64-linux"];
};
}
Configuring a formatter
If you create a file called formatter.nix
, blueprint will wire up the package it defines to nix fmt
.
I most humbly suggest using treefmt 🙏, but you aren’t restricted in what package you can use.
{
pkgs,
inputs,
...
}:
inputs.treefmt-nix.lib.mkWrapper pkgs {
projectRootFile = "flake.nix";
####################
# elided for brevity
####################
}
As you can see in the example above, blueprint is passing an instance of nixpkgs and the flake’s inputs in the function argument. There are a few other attributes available, but we’ll cover those in the next section.
Defining a package
All packages are defined inside the packages/
directory.
They can be defined inside a single file, packages/foo.nix
, or a directory foo
with a default.nix
e.g. packages/foo/default.nix
.
{
pname, # name of the file without the `.nix` suffix, or the name of the directory
flake, # maps to inputs.self
inputs, # maps to the flake's inputs
perSystem, # lets you write perSystem.gomod2nix.buildGoApplicaiton instead of inputs.gomod2nix.packages.${system}.buildGoApplication
pkgs, # a nixpkgs instance
}: let
inherit (pkgs) lib;
in
perSystem.gomod2nix.buildGoApplication rec {
inherit pname;
####################
# Elided for brevity
####################
}
Devshells
By now you are probably getting a feel for how things work, and it will come as no surprise that devshells are defined
in the devshells/
directory, with the default going in devshells/default.nix
.
They work the same way as packages, and you can define them in devshells/foo.nix
or devshells/foo/default.nix
.
{
pkgs,
perSystem,
...
}:
pkgs.mkShellNoCC {
env.GOROOT = "${pkgs.go}/share/go";
packages =
(with pkgs; [
go
goreleaser
golangci-lint
delve
pprof
graphviz
cobra-cli
enumer
perSystem.gomod2nix.default
])
++ # include formatters for development and testing
(import ../packages/treefmt/formatters.nix pkgs);
}
Modules
You can define modules for NixOS, nix-darwin and home-manager by placing the module definition in the correct subdirectory:
modules/nixos
modules/darwin
modules/home
As with packages, you can define the module in modules/nixos/foo.nix
or
modules/nixos/foo/default.nix
.
One cool feature, which I didn’t know about until the time of writing, is this:
{
flake,
inputs,
perSystem,
}: { pkgs, config, ... }: {
config.services.foo = {
enable = config.services.bar.enable;
package = perSystem.self.foo;
};
}
Before exposing this module in the flake outputs, blueprint will execute the outer function, passing the flake and so on in which the module is defined. This allows you to conveniently refer to packages and inputs defined within your flake.
DISCLAIMER: I’ve not had a chance to use with this new pattern yet
UPDATE: about 5 mins after posting, it was pointed out to me that this feature is a bit broken and likely to be removed in the near future 🤷 You can follow the discussion here.
Host configs
Host configurations can be placed in hosts/<name>
, with NixOS configs using a hosts/foo/configuration.nix
file and nix-darwin
configs being placed in hosts/foo/darwin-configuration.nix
.
As with everything else, blueprint will take care of providing inputs
, flake
and perSystem
as module args:
{
inputs,
flake,
perSystem
}: {
imports = [ inputs.disko.nixosModules.disko ];
config.nixpkgs.hostPlatform = "x86_64-linux";
config.networking.hostName = "foo";
}
Flake checks
What blueprint does about checks is one of the main reasons I like using it so much.
Without any extra configuration, blueprint will do the following:
- add a check for each devshell e.g.
checks.<system>.devshell-<pname>
- add a check for each package e.g.
checks.<system>.pkgs-<pname>
- in addition to package checks, it will also add all the packages define in a package’s
passthru.tests
e.g.checks.<system>.pkgs-<pname>-<test_name>
- for each host configuration, it will add the system closure as a check e.g.
checks.<system>.nixos-<hostname>
This means you can easily validate all your devshells, packages and so on with a simple nix flake check
and no
manual plumbing!
And if you want some additional checks, you can always put them in checks/
with the same approach as
defining a package.
Summary
In this short post, I’ve covered a lot of what blueprint can do, focusing on how I’ve been mainly making use of it. If you want some examples, you can look at treefmt and data-mesher, or check out the docs.
I feel it strikes a nice balance between flexibility and simplicity, and lets me just get shit done.
Those who know me, know that I have often described how much I enjoy working with Go because it’s like “coding with crayons”, letting me focus on the task at hand rather than fancy language constructs.
Well, on a similar note, when compared with solutions like flake-parts, you could say blueprint is “flaking with crayons” 😉.