Last updated 2021-12-30, see Changelog
This article shows how to use Nix Flakes to build LaTeX documents. It is not particularly beginner-friendly to keep it at manageable size.
If you don’t know much about Nix and are in a hurry, I recommend this article for a quick overview of the language and Flakes. A proper way to learn about Nix is the Nix Pills series, and this series about Nix Flakes.
The proper way to learn LaTeX is to take any vaguely math-related academic course and be peer-pressured into trying it out until it works. Jokes aside, this probably isn’t an interesting read for people who are not already familiar with LaTeX; while I will explain the things I’m doing with Nix, the LaTeX code I will just throw at you assuming you can read it.
Metered connection users: Be aware that the instructions in this article download quite a bit of data from the internet.
In an empty directory, let’s start a
document.tex file like this:
Now we want to tell Nix how to build this document.
To do this, create a file
flake.nix with the following content:
Our inputs are
nixpkgs, the main Nix package repository, from which we primarily need TeX Live, and
flake-utils, a library that provides some convenience functions.
For defining our outputs, we use
flake-utils) to define an output package for each system in
allSystems – we do want users on any system to be able to compile our document.
The important bit
pkgs.texlive.combine builds a TeX Live installation containing the TeX Live packages we specify.
For building our minimal document, we start with
scheme-minimal and include
latex-bin (to have
latexmk (our helper script to build the document).
Then, we define our output package document.
Since building LaTeX document requires no C compiler, we use
We need the phases unpack (to access our source code), build (to typeset the document) and install (to copy the PDF into
latexmk is a script that continuously calls our LaTeX processor (in this case,
lualatex) until the document reaches a fixpoint.
lualatex needs writable cache directories, which we create and communicate via environment variables.
-interaction=nonstopmode will cause
lualatex to not stop and ask for user input in case an error is encountered, as it would by default.
By the way, we use
lualatex instead of
pdflatex simply because it is the more modern alternative, supporting UTF-8, TTF/OTF Fonts, etc.
In the install phase, we create the
$out directory and copy the created document into it.
We could instead make our document itself be
$out (because it is the only output file of our derivation), but having a PDF file without
Now let’s pin our input flakes to their current versions by doing
For those not familiar with Flakes, this will create a file
flake.lock (feel free to explore its contents).
From now on, we are working on specific versions of our inputs, which are described in
One last thing before we can build our document:
self will only contain those files of our source that are checked into version control.
So we’ll do
When that’s done, we can build our document with
This will take some time, but will eventually create a directory
result which contains
result is a symlink which can be inspected via
And it points to our
As shown by this minimal example, our
flake.nix is not just a build system, but also manages all dependencies that are required to build our document.
Producing Identical Documents added 2021-11-30
To be truly reproducible, the PDF file we create must always be exactly the same. This is currently not the case for two reasons:
- LaTeX likes to put the generation date into the document.
This obviously happens when you query it e.g. with
\today, but the date also gets filled into the PDF’s Creation date attribute.
- The resulting PDF has a seemingly random ID value added into the PDF’s xref table.
The fix to the first problem depends a bit on whether you’re using
\today, and if so, what for.
For example, when rendering a letter, you do want the date on the letter to be the one from when you generated it (or more precisely, when you sent it, but we cannot do anything about it after the PDF has been created).
The tool we need to solve the problem is the environment variable
If we set it to a Unix timestamp, LaTeX will use that instead of the current date.
We thus modify the call to
latexmk like this:
self.lastModified is set to the Unix timestamp of the last commit in our repository.
This seems to be a reasonable date to set, but in the case of a letter, I would actually advise to explicitly set the date, e.g.
This way, you will always know when you sent the letter.
I used the
date utility so that the date is readable.
You can of course put it into a nix variable in the Flake and interpolate it into the command if you want.
Now that we have fixed the date, we still have the ID. That ID is actually calculated from the system date and time, and the full path of the generated PDF file, and thus we won’t be able to modify it to our needs from the outside. There are however TeX commands we can use:
XeTeX is the only backend that seems not to be able to omit the ID, so the command is setting it to some literal value. Since we’re using LuaLaTeX, we want the LuaTeX solution. And since this is irrelevant to the document’s content, let’s prepend it to the input via latexmk:
With this, we have a truly reproducible PDF output. Now, let’s explore what happens when we use packages in our LaTeX document.
TeX Live Packages
Let’s say we want to have a nice tabular in our
However, our current TeX Live configuration does not provide
The packages we provide in
pkgs.texlive.combine are defined by tlmgr, TeX Live’s package manager.
Usually, the name we give in
\usepackage is the name of the package we need to include, so let’s test that:
Don’t forget to commit all changes to git before building (we’ll just amend our initial commit):
nicematrix is indeed the correct package here, we’ll run into an error.
It turns out,
nicematrix requires some additional packages to work and we didn’t include them.
Can you figure out which ones? I’ll wait.
If you actually tried to tackle this problem, you have probably read the log, which tells you some
.sty files are missing, and then tried to include their names in
That only brings you so far, because some
.sty files are included in packages that carry a different name.
The following is the complete list of packages we need:
For reference, all existing packages are listed in this file, however this list is hardly helpful without meta information about the packages.
In a usual TeX Live installation, you could use
tlmgr search --file <missing> to find out which package contains a file, but nixpkgs does not provide this utility.
For all I know, that information is not easily queryable on the internet either.
Of course, we only need to run around and collect all these packages because we started with the minimal scheme. Switching to the basic scheme will provide almost all packages we need:
This shows that we basically choose how much work we want to put into listing our TeX dependencies.
If we start with a larger scheme, it is less work but we will download more packages than necessary.
The laziest way would of course be to just include
Nix’ philosophy is instead to only list the dependencies we actually need.
I would say that starting with
scheme-basic is generally fine.
Don’t forget to check out the new document we can now create with
System Fonts rewritten 2021-12-28
While TeX Live does provide us with a lot of fonts to choose from, we might eventually want to use a font no available there.
Assume we want to use the Fira Code.
This font is packaged in
Let’s have a quick look at what is contained in that package:
(Output may be nicer with
exa -T if you have it).
This gives us (store path stripped):
Now we need to set the
OSFONTDIR environment variable so that LuaTeX can find it (mind that having the font package as build input does not make the font visible to LuaTeX).
We also need to add
fontspec to our
We can now reference the font in our document.
However, we might not be completely sure about the name we need to use to refer to the font – is it
Font files tend to be a bit inconsistent about this.
So let us check it:
This will give us some lines like
Fira Code Light, is the correct one (I am not quite sure why, but the former won’t work).
Thus, we update our
Save and run
The second column in the document will now use the Fira Code font.
Local Font Files
You may want to use fonts that are neither available as TeX Live package, nor in nixpkgs.
Maybe you want to use a fancy commercial font.
While it is no problem to append the working directory or a
fonts subdirectory to
OSFONTDIR, you can also define a separate derivation for that font:
Then, you can use the font just like a font from nixpkgs.
Actually, you want to have that font package in a separate flake, because if you set
src = self; here, this derivation will unnecessarily be rebuilt every time anything in your repository changes.
You can refer to local flakes as inputs to your document flake if you don’t want to publish the font flake.
Finally, if a font is available somewhere on the internet, you can either use
pkgs.fetchurl to retrieve it when building, or declare it as input to your Nix Flake.
Configurable Documents improved 2021-12-28
Having a single document as output is fine for a lot of use-cases. But what if our document has data inputs, for example because we want to generate bulk letters? In this case, our output should not be the document itself, but a script that takes the relevant data as input and generates the document. Let’s try and modify our setup to do that.
The first step towards our goal is to have our package output a script that basically does what our build step currently does: Build the document. For this, we remove the build step from our package and modify the install step:
Mind how our
buildInputs have moved to
This is because these are now runtime dependencies and thus need to be part of the closure of the generated derivation.
That is achieved by putting them in the
I put the script we output into a variable
SCRIPT, which will be available as environment variable during our build.
Originally, I used
cat with a HEREDOC to write the script, however that was horrible since all
$ that should be in the final script would have needed to be escaped.
printenv is far cleaner.
Note how we use
builtins.placeholder to access the output directory since
$out is a build-time variable and therefore not available in our script, which runs at runtime.
builtins.placeholder outputs the correct path at build time.
SOURCE_DATE_EPOCH since when our derivation is a generator, we might want to use the actual generation date.
Since the PDF itself is not part of the derivation anymore, it is okay to generate different documents depending on the date; and the user can still set the variable when calling the generator to inject a custom date.
Our output directory now contains the generated script in
share as we need those files at runtime to build the document.
If you use any other local files (fonts, images, etc) in your document, you need to copy those as well.
Before, our build environment provided a temporary directory to build the document.
Now with our script, we don’t have that anymore – the user may call the script from anywhere and that is our working directory then.
Therefore, we need to create a temporary directory manually via
mktemp -d so that the current working directory is not cluttered with intermediate LaTeX files – the user only wants the resulting
Fun fact: By explicitly depending on
pkgs.coreutils, we circumvent a problem with
mktemp that haunts macOS and BSD users:
mktemp requires a template as parameter, while the one in GNU coreutils does not.
This makes it difficult to write a script that works with both versions, a problem which we nicely circumvent by explicitly using the GNU coreutils everywhere.
Let’s try it out:
This should create the
document.pdf in your working directory.
We can replace the last two commands with
Now that we can do this, let’s make the document fillable with user-provided values.
latexmk provides a nice feature that executes TeX code before the main document.
We will set this up in our flake in a moment, for now let’s assume the commands
\receiver are available and update our
flake.nix, we now update the
latexmk call to define those two commands:
Now we can do:
nix run expects as first argument the Flake to run, so if we provide parameters, we must put
. first to reference the flake in our working directory.
This should give us a
document.pdf containing the two given names.
By the way, if we ever push this repository to GitHub, e.g. at
example/nix-flakes-latex, we can then run it anywhere via
Nix Flakes allow us not just to precisely specify TeX Live packages we need to build our document, but also to include external resources as additional dependencies.
By pinning the versions of our inputs in
flake.lock, it guarantees us that the document can be reproducibly built anywhere.
Now you might wonder, what do we really need all this for? Are LaTeX documents not like „write once, typeset, never touch the source again“? Well, I’ll have you know that I regularly build my pen & paper character sheets with LaTeX, they are fillable with values and do depend on external artwork. The sources for that are available on GitHub if you want to have a look, but be warned that everything is German.
Apart from that, I stumbled upon LaTeX code that just didn’t want to compile with modern TeX Live more than once. Using Nix Flakes also makes me feel safe enough to not commit the PDF file to the repository (just in case the source doesn’t compile at some point in the future).
- Simplified commands to not use subshells.
- Rewrote the section about fonts. Originally it described how to download a font from the internet and use it, but the more likely use-case would be to fetch fonts from nixpkgs. Therefore, the article now shows how to do that, and just mentions that you can also fetch one from some URL.
- Also, use
OSFONTDIRto tell LuaTeX where to find the font instead, which is more versatile than explicitly referencing a local path in the TeX source.
- Instead of using
catand a HEREDOC to output a script, the code now uses an env variable which removes the need for crazy
- Added section describing how to produce identical documents.