Three guys at a bar drinking pints dressed offensively 'Irish'

As it’s a national holiday I’m going to keep this short, seeing as how I’m somewhat obligated to fulfill some outdated but not entirely inaccurate stereotypes.

Documentation

This past week or so I’ve been beefing up the docs for Ethereum.nix. And as you might expect I reached a point at which I wanted to generate some documentation for our NixOS modules.

That’s when I came across nixosOptionsDoc.

This nifty little lib function traverses all those options declarations in your modules, extracts the names and descriptions and so on that we all studiously ensure are well written and up to date, and spits them out in a variety of different formats.

For my use case I wanted them in markdown so I could import them into an MkDocs based website. And in doing so I learned an important lesson about how to structure my modules going forward.

Separate your options

In order to generate documentation for our modules we must first evaluate them. We do that with lib.evalModules:

{ lib, ...}: let
    eval = lib.evalModules {
        modules = [
            ./module-a.nix
            ./module-b.nix
            ./module-c.nix
        ];
    };
in {
    # ...
}

If, like me, you have been writing your modules with a mix of options and config sections in the same file, you will hit the same problem I did: you must include any modules your config refers to.

This means including many of the standard NixOS modules for things such as networking and so on. Which in turn means the generated documentation you get will also include those modules.

This is a lot of extra fluff we are not interested in. We only want documentation for our modules. So what’s the solution?

Separate your options into a separate file.

Once we have our options separated out, we can safely evaluate them and in turn generate their docs.

{ lib, runCommand, nixosOptionsDoc, ...}: let
    # evaluate our options
    eval = lib.evalModules {
        modules = [
            ./options-a.nix
            ./options-b.nix
            ./options-c.nix
        ];
    };
    # generate our docs
    optionsDoc = nixosOptionsDoc {
        inherit (eval) options;
    };
in
    # create a derivation for capturing the markdown output
    runCommand "options-doc.md" {} ''
        cat ${optionsDoc.optionsCommonMark} >> $out
    ''

With our options doc in markdown format we can symlink this into an MkDocs site structure and job done!

{ lib, pkgs, ...}: let
    inherit (pkgs) stdenv mkdocs python310Packages;
    options-doc = pkgs.callPackage ./options-doc.nix {};
in stdenv.mkDerivation {
    src = ./.;
    name = "docs";

    # depend on our options doc derivation
    buildInput = [options-doc];

    # mkdocs dependencies
    nativeBuildInputs = [
      mkdocs
      python310Packages.mkdocs-material
      python310Packages.pygments
    ];

    # symlink our generated docs into the correct folder before generating
    buildPhase = ''
      ln -s ${options-doc} "./docs/nixos-options.md"
      # generate the site
      mkdocs build
    '';

    # copy the resulting output to the derivation's $out directory
    installPhase = ''
      mv site $out
    '';
}

Get creative

You aren’t necessarily restricted to putting all of your options documentation in one markdown file (or even markdown at all).

If you look here in Ethereum.nix you can see I’m being a bit fancier by traversing the file system looking for options.nix files and then generating a separate markdown file for each.

This lets me have a separate section for each module.

Summary

I’ve shown how easy it is to generate documentation from your NixOS modules.

I’ve also highlighted how you need to be careful about separating out your options declarations if you want to reduce the scope of the documentation being generated.

And on that note, I have somewhere else to be.

Sláinte 🍻.

— Edit: 2023-03-17 20:00 —

As was pointed out over on discourse, if I had spent a bit more time reviewing the arguments for lib.evalModules I would not have needed to separate the options out, as you can instead pass it check = false.

Alternatively, you can ensure one of the included modules includes { _module.check = false; }.

On reflection I have to admit I kinda like having the separation, and I have seen other projects doing something similar. But don’t go splitting out your options just to satisfy the constraints of generating your docs.