Decision 0082
0082. A flake with no lock is not a value
Architecture decision record 0082: 0082. A flake with no lock is not a value
Status: accepted. Makes true a claim ADR-0001 has made since the first commit.
Context
This repository has never had a flake.lock. Not removed, not ignored, not stale — absent, from
the founding commit to today.
So there is no revision of anything in this tree. nixpkgs resolves to whatever nixos-unstable
points at in the second the build starts, and impermanence, lanzaboote, crane and
rust-overlay resolve to whatever their branches point at in that same second. Two builds of the
same commit, an hour apart, are builds of different operating systems.
ADR-0001 says the opposite in as many words: “nixpkgs is consumed pinned
through Tacid channels, never raw nixos-unstable.” MANIFESTO.md’s second principle is that the
machine is a value — declared, diffable, rebuildable, with no hidden state. A branch on somebody
else’s server, read at build time, is hidden state, and it is the largest single input this project
has.
Two consequences, both of which had already happened by the time anybody looked. A third was
guessed at and is wrong, which is worth recording beside them: the hour CI’s Nix job takes is not
an unpinned input. The nixos-unstable branch is advanced by Hydra only once the channel’s tests
pass and the cache is populated, so it names a built revision by construction, and pinning changes
nothing about that. The hour is this project’s own Rust, compiled from source on every push and
compiled three times over — the rust job, the injection job and the Nix sandbox each build it —
with nothing cached between runs. What fixes that is a binary cache for this tree’s own
derivations, and it is not this decision.
A red run could mean nothing at all. An input moving under a tree is a failure with no change on this side, and a project whose CI goes red without a commit teaches its readers to stop believing CI. That is a worse loss than the hour.
And the inputs this tree declares had never been exercised. Every virtual-machine test run
while this was written passed --override-input for all five, against copies on disk, because the
sandbox they run in cannot reach the fetcher flake.nix names. So the one arrangement that was
proven is the one no user has, and the arrangement every user has was proven by nothing. That is
the question ADR-0069 asks, answered
badly: the whole Nix layer rested on a file that did not exist, and nothing checked.
The second half of this is the fetcher, which is why the lock could not simply be written.
github:NixOS/nixpkgs/nixos-unstable does not name a revision, so Nix resolves the branch through
api.github.com — a second service, rate-limited per address, that is not the one the repository is
served from. flake.nix already says this, in a comment written for lanzaboote:
git+httpsrather thangithub:throughout, for this input and for the two it needs: thegithub:fetcher goes throughapi.github.com, which is rate-limited per address and is a second service to be reachable. A git fetch over https is the protocol the repository actually speaks, works against any host, and is what a machine rebuilding itself behind a proxy can do.
That paragraph is correct and it governed one input out of five. A rule kept at one door is not a
rule (ADR-0076), and the three doors it
was not kept at are the ones that matter — nixpkgs is the input a machine cannot rebuild itself
without.
It is not a hypothetical. The environment this was written in can reach github.com over git and
cannot reach api.github.com or the archive endpoint at all, so the flake as declared could not be
locked or evaluated there. Any machine behind a filtering proxy, on a mirror, or on a GitHub
Enterprise host is in the same position, and “rebuilds itself from its own declaration, anywhere” is
the product rather than a convenience.
Decision
The lock is committed, and it pins nixpkgs at 3ed67ec0a4d3c7ab4ae1f04f8ee8df07bfa506a2.
That is the revision every test in this tree has actually been run against, which is the only
revision a lock has any business naming. Pinning what was tested is what a lock is for.
Every input is fetched over git, by the protocol its repository speaks. No input resolves through a vendor’s API. The comment quoted above now describes the file it is in.
The identity of the two fetchers was checked rather than assumed. The concern with moving
nixpkgs off github: is the binary cache: a source tree that differs by one byte is a different
derivation and a runner downloads nothing. A git checkout and a GitHub archive of one revision
differ only through export-ignore in .gitattributes and through submodules. nixpkgs has neither
— checked, on the revision being pinned — so the trees are identical, the derivations are identical,
and cache.nixos.org is hit exactly as before.
Consequences
- Two builds of one commit are two builds of one operating system. This is the first day that is true.
- A red run means a change in this repository again.
- The pinned revision is a published channel release —
releases.nixos.org/nixos/unstable/nixos-26.11pre1066106.3ed67ec0a4d3answers — socache.nixos.orgis complete for it. That is a property of the revision rather than of pinning, and it is the reason a lock may name this one without making anybody build more than they did. nix flake updatestill works and still advances every input; what changes is that advancing them is now a commit somebody makes, rather than a thing that happens between two builds.- The first fetch of
nixpkgsis a shallow git fetch rather than a CDN tarball, which is slower on a cold machine and is paid once.github:is the convention, and this tree now departs from it deliberately. - Anyone building this flake behind a proxy that permits git and refuses
api.github.comcan now do so: a contributor with a fresh clone, a runner, anix buildof the repository. An installed Tacid machine was never in that position and this changes nothing for it — it rebuilds from the store paths it carries, with--override-inputfor every input (ADR-0034, ADR-0037), so it resolves no URL at all. Which is the same asymmetry as the rest of this record, one layer further out: the machine was insulated, and the project that builds the machine was not. - The virtual-machine tests can be run against the flake as declared. They were run through
--override-inputagainst copies on disk because the flake as declared could not be evaluated where they run; with every input fetched over git and pinned, the override is no longer the only way, and the arrangement under test becomes the arrangement users have. The overrides are still the fast path for an edit-and-run loop, which is a different thing from being the only path.
Alternatives rejected
Keep github: and generate the lock somewhere with network access. This is the smallest change
and it leaves the reason the lock was missing exactly where it was. The fetcher would still need a
second service, the sandbox that runs the virtual-machine tests would still be unable to evaluate
the flake as declared, and those tests would go on proving an arrangement no user has.
Keep github: and write the hashes by hand. A narHash cannot be checked without fetching what
it hashes, so this is a number entered on faith into the one file whose whole purpose is that
nothing is taken on faith. It is also self-defeating: wrong, it fails every build for everyone; and
the environment that would have to write it is the environment that cannot verify it.
Pin nixos-unstable as it stands today rather than the tested revision. A newer revision is not
a better one; it is one nothing here has been run against, chosen for being newer. That is the habit
the lock exists to break, performed while writing the lock.
Leave it unlocked and let the channel do the pinning.
ADR-0013’s channels do pin what a machine follows, so a released Tacid was
never going to hand a user nixos-unstable HEAD. But the channel is downstream of this repository:
it pins what CI built, and what CI built was whatever the branch said that morning. An unpinned
input does not become pinned by being copied.