The nix configuration language is very powerful and contrary to other opinions I find it quite easy to read, once you get the hang of it. Since you can interpolate variables in strings, it can easily be used as a rudimentary templating system on its own. However this has the drawback that your configuration will contain the whole "template", which can get get very long and hard to read.

The problem

Imagine you want to use home-manager's xdg.configFile option to write a file containing some variables. This example is taken from my Neovim configuration. I use the same color definitions in multiple places around the system and want to write a lua file which I can then base the editors colorscheme on. The colors are defined in ./vars.nix, a simple file containing only an attribute set with the hex codes


{
  colors = {
    Black = "24283B";
    DarkGrey = "313547";
    Grey = "393f59";
    BrightGrey = "4b4f5e";
    # more colors...
  };
}

The simple approach is to just interpolate the values. This is okay for short snippets, but get's annoying soon. For the file above the code would look something like this

{
  vars = import ../vars.nix;

  xdg = {
    enable = true;
    configFile = {
      nvim_lua_nixcolors = {
        target = "nvim/lua/nixcolors.lua";
        text = ''
          local M =  {}
          M.Black         = "#${vars.colors.Black}"
          M.DarkGrey      = "#${vars.colors.DarkGrey}"
          M.Grey          = "#${vars.colors.Grey}"

          -- More colors ... --

          M.Magenta       = "#${vars.colors.Magenta}"
          M.BrightMagenta = "#${vars.colors.BrightMagenta}"
          return M
        '';
      };
    };
  };
}

We can't keep doing that, ansible users will make fun of us. It would be better to use a real templating features and have the data, templates and the configuration in separate files. You don't need a templating framework for that, a few lines of simple code will do, here is how.

The solution

To further specify the

Mustache is one of the most used templating languages, let's use that one. The idea is simple:

  1. Write our templates in the mustache format
  2. Convert the nix attribute set containing the input data to JSON
  3. Render the template where we want it declaratively

Conversion is easy. Nix already has builtin function for it, builtins.toJSON. It accepts an attribute set and returns a string containing JSON. Perfect.

To avoid escaping issues, we will pass the data and template as file paths function instead of strings. stddenv.mkDerivation has some less known extended attributes, which allow passing in temporary files to the derivation.

passAsFile = [ "jsonData" ];
jsonData = builtins.toJSON data;

This sets an environment variable called $jsonDataPath inside the derivation, pointing to the temporary file. The path to the mustache template will be passed directly as another function argument.

To do the actual rendering, any mustache parser can be used. I went for mustache-go because it is already in packaged in nixpkgs and has minimal dependencies. It's command line interface is simple:

mustache data.json template.mustache > rendered_file

And that's already most of the code. Our derivation will have call that in the buildPhase and use the installPhase to copy the file to the store, returning the path to it.

{
  buildPhase = ''
    ${pkgs.mustache-go}/bin/mustache $jsonDataPath ${template} > rendered_file
  '';
  
  installPhase = ''
    cp rendered_file $out
  '';
}

You just build your own templating engine using nix! For completeness, here is the full code of the function


{ 
  vars = import ../vars.nix;

  templateFile = name: template: data:
    pkgs.stdenv.mkDerivation {

      name = "${name}";

      nativeBuildInpts = [ pkgs.mustache-go ];

      # Pass Json as file to avoid escaping
      passAsFile = [ "jsonData" ];
      jsonData = builtins.toJSON data;

      # Disable phases which are not needed. In particular the unpackPhase will
      # fail, if no src attribute is set
      phases = [ "buildPhase" "installPhase" ];

      buildPhase = ''
        ${pkgs.mustache-go}/bin/mustache $jsonDataPath ${template} > rendered_file
      '';

      installPhase = ''
        cp rendered_file $out
      '';
    };
}

Now to use it, just rewrite the code of the beginning to use it, passing name, the path to the template and an attribute set containing the data as parameters.

{
  # ...
  xdg = {
    enable = true;
    configFile = {
      nixcolors-lua = {
        target = "nvim/lua/nixcolors.lua";
        source =
          templateFile "nixcolors.lua" ./nixcolors.lua.mustache vars.colors;
      };
    };
  };
}

After rebuilding your configuration, a symlink will be placed in ~/.config/nvim/lua/nixcolors.lua containing the rendered file.