The msvcrt.zip MkDocs Setup
The way this blog is set up, we've actually got two blog domains set up, each being published to automatically by a workflow on Tangled. One of these domains hosts the public site you're probably reading this on (https://blog.msvcrt.zip) and the other, more secret domain hosts the same site, but with drafts enabled.
That's the most interesting part of the setup, and it's most of what this post will be about, though there's a couple other things I'd like to document here for anyone else trying to do something similar.
Preamble: Tangled Pipelines
You can read in much more detail about Tangled's pipelines on their blog, but you need to know a little about how they work to understand the issues this post is going to solve.
Tangled pipelines use Nixery containers to set up a build runner, and then run the commands specified in the workflow inside that container. They're very similar in concept to Github Actions, and a lot of the workflow format may feel familiar if you're used to Github Actions, but they're also a lot simpler. And because they use Nixery containers, you have access to every package that's available in nixpkgs, and that makes them very powerful (even if you have to write more of your workflows yourself).
Nix, and therefore Nixery, has a unique structure where packages are installed
to /nix/store instead of the traditional locations.1
How our pipeline works
I'll go over draft.yml, because it's basically the same as main.yml but it
has the more interesting bits in it :)
Like the Github actions you might be more familiar with, this starts off with a
section saying when to run the pipeline. Our draft pipeline only runs on pushes
to the draft branch, and our main pipeline only runs on pushes to main.
After that we have the dependencies (boring), a suspicious environment variable
we'll get to later, and then the actual build steps. The build steps are mostly
just normal commands, with one command per step here, and they work about how
you'd expect: run uv sync to set up the project, run mkdocs build through
uv to build the site, then run git-pages-cli to deploy it to git-pages.
The magic here that makes the draft pipeline interesting is in two environment
variables: BUILD_DRAFTS and SITE_URL. BUILD_DRAFTS instructs MkDocs to,
well, build drafts when building the site; SITE_URL lets us override the URL
it uses for things like social cards to point to the drafts site. SITE_URL,
along with most of the other environment variables, come from the repository
secrets set up in Tangled:
DRAFTS_URLis the URL the drafts site is set up on, and is just copied toSITE_URLwhen building the siteDRAFTS_SECRETis the secret used to publish to git-pages
Each of these have their corresponding DEPLOY_ counterparts as well, used for
the main workflow.
This is enabled by some neat functionality provided by pyyaml-env-tag, which MkDocs pulls in as a dependency: it allows you to use special tags in the config to set values based on the environment:
And that's it! There's nothing else interesting here to talk about.
...oh.
Checkhov's Nix2
While this workflow mostly worked, at first, there was one little issue: it couldn't actually generate images for social cards, because Material for MkDocs depends on cairosvg for that, which depends on libcairo, which... it can't find by default, even if it's installed in the Nixery container.
You see, with the way Nix works, shared objects get installed into /nix/store
and the cairosvg package can't actually find it there. This results in a bunch
of warnings printed out at build time saying
no library called "libcairo-2" was found and, while the site will still build
and deploy, any images that MkDocs tried to generate will be missing and social
cards will be broken.
This was a surprisingly annoying issue to solve: only one other person seems to have encountered it at all, and their github discussion ended up locked with three upvotes and zero comments. Most people using Nix don't run into any issues like this, because most of the time, Nix handles the library load paths perfectly fine, but something about the intersection of Nix, Nixery, cairo, and Python seems to be the perfect conditions for things to break.
I'm not actually sure that both of the steps I took to mitigate this are required, but there's two things I did before getting things to work:
- Forcing uv to build the cairo packages from source so it can find the right library locations,3 and
- Getting the install path for cairo from Nix and shoving it into
LD_LIBRARY_PATHso that the library can actually be picked up.
Unfortunately, Thing 2 basically doubled the time it takes for the pipeline to complete: it causes nixpkgs to be extracted into the git cache, and nixpkgs is very big, so this takes a very long time to actually happen. But it makes sure everything works, and because (aside from the AppView) I'm using Tangled on my own infra,4 the minute-long pipelines aren't much of a concern.
The results
Of course, the results are pretty nice. I've got things set up so that updates can be published automatically once I commit them, I can push drafts and send them to smaller groups of people for feedback, and I can be unreasonably cagey about the drafts URL (it's not hard to find, or guess, and it doesn't have to be).
The workflows take about a minute to run, which is slower than I'd like, but it is a fairly reasonable time overall. It would be much faster with a better solution to the cairo library problem, but I don't know enough about Nix to fix that myself, and this works for now. (If you have any suggestions on how to fix it, let me know!)
-
This is foreshadowing :) ↩
-
You know what they say: if you mention Nix in the first act, in the second act or third act it absolutely must go off. ↩
-
This did nothing on its own, and I'm not convinced it's useful anymore. But it doesn't really hurt; they're fast builds. ↩
-
Hey, there's another post today about just that! ↩