In recent months I've been building more and more of my packages with Nix. However, most of my personal systems are still running Arch. While NixOS is making headways there and is probably the eventual destination for all my systems, I'm not interested in migrating all my personal infrastructure all at once.
Instead, what I really want to do is having my Nix tooling output Arch packages that I can install on those systems in this transitional period. After some experimentation, I got it working for one of my packages, this blog, and this post details the steps it took.
Building the Blog via Flake
As with all my Nix packages, I'm using Flakes for this. I wrote a guide
introducing flakes if you need an introduction. I'd converted this blog
over to a flake some time ago, and was deploying by having my flake
output a .tar.zst
and unpacking it on my remote host. This was a little more
manual than I would have liked, but it was an important component that I reused
in building the Arch package.
The blog is built using Zola, so the actual process of building the blog amounts
to installing Zola, then running zola build
. As a Nix build step then, it looks like
this:
{
packages.x86_64.default = let
outputHost = "https://tonyfinn.com";
in pkgs.stdenv.mkDerivation {
pname = "tonyfinn.com";
version = "0.1";
src = ./.;
nativeBuildInputs = [ pkgs.zola ];
buildPhase = ''
zola build --base-url outputHost --output-dir $out
'';
installPhase = "echo Nothing to do";
};
}
The outputHost
is assigned to a variable so I can quickly change it for testing purposes.
Here I just have Zola build directly into the output directory. Now I can run nix build
and have the HTML of my site published to the result
directory on any system with Nix.
This is a good first start, but as far as getting it onto another host, it's handy to have a single file. Also Nix's behaviour of setting timestamps to 1970-01-01 can break the default caching behaviour of the web server I use.
So as a second step, I pack the output into a .tar.zst
and set the mtime on the tar to the
last commit date of the git repo my site is hosted. Setting the mtime
means that the newly
installed files will look newer to the previous ones when the webserver is decided if the response
matches the users cache or not. I define a second flake output for this:
{
packages.x86_64-linux.tzst = pkgs.stdenv.mkDerivation {
pname = "tonyfinn.com-tzstd";
version = "0.1";
src = self.packages.x86_64-linux.default;
nativeBuildInputs = [ pkgs.zstd ];
buildPhase = ''
mtime=$(date -d '@toString self.lastModified' '+%Y%m%d %H:%M')
tar -cf tonyfinn.com.tar.zst --zstd "--mtime=$mtime" --mode=a+rX,u+w --dereference -C $src .
'';
installPhase = ''
mkdir -p $out
cp tonyfinn.com.tar.zst $out/
'';
};
}
This uses the date
command to convert the self.lastModified
property of flakes from a unix
time stamp to a format that tar
will take in its mtime
property. Once the tar file is created,
it's populated in the result
directory.
Building an Arch Package
Now that I have tarball with the desired contents of my Arch package, it's time to package it up.
The arch package format is a little awkward using binary files like .MTREE
, so rather than try
to synthesize the files or build a custom tool for this, I just install the Arch packaging tools
with Nix and have them build a package from the Nix output. This means I need a PKGBUILD
:
pkgname=tonyfinn.com
pkgver=1
pkgrel=1
arch=('any')
source=('tonyfinn.com.tar.zst')
md5sums=('SKIP')
This is a minimal package build which creates the directory to host the web assets at the Arch recommended location and unpacks the tar into it. Now to use this from Nix, I created a third and final build output in my flake:
{
packages.x86_64-linux.arch = pkgs.stdenv.mkDerivation {
pname = "tonyfinn.com-arch";
version = "0.1";
src = self.packages.x86_64-linux.tzst;
dontUnpack = true;
nativeBuildInputs = [ pkgs.zstd pkgs.pacman pkgs.fakeroot pkgs.libarchive ];
buildPhase = ''
cp $src/tonyfinn.com.tar.zst .
cp ./PKGBUILD ./PKGBUILD
PKGEXT=".pkg.tar.zst" makepkg --config pkgs.pacman/etc/makepkg.conf
'';
installPhase = ''
mkdir -p $out
cp *.pkg.tar.zst $out/
touch -m -d '@toString self.lastModified' $out/*.pkg.tar.zst
'';
};
}
So let's decompose this a bit:
src = self.packages.x86_64-linux.tzst;
dontUnpack = true;
Here I tell Nix to use the output of the tzst flake output as the src for this
arch flake output. I also tell Nix not to unpack it - I want a single input file
to makepkg
so I don't need a huge list of input files in the PKGBUILD
.
nativeBuildInputs = [ pkgs.zstd pkgs.pacman pkgs.fakeroot pkgs.libarchive ];
This is the list of dependencies that makepkg
needs to run in an isolated environment.
{
buildPhase = ''
cp $src/tonyfinn.com.tar.zst .
cp ./PKGBUILD ./PKGBUILD
PKGEXT=".pkg.tar.zst" makepkg --config pkgs.pacman/etc/makepkg.conf
'';
}
This copies the PKGBUILD
and tonyfinn.com.tar.zst
files into the build directory
alongside each other, so that makepkg
can find the tonyfinn.com.tar.zst
file.
Then I run makepkg
. The PKGEXT=".pkg.tar.zst"
uses zstd compression for smaller
packages (the default is gzip), and the --config ${pkgs.pacman}/etc/makepkg.conf
argument points makepkg to its configuration file. Without this second file it will
look in /etc/makepkg.conf
, which would work if you're running an impure build
on an Arch host system, but I also want to be able to build this package from my NixOS
laptop.
{
installPhase = ''
mkdir -p $out
cp *.pkg.tar.zst $out/
touch -m -d '@toString self.lastModified' $out/*.pkg.tar.zst
'';
}
Finally I copy the built nix package to my flake output. I also set the mtime to the last modified date rather than the default because it makes my life easier.
Fixing the package version
This is almost complete. The one problem left is that the hardcoded pkgver
, which
means this will always produce a package with version 1-1
and so pacman
will not
see it as an update to an already installed package. There's a few ways around this,
but the way I decided to tackle this was to use the last modified date as the version
number and to pass it into the makepkg build as a source file.
To do this I updated the Nix buildPhase
like so:
{
buildPhase = ''
pkgver=$(date -d '@toString self.lastModified' +%Y%m%d)
cp $src/tonyfinn.com.tar.zst .
cp ./PKGBUILD ./PKGBUILD
chmod +w ./PKGBUILD
echo $pkgver > PKGVER
PKGEXT=".pkg.tar.zst" makepkg --config pkgs.pacman/etc/makepkg.conf
'';
}
The first line computes the last modified date as a YYYYMMDD string, which is then written
out to a file called PKGVER
in the build directory. Then inside the PKGBUILD
, I add
a dynamic pkgver by adding a pkgver
function which just prints out the content of that
PKGVER
file:
Finally I update the source
and md5sums
arrays to include this new file in the makepkg
input:
source=('tonyfinn.com.tar.zst' 'PKGVER')
md5sums=('SKIP' 'SKIP')
(Note for those unfamiliar with arch packages, this does not replace the hardcoded pkgver
,
you need both! Also using this dynamic pkgver requires the PKGBUILD
to be writable, which is why
I did the chmod +x ./PKGBUILD
in my buildPhase
).
Now it'll create a package file like tonyfinn.com-20230123-1.pkg.tar.zst
with version 20230123
which can be installed on an arch system with pacman -U <path to package file>
and uninstalled
and otherwise integrated.