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.
{ 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.