Over the past couple of years, I’ve been using Hugo on and off for my blog, as well as for some static websites I maintain. On many occasions, I have started to hack on my site, only to realsize that my theme – as submoduled into the repository – is no longer compatible with the most recent version of Hugo that I have installed on my system.
This blog post describes how to migrate from an environment controlled by the host operating system to a reproducible environment regardless of the host.
Installing Nix
If you’d like to follow along this tutorial, then you should have installed Nix on your system.
Nix can be installed by following the instructions
here. At the time of writing this
guide, Nix can be installed by simply running curl
https://nixos.org/nix/install | sh
as a non-root user.
Please note that if you would like to uninstall nix, simply remove the
directory /nix
.
Reproducible environment
I just want to copy/paste
If you’re in a hurry and just want to copy/paste this environment to your own Hugo-based project then you’re going to need:
- shell.nix
- nixpkgs.nix
- .nixpkgs-version.json
- All the files/folders in pkgs/
- And all the files/folders in config/
Describing the environment
To achieve reproducibility, we’re going to rely on the awesome command
by Nix named nix-shell
. This command allows you to create a shell
environment given a list of packages and shell hooks. Go ahead, try it
with nix-shell -p hello
; This should give you a shell with a modified
PATH
giving you access to the command hello
.
# -f <nixpkgs> is not a required argument, but it's good to know about it
$ nix-shell -f '<nixpkgs>' -p hello
$ hello
Hello, world!
$ EOF # Ctrl-D
$ hello
hello: command not found
One can simply run nix-shell -p hugo
to install and use Hugo. Using
nix-shell like that has some limitations:
- There’s no guarentee that the same version of Hugo is going to be
used. Unless you specify
-I nixpkgs=/path/to/fixed/nixpkgs
. - There’s no way to write shell hooks to setup the theme at the correct place.
These limitations can be avoided by expressing the desired environment
in a Nix expression, automatically loaded by nix-shell. Create a file
named shell.nix
at the root of your website with the following
contents:
let
# See https://nixos.wiki/wiki/FAQ/Pinning_Nixpkgs for more information on pinning
nixpkgs = builtins.fetchTarball {
# Descriptive name to make the store path easier to identify
name = "nixpkgs-unstable-2019-02-26";
# Commit hash for nixos-unstable as of 2019-02-26
url = https://github.com/NixOS/nixpkgs/archive/2e23d727d640f0a96b167d105157f6e7183d8f82.tar.gz;
# Hash obtained using `nix-prefetch-url --unpack <url>`
sha256 = "15s7qjw4qm8mbimiv5fcg0nlgpx4gsws2kbx8z1qzqrid8jg76f8";
};
in
{ pkgs ? import nixpkgs {} }:
with pkgs;
let
hugo-theme-terminal = runCommand "hugo-theme-terminal" {
pinned = builtins.fetchTarball {
# Descriptive name to make the store path easier to identify
name = "hugo-theme-terminal-2019-02-25";
# Commit hash for hugo-theme-terminal as of 2019-02-25
url = https://github.com/panr/hugo-theme-terminal/archive/487876daf1ebdf389f03a2dfdf6923cea5258e6e.tar.gz;
# Hash obtained using `nix-prefetch-url --unpack <url>`
sha256 = "17gvqml1wl14gc0szk1kjxi0ya995bmpqqfcwn9jgqf3gdx316av";
};
patches = [];
preferLocalBuild = true;
}
''
cp -r $pinned $out
chmod -R u+w $out
for p in $patches; do
echo "Applying patch $p"
patch -d $out -p1 < "$p"
done
'';
in
mkShell {
buildInputs = [
hugo
];
shellHook = ''
mkdir -p themes
ln -snf "${hugo-theme-terminal}" themes/hugo-theme-terminal
'';
}
Now, simply run nix-shell
to get access to Hugo version 0.54. You
should also find the theme symlinked at themes/hugo-theme-terminal
to
a path in /nix/store/...
.
Understanding the shell nix expression
Let’s break the shell.nix
into multiple pieces, so we can address them
separately. But before we dive in, keep in mind the following:
- shell.nix is expected to be a Nix expression returning a derivation.
- A derivation is a description of building something from other stuff.
- A Nix expression is a function. Functions in Nix follow the following format
arg: body
when in this case the arg is expected to be a set such as{ pkgs }: body
.
So the expected barebones shell.nix should actually look like:
{ pkgs ? import <nixpkgs> {} }:
with pkgs;
mkShell {
buildInputs = [
hugo
];
}
The ?
operator seen in pkgs ? import <nixpkgs> {}
above specifies the
default value of the pkgs argument.
Now, let’s talk about the first part of the script.
# See https://nixos.wiki/wiki/FAQ/Pinning_Nixpkgs for more information on pinning
nixpkgs = builtins.fetchTarball {
# Descriptive name to make the store path easier to identify
name = "nixpkgs-unstable-2019-02-26";
# Commit hash for nixos-unstable as of 2019-02-26
url = https://github.com/NixOS/nixpkgs/archive/2e23d727d640f0a96b167d105157f6e7183d8f82.tar.gz;
# Hash obtained using `nix-prefetch-url --unpack <url>`
sha256 = "15s7qjw4qm8mbimiv5fcg0nlgpx4gsws2kbx8z1qzqrid8jg76f8";
};
The above code block is defining a variable named nixpkgs
to a
function call builtins.fetchTarball
with one argument: the set that is
defining the name
, url
and the sha256
. Nix is a lazy language, so
nixpkgs will remain a function until it’s actually invoked! Once
invoked, the function will get evaluated and the return value is simply
a path on your filesystem within the nix store.
hugo-theme-terminal = runCommand "hugo-theme-terminal" {
pinned = builtins.fetchTarball {
# Descriptive name to make the store path easier to identify
name = "hugo-theme-terminal-2019-02-25";
# Commit hash for hugo-theme-terminal as of 2019-02-25
url = https://github.com/panr/hugo-theme-terminal/archive/487876daf1ebdf389f03a2dfdf6923cea5258e6e.tar.gz;
# Hash obtained using `nix-prefetch-url --unpack <url>`
sha256 = "17gvqml1wl14gc0szk1kjxi0ya995bmpqqfcwn9jgqf3gdx316av";
};
patches = [];
preferLocalBuild = true;
}
''
cp -r $pinned $out
chmod -R u+w $out
for p in $patches; do
echo "Applying patch $p"
patch -d $out -p1 < "$p"
done
'';
Much like the first section, this one is defining the variable
hugo-theme-terminal
to resolve to a simple derivation that provides
the hugo theme. Notice that I did not say install the theme, as the
runCommand
barely downloads the theme and extracts it into a path that
gets returned and assigned to hugo-theme-terminal
.
mkShell {
buildInputs = [
hugo
];
shellHook = ''
mkdir -p themes
ln -snf "${hugo-theme-terminal}" themes/hugo-theme-terminal
'';
}
And finally, we get to the shell declaration itself. mkShell
is a
function that returns a derivation, a derivation that can only be used
with nix-shell
but cannot be built with nix-build
.
The shellHook
is a bash script that gets invoked within the same
folder where the shell.nix
is located. I’m using this shellHook here
to create the themes folder and to symlink the theme we created earlier
with the runCommand
to themes/hugo-theme-terminal
.