Hello Moon
A slightly more complete hello world tutorial.
This tutorial implements a very typical local
Cell and its Cell Blocks for a somewhat bigger project.
It also makes use of more advanced functions of std
.
Namely:
std.growOn
instead ofstd.grow
std.harvest
to provide compatibility layers of "soil"- non-default Cell Block definitions
- the input debug facility
The terms "Block Type", "Cell", "Cell Block", "Target" and "Action" have special meaning within the context of std
.
With these clear definitions, we navigate and communicate the code structure much more easily.
In order to familiarize yourself with them, please have a quick glance at the glossary.
File Layout
Let's start again with a flake:
./flake.nix
{
inputs.std.url = "github:divnix/std";
inputs.nixpkgs.url = "nixpkgs";
outputs = {std, ...} @ inputs:
/*
brings std attributes into scope
namely used here: `growOn`, `harvest` & `blockTypes`
*/
with std;
/*
grows a flake "from cells" on "soil"; see below...
*/
growOn {
/*
we always inherit inputs and expose a deSystemized version
via {inputs, cell} during import of Cell Blocks.
*/
inherit inputs;
/*
from where to "grow" cells?
*/
cellsFrom = ./nix;
/*
custom Cell Blocks (i.e. "typed outputs")
*/
cellBlocks = [
(blockTypes.devshells "shells")
(blockTypes.nixago "nixago")
];
/*
This debug facility helps you to explore what attributes are available
for a given input until you get more familiar with `std`.
*/
debug = ["inputs" "std"];
}
/*
Soil is an idiom to refer to compatibility layers that are recursively
merged onto the outputs of the `std.grow` function.
*/
# Soil ...
# 1) layer for compat with the nix CLI
{
devShells = harvest inputs.self ["local" "shells"];
}
# 2) there can be various layers; `growOn` is a variadic function
{};
}
This time we specified cellsFrom = ./nix;
.
This is gentle so that our colleagues know immediately which files to either look or never look at depending on where they stand.
We also used std.growOn
instead of std.grow
so that we can add compatibility layers of "soil".
Furthermore, we only defined two Cell Blocks: nixago
& devshells
. More on them follows...
./nix/local/*
Next, we define a local
cell.
Each project will have some amount of automation.
This can be repository automation, such as code generation.
Or it can be a CI/CD specification.
In here, we wire up two tools from the Nix ecosystem: numtide/devshell
& nix-community/nixago
.
Please refer to these links to get yourself a quick overview before continuing this tutorial, in case you don't know them, yet.
A very short refresher:
- Nixago: Template & render repository (dot-)files with nix. Why nix?
- Devshell: Friendly & reproducible development shells — the original ™.
Some semantic background:
Both, Nixago & Devshell are Component Tools.
(Vertical) Component Tools are distinct from (Horizontal) Integration Tools — such as
std
— in that they provide a specific capability in a minimal linux style: "Do one thing and do it well."Integration Tools however combine them into a polished user story and experience.
The Nix ecosystem is very rich in component tools, however only few integration tools exist at the time of writing.
./nix/local/shells.nix
Let's start with the cell.devshells
Cell Block and work our way backwards to the cell.nixago
Cell Block below.
More semantic background:
I could also reference them as
inputs.cells.local.devshells
&inputs.cells.local.nixago
.But, because we are sticking with the local Cell context, we don't want to confuse the future code reader. Instead, we gently hint at the locality by just referring them via the
cell
context.
{
inputs,
cell,
}: let
/*
I usually just find it very handy to alias all things library onto `l`...
The distinction between `builtins` and `nixpkgs.lib` has little practical
relevance, in most scenarios.
*/
l = nixpkgs.lib // builtins;
/*
It is good practice to in-scope:
- inputs by *name*
- other Cells by their *Cell names*
- the local Cell Blocks by their *Block names*.
However, for `std`, we make an exeption and in-scope, despite being an
input, its primary Cell with the same name as well as the dev lib.
*/
inherit (inputs) nixpkgs;
inherit (inputs.std) std lib;
inherit (cell) nixago;
in
# we use Standard's mkShell wrapper for its Nixago integration
l.mapAttrs (_: lib.dev.mkShell) {
default = {...}: {
name = "My Devshell";
# This `nixago` option is a courtesy of the `std` horizontal
# integration between Devshell and Nixago
nixago = [
# off-the-shelve from `std`
(lib.cfg.conform {data = {inherit (inputs) cells;};})
lib.cfg.lefthook
lib.cfg.adrgen
# modified from the local Cell
nixago.treefmt
nixago.editorconfig
nixago.mdbook
];
# Devshell handily represents `commands` as part of
# its Message Of The Day (MOTD) or the built-in `menu` command.
commands = [
{
package = nixpkgs.reuse;
category = "legal";
/*
For display, reuse already has both a `pname` & `meta.description`.
Hence, we don't need to inline these - they are autodetected:
name = "reuse";
description = "Reuse is a tool to manage a project's LICENCES";
*/
}
];
# Always import the `std` default devshellProfile to also install
# the `std` CLI/TUI into your Devshell.
imports = [std.devshellProfiles.default];
};
}
The nixago = [];
option in this definition is a special integration provided by the Standard's devshell
-wrapper (std.lib.mkShell
).
This is how std
delivers on its promise of being a (horizontal) integration tool that wraps (vertical) component tools into a polished user story and experience.
Because we made use of std.harvest
in the flake, you now can actually test out the devshell via the Nix CLI compat layer by just running nix develop -c "$SHELL"
in the directory of the flake.
For a more elegant method of entering a development shell read on the direnv section below.
./nix/local/nixago.nix
As we have seen above, the nixago
option in the cell.devshells
Cell Block references Targets from both lib.cfg
.
While you can explore lib.cfg
here, let's now have a closer look at cell.nixago
:
{
inputs,
cell,
}: let
inherit (inputs) nixpkgs;
inherit (inputs.std) lib;
/*
While these are strictly specializations of the available
Nixago Pebbles at `lib.cfg.*`, it would be entirely
possible to define a completely new pebble inline
*/
in {
/*
treefmt: https://github.com/numtide/treefmt
*/
treefmt = lib.cfg.treefmt {
# we use the data attribute to modify the
# target data structure via a simple data overlay
# (`divnix/data-merge` / `std.dmerge`) mechanism.
data.formatter.go = {
command = "gofmt";
options = ["-w"];
includes = ["*.go"];
};
# for the `std.lib.dev.mkShell` integration with nixago,
# we also hint which packages should be made available
# in the environment for this "Nixago Pebble"
packages = [nixpkgs.go];
};
/*
editorconfig: https://editorconfig.org/
*/
editorconfig = lib.cfg.editorconfig {
data = {
# the actual target data structure depends on the
# Nixago Pebble, and ultimately, on the tool to configure
"*.xcf" = {
charset = "unset";
end_of_line = "unset";
insert_final_newline = "unset";
trim_trailing_whitespace = "unset";
indent_style = "unset";
indent_size = "unset";
};
"{*.go,go.mod}" = {
indent_style = "tab";
indent_size = 4;
};
};
};
/*
mdbook: https://rust-lang.github.io/mdBook
*/
mdbook = lib.cfg.mdbook {
data = {
book.title = "The Standard Book";
};
};
}
In this Cell Block, we have been modifying some built-in convenience lib.cfg.*
pebbles.
The way data
is merged upon the existing pebble is via a simple left-hand-side/right-hand-side data-merge
(std.dmerge
).
Background on array merge strategies:
If you know how a plain data-merge (does not magically) deal with array merge semantics, you noticed: We didn't have to annotate our right-hand-side arrays in this example because we where not actually amending or modifying any left-hand-side array type data structure.
Would we have done so, we would have had to annotate:
- either with
std.dmerge.append [/* ... */]
;- or with
std.dmerge.update [ idx ] [/* ... */]
.But lucky us (this time)!
Command Line Synthesis
With this configuration in place, you have a couple of options on the command line.
Note, that you can access any std
cli invocation also via the std
TUI by just typing std
.
Just in case you forgot exactly how to access one of these repository capabilities.
Debug Facility:
Since the debug facility is enabled, you will see some trace output while running these commands. To switch this off, just comment the
debug = [ /* ... */ ];
attribute in the flake.It looks something like this:
trace: inputs on x86_64-linux trace: { cells = {…}; nixpkgs = {…}; self = {…}; std = {…}; }
Invoke devshell via nix
nix develop -c "$SHELL"
By quirks of the Nix CLI, if you don't specify -c "$SHELL"
, you'll be thrown into an unfamiliar bare bash
interactive shell.
That's not what you want.
Invoke the devshell via std
In this case, invoking $SHELL
correctly is taken care for you by the Block Type's enter
Action.
# fetch `std`
$ nix shell github:divnix/std
$ std //local/devshells/default:enter
Since we have declared the devshell Cell Block as a blockTypes.devshells
, std
augments it's Targets with the Block Type Actions.
See blockTypes.devshells
for more details on the available Actions and their implementation.
Thanks to the cell.devshells
' nixago
option, entering the devshell will also automatically reconcile the repository files under Nixago's management.
Explore a Nixago Pebble via std
You can also explore the nixago configuration via the Nixago Block Type's explore
-Action.
# fetch `std`
$ nix shell github:divnix/std
$ std //local/nixago/treefmt:explore
See blockTypes.nixago
for more details on the available Actions and their implementation.
direnv
Manually entering the devshell is boring.
How about a daemon always does that automatically & efficiently when you cd
into a project directory?
Enter direnv
— the original (again; and even from the same author) 😊.
Before you continue, first install direnv according to it's install instructions. It's super simple & super useful ™ and you should do it right now if you haven't yet.
Please learn how to enable direnv
in this project by following the direnv how-to.
In this case, you would adapt the relevant line to: use std nix //local/shells:default
.
Now, you can simply cd
into that directory, and the devshells is being loaded.
The MOTD will be shown, too.
The first time, you need to teach the direnv
daemon to trust the .envrc
file via direnv allow
.
If you want to reload the devshell (e.g. to reconcile Nixago Pebbles), you can just run direnv reload
.
Because I use these commands so often, I've set: alias d="direnv"
in my shell's RC file.