Adding a Nixago Block
As with any cell block, we'll first add it to our flake.nix
:
{
inputs.std.url = "github:divnix/std";
inputs.nixpkgs.url = "nixpkgs";
inputs.rust-overlay.url = "github:oxalica/rust-overlay";
outputs = { std, ... } @ inputs:
std.growOn
{
inherit inputs;
cellsFrom = ./nix;
cellBlocks = [
(std.blockTypes.runnables "apps")
(std.blockTypes.devshells "devshells")
(std.blockTypes.functions "toolchain")
# The `nixago` type is used for holding Nixago configurations. We name
# it configs to remove some ambuiguity.
(std.blockTypes.nixago "configs")
];
}
{
packages = std.harvest inputs.self [ [ "example" "apps" ] ];
devShells = std.harvest inputs.self [ "example" "devshells" ];
};
}
This is a fairly trivial addition, the only thing worth noting is that we call
it configs
to reduce ambiguity because most people are not aware of what
Nixago does.
Defining the Block
The cell block is where the meat of our configuration lies. We'll add the
following to /cells/example/configs.nix
:
# This cell block holds our Nixago expressions for generating configuration
# files for the various tools we want to configure in our repository. We title
# it `configs.nix` because Nixago is less well-known and this name points to the
# purpose of the cell block.
#
# For an introduction to Nixago, see here:
# https://nix-community.github.io/nixago/
{ inputs
, cell
}:
let
inherit (inputs) nixpkgs std;
l = nixpkgs.lib // builtins;
in
# The structure is an attribute set where the value is an attribute set that
# is ultimately passed to the `make`[1] function from Nixago. The available
# arguments for the `make` function can be seen here[2].
#
# `std` allows us to pass additional pass-through arguments that can influence
# the behavior of our development shells. This is primarily used so we can
# include the necessary packages for the tools we want to configure into the
# development environment.
#
# Additionally, `std` automatically includes any shell hooks generated by Nixago
# into the appropriate `devshell` option. This is ultimately what allows Nixago
# to generate the configurations when we enter the development shell.
#
# [1]: https://github.com/nix-community/nixago/blob/master/lib/make.nix
# [2]: https://github.com/nix-community/nixago/blob/master/modules/request.nix
{
# The `std` framework ships with some "pre-configured" services that we can
# import and use here. For a list of all of them, see here[1]. These are setup
# such that we can use a functor to dynamically extend them with additional
# attributes or overrides. This is why they appear to look like functions.
#
# In most cases, when using these pre-configured services, we only need to be
# concerned with setting the `configData` attribute. This is what ultimately
# ends up in the generated configuration file and is dependent on what tool
# is being configured.
#
# Conform[2] is a tool that allows us to enforce policies on our commit
# messages. We configure it here to only allow commits that follow the
# Conventional Commits specification[3].
#
# [1]: https://github.com/divnix/std/tree/main/cells/std/nixago
# [2]: https://github.com/siderolabs/conform
# [3]: https://www.conventionalcommits.org/en/v1.0.0/
conform = std.std.nixago.conform {
# The configuration of Conform is a bit different than the expected file
# format. This is to prevent excessive nested attribute sets. In this case,
# we only need to specify either a `commit` or `license` parent attribute
# and then the child contents match what is specified in the Conform README.
configData = {
commit = {
header = { length = 89; };
conventional = {
# Only allow these types of conventional commits (inspired by Angular)
types = [
"build"
"chore"
"ci"
"docs"
"feat"
"fix"
"perf"
"refactor"
"style"
"test"
];
};
};
};
};
# Lefthook is a pre-commit hook manager.
lefthook = std.std.nixago.lefthook {
configData = {
commit-msg = {
commands = {
# Runs conform on commit-msg hook to ensure commit messages are
# compliant.
conform = {
run = "${nixpkgs.conform}/bin/conform enforce --commit-msg-file {1}";
};
};
};
pre-commit = {
commands = {
# Runs treefmt on pre-commit hook to ensure checked-in source code is
# properly formatted.
treefmt = {
run = "${nixpkgs.treefmt}/bin/treefmt {staged_files}";
};
};
};
};
};
# Prettier is a multi-language code formatter.
prettier = std.lib.dev.mkNixago {
# We mainly use it here to format the Markdown in our README.
configData = {
printWidth = 80;
proseWrap = "always";
};
output = ".prettierrc";
format = "json";
};
# Treefmt is an aggregator for source code formatters. Our codebase has
# markdown, Nix, and Rust, so we configure a formatter for each.
treefmt = std.std.nixago.treefmt {
configData = {
formatter = {
nix = {
command = "nixpkgs-fmt";
includes = [ "*.nix" ];
};
prettier = {
command = "prettier";
options = [ "--write" ];
includes = [ "*.md" ];
};
rustfmt = {
command = "rustfmt";
options = [ "--edition" "2021" ];
includes = [ "*.rs" ];
};
};
};
# This is the pass-through feature where we can pass attributes to devshell.
# In this case, we're asking devshell to include the `nixpkgs-fmt` and
# `prettier` packages in the development environment. The `rustfmt` package
# is already included within the Rust toolchain (see toolchain.nix).
packages = [
nixpkgs.nixpkgs-fmt
nixpkgs.nodePackages.prettier
];
};
}
This is significantly more code, but a majority of it is the actual configuration data we're using to generate our files.
The output structure of the cell block is an attribute set where the value is another attribute set that must conform to two things:
- The module structure enforced by Nixago
- Any additional pass-through data, which in this case means data intended for
devshell
Module Structure
The module structure is fairly easy to grasp and a quick overview can be seen in the Nixago quick start guide. The three main options are:
configData
: The raw configuration data used to generate the output fileoutput
: The name of the output fileformat
: The format of the output file
There are a few additional advanced options, but the above three options are enough to cover a majority of use cases.
The std
framework provides several "pre-configured" expressions that we can
make use of to lessen the verbosity of our cell block. These expressions can be
found here. Attentive readers will notice that it appears we are
"calling" these expressions as if they were functions. This is because they use
functors to allow dynamically merging/overriding the arguments passed to them.
So in many of the examples seen in our code, we're essentially extending the
existing structure given to us by std
and adding our raw configuration data to
it. It's not strictly necessary to do this, we could just define the whole
structure ourselves, but using these shortcuts helps us type a bit less.
Pass-Through
To get our Nixago configurations to generate, we must pass off the shell hooks
to devshell
. We'll tackle this part soon, but the important thing to
understand is that these data structures will eventually pass through to
devshell
. What this means is that we can add additional attributes to our data
structure that will in turn provide additional configuration to devshell
.
If you examine the treefmt
configuration in our example, you'll see that it
also includes the packages
attribute which is not a part of the Nixago module
structure. Nixago will ignore this attribute, but when devshell
sees it, it
will automatically include those packages in our environment. This allows us to
define our dependency as close to our configuration as possible while also
ensuring our configuration works as expected (i.e. treefmt
needs prettier
to
work as expected).
The Tools
The remainder of the code is responsible for configuring the actual tools we'll be using. Each of these will be briefly discussed below.
Conform
The conform tool allows us to specify policies that will be enforced against our commits. This is an invaluable tool in open-source projects and can help bring uniformity to commit messages and improve the generation of change logs.
In our case, we're specifying that commit message headers can be no longer than 89 characters and that the message itself must conform (ha!) to the conventional commit specification. Additionally, the type section of the conventional commit header must be one of the items given in the list (the list itself is inspired by the Angular project).
Lefthook
The lefthook tool automatically manages git hooks for us. These are often referred to as pre-commit hooks and have been a best practice in many projects for reducing the feedback cycle when developing against a project.
In our case, we use lefthook
to enforce our commit messages using the policy
specified with conform
. Additionally, we automatically call treefmt
on the
files being checked in to ensure that all of our revision-controlled source code
is properly formatted.
Prettier
The prettier tool is a general-purpose code formatter that supports several languages.
In our case, we're simply using it to format our markdown files. The primary benefit is that it can help enforce the 80-character line limit being imposed across all of our code.
Treefmt
The treefmt tool acts as an aggregator for code formatters. Instead of having
to call each formatter individually, we instruct treefmt
which formatters we
want to run on which types of files and it will handle the rest for us.
In our case, we're adding formatters for the three primary languages that exist in our repository: Rust, Nix, and Markdown.
Devshell Integration
As mentioned earlier, the last thing we need to do is to inform our devshell
configuration about our newly added Nixago configurations. We'll add the
following to our /nix/example/devshells.nix
file:
{ inputs
, cell
}:
let
inherit (inputs) nixpkgs std;
l = nixpkgs.lib // builtins;
in
l.mapAttrs (_: std.std.lib.mkShell) {
default = { ... }: {
# ...
# Nixago uses shell hooks for generating configuration files. In order for
# that to work, devshell must add them to its own configuration. To ensure
# this happens, we specify the configurations we would like generated using
# the `nixago` attribute.
nixago = [
cell.configs.conform
cell.configs.lefthook
cell.configs.prettier
cell.configs.treefmt
];
# ...
};
}
With this complete, we can now reload our development shell and watch Nixago generate all of our configuration files for us:
$ direnv reload
# ...
nixago: updating repositoriy files
nixago: '.conform.yaml' link updated
nixago: '.conform.yaml' added to .gitignore
nixago: 'lefthook.yaml' link updated
nixago: 'lefthook.yaml' added to .gitignore
nixago: '.prettierrc' link updated
nixago: '.prettierrc' added to .gitignore
nixago: 'treefmt.toml' link updated
nixago: 'treefmt.toml' added to .gitignore