diff --git a/content/posts/pinning-nixos-with-npins.md b/content/posts/pinning-nixos-with-npins.md
new file mode 100644
index 0000000..82f8537
--- /dev/null
+++ b/content/posts/pinning-nixos-with-npins.md
@@ -0,0 +1,368 @@
++++
+date = "2024-05-20"
+draft = false
+path = "/blog/pinning-nixos-with-npins"
+tags = ["nix"]
+title = "Pinning NixOS with npins, or how to kill channels forever without flakes"
++++
+
+> Start of Meetup: "hmm, Kane is using nixos channels, that's not good, it's going to gaslight you"
+> 6 hours later: Utterly bamboozled by channels
+> 6.5 hours later: I am no longer using channels
+
+\- [@riking@social.wxcafe.net](https://social.wxcafe.net/@riking/112465844452065776)
+
+Nix channels, which, just like Nix, is a name overloaded to mean several
+things, are an excellent way to confuse and baffle yourself with a NixOS
+configuration by making it depend on uncontrolled and confusing external
+variables rather than being self-contained. You can see [an excellent
+explanation of the overloaded meanings of "channels" at samueldr's
+blog][samueldr-channels]. In this post I am using "channels" to refer to the
+`nix-channel` command that many people to manage what `` points to,
+and thus control system updates.
+
+[samueldr-channels]: https://samuel.dionne-riel.com/blog/2024/05/07/its-not-flakes-vs-channels.html
+
+It is a poorly guarded secret in NixOS that `nixos-rebuild` is simply a bad
+shell script; you can [read the sources here][nixos-rebuild]. I would even go
+so far as to argue that it's a bad shell script that is a primary contributor
+to flakes gaining prominence, since its UX on flakes is so much better: flakes
+don't have the `/etc/nixos` permissions problems *or* the pains around pinning
+that exist in the default non-flakes `nixos-rebuild` experience. We rather owe
+it to our users to produce a better build tool, though, because `nixos-rebuild`
+is *awful*, and there are currently the beginnings of efforts in that direction
+by people including samueldr; `colmena` is also an example of a better build
+tool.
+
+Both the permissions issue and the pinning are extremely solvable problems
+though, which is the subject of this post. [Flakes have their
+flaws][samueldr-flakes] and, more to the point, plenty of people just don't
+want to learn them yet, and nobody has yet met people where they are at with
+respect to making this simplification *without* doing it with flakes.
+
+This is ok! Let's use something more understandable that does the pinning part
+of flakes and not worry about the other parts.
+
+[samueldr-flakes]: https://samuel.dionne-riel.com/blog/2023/09/06/flakes-is-an-experiment-that-did-too-much-at-once.html
+
+This blog post teaches you how to move your NixOS configuration into a repo
+wherever you want, and eliminate `nix-channel` altogether, instead pinning the
+version of `` and NixOS in a file in your repo next to your config.
+
+[nixos-rebuild]: https://github.com/nixos/nixpkgs/blob/b5c90bbeb36af876501e1f4654713d1e75e6f972/pkgs/os-specific/linux/nixos-rebuild/nixos-rebuild.sh
+
+# Background: what NixOS builds actually do
+
+First, let's say how NixOS builds actually work, skipping over all the remote
+build stuff that `nixos-rebuild` also does.
+
+For non-flakes, `` is evaluated; that is, [`nixos/default.nix`][nixos-defaultnix] in
+``. This resolves the `NIX_PATH` entry `` as the first
+user-provided NixOS module to evaluate, or alternatively
+`/etc/nixos/configuration.nix` if that doesn't exist. For flake configurations,
+substitute `yourflake#nixosConfigurations.NAME` in your head in place of
+``.
+
+[nixos-defaultnix]: https://github.com/nixos/nixpkgs/blob/6510ec5acdd465a016e5671ffa99460ef70e6c25/nixos/default.nix
+
+The default `NIX_PATH` is the following:
+
+```
+nix-path = $HOME/.nix-defexpr/channels nixpkgs=/nix/var/nix/profiles/per-user/root/channels/nixpkgs /nix/var/nix/profiles/per-user/root/channels
+```
+
+That is to say, unless it's been changed, `` will reference root's
+channels, managed with `nix-channel`.
+
+Next, the attribute `config.nix.package` of `` is evaluated then
+built/downloaded (!!) unless it is a flake config (or `--no-build-nix` or
+`--fast` is passed). Then the attribute `config.system.build.nixos-rebuild` is
+likewise evaluated and the `nixos-rebuild` is re-executed into the one from the
+future configuration instead of the one from the current configuration, unless
+`--fast` is passed.
+
+Once your configuration has been evaluated once or twice pointlessly, it is
+evaluated a third time, for the attribute `config.system.build.toplevel`, and
+that is built to yield the new system generation.
+
+This derivation is what becomes `/run/current-system`: it contains a bunch of
+symlinks to everything that forms that generation such as the kernel, initrd,
+`etc` and `sw` (which is the NixOS equivalent of `/usr`).
+
+Finally, `the-build-result/bin/switch-to-configuration` is invoked with an
+argument `switch`, `dry-activate`, or similar.
+
+---
+
+From this information, one could pretty much write a NixOS build tool: it really is
+just `nix build -f '' config.system.build.toplevel` (in old
+syntax, `nix-build '' -A config.system.build.toplevel`), then
+`result/bin/switch-to-configuration`. That's all it does.
+
+# Background: what is npins anyway?
+
+[`npins`][npins] is the spiritual successor to [niv], the venerable Nix pinning
+tool many people used before switching to flakes. But what is a pinning tool
+for Nix anyway? It's just a tool that finds the latest commit of something,
+downloads it, then stores that commit ID and the hash of the code in it in a
+machine-readable lock file that you can check in. When evaluating your Nix
+expressions, they can use `builtins.fetchTarball` to obtain that exact same
+code every time.
+
+That is to say, a pinning tool lets you avoid having to copy paste Git commit
+IDs around, and ultimately does something like this in the end, which hands you
+a path in the Nix store with the code at that version.
+
+```nix
+builtins.fetchTarball {
+ # https://github.com/lix-project/lix/tree/main
+ url = "https://github.com/lix-project/lix/archive/992c63fc0b485e571714eabe28e956f10e865a89.tar.gz";
+ sha256 = "sha256-L1tz9F8JJOrjT0U6tC41aynGcfME3wUubpp32upseJU=";
+ name = "source";
+};
+```
+
+Let's demystefy how pinning tools work by writing a trivial one in a couple of
+lines of code.
+
+First, let's find the latest commit of nixos-unstable with `git ls-remote`:
+
+```
+~ » git ls-remote https://github.com/nixos/nixpkgs nixos-unstable
+4a6b83b05df1a8bd7d99095ec4b4d271f2956b64 refs/heads/nixos-unstable
+~ » git ls-remote https://github.com/nixos/nixpkgs nixos-unstable | cut -f1
+4a6b83b05df1a8bd7d99095ec4b4d271f2956b64
+```
+
+Then we can construct an archive URL for that commit ID, and fetch it into the
+Nix store:
+
+```
+~ » nix-prefetch-url --name source --unpack https://github.com/nixos/nixpkgs/archive/4a6b83b05df1a8bd7d99095ec4b4d271f2956b64.tar.gz
+0zmyrxyrq6l2qjiy4fshjvhza6gvjdq1fn82543wb2li21jmpnpq
+```
+
+And finally fetch it from a Nix expression:
+
+```
+~ » nix repl
+Lix 2.90.0-lixpre20240517-0d2cc81
+Type :? for help.
+nix-repl> nixpkgs = builtins.fetchTarball { url = "https://github.com/nixos/nixpkgs/archive/4a6b83b05df1a8bd7d99095ec4b4d271f2956b64.tar.gz"; name = "source"; sha256 = "0zmyrxyrq6l2qjiy4fshjvhza6gvjdq1fn82543wb2li21jmpnpq"; }
+nix-repl> nixpkgs
+"/nix/store/0aavdx9m5ms1cj5pb1dx0brbrbigy8ij-source"
+```
+
+This is essentially exactly what npins does, minus the part of saving the
+commit ID and hash into `npins/sources.json`.
+
+We could write a simple shell script to do this, perhaps called
+`./bad-npins.sh`:
+
+```bash
+#!/usr/bin/env bash
+
+name=nixpkgs
+repo=https://github.com/nixos/nixpkgs
+branch=nixos-unstable
+
+tarballUrl="$repo/archive/$(git ls-remote "$repo" nixos-unstable | cut -f1)"
+sha256=$(nix-prefetch-url --name source --unpack "$tarballUrl")
+
+# initialize sources.json if not present
+[[ ! -f sources.json ]] && echo '{}' > sources.json
+
+# use sponge from moreutils to deal with jq not having the buffering to safely
+# do in-place updates
+< sources.json jq --arg sha256 "$sha256" --arg url "$tarballUrl" --arg name "$name" \
+ '.[$name] = {sha256: $sha256, url: $url}' \
+ | sponge sources.json
+```
+
+and then from Nix we can load the sources:
+
+```nix
+let
+ srcs = builtins.fromJSON (builtins.readFile ./sources.json);
+ fetchOne = _name: { sha256, url, ... }: builtins.fetchTarball {
+ name = "source";
+ inherit sha256 url;
+ };
+in
+builtins.mapAttrs fetchOne srcs
+```
+
+Result:
+
+```
+~ » nix eval -f sources.nix
+{ nixpkgs = "/nix/store/0aavdx9m5ms1cj5pb1dx0brbrbigy8ij-source"; }
+```
+
+We now have a bad pinning tool! I wouldn't recommend using this shell script, since
+it doesn't do things like check if redownloading the tarball is necessary, but
+it is certainly cute and it does work.
+
+`npins` is pretty much this at its core, but well-executed.
+
+[npins]: https://github.com/andir/npins
+[niv]: https://github.com/nmattia/niv
+
+# Fixing the UX issues
+
+We know that:
+
+1. `` as seen by `nixos-rebuild` determines what version of nixpkgs
+ is used to build the configuration.
+2. Where the configuration is is simply determined by ``
+3. Both instances of duplicate configuration evaluation are gated on `--fast`
+ not being passed.
+
+So, we just have to invoke `nixos-rebuild` with the right options and
+`NIX_PATH` such that we get a config from the current directory with a
+`nixpkgs` version determined by `npins`.
+
+Let's set up npins, then write a simple shell script.
+
+```
+$ npins init --bare
+$ npins add --name nixpkgs channel nixos-unstable
+```
+
+You can also use `nixos-23.11` (or future versions once they come out) in place
+of `nixos-unstable` here, if you want to use a stable nixpkgs.
+
+Time for a simple shell script. Note that this shell script uses `nix eval`,
+which we at *Lix* are very unlikely to ever break in the future, but it does
+require `--extra-experimental-features nix-command` as an argument if you don't
+have the experimental feature enabled, or
+`nix.settings.experimental-features = "nix-command"` in a NixOS config. (The
+experimental feature can be hacked around with
+`nix-instantiate --json --eval npins/default.nix -A nixpkgs.outPath | jq -r .`,
+which works around `nix-instantiate --eval` missing a `--raw` flag, but this is
+kind of pointless since we are about to use flakes features in a second)
+
+```bash
+#!/usr/bin/env bash
+
+cd $(dirname $0)
+
+# assume that if there are no args, you want to switch to the configuration
+cmd=${1:-switch}
+shift
+
+nixpkgs_pin=$(nix eval --raw -f npins/default.nix nixpkgs)
+nix_path="nixpkgs=${nixpkgs_pin}:nixos-config=${PWD}/configuration.nix"
+
+# without --fast, nixos-rebuild will compile nix and use the compiled nix to
+# evaluate the config, wasting several seconds
+sudo env NIX_PATH="${nix_path}" nixos-rebuild "$cmd" --fast "$@"
+```
+
+# Killing channels
+
+Since building the config successfully, we can now kill channels to stop their
+reign of terror, since we no longer need them to build the configuration at
+all. Use `sudo nix-channel --list` and then `sudo nix-channel --remove
+CHANNELNAME` on each one. While you're at it, you can also delete `/etc/nixos`
+if you've moved your configuration to your home directory.
+
+Now we have a NixOS configuration built without using channels, but once we are
+running that system, `` will still refer to a channel (or nothing, if
+the channels are deleted), since we didn't do anything to `NIX_PATH` on the
+running system. Also, the `nixpkgs` flake reference will point to the latest
+`nixos-unstable` at the time of running a command like `nix run nixpkgs#hello`.
+Let's fix both of these things.
+
+For context, *by default*, on NixOS 24.05 and later, due to [PR
+254405](https://github.com/NixOS/nixpkgs/pull/254405), *flake*-based NixOS
+configs get pinned `` and a pinned `nixpkgs` flake of the exact same
+version as the running system, such that `nix-shell -p hello` and `nix run
+nixpkgs#hello` give you the same `hello` every time: it will always be the same
+one as if you put it in `systemPackages`. That setup works by setting
+`NIX_PATH` to refer to the flake registry `/etc/nix/registry.json`, which then
+is set to resolve `nixpkgs` to `/nix/store/xxx-source`, that is, the nixpkgs of
+the current configuration.
+
+We can bring the same niceness to non-flake configurations, with the exact same
+code behind it, even!
+
+Let's fix the `NIX_PATH`. Add this module worth of code into your config
+somewhere, say, `pinning.nix`, then add it to `imports` of `configuration.nix`:
+
+```nix
+{ config, pkgs, ... }:
+let sources = import ./npins;
+in {
+ # We need the flakes experimental feature to do the NIX_PATH thing cleanly
+ # below. Given that this is literally the default config for flake-based
+ # NixOS installations in the upcoming NixOS 24.05, future Nix/Lix releases
+ # will not get away with breaking it.
+ nix.settings = {
+ experimental-features = "nix-command flakes";
+ };
+
+ # FIXME(24.05 or nixos-unstable): change following two rules to
+ #
+ # nixpkgs.flake.source = sources.nixpkgs;
+ #
+ # which does the exact same thing, using the same machinery as flake configs
+ # do as of 24.05.
+ nix.registry.nixpkgs.to = {
+ type = "path";
+ path = sources.nixpkgs;
+ };
+ nix.nixPath = ["nixpkgs=flake:nixpkgs"];
+}
+```
+
+# New workflow
+
+When you want to update NixOS, use `npins update`, then `./rebuild.sh`
+(`./rebuild.sh dry-build` to check it evaluates, `./rebuild.sh boot` to switch
+on next boot, etc). If it works, commit it to Git. The version of nixpkgs comes
+from exactly one place now, and it is tracked along with the changes to your
+configuration. Builds are faster now since we don't evaluate the configuration
+multiple times.
+
+Multiple machines can no longer get desynchronized with each other. Config
+commits *will* build to the same result in the future, since they are
+self-contained now.
+
+# Conclusion and analysis
+
+We really need to improve `nixos-rebuild` as the NixOS development community.
+It embodies, at basically every juncture, obsolescent practices that confuse
+users and waste time. Modern configurations should be using either
+npins/equivalent or flakes, both of which should be equally valid and easy to
+use choices in all our tooling.
+
+Flags like `--no-rebuild-nix` come from an era where people were building
+flake-based configs from a Nix that didn't even *have* flakes, so they needed
+to be able to switch to an entirely different *Nix* to be able to evaluate
+their config. We should never be rebuilding Nix by default before re-evaluating
+the configuration in 2024. The Nix language is much, much more stable these
+days, almost frozen like a delicious ice cream cone, and so the idea of
+someone's config requiring a brand new Nix to merely evaluate is bordering on
+absurd.
+
+It doesn't help that this old flakes hack actually breaks cross compiling
+NixOS configs, for which `--fast` is thus mandatory. The re-execution of
+`nixos-rebuild` is more excusable since there is [still work to do on that like
+capturing output to the journal](https://github.com/NixOS/nixpkgs/pull/287968),
+but it is still kind of bothersome to eat so much evaluation time about it; I
+wonder if a happier medium is that it would just build `pkgs.nixos-rebuild`
+instead of evaluating all the modules, but that has its own drawback of ignoring
+overlays in the NixOS config...
+
+Another tool that [needs rewriting, documentedly
+so](https://github.com/NixOS/nixpkgs/issues/293543) is `nixos-option`, which is
+a bad pile of C++ that doesn't support flakes, and which could be altogether
+replaced by a short bit of very normal Nix code and a shell script.
+
+There's a lot of work still to do on making NixOS and Nix a more friendly
+toolset, and we hope you can join us. I (Jade) have been working along with
+several friends on , a soon-to-be-released fork of CppNix
+2.18 focused on friendliness, stability, and future evolution. People
+in our community have been working on these UX problems outside Nix itself
+as well. We would love for these tools to be better for everyone.
diff --git a/content/posts/pinning-packages-in-nix.md b/content/posts/pinning-packages-in-nix.md
index 05bef38..fa393d6 100644
--- a/content/posts/pinning-packages-in-nix.md
+++ b/content/posts/pinning-packages-in-nix.md
@@ -289,11 +289,11 @@ the NixOS option
The primary reason that Nix doesn't allow trivially overriding packages with a
different version is that it is a generalized build system building software
-that has non-uniform expectations of how to be built. One can in indeed see
+that has non-uniform expectations of how to be built. One can indeed see
that the "replace one version with some other in some file" idea is *almost*
-reality in languages using `mkDerivation` directly, though one might have to
+reality in languages that use `mkDerivation` directly, though one might have to
tweak other build properties sometimes. Architectural problems in nixpkgs
-prevent this working for several ecosystems.
+prevent this working for several ecosystems, though.
Another sort of issue is that nixpkgs tries to provide a mostly [globally
coherent] set of software versions, where, like most Linux distributions, there