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 of std.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.