2026-05-12
After updating some stuff on my Raspberry Pi and trying to deploy a new NixOS generation, I encountered this build error:
$ nixos-rebuild switch --flake .#rpi --target-host home --sudo
[..]
error: Cannot build '/nix/store/2j8nsb924fjw7vgkrsmhi9k1fs3jka58-bisca-backend-aarch64-unknown-linux-gnu-0.1.0.drv'.
Reason: builder failed with exit code 101.
Output paths:
/nix/store/80qy9xhbqhr9vrw50jylzakhcbm83a4b-bisca-backend-aarch64-unknown-linux-gnu-0.1.0
[..]
> cargo:warning=aarch64-unknown-linux-gnu-gcc: error: unrecognized command-line option '-m64'
>
> --- stderr
>
>
> error occurred in cc-rs: command did not execute successfully (status code exit status: 1): LC_ALL="C" "aarch64-unknown-linux-gnu-gcc" "-O0" "-ffunction-sections" "-fdata-sections" "-fPIC" "-m64" "-w" "-DSQLITE_CORE" "-DSQLITE_DEFAULT_FOREIGN_KEYS=1" "-DSQLITE_ENABLE_API_ARMOR" "-DSQLITE_ENABLE_COLUMN_METADATA" "-DSQLITE_ENABLE_DBSTAT_VTAB" "-DSQLITE_ENABLE_FTS3" "-DSQLITE_ENABLE_FTS3_PARENTHESIS" "-DSQLITE_ENABLE_FTS5" "-DSQLITE_ENABLE_JSON1" "-DSQLITE_ENABLE_LOAD_EXTENSION=1" "-DSQLITE_ENABLE_MEMORY_MANAGEMENT" "-DSQLITE_ENABLE_RTREE" "-DSQLITE_ENABLE_STAT2" "-DSQLITE_ENABLE_STAT4" "-DSQLITE_SOUNDEX" "-DSQLITE_THREADSAFE=1" "-DSQLITE_USE_URI" "-DHAVE_USLEEP=1" "-D_POSIX_THREAD_SAFE_FUNCTIONS" "-DHAVE_ISNAN" "-DHAVE_LOCALTIME_R" "-DSQLITE_ENABLE_UNLOCK_NOTIFY" "-o" "/build/backend/target/release/build/libsqlite3-sys-a4661d61eaf4765c/out/c877a2978823c39d-sqlite3.o" "-c" "sqlite3/sqlite3.c"
>
>
> warning: build failed, waiting for other jobs to finish...
For full logs, run:
nix log /nix/store/2j8nsb924fjw7vgkrsmhi9k1fs3jka58-bisca-backend-aarch64-unknown-linux-gnu-0.1.0.drv
Now, the inner details of bisca are not really interesting here. What's relevant is that it is a Rust service which needs to be deployed on an aarch64 rpi zero, and since it's some private stuff I wrote, there's of course no cached package on cache.nixos.org. And since I can't really build it on the rpi due to CPU/memory constraints, I need to cross compile it on my laptop.
For that there are a couple of options: emulate a full aarch64 system with QEMU binfmt (slow) or do proper cross compilation with x86_64 native compilers that produce aarch64. Here I'm going for the latter:
{
config,
bisca,
lib,
pkgs,
...
}:
let
buildSystem = pkgs.stdenv.buildPlatform.system;
system = pkgs.stdenv.hostPlatform.system;
crossPkgs = import bisca.inputs.nixpkgs {
localSystem = buildSystem;
crossSystem = system;
overlays = [ bisca.overlays.default ];
};
in
{
# ..
services = {
bisca = {
enable = true;
backend.package = crossPkgs.bisca-backend;
};
}
Enough context, let's go back to the actual issue. Given this is my first time
dealing with a random failure after bumping nixpkgs, I have to first figure
out a way to debug it, which is what this write-up is all about.
The initial plan was almost naive: run git bisect on nixpkgs, with the
$LAST_WORKING_REV from my nix-config as good commit and nixpkgs-unstable
as bad commit. Then, for each commit try to build bisca pointing its nixpkgs
input to my local repo where I was bisecting:
$ nix build ~/nix-config#nixosConfigurations.rpi.config.services.bisca.backend.package \
--override-input bisca/nixpkgs "git+file://~/nixpkgs?rev=${rev}"
With --override-input we achieve exactly that: point bisca's nixpkgs to our
local repo, and since crossPkgs is based on bisca's own nixpkgs (the
crossPkgs = import bisca.inputs.nixpkgs line), the package will be built with
that revision.
Let's put everything together:
$ git bisect start nixpkgs-unstable b40629efe5d6ec48dd1efba650c797ddbd39ace0
Bisecting: 15846 revisions left to test after this (roughly 14 steps)
[3113894db88dad207079a49632efb947f11cf80d] Merge master into staging-nixos
$ git bisect run sh -c '
nix build ~/nix-config#nixosConfigurations.rpi.config.services.bisca.backend.package \
--override-input bisca/nixpkgs "git+file://~/nixpkgs?rev=$(git rev-parse HEAD)" \
--no-link
'
now we can go grab a coffee, and once we are back git bisect should have found
the first bad revision: after all, we are just building bisca ~14 times, right?
Well, not that fast. With this approach we'll most likely come back to a nix-daemon busy compiling random things, rather than to a finished bisect.
Why is that? Because we are allowing every single commit in nixpkgs to be used
as nixpkgs input, and this means we can end up in some revision that produces
derivations that are not cached by Hydra. To see why some commits are cached and
others aren't, a quick (simplified) recap on how branching works in nixpkgs is
in order.
Problem statement: all changes landing on master cause Hydra to rebuild
everything that depends on said changes. This might end up causing a mass
rebuild
(which is bad use of the infra) so commits usually get merged and piled up in
the staging branch and then regularly merged into staging-next, which is
manually built/tested on Hydra. If there are no major regressions, this gets
merged into master.
Here's the same thing as a diagram from the CONTRIBUTING doc:
The relevant bit for us: Hydra builds and caches (besides manual runs) what
lands on the tip of master and staging-next (master then gets
fast-forwarded into nixpkgs-unstable when the build is successful). The
staging branch itself is not built on Hydra, and neither are mid-PR commits
inside any of the merges.
So if we land on a mid-PR commit, we'll likely hit cache misses, which could require rebuilding lots of stuff (including rustc, llvm etc). And that takes quite a while.
Can we optimize this? Partially yes: we walk only the branch's mainline (merge
commits and commits pushed directly onto master, cached by Hydra), avoiding
the inner commits of the branches/PRs that got merged in.
Why partial: this keeps us in cached territory while we're following
master's mainline, and even when we peel into a staging-next -> master merge
we stay cached (staging-next tips are built by Hydra too). But the regression
likely lives in a staging -> staging-next merge, and once we peel into that
range we're walking the staging mainline, which Hydra doesn't build. So we'll
have to deal with cache misses eventually, just on a much smaller set of
commits (and I'd expect commits within the same staging branch to share more
derivations, so fewer cache misses as the bisect narrows down).
Great, how do we do that in practice? git bisect has exactly the right tool for following the mainline of a branch. From its man:
--first-parent
Follow only the first parent commit upon seeing a merge commit.
In detecting regressions introduced through the merging of a branch, the merge commit will be identified as introduction of the bug and its ancestors will be ignored.
This option is particularly useful in avoiding false positives when a merged branch contained broken or non-buildable commits, but the merge itself was OK.
Let's give it another try with:
$ git bisect start --first-parent nixpkgs-unstable b40629efe5d6ec48dd1efba650c797ddbd39ace0
This time, after a few minutes git bisect is done:
$ git bisect log
[..]
# first bad commit: [9cadaf6932b7c926e468f777549d57f04a7212da] staging-next 2026-04-07 (#507470)
and without much surprise.. it's a merge commit.
As anticipated, the next step is to peel/bisect into the staging-next -> master specific PR and find which staging -> staging-next PR introduced the
breakage. We start from the merge commit that git bisect gave us, get its first
parent (in master's mainline, good one) and its second parent
(staging-next's HEAD, bad one), and bisect the mainline commits in that range:
$ git bisect start --first-parent 9cadaf6932b7c926e468f777549d57f04a7212da^2 9cadaf6932b7c926e468f777549d57f04a7212da^1
without much surprise, this also didn't take long:
$ git bisect log
[..]
# first bad commit: [d79f9e22fa7067d6ae209c14c207ec0af3fea5d4] Merge branch 'staging' into staging-next
Now unfortunately we move into the painful part: we need to bisect the staging
branch, where almost all PRs get merged into, and which is not built on Hydra:
$ git bisect start --first-parent d79f9e22fa7067d6ae209c14c207ec0af3fea5d4^2 d79f9e22fa7067d6ae209c14c207ec0af3fea5d4^1
[..]
Bisecting: 181 revisions left to test after this (roughly 8 steps)
Roughly 8 commits to test, with all their cache misses. Still an improvement from the initial 14 commits to test though.
For this last round I had to let git bisect run for a few hours, but this time
it gave me an actual commit!
$ git bisect log
[..]
# first bad commit: [21ad3e929a5074dec3565eb53d29ed7ad715996e] treewide: Remove continuation escape at end of commands (#497857)
The next thing to do was to look into the bad commit, try to revert it on top of
nixpkgs-unstable to see if bisca would build with that (it did), and fill a
PR.
Quiz time: what was wrong with the buggy commit? (solution)
This was fun, and next time something breaks it'll be definitely easier to hunt down what's causing the breakage, but in the meantime, what happens while my fix gets reviewed and merged? Do I keep my rpi broken? Of course not, I just point bisca's nixpkgs to my branch:
{
inputs = {
# ..
bisca = {
url = "git+ssh://git@github.com/jibi/bisca";
inputs.nixpkgs.url = "github:jibi/nixpkgs/partial-revert-remove-trailing-line-continuations";
};
and everything will build fine.

View of the Croda del Becco (Seekofel) peak of the Dolomites from Lago di Braies (Pragser Wildsee).