Trying to use nix
👋 This page was last updated ~2 years ago. Just so you know.
Thanks to my sponsors: Peter Shih, Justy, Egor Ternovoi, Morgan Rosenkranz, Romain Kelifa, James Leitch, Jonathan Adams, Beth Rennie, James Brown, Jelle Besseling, Geoffrey Thomas, Richard Pringle, avborhanian, Aiden Scandella, Dom, Ivo Murrell, James Rhodes, Brandon Piña, Walther, Boris Dolgov and 267 more
Now that my website is deployed as a container image, I wanted to give
nix a try. I’m still doing it the old-fashioned way right
now: with a Dockerfile
, running cargo
in a “builder” image, copying stuff
out of there into a slimmer image (that still has an Ubuntu base, even though
distroless images are a
thing now).
But why?
I was mostly interested in nix because some parts of my website have pretty big
native dependencies. futile
itself mostly relies on sqlite3 and some JS engine
(used to be quickjs, currently duktape because MSVC Windows builds). But the
asset processing pipeline, salvage
(which I’d like to integrate with futile
at some point) has a bunch more!
Not only does it need Google Chrome headless for diagrams (which are made in the excellent diagrams.net desktop application), it then exports those to PDF, which are converted to SVG (baking text into paths, so you don’t need to have my fonts installed locally to view/print them), and then optimized further. That requires poppler and cairo, which requires glib, gobject, gio, etc.
And then there’s webp processing, avif processing, etc.
In fact I wrote a whole series about this, how I got everything to link statically (except Google Chrome, which is a standalone executable) on both Linux and Windows, that involved the Meson build system, etc.
Nowadays I don’t care so much about the Windows part, everything runs in a Linux VM anyway, but I figured nix would let me simplify a lot of this — no matter which distribution I chose to use day-to-day, all the dependencies would be statically defined in a single place, I could even apply my own patches if I wanted (which I do for the video processing pipeline — to get ffmpeg and libsvtav1 to play nice with each other), and I could link dynamically against libraries, which might speed up the link step and trim down the final executable quite a bit.
Also, apparently you can generate Docker images with nix, which sounded great.
Success (but at what cost?) with crane
So I started learning nix! I learned there’s quite a few ways to build Rust code with nix, a lot of folks use naersk, but after a couple tries, I gravitated towards crane instead.
A still-experimental but not-so-new thing in nix are “flakes”, I figured I’d make
a flake to build futile
(starting small), and got as far as this:
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
crane = {
# url = "github:ipetkov/crane";
url = "path:/home/amos/bearcove/crane";
inputs.nixpkgs.follows = "nixpkgs";
};
flake-utils.url = "github:numtide/flake-utils";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs = {
nixpkgs.follows = "nixpkgs";
flake-utils.follows = "flake-utils";
};
};
};
outputs = { self, nixpkgs, crane, flake-utils, rust-overlay }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs {
system = system;
overlays = [ (import rust-overlay) ];
};
rustToolchain = pkgs.pkgsBuildHost.rust-bin.fromRustupToolchainFile
./rust-toolchain.toml;
lib = crane.lib.${system}.overrideToolchain rustToolchain;
shipyardToken = builtins.readFile ./secrets/shipyard-token;
craneLib = lib.appendCrateRegistries [
(lib.registryFromDownloadUrl {
dl = "https://crates.shipyard.rs/api/v1/crates";
indexUrl = "https://git.shipyard.rs/bearcove/crate-index.git";
curlOptsList = [ "-H" "user-agent: shipyard ${shipyardToken}" ];
})
];
futile = craneLib.buildPackage {
src = ./.;
nativeBuildInputs = with pkgs; [ pkg-config protobuf clang_14 mold ];
installCargoArtifactsMode = "use-symlink";
};
in { packages.default = futile; });
}
Wait, did you add nix syntax highlighting to futile
just now for this?
…maybe? Most of the work was already done.
I have no idea if this is idiomatic, it’s also complicated by the fact that I use a private crate registry, so I immediately had to go contribute to crane, and despite my best efforts, I still don’t deem it usable for now.
Building and installing a futile
binary via nix still takes a long time, and
the main offender is… removing references to /nix/store
paths! There’s a
long and interesting discussion
going on about it right now, I might even get a chance to sneak some Rust in
your Rust, but for now I’ve decided it’s outside the scope of this article, and
we’ll revisit this all later.
It’s time to tackle the one thing I actually meant to do with this series, which is: adding GitHub login.
Here's another article just for you:
A dynamic linker murder mystery
I write a ton of articles about rust. And in those articles, the main focus is about writing Rust code that compiles. Once it compiles, well, we’re basically in the clear! Especially if it compiles to a single executable, that’s made up entirely of Rust code.
That works great for short tutorials, or one-off explorations.
Unfortunately, “in the real world”, our code often has to share the stage with other code. And Rust is great at that. Compiling Go code to a static library, for example, is relatively finnicky. It insists on being built with GCC (and no other compiler), and linked with GNU ld (and no other linker).