I use Nix for lots of things. I use it to configure my Linux machines (all running NixOS). I use it to configure my work laptop using NixDarwin. I use Home Manager to handle all my configurations and dotfiles.

I’ve talked before about why I like it, and how I use it to remember things I’ve done because the configurations can be commented and work as documentation for why I did something.

One problem I’ve consistently run into though is remembering what tools I’ve installed, or what they’re for. An example of this was the fend calculator. I came across it one day, added it to my installed packages, and then promptly forgot it. The name doesn’t really help, and I had forgotten to leave a comment in the package list telling me what it was for.

So then it hit me. What if I can dynamically generate a man page for my installed tools, using the NixOS or Home Manager package lists? Then I’d just have to remember that it exists and then I’d be able to look things up easily.

Writing a man page derivation

I started by writing a derivation for making the man page. I knew I wanted to take some input list of packages and then generate a Markdown file that I could give to Pandoc to turn into a man page. *This could be done using the man page native format, but I want to remember the syntax and I rarely write man pages.

hm-pkgs-manpage.nix
{ lib, pkgs, pandoc, packageList ? [ ] }:
let
  inherit (builtins) baseNameOf;
  inherit (lib.lists) sort;
  inherit (lib) concatMapStrings getExe;
 
  # Sort the packages by name
  sortedPkgs =
    sort (a: b: (a.name or a.pname) < (b.name or b.pname)) packageList;
 
  # Helper function to map a package to a Markdown definition
  # consisting of the package executable (if it exists) and the
  # package's description (from `meta.description`).
  pkgItem = pkg:
    let
      binary = pkg.meta.mainProgram or "";
      pkgName = pkg.name or pkg.pname;
      name = if binary != "" then
        baseNameOf binary
      else
        "${pkgName} (_missing meta.mainProgram_)";
      desc = pkg.meta.description or "No description";
    in ''
      **${name}**
 
      :  ${desc}
 
    '';
in pkgs.stdenvNoCC.mkDerivation {
  pname = "hm-pkgs-manpage";
  version = "1.0";
  src = null;
  # Need this to avoid trying to unpack a non-existent source
  dontUnpack = true;
 
  buildPhase = ''
    mkdir -p man
    cat > man/hm-pkgs.md <<- 'EOF'
    % hm-pkgs(1) Home Manager Packages | User Commands
    % Jeremy Shoemaker
    # NAME
    hm-pkgs - list of installed Home Manager packages
 
    # DESCRIPTION
    This man page is generated dynamically from my Home Manager configuration.
 
    It lists installed packages and their descriptions to help me remember what tools I've installed and what they do.
 
    # PACKAGES
    ${concatMapStrings pkgItem sortedPkgs}
 
    # SEE ALSO
    sys-pkgs(1)
    EOF
 
    ${getExe pandoc} -s -t man man/hm-pkgs.md -o man/hm-pkgs.1
  '';
 
  installPhase = ''
    mkdir -p $out/share/man/man1
    gzip -c man/hm-pkgs.1 > $out/share/man/man1/hm-pkgs.1.gz
  '';
 
  meta = with lib; {
    description = "Dynamically generated man page for installed tools";
    license = licenses.mit;
    platform = platforms.unix;
  };
}

Then, to use it, I add a Home Manager module that adds it to the installed packages while giving it the package list.

{ config, pkgs, ... }:
 
{
  home.packages = [
    (pkgs.callPackage ./hm-pkgs-manpage.nix { packageList = config.home.packages; })
  ];
}

And that’s it!

After applying the configuration, running man hm-pkgs results in something like the following:

hm-pkgs(1)                                      User Commands                                      hm-pkgs(1)
 
NAME
       hm-pkgs - list of installed Home Manager packages
 
DESCRIPTION
       This man page is generated dynamically from my Home Manager configuration.
 
       It  lists  installed packages and their descriptions to help me remember what tools I’ve installed and
       what they do.
 
PACKAGES
       aspell-env (missing meta.mainProgram)
              No description
 
       atuin  Replacement for a shell history which records additional commands  context  with  optional  en‐
              crypted synchronization between machines
 
       aws    Unified tool to manage your AWS services
 
       bat    Cat(1) clone with syntax highlighting and Git integration
 
       btop   Monitor of resources
 
       choose Human-friendly and fast alternative to cut and (sometimes) awk
 
       comma  Runs programs without installing them
 
       coreutils-9.7 (missing meta.mainProgram)
              GNU Core Utilities
...

No recursion errors?!

I was surprised when I wrote this that it just worked, because I expected to run into recursion errors because the package is using the package list which it is itself a member of. But then I thought about it and realized that it’s not a problem here because we aren’t taking something from config and putting it into a place where it might end up back in the config in the same place.

Anyone who has done a lot of NixOS module work has run into this issue, so I’m glad it didn’t show up here.

What about system packages?

This technique would also work for system-level packages, since this example is just listing packages installed by Home Manager. I wrote a similar one (the sys-pkgs mentioned in the SEE ALSO section of the man page generated above) for my system-level configs, and though I haven’t tried it on NixOS yet *I’m worried it will be huge from all the packages., it works fine on NixDarwin.