Skip to content

Build kernels from upstream tarballs + an IGLOO patch series, via Nix/kernelsmith - #59

Merged
lacraig2 merged 28 commits into
mainfrom
nix-patchset
Aug 15, 2026
Merged

Build kernels from upstream tarballs + an IGLOO patch series, via Nix/kernelsmith#59
lacraig2 merged 28 commits into
mainfrom
nix-patchset

Conversation

@lacraig2

@lacraig2 lacraig2 commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Replaces the two long-lived fork branches of rehosting/linux with pristine
upstream release tarballs + an IGLOO patch series
, and replaces the Docker
build with a Nix one driven by kernelsmith.

Nothing downstream changes shape: kernels-latest.tar.gz and
kernel-devel-all.tar.gz come out the same, so penguin needs no edits. Moving
penguin to consume this as a flake input is a separate, later change.

Why the fork branches had to go

The old linux/6.13 submodule pin was not a release. It was a tree based on
v6.13~5 with three upstream commits cherry-picked forward -- a base nobody
would choose deliberately, and one you cannot describe except by pointing at the
SHA. Those three commits are simply present in the 6.13 tarball this branch
builds from.

The IGLOO delta is now patches/{4.10,6.13}/ with quilt-style series files (38
and 33 patches, plus 5 shared core/ patches), and patches/base.json naming
the upstream tag and its hash. scripts/verify-series.sh proves a series
reproduces its fork branch exactly, so the migration is checkable rather than
asserted.

Why the toolchains had to go

The Docker build got its cross compilers from unversioned
wget https://musl.cc/*-cross.tgz downloads. Every kernel we have ever shipped
therefore has no recorded compiler identity
-- there is no way to rebuild an
old release with the compiler that built it. kernelsmith resolves (kernel
version, arch) -> a pinned toolchain, so that identity is now in the lockfile.

What's built

19 cells: 7 targets on 4.10, 12 on 6.13. Each produces a kernel, perf,
igloo.ko, and osi/cosi analysis output, all individually addressable
(nix build .#packages.x86_64-linux."perf-6.13-mipsel") so a broken perf no
longer requires rebuilding the entire release tarball to see one compiler error.

igloo.ko is built per cell via kernelsmith's new buildModule
(rehosting/kernelsmith#3). This is an acceptance test for the kernel dev
output
, not a claim about which repo should own the driver build: a build tree
that cannot compile the one out-of-tree module we care about is broken, and this
is where that should be caught.

Two silent-failure bugs found and fixed

Both had shipped. Both were invisible to every existing check because the build
succeeded and the artifact existed.

  1. perf shipped one binary as four. It inherited kernel.nix's powerpc family
    collapse and emitted byte-identical big-endian 64-bit binaries for powerpc,
    powerpcle, powerpc64 and powerpc64le. Three of the four cannot run on
    their guest.

  2. powerpcle was a phantom target. Its config asks for
    CONFIG_CPU_LITTLE_ENDIAN=y, which arch/powerpc/platforms/Kconfig.cputype
    makes depends on PPC_BOOK3S_64. olddefconfig drops it without a word and
    the kernel comes out big-endian -- byte-identical to powerpc's. A 32-bit
    little-endian powerpc kernel is not expressible in mainline Linux, so the
    target is retired, with configs/6.13/powerpcle.unused recording why and
    preserving the original two lines.

Both are the same failure shape -- an ELF whose class or endianness disagrees
with its name -- so nix/shape.nix now asserts that for every artifact from the
target name alone (deliberately not from the toolchain or the config; those are
what's being checked). It costs nothing and would have caught both.

A third: 0019-add-syscalls.h.patch was corrupt, not merely churny

2729 lines -> 163 (109 insertions, 10 deletions real). Every context line in
that patch carried a leading space that was never stripped -- it was generated
by treating a unified diff's body as file content. That is in the shipped tree,
not just the patch:

pristine 6.13 IGLOO (before)
# directives at column 0 120 9
# directives indented one space 0 119
struct x; forward decls indented 0 62

Indented preprocessor directives are legal C, which is why this shipped in Aug
2025 without a compiler ever complaining. Repaired by reconstructing the header
against pristine upstream; the remaining 18 patches of the series rebased clean.

Verified: diff -w is empty and syscalls.h is the only file that differs
tree-wide; the series re-imports from scratch (33 commits, clean); and a rebuilt
kernel-6.13-armel has byte-identical .text, .rodata, .data,
.init.text and __ksymtab
. The only differences anywhere are .debug_line
(-12 bytes), .debug_info and the 20-byte GNU build-id -- DWARF records column
numbers, so de-indenting moves them. The raw boot image differs in exactly those
20 contiguous bytes out of 26.7 MB.

For scale: 4.10's equivalent patch was already clean at 141 lines, and a scan of
all 71 patches finds no other instance. Before the repair the two read as 141 vs
2729, which looked like "6.13 needs far more syscall machinery" and was purely
an artifact.

Verified

  • 19/19 kernels, 19/19 perf, 19/19 igloo.ko, shape-check green.
  • Both release tarballs bit-reproducible under nix build --rebuild, as is a
    spot-checked kernel cell.
  • verify-series.sh reproduces both fork branches from their series.

CI

Green on rehosting-arc: series applies for both versions, and build {4.10,6.13}-armel covering kernel + perf + igloo.ko per cell.

Nix install and Cachix wiring come from rehosting/ci/actions/nix-setup@v1,
the org's shared action, rather than anything defined here. Getting there
surfaced three things worth recording:

  • An earlier draft referenced rehosting/nix-setup@v1, which does not exist.
    The real action lives at rehosting/ci/actions/nix-setup@v1; nine workflows
    across penguin, fw2tar, qemu and vpnguin already use it.
  • The Arc pods run as a uid with no passwd entry, so cachix use fails with
    $USER must be set. The USER/LOGNAME/TMPDIR/XDG_CACHE_HOME block is
    a precondition for Nix on this pool, not decoration.
  • Per nix-setup's skip-push default, PR runs pull from
    rehosting-tools but do not push
    ; pushes to main populate it. This
    matches the rest of the org. The expensive closures (the cross toolchains,
    ~250 MiB) are already cached, so PRs substitute rather than rebuild them.

One unrelated fix rides along: ci: pin buildkit v0.30.0. The Docker
build-image job started failing with can't mask dir "/proc/acpi" because
setup-buildx-action's default buildkit drifted onto a version carrying that
regression. main last ran 2026-07-28 and will hit the same thing on its next
run.

Known gaps, stated plainly

  • kernelToolchainFor in nix/kernel.nix still carries a TODO(kernelsmith) --
    it belongs upstream in kernelsmith, not here.
  • A green, fast CI run here does not prove Cachix substitution works: the
    Arc pool shares one /nix per node, so paths built by an earlier job are
    already in the local store. That will only be visible on a cold node.
  • Not migrated, deliberately, and better as follow-ups than as more commits
    here: rehosting/ci's nix-release.yml (which is what the "Compile and
    release" job does by hand) and update-flake-lock.yml (newly relevant --
    this branch gives the repo its first lockfile).

…eries

Replaces the Docker + submodule build with a Nix flake sourcing the kernel as
a pristine kernel.org tarball plus an explicit IGLOO patch series.

Why:
- Toolchains were unversioned `wget https://musl.cc/*-cross.tgz` downloads, so
  shipped kernels had no recorded compiler identity. kernelsmith resolves the
  (kernel version, arch) -> toolchain mapping from pinned Bootlin SDKs instead.
- .gitmodules named the WRONG branches: it declared main_6.7 for linux/6.13
  while the pin was the head of main_6.13 -- trees 89,775 commits apart. A
  `git submodule update --remote` would have silently regressed the kernel.
  Basing on a release tag makes that class of drift unrepresentable.
- Taking the kernel as a derivation input is what makes MODVERSIONS CRC skew
  between vmlinux and igloo.ko unrepresentable rather than merely detectable.

Series, verified by scripts/verify-series.sh against the branches they replace:
- 4.10: 38 patches on v4.10; matches new_4.10 except four upstream
  export-ignore'd paths absent from every release tarball (.gitattributes,
  .get_maintainer.ignore, two arch/sh linker scripts) -- none build-affecting.
- 6.13: 33 patches on v6.13; tree a1e70bc2 is byte-identical to main_6.13.
  The four non-IGLOO commits main_6.13 carried are all upstream: it forked
  immediately before the final x86_urgent merge and cherry-picked them back.

Only 5 of the 23 same-subject patches are byte-identical across versions -- the
new-file additions -- so core/ holds 5 and the rest are per-version. Shared
files were never shared patches; hook-site code differs because its context does.

configs/ is unchanged: the cpp -P -undef assembly runs inside the derivation, so
no linuxManualConfig migration is needed.

18 of 20 cells build; loongarch64 and riscv64 await kernelsmith arch support and
are listed as `missingArches` rather than silently dropped.
nixpkgs' multiple-outputs setup hook relocates include/ to `outputDev`, which
falls back to "out" when no output is literally named "dev". Calling it "devel"
therefore moved the kernel headers into $out and left a kernel-devel tree that
looked complete in a directory listing but could not build a module.

Found by building igloo.ko against the output rather than inspecting it; the
dev closure goes 34M -> 91M once include/ actually lands there.
Kernel 4.10 validates its O= output directory with `cd $dir && /bin/pwd`, and
there is no /bin/pwd in the sandbox, so the build died as:

    Makefile:141: *** failed to create output directory "/build/build"

which points nowhere near the cause. The Docker build only worked because the
Ubuntu image happens to ship coreutils at /bin. Also patchShebangs the kbuild
helpers, whose #!/usr/bin/awk-style shebangs otherwise fail "not found" and
cascade into Kconfig syntax errors.

Both fixes live in the builder, NOT the patch series: they are environment
adaptations rather than IGLOO changes, and the series has to remain a faithful
description of the fork branch for verify-series.sh to mean anything.

4.10/armel now builds end to end on kernelsmith's k4 band (Bootlin gcc 9.3.0):
240M vmlinux + 7.1M zImage + Module.symvers, ARM EABI5. This was the plan's
biggest unretired risk; the compiler era was never the problem.
"The series applies cleanly to the pinned upstream tarball" is the permanent
invariant: without it enforced, someone edits a tree, forgets to export, and the
committed series quietly stops describing reality.

Comparing against the fork branch is a MIGRATION check -- once main_4.10 and
main_6.13 are retired there is nothing to compare against -- so it is now an
optional 3rd/4th argument rather than required.

The workflow has NOT been exercised on the self-hosted runners; treat the runner
and Cachix wiring as a first draft. It does push to Cachix from PRs, not only
releases, since PR builds never populating the cache is why cross-toolchain
closures get rebuilt from source elsewhere in this stack.
…ed vmlinux

Adds the two analysis artifacts shipped alongside every kernel, and the
release seam that reassembles the tarballs penguin already consumes.

The packaging was the easy half; the finding is provenance. Both extractors
are unpinned fetches of a default branch at image-build time:

  * extract_kernelinfo -- wget'd from refs/heads/main of panda-re/panda-ng,
    a repo this stack is retiring in favour of qemu/.
  * dwarf2json -- `git clone --depth 1` of the default branch of
    rehosting/dwarf2json, a FORK. nixpkgs ships UPSTREAM volatility's tool
    under the same attribute name, so reaching for pkgs.dwarf2json would
    silently produce different ISFs -- and penguin loads them at runtime
    (pyplugins/apis/kffi.py reads cosi.<arch>.json.xz). The fork is packaged
    with buildGoModule from a pinned rev instead.

Every osi.config and COSI ISF shipped to date was produced by whatever those
two branches said on image-build day.

kernel.nix gains a third output, `vmlinux`, holding the UNSTRIPPED image.
_in_container_build.sh extracts osi/cosi from the build-tree vmlinux and
strips the shipped copy afterwards; a derivation cannot reproduce that
ordering, so stripping in place left the analysis derivations reading a
vmlinux with no debug info on exactly the mips*/powerpc* targets that ship
one. $out now carries a vmlinux only where it is the deliverable, matching
build.sh more closely than before. Named `vmlinux` and not `debug`, which
nixpkgs' multiple-outputs machinery treats specially -- the same trap class
as the earlier dev/devel mistake.

VALIDATED against the profile shipped in rehosting/penguin:latest, for
6.13/armel: 61 lines both, identical key set, and every value identical
except two:
  - task.per_cpu_offsets_addr, off by 8 bytes
  - name, which embeds the kernel build timestamp; ours is '@0' because the
    build sets KBUILD_BUILD_TIMESTAMP for reproducibility. Self-consistent,
    since the kernel and its profile come from the same derivation.
Every other struct offset and address matches exactly, across a completely
different toolchain.

Known contract gap: perf.<target> is not reproduced. build.sh swallows its
failure, so the shipped tarball already contains whatever compiled that day;
no consumer was found in penguin. See nix/release.nix.
…toolchain

6.13/powerpc64le did not build: CONFIG_COMPAT (a defconfig default we never
chose -- configs/6.13/powerpc64le sets only CONFIG_CPU_LITTLE_ENDIAN) makes
kbuild compile a 32-bit vDSO, and the build died with

    cc1: error: '-m32' not supported in this configuration

The cause is structural. get_cc compiles EVERY powerpc* target with a single
powerpc64 big-endian toolchain -- bitness and endianness come from the
kernel's own arch Makefile via Kconfig, not from the triple. kernelsmith
models the four variants as four independent arches, and the per-variant
Bootlin toolchains are not equivalent:

    powerpc64   (BE, power8):  -m32 OK    -m64 OK  -mlittle/-mbig OK
    powerpc64le (LE, power8):  -m32 FAIL

Production confirms the arrangement: the shipped ppc64le kernel in
rehosting/penguin:latest reports 'powerpc64-linux-musl-gcc (GCC) 11.2.1' --
a big-endian-triple compiler built the little-endian kernel. So this is a
fidelity fix, not a workaround.

Verified the obvious hazard -- a BE-default compiler emitting a big-endian
vDSO into an LE kernel -- does NOT occur: -mlittle-endian comes from
KBUILD_CPPFLAGS/cflags for CONFIG_CPU_LITTLE_ENDIAN and vdso32's
CC32FLAGSREMOVE strips only -mcmodel=/-mabi=/-mcall-aixdesc, so -m32 lands on
top of it. The built artifacts are ELF32 little endian:

    sigtramp32-32.o   Class: ELF32  Data: little endian
    vdso32.so.dbg     Class: ELF32  Data: little endian

All four powerpc cells now build. Bonus: powerpcle no longer needs a
from-source musl-cross-make toolchain (Bootlin ships no powerpcle at all),
so the matrix got cheaper.

TODO(kernelsmith): upstream this as a kernel-specific resolver
(kernelToolchainFor), NOT as a change to toolchainFor -- userland musl for
powerpc64le should still use the powerpc64le triple.
rehosting/kernelsmith#2 is merged, so the branch no longer needs
--override-input to resolve a toolchain for those two arches. All 20 cells
evaluate from the pinned input alone.
The shell build ran perf with `|| echo Warning` and copied it behind a
`[ -f ]` guard, so an arch whose perf did not compile shipped without one --
rehosting/penguin:latest carries perf for 3 of 13 targets as a result. perf.nix
already fails loudly instead; these are the cells that were failing, plus one
that was silently wrong.

  4.10/x86_64      perf resolves its tools headers with
                   -I tools/arch/$(ARCH)/include/uapi, using ARCH verbatim
                   rather than the SRCARCH kbuild derives from it, and
                   tools/arch/x86_64/ does not exist. Reported as a missing
                   uapi/asm/mman.h, which names the wrong file entirely.

  4.10/mips64{eb,el}
                   the musl ld defaults to the n32 emulation while the objects
                   are n64. LD= on the make command line does not fix it:
                   tools/perf/Makefile does `unexport MAKEFLAGS`, so
                   command-line variables never reach the nested tools/lib/*
                   builds -- which is exactly where the link fails. A PATH shim
                   survives that. Not LDEMULATION, which the HOST ld building
                   fixdep would also pick up.

  6.13/loongarch64 the only glibc target. perf's cross-compile feature
                   detection fails, so it defines its own gettid and
                   pthread_attr_setaffinity_np, colliding with glibc's real
                   ones. Also needs the WRAPPED cross cc, for the sysroot, and
                   glibc's separate static output. Its dlfilters cannot link at
                   all -- binutils 2.41 segfaults on a .so against a
                   static-only glibc -- so drop dlfilters everywhere: they are
                   dlopen plugins, dead weight in a -static perf, and never
                   shipped.

The silently-wrong one: perf.nix had inherited kernel.nix's powerpc family
collapse, which routes all four variants through a single biarch powerpc64 BE
toolchain. Correct for a kernel -- freestanding, and arch/powerpc/Makefile
takes bitness and endianness from Kconfig -- and wrong for perf, which has no
Kconfig and links -static against a target libc. It produced four
byte-identical big-endian 64-bit binaries, three of which cannot run on their
guest. Every cell built, so nothing complained.

20/20 cells now build, and the powerpc four are 32-bit BE, 32-bit LE, 64-bit BE
and 64-bit LE respectively.
…cell outputs

Slice 3. Two things:

1. Per-cell perf/osi/cosi/driver outputs. Without them a broken perf could only
   be reached through the whole release tarball, which rebuilds every cell to
   show you one compiler error.

2. nix/driver.nix builds igloo.ko for each cell from THIS FLAKE's kernel
   derivation, through kernelsmith's new buildModule, with no tarball round
   trip. That is the acceptance test for the kernel `dev` output: a build tree
   that cannot build the one module we care about is broken, and this repo is
   where that should be caught.

   The CRC property is the point. _in_container_build.sh extracts
   kernel-devel-all.tar.gz to a scratch directory and builds against whatever is
   there, so nothing structurally prevents pairing a module with a different
   kernel than the one it is inserted into. Here the kernel derivation is an
   input, so a mismatched pair is not representable.

igloo_driver is added as a source-only input for this. That is not a claim
about which repo should own the driver build long-term -- under the
provider/consumer split both linux_builder and igloo_driver are consumers of
kernelsmith, and igloo_driver owning its own build is the better end state.
Fixing the powerpc family collapse exposed a target that is not real.

arch/powerpc/platforms/Kconfig.cputype:

    config CPU_LITTLE_ENDIAN
        depends on PPC_BOOK3S_64

A 32-bit little-endian powerpc kernel cannot be expressed in mainline Linux.
configs/6.13/powerpcle sets CONFIG_CPU_LITTLE_ENDIAN=y, olddefconfig silently
drops it, and the built vmlinux is byte-identical to powerpc's (verified by
sha256 -- same hash, both 32-bit MSB). configs/4.10/powerpcle is already
retired as .unused.

So powerpcle's kernel is big-endian, and building perf 'as the target name
implies' would ship a little-endian userspace binary that cannot run on it.
Point perf at the powerpc toolchain instead, so the pair is at least
self-consistent.

The larger question -- whether this cell should exist at all, given it costs a
kernel, a perf, an osi/cosi extraction and a driver build per release to
duplicate 'powerpc' under a name that claims otherwise -- is for draft 34, not
for this commit.
The push step was guarded by `if: env.CACHIX_AUTH_TOKEN != ''` while the
token was set in that same step's `env:` block. A step's own env is not in
scope for its own `if:`, so the condition was always false and the cache was
never populated -- the exact failure the workflow was written to prevent.
Hoisted to job level, where the `if:` can see it.

Also:
- The gate now builds kernel + perf + module for its two cells, not just the
  kernel. perf and the module fail independently of the kernel, and both have
  done so recently.
- A dispatch-only full-matrix job (20 cells + both release tarballs), with
  per-cell OK/FAIL lines so a single failure is attributable rather than buried.
- Fixed the base.json path, which went through a pointless
  patches/<version>/../ indirection.

Still unexercised on the self-hosted runners; the note at the top stands until
a real run.
Two bugs shipped on this branch that every other check passed: perf emitted
four byte-identical big-endian 64-bit binaries across the powerpc family, and
configs/6.13/powerpcle asked for an endianness Kconfig cannot grant so the
kernel came out big-endian. Both built cleanly and produced files of the right
name in the right place.

They are one failure class -- an ELF whose class or byte order disagrees with
what its name promises -- and reading the ELF header catches it for nothing.

Checks the unstripped `vmlinux` output rather than $out: only some targets
deliver a vmlinux in $out (the rest ship zImage/bzImage/Image, compressed blobs
with no readable class), so checking $out would silently skip armel, arm64,
loongarch64, riscv64 and x86_64 -- five of thirteen, which is the same partial
coverage the check exists to prevent. Missing artifacts now fail rather than
being skipped.

powerpcle is excluded, with the exclusion reported as SKIP rather than passing
quietly: a 32-bit little-endian powerpc kernel is not expressible in mainline
Linux, so there is no honest expectation to assert -- LSB would fail a build
behaving as well as it can, MSB would bless a name that lies. Draft 34 carries
the recommendation to retire the target.
arch/powerpc/platforms/Kconfig.cputype:

    config CPU_LITTLE_ENDIAN
        bool "Build little endian kernel"
        depends on PPC_BOOK3S_64

PPC_BOOK3S_64 is 64-bit only, so on a 32-bit config the symbol is not
selectable. configs/6.13/powerpcle was '#include "powerpc"' plus
CONFIG_CPU_LITTLE_ENDIAN=y; olddefconfig dropped the line without a word and
built a big-endian kernel. vmlinux.powerpcle was verified BYTE-IDENTICAL
(sha256) to vmlinux.powerpc.

The cell cost a kernel, a static perf, an osi + cosi extraction and an igloo.ko
per release, to ship 'powerpc' twice under a name asserting the opposite -- and
had penguin ever selected it for genuinely little-endian firmware it would have
been handed a big-endian kernel silently. (powerpc LE hardware starts at
POWER8/ppc64le, so the likely device impact is zero, but that is luck.)

4.10 already retired this target. This brings 6.13 into line: 19 cells, not 20.
The .unused file keeps the reasoning and the original two lines, so nobody
rediscovers this from scratch.

Drops the powerpcle special case from perf.nix (toolchainArch is now the
identity for every cell in the matrix) and the exclusion from shape.nix, which
was only there because no honest expectation could be written for the target.

Also pins kernelsmith to the pushed buildmodule branch, so driver cells build
with no --override-input.
The branch pin was only ever a bridge while rehosting/kernelsmith#3 was in
review. It merged rebased, so the commit hashes changed -- but the input's
narHash did not, and all 19 driver cells resolve to the same store paths with
no rebuild. Same content, permanent ref.
Every job in the previous draft died at "Set up job" with

    Unable to resolve action rehosting/nix-setup, repository not found

before running a step -- the referenced action repo does not exist. It is now a
local composite action, version-locked to the workflow that uses it and unable
to 404, mirroring the install-nix-action setup that already works on
rehosting-arc in hyperfs.

Also sets the substituters in the runner's nix.conf rather than relying on
flake.nix's nixConfig, which is ignored for non-trusted users -- the failure
mode there is not an error but a silent full rebuild of every cross toolchain.
install-nix-action downloads the installer fine and then fails with

    you do not have 'xz' installed, which I need to unpack the binary tarball

The rehosting-arc image has no xz. Guarded on command -v so it costs nothing on
a runner that already has it.
2729 lines -> 163. The real content is 109 insertions and 10 deletions; the
other ~2600 lines were an artifact.

Every context line in the patch carried a leading space that was never
stripped: it was generated by treating a unified diff's body as file content,
so the " " that marks a context line became part of the line. That is not
confined to the patch file -- it is in the tree we ship:

                                        pristine 6.13    IGLOO (before)
    # directives at column 0                      120                 9
    # directives indented one space                 0               119
    struct x; forward decls indented                0                62

Indented preprocessor directives are legal C, which is why this shipped in
Aug 2025 and no compiler has complained since.

Repaired by reconstructing the header against pristine upstream: where a line
is equal modulo whitespace, upstream's formatting wins; IGLOO's genuinely new
lines are kept verbatim. The amended commit was then rebased under the
remaining 18 patches of the series, all of which applied clean.

Verified:

  - `diff -w` old vs new syscalls.h is empty, and across the whole patched
    tree syscalls.h is the ONLY file that differs.
  - The committed series re-imports from scratch: 33 commits apply cleanly to
    the pinned upstream tarball and yield the same header.
  - kernel-6.13-armel rebuilt and compared section by section: .text, .rodata,
    .data, .init.text and __ksymtab are byte-identical. The only differences
    anywhere are .debug_line (-12 bytes), .debug_info, and the 20-byte GNU
    build-id -- DWARF records column numbers, so de-indenting moves them, and
    the build-id is a hash over the linked image. The raw boot image differs in
    exactly those 20 contiguous build-id bytes out of 26.7 MB.

So: no change to the compiled kernel's behaviour, and the worst rebase hazard
in the series is gone. For comparison, 4.10's equivalent patch was already
clean at 141 lines; a scan of all 71 patches finds no other instance.

Note for anyone using the round-trip tooling: export-series.sh renumbers the
whole series (format-patch numbers 1..33, while the committed names come from
an older 37-commit export and have gaps where core/ patches sit), so running it
for a one-patch fix yields 33 renames. This change regenerated only 0019's body
and kept its original header. Worth making export stable against existing names.
The previous commit added a local composite action because
`rehosting/nix-setup@v1` 404'd. The action does exist -- as
`rehosting/ci/actions/nix-setup@v1`, alongside arc-registry-setup, pull-image
and cleanup-pr-cache. Only the path was wrong, and nine workflows across
penguin, fw2tar, qemu and vpnguin already consume it.

The shared action is a superset of what was hand-rolled here: it installs the
xz/curl the Arc pods lack (same fix, same reasoning), sets
accept-flake-config, and exposes enable-kvm -- which matters on these runners,
where install-nix-action's KVM/udevadm step fails on pods that expose /dev/kvm
without a working udevd. Set to false; nothing here needs a VM.

It also owns the Cachix wiring via cachix-action, so the manual `nix profile
install nixpkgs#cachix` + `cachix push` steps are gone, and with them the
job-level CACHIX_AUTH_TOKEN that only existed so a step-level `if:` could see
it.

One behaviour change, deliberate: nix-setup's skip-push default means PR runs
PULL from rehosting-tools but do not push, with pushes to main populating it.
That is the org convention (the rationale is fork PRs and pull-only tokens
403ing the post step) and it differs from what this workflow did before, which
pushed from PRs too. Acceptable because the expensive closures -- the cross
toolchains -- are already in the cache from the earlier run, so PRs now
substitute them rather than rebuilding.

Switches to secrets.CACHIX_REHOSTING, which is what the other nix workflows in
the org use; CACHIX_AUTH_TOKEN also exists at org level but is not the
convention.
nix-setup's Cachix step failed on both jobs with

    $USER must be set. If running in a container, try setting USER=root.

The Arc pods run as a uid with no passwd entry, so $USER and $LOGNAME are
unset and HOME-relative cache paths are not writable. `cachix use` refuses to
run in that state.

rehosting/qemu's nix workflow already carries exactly this block; I dropped it
when adapting that workflow on the assumption it was incidental. It is a
precondition for running Nix on this runner pool.

TMPDIR and XDG_CACHE_HOME are created before Nix runs, since pointing at a
path that does not exist fails the same way slightly later.
Unrelated to this branch's contents -- infrastructure drift that happens to
show up as a red check on it.

setup-buildx-action's default buildkit image has moved onto a version carrying
the /proc/acpi masking regression, so build-image now dies with

    can't mask dir "/proc/acpi": mount src=tmpfs, dst=/proc/acpi,
    flags=MS_RDONLY, data=nr_blocks=1,nr_inodes=1: invalid argument

on the kernel-5.4 Arc runners. The workflow already knew not to pin
moby/buildkit:master for exactly this reason, but trusting the default is no
longer sufficient. main last ran 2026-07-28 and so has not hit it yet; it will
on its next run.

v0.30.0 is the same pin igloo-dev applies in its source-nightly publish for
this regression.
penguin's flake pins the release TARBALL (`inputs.kernels`, flake = false) and
relies on Nix unpacking it. For a Nix consumer to take these kernels from this
flake instead, going through `kernels-latest` would mean packing ~335 MB of
archive purely so the consumer can unpack it again.

`kernelsTree` is that payload as a plain directory -- `<version>/<artifacts>`,
which is exactly the layout penguin lays down at /igloo_static/kernels/. It is
exposed as the `kernels` package.

No duplication of the assembly: kernelsTarball is now defined in terms of
kernelsTree, so the two cannot drift. The README.txt and the reproducible-tar
flags are unchanged, so `kernels-latest` is byte-identical to before.
@lacraig2

Copy link
Copy Markdown
Contributor Author

Downstream consumer: rehosting/penguin#932 takes packages.x86_64-linux.kernels from this branch as a flake input, replacing penguin's pinned kernels-latest.tar.gz artifact.

That PR is the concrete answer to "who consumes this and does the seam hold": mk-igloo-static.nix is unchanged, and iglooStatic/dockerImage both evaluate against the tree output added in 8ff5c95. It is pinned at nix-patchset and blocked on this merging.

It also surfaces what the switch changes for penguin's shipped image — perf for 19/19 cells instead of 7/19, and powerpcle gone (verified safe: it is a userspace ABI under powerpc64le in penguin's abi_info.py, not a kernel arch).

@lacraig2

Copy link
Copy Markdown
Contributor Author

Built .#kernels-latest and diffed its manifest against the live v3.6.5 release (the Docker-built one penguin pins today). The two differ in exactly two ways, in opposite directions:

Only in the shipped Docker tarball

kernels/6.13/cosi.powerpcle.json.xz
kernels/6.13/osi.powerpcle.config
kernels/6.13/vmlinux.powerpcle

Only in the nix tarball

4.10: perf.mips64eb perf.mips64el perf.x86_64
6.13: perf.arm64 perf.mips64eb perf.mipseb perf.mipsel
      perf.powerpc perf.powerpc64 perf.powerpc64le
      perf.riscv64 perf.x86_64

Everything else is present in both — every kernel image, every osi/cosi for a real target, README.txt. perf coverage goes 7/19 -> 19/19; the release has been shipping without perf on 12 of 19 cells because build.sh swallowed the failures.

To be precise about what this does and does not show: this is a manifest comparison, not a byte one. The file contents necessarily differ — the two are built by entirely different toolchains (unversioned musl.cc downloads vs kernelsmith pins), which is much of the point of the change.

Lets the nix path produce a real, downloadable release before it takes over
the version line, so downstream consumers can be tested against an actual
artifact rather than a branch.

The tag prefix is deliberately NOT `dev_*`: build.yml already triggers on that,
so a shared prefix would have both pipelines cut a release for the same tag and
race version-increment for the same vX.Y.Z. `nixdev_*` does not match `dev_*`
(the globs are anchored), so the two release paths stay independent while both
exist. The tag itself is the version -- no version-increment here -- and the
release is always a prerelease.

The full 19-cell matrix now runs on such a tag as well as on dispatch, which
has a second effect that matters downstream: a tag is a push event, so
nix-setup's skip-push default does not apply and everything built lands in
rehosting-tools. A consumer pinning this flake (rehosting/penguin#932) then
substitutes all 19 kernels instead of cross-building them in its own CI.

Also logs the tarball manifest and per-version perf coverage, so the difference
from the Docker release is visible in the run without downloading anything.
lacraig2 added a commit to rehosting/penguin that referenced this pull request Aug 14, 2026
An immutable ref, so this PR's CI tests a fixed tree instead of whatever
nix-patchset happens to point at when a job starts.

The tag also runs linux_builder's full 19-cell matrix and, being a push event
rather than a PR, pushes all of it to rehosting-tools. CI here therefore
substitutes the kernels rather than cross-building 19 of them, which is what
makes this testable at all in a normal CI budget.

Repin to a release tag (or main) once rehosting/linux_builder#59 lands.

Evaluates to the same igloo-static derivation as the branch pin did -- the
tagged commit differs only in workflow YAML.
@lacraig2

Copy link
Copy Markdown
Contributor Author

Cut nixdev_0.1.0 from this branch — the first nix-built release. Full matrix green, both tarballs published as a prerelease, and CI's own manifest confirms the perf coverage:

4.10: perf for 7 of 7 targets
6.13: perf for 12 of 12 targets

One regression this surfaced, worth fixing before nix takes over the version line. kernel-devel-all.tar.gz is 479 MB vs the Docker path's 253 MB — 89% larger. Cause: _in_container_build.sh deliberately slims the staged devel tree and the dev output in kernelsmith does not replicate it:

find "$OUTDIR" -name '*.cmd' -delete
find "$OUTDIR" -name '*.o' ! -path '*/arch/powerpc/lib/crtsavres.o' \
     ! -path '*/scripts/*' ! -path '*/tools/*' -delete
find "$OUTDIR" -name '*.c'  ! -path '*/scripts/*' ! -path '*/tools/*' -delete

An M= build compiles the module's own sources against prebuilt objects + headers and never reads in-tree .c/.o/.cmd. The nix dev output already drops arch/*/boot, realmode and all of tools/ except objtool, but keeps sources and objects — 91 MB/cell raw, of which include/ is 57 MB and arch/ 26 MB.

Consumer is igloo_driver (its build.sh and CI download this), not penguin — so it does not block #932, but it does mean igloo_driver CI would pull nearly double.

The fix belongs in kernelsmith's dev output rather than here, since that is what makes it a devel tree rather than a raw build directory — which is also what every distro kernel-devel package does. Note the crtsavres.o exception is load-bearing: buildModule stages that file for ppc32 module links, and the Docker script carries the identical exception for the identical reason.

@lacraig2

Copy link
Copy Markdown
Contributor Author

nixdev_0.1.0 ships a 4.10/x86_64 kernel that does not boot

Found while triaging rehosting/penguin#932, which switched to these kernels. run_tests (x86_64, 4.10) fails there while every other 4.10 arch passes.

It is not the config. The nix and Docker .config for 4.10/x86_64 are byte-identical (diff = 0 lines). It is the compiler.

Booted both directly, no penguin involved:

$ qemu-system-x86_64 -M pc -m 512 -kernel <docker>/4.10/bzImage.x86_64 \
      -append "console=ttyS0 panic=1" -nographic
[    0.000000] Linux version 4.10.0 (gcc version 6.5.0 (GCC)) #1 SMP ...
   -> 19313 bytes of kernel output

$ qemu-system-x86_64 -M pc -m 512 -kernel <nix>/4.10/bzImage.x86_64 ...
   -> 285 bytes. Not one kernel line.

With earlyprintk=serial the failure point is exact — it decompresses fine and dies in early startup, before console init:

Decompressing Linux... Parsing ELF... done.
Booting the kernel.
<nothing, ever>

Why x86_64 specifically

bootlin-sources.nix says it outright:

# ---- k4: gcc 9.3.0 (2020.08-1); x86_64 has no musl that old -> 2021.11-5 (gcc 10.3) ----
(t "x86_64" "k4" "2021.11-5" "sha256-KUNhf2U3...")

x86_64 is the only arch in the k4 band that does not get the band's intended compiler. Every other 4.10 target builds with gcc 9.3.0 and boots; x86_64 falls forward to gcc 10.3.0 because Bootlin has no x86_64 musl toolchain that old. The Docker build used gcc 6.5.0 here.

Confirmed by contrast — same nix tree, same run:

cell compiler boots
4.10 armel gcc 9.3.0 (Buildroot 2020.08) yes
4.10 x86_64 gcc 10.3.0 (Buildroot 2021.11) no
6.13 x86_64 (k6) yes — 25 KB of output, matching Docker's 25 KB

So this is 4.10 + x86_64 + gcc 10.3 only. 6.13/x86_64 is fine.

Two things to fix

  1. The toolchain. k4/x86_64 should not silently take a newer band's compiler. The fromsource-extra mechanism already exists for exactly this — it routes k3-x86_64 to musl-cross-make because "Bootlin's musl for those starts too late." k4/x86_64 has the same problem and should get the same treatment, pinned to the band's gcc 9.3 (or lower).

  2. Nothing boots a kernel in CI. shape-check cannot catch this: the bzImage has the right class, byte order and machine, and is the right size (8.1 MB, same as Docker's). It compiles, it packages, it is dead. kernelsmith has boot.nix; nix.yml should gate on a boot smoke test for at least one cell per (band, arch), or this class recurs silently the next time a toolchain moves.

The fallback-forward is a reasonable compromise to have made — it is just not one that can be made without a boot test behind it.

@lacraig2

Copy link
Copy Markdown
Contributor Author

Correction: I over-attributed this to the gcc version

My comment above concluded "it is the compiler," on the strength of same-config/different-compiler. I have since tested the compiler axis directly and that conclusion does not hold as stated.

Built linux_builder's patched 4.10 source with the exact same .config, natively on x86_64 (no cross toolchain involved), against two different compilers:

build result
gcc 10.5.0 285 bytes — dead, same as the Bootlin gcc 10.3 cross build
gcc 9.5.0 285 bytes — also dead

So the boundary is not between gcc 9 and gcc 10, and "route k4/x86_64 to the band's gcc 9.4 instead of Bootlin's 10.3" — my proposed fix — would very likely not have fixed it. Good thing to find out before pinning something and declaring victory.

What still stands, unchanged and re-verified:

  • nix 4.10/bzImage.x86_64 produces zero kernel output; the Docker one produces 19,313 bytes.
  • The two .config files are byte-identical.
  • It decompresses and dies before console init (Decompressing Linux... Parsing ELF... done. Booting the kernel. then silence).
  • 6.13/x86_64 from the same nix tree boots fine.

What is now open is which variable it is. Two candidates remain, and I am testing both:

  1. A lower compiler boundary. Docker's working kernel is gcc 6.5.0. Everything from 9.5 up is dead. The cutoff could be anywhere in 7.x/8.x. Testing patched-source + gcc 7.5.

  2. Source divergence. This is the one I should have considered first. The nix path builds pristine tarball + patch series; the Docker path builds the fork branch. Those are asserted to be equivalent by verify-series.sh — but note what CI actually runs:

    ./scripts/verify-series.sh "${{ matrix.version }}" "${{ steps.tarball.outputs.path }}"

    No <fork-ref>, no <git-dir>. Per the script's own header, the fork-branch comparison is opt-in; without those two arguments it only proves "the series applies cleanly", not "the result matches the branch it replaces." So a 4.10 series that reconstructs a different tree than the fork would pass this gate silently. Testing pristine-source + gcc 9.5 to isolate it.

Either way the second half of my earlier comment gets stronger, not weaker: nothing boots a kernel in this pipeline. kernelsmith's boot.nix does have a k4-x86_64 cell, but it boots 5.10.229 — the k4 band spans 4.x and 5.x and is boot-validated only at the top of that range, so 4.10 has never been booted by anything. That gap is what let this ship in nixdev_0.1.0.

Will post the isolating result.

nixdev_0.1.0 published a 4.10/x86_64 bzImage that does not boot. It
prints nothing -- not one kernel line. Every gate on this branch passed
it:

  * the build succeeded
  * shape-check passed (a bzImage's ELF class says nothing about whether
    it runs; it is also the same 8.1 MB as the Docker build's)
  * the series gate passed (it only proves the patches APPLY)
  * kernelsmith's own boot.nix passed -- but its k4 cell boots 5.10.229,
    and the k4 band spans 4.x AND 5.x, so 4.10 had never been booted by
    anything, here or upstream

It was caught by a downstream consumer's integration tests
(rehosting/penguin#932), which is two repos and one release too late.

nix/boot.nix boots each shipped kernel headless under qemu with no
rootfs and asserts two things: the kernel banner appears, and it reached
the root-fs stage (a VFS/panic marker -- the whole pre-userspace path).
Verified against both outcomes before committing:

  boot-6.13-armel    ok
  boot-4.10-x86_64   FAIL: no kernel banner -- it never started

The machine/console table is transcribed from penguin's
src/penguin/arch_registry.py, so each kernel is booted on the machine
penguin will actually run it on -- booting it on some other qemu model
is not the claim any consumer needs. 32-bit powerpc is unbootable by
declaration (arch_registry records qemu_machine=None for it); that is an
explicit SKIP entry rather than a silent omission, and an unknown target
is a hard error rather than a pass.

Wired into both CI jobs: the per-cell gate boots its cell, and the full
matrix boots all 19.

This does NOT fix 4.10/x86_64 -- it makes it impossible to ship. The
cause is still open; see the PR discussion. It is not the compiler
(reproduced with gcc 6.5/7.5/9.5/10.5) and the .config is byte-identical
to the Docker build's.
@lacraig2

Copy link
Copy Markdown
Contributor Author

Second correction: it is not the source either, and my local harness was invalid

I said above I was testing "a lower compiler boundary" vs "source divergence." Both lines are now closed, and neither hypothesis is supported. Posting the negative result because I put two wrong attributions on this PR already.

Compiler: ruled out

Built linux_builder's patched 4.10 source, same byte-identical .config, natively on x86_64, across four compilers:

gcc result
10.5.0 dead (285 bytes)
9.5.0 dead
7.5.0 dead
6.5.0 — the exact version the working Docker kernel reports dead

So my original "route k4/x86_64 off Bootlin's gcc 10.3 onto the band's 9.4" would not have fixed anything.

Source: not shown, because the harness is broken

The control that was supposed to isolate source — pristine upstream 4.10, zero IGLOO patches, same config, gcc 6.5 — is also dead, 285 bytes.

That invalidates the harness, not the kernel. If my local native build cannot produce a booting 4.10/x86_64 from untouched upstream, it cannot discriminate anything, and every gcc result in the table above is void as evidence about the real build. (Disabling nixpkgs' cc-wrapper hardening did not change it; host binutils 2.41 being far too new for 4.10's x86 boot path is my next guess, but I am not going to assert a third cause I have not proven.)

I should have validated the harness against a known-good build before drawing conclusions from it. That is the actual mistake here, and it is why this comment exists twice.

What is actually established

All directly observed, same qemu invocation throughout:

artifact result
nix 4.10/bzImage.x86_64 (nixdev_0.1.0) 285 bytes — no kernel banner
Docker 4.10/bzImage.x86_64 (releases/latest) 19,313 bytes, boots to VFS panic
nix 6.13/bzImage.x86_64 25,354 bytes, boots — matches Docker's 25,347
the two 4.10 .config files byte-identical

With earlyprintk=serial the nix image decompresses and dies before console init:

Decompressing Linux... Parsing ELF... done.
Booting the kernel.
<nothing, ever>

The cause is open. 4.10/x86_64 should be treated as broken in nixdev_0.1.0.

What I have landed (e707180)

nix/boot.nix — boot every shipped kernel headless under qemu, assert the banner appears and that it reached the root-fs stage. Verified against both outcomes:

boot-6.13-armel    ok
boot-4.10-x86_64   FAIL: no kernel banner -- it never started

Machine/console table transcribed from penguin's src/penguin/arch_registry.py, so each kernel boots on the machine penguin will actually run it on. 32-bit powerpc is an explicit SKIP (arch_registry records qemu_machine=None); an unknown target is a hard error, not a pass. Wired into both CI jobs.

This does not fix 4.10/x86_64 — it makes it unshippable, which is the part that should not have been missing.

One more gap worth fixing separately

CI runs verify-series.sh <version> <tarball> with no <fork-ref>/<git-dir>, so per the script's own header it proves only "the series applies cleanly" — never that the reconstructed tree matches the fork branch it replaces. That is a real hole in the migration argument regardless of whether it turns out to be this bug. (I could not use it as a control here: main_4.10 HEAD has moved well past the export point — it now has drivers/gva_hc and fs/hyperfs where the series produces drivers/igloobase.)

Wiring boot.nix up against the full matrix turned up three harness bugs.
All three reported a healthy kernel as dead, which is the failure mode a
boot gate can least afford -- it trains you to distrust the gate.

1. The image glob was not a glob. The comment claimed it globbed "rather
   than tabulated" to avoid drift; the code hard-coded four names
   (bzImage/zImage/Image/vmlinux). 6.13/loongarch64 ships
   vmlinuz.efi.loongarch64 and failed as "no bootable image" while the
   kernel was fine. Now ranks every *.<target> artifact, preferring the
   arch's wrapped image over raw vmlinux -- the wrapped one is what
   penguin boots -- and warns rather than fails on an unranked name.

2. loongarch64 needs two things nothing else here does, both of which
   penguin already supplies (penguin_run.py:659):
     * >1G RAM -- qemu's virt machine refuses to start below it
     * EFI firmware -- kernel_fmt is vmlinuz.efi, a PE image, and the
       built-in loader rejects it ("The image is not ELF")
   Booting its ELF vmlinux instead would have passed this test while
   testing an image penguin never runs.

   The pinned nixpkgs (24.05, qemu 8.2.7) predates nixpkgs shipping
   edk2-loongarch64-code.fd, so this adds a separate nixpkgs-qemu input
   for the harness alone. Worth doing on its own terms: what the kernel
   builds against and what we boot on should not be forced to move
   together, and the latter should track what penguin runs.

3. Asserting on the "Linux version" banner alone was too strict. On
   loongarch64 the EFI stub hands over after console setup, so the
   earliest printks never reach the serial log -- a kernel that booted
   to a root-fs panic was reported as never having started. Any
   timestamped printk is the portable evidence; the genuinely dead
   4.10/x86_64 image emitted none, so this still catches it.

Full matrix with kernelsmith#5: 18 boot, 1 declared SKIP (32-bit
powerpc, which arch_registry.py records as having no qemu machine).
4.10/x86_64 -- the cell that started all of this -- now reaches the
root-fs stage.

flake.lock pins kernelsmith ahead of main pending kernelsmith#5.
@lacraig2

Copy link
Copy Markdown
Contributor Author

Root cause of 4.10/x86_64: binutils, not gcc

Correcting my two earlier comments on this thread — the gcc attribution was wrong, and so was my subsequent claim that the native harness was invalid. That gcc sweep (6.5 / 7.5 / 9.5 / 10.5, all dead) was a well-controlled result ruling gcc out, and it left binutils as the one axis never varied — it sat at 2.41 throughout.

binutils 2.31 began emitting R_X86_64_PLT32 where it previously emitted R_X86_64_PC32. Linux only learned that relocation in 4.16 (b21ebf2fb4cd). Any x86_64 kernel older than that, built with binutils ≥ 2.31, links and packages perfectly and does not boot. Bootlin's k4 x86_64 cell is binutils 2.36.1.

Fix is in rehosting/kernelsmith#5 — resolve k4-x86_64 to the existing k3 cell (gcc 6.5.0 + binutils 2.27). This mirrors what rehosting/embedded-toolchains did by hand: an x86_64-legacy stage pinning binutils 2.30, the last release before 2.31, selected for (x86_64, 4.10) alone in _in_container_build.sh. The reason was never written down, so it didn't survive the port to a version-keyed resolver.

Isolation is clean: gcc held at 6.5.0 across both 4.10 runs, only the linker moved, and the kernel went from silent to reaching the root-fs stage.

Full boot matrix now green

4.10:  armel arm64 mipsel mipseb mips64el mips64eb x86_64          7 ok
6.13:  armel arm64 mipsel mipseb mips64el mips64eb powerpc64
       powerpc64le riscv64 loongarch64 x86_64                     11 ok
6.13-powerpc: SKIP (arch_registry.py records qemu_machine=None)

Three harness bugs this exposed (0cee4bf)

Wiring boot.nix against the full matrix found three cases where a healthy kernel was reported as dead — the failure mode a boot gate can least afford:

  1. The image glob was not a glob. Its comment claimed it globbed "rather than tabulated"; the code hard-coded four names. 6.13/loongarch64 ships vmlinuz.efi.loongarch64 and failed as "no bootable image".
  2. loongarch64 needs >1G RAM and EFI firmware — both of which penguin already passes (penguin_run.py:659). Its kernel_fmt is vmlinuz.efi, a PE image the built-in loader rejects. Booting its ELF vmlinux instead would have passed this test while testing an image penguin never runs. The pinned nixpkgs (24.05, qemu 8.2.7) predates edk2-loongarch64-code.fd, so the harness now takes qemu from a separate nixpkgs-qemu input.
  3. Asserting on the Linux version banner alone was too strict. On loongarch64 the EFI stub hands over after console setup, so the earliest printks never reach the serial log. Any timestamped printk is the portable evidence — the genuinely dead 4.10/x86_64 image emitted none, so the dead-kernel case is still caught.

flake.lock pins kernelsmith ahead of main pending rehosting/kernelsmith#5; re-lock once that merges.

…landed

Drops the temporary pin at the PR branch. Also picks up kernelsmith's
$dev slimming (kernelsmith#4), which changes what an `M=` module build
reads out of the kernel-devel tree -- igloo_driver is the consumer that
exercises that, and its matrix is what confirms it.

Boot matrix against kernelsmith main is byte-identical to the branch
build (same /nix/store path): 18 ok, 1 declared SKIP.
The nix path builds every cell, boots every cell, and cuts the releases.
Nothing reads the Docker path any more, so it goes:

  Dockerfile, .Dockerignore          the embedded-toolchains image
  build.sh, _in_container_build.sh   the in-container build
  .github/workflows/build.yml        "Compile and release"
  .github/workflows/clear_cache.yml  its Harbor kernel-cache janitor
  .gitmodules, linux/{4.10,6.13}     the two fork branches patches/ replaced

The submodules were already dead and already wrong: .gitmodules declared
main_6.7 for linux/6.13, and the pin was a tree 89,775 commits from the
branch it named (see nix/source.nix, patches/README.md).

ONE capability came with build.sh rather than with Docker, so it is
ported rather than dropped: `--config-only`, which cpp-assembles a config,
runs savedefconfig, and diffs. nix/lint.nix does the same and is exposed
as `.#config-lint-<version>-<target>` and `.#config-lint`. It stays
ADVISORY -- the Docker version discarded its own exit status, because
savedefconfig prunes defaults and de-duplicates, so a readable #include-
structured config never equals its savedefconfig form. Asserting on it
would mean giving up the #include structure to satisfy the check.

Comments across nix/ still name _in_container_build.sh where they are a
faithful port of a specific step in it. Those are deliberate provenance
-- they record WHY a piece of the build looks the way it does -- and the
README now says so, and says where the file went.

The README described the submodule workflow and pointed at
panda-re/linux_builder for releases. Rewritten around the flake: quoting
cell names, where source comes from, the three checks (and why boot-check
is the one that matters), config linting, cutting a nixdev_* release, and
who consumes this repo. It also records the two things the Docker path did
silently: unversioned musl.cc toolchain downloads, so no kernel this repo
ever released has a recorded compiler identity; and the hand-built
x86_64-legacy toolchain pinned to binutils 2.30 whose unwritten reason was
the R_X86_64_PLT32 boundary.

Verified: `nix flake check` passes, and kernel-6.13-armel and boot-check
resolve to the same store paths as before this commit.

NOTE FOR SEQUENCING -- this removes the workflow that cuts non-prerelease
v* releases, which two Docker-path consumers still pull:
  * penguin main pins linux_builder v3.6.5/kernels-latest.tar.gz
  * igloo_driver's Docker CI pulls releases/latest/kernel-devel-all.tar.gz
Both keep working (existing releases stay downloadable) but neither can
receive a NEW kernel release after this lands. penguin#932 and
igloo_driver#100 are the nix-path replacements for those two consumers and
should merge first.
configs/<version>/<target> is a cpp fragment that #includes shared
pieces, so the file you edit is almost never the file an option comes
from -- and the config the kernel is BUILT with is a third thing again,
because olddefconfig runs afterwards and drops anything whose
dependencies are unmet. Three questions follow, one tool each.

.#config-required -- THE GATE. Asserts every cell ENDED UP with the
options IGLOO needs, reading the post-olddefconfig .config out of the
kernel's dev output rather than the fragments. That distinction is the
whole point: a fragment saying CONFIG_MODVERSIONS=y proves nothing about
what shipped. Seven options, each recorded with what breaks without it.
MODVERSIONS is the sharpest -- without it a mismatched igloo.ko loads
SILENTLY instead of being rejected, which is strictly worse than the
mismatch we spent this branch chasing. Reports every violation across
every cell before failing, rather than stopping at the first.

.#config-explain -- provenance. Resolves the #include chain and shows
where an option is set, which assignment wins, and via what path. It
exists because grep does not answer the question: CONFIG_IGLOO is set in
all-common.inc and no target sets it directly, so grepping a target
config finds nothing at all. Builds no kernel, so it stays instant.

.#config-redundant -- separates two things that look alike and are not:
an option assigned twice (always a bug, only the last has effect) and an
option that did not survive olddefconfig (usually an unnoticed
dependency). Advisory.

What they found on this tree:
  * 595 duplicated assignments across the matrix, including
    CONFIG_MODULES twice in 6.13/all-common.inc, 3 lines apart
  * conflicting values where 6.13/powerpc re-declares =m over
    all-common.inc's =y
  * 2-68 options per cell that do not survive olddefconfig
All 19 cells DO satisfy the contract today; the gate is green.

Wired into CI's full-matrix job: config-required as a gate next to
boot-check (both exist for kernels that build and boot and then quietly
do not do their job), the redundancy report as continue-on-error so the
output lands in the log without failing the build.

The gate deliberately does not pipe python into tee: a pipeline's status
is its last command, so a failing check would be masked by a succeeding
tee unless pipefail is set. stdenv does set it -- but a gate that
silently passes when a shell option changes is the exact failure class
this file exists to catch.

Verified by negative test: adding a bogus required option fails all 19
cells with the per-cell reason, and removing it returns to green.
Removing build.yml took the release mechanism with it. nix.yml only ever
cut PRERELEASES from nixdev_* tags -- deliberately, so it could not race
the Docker pipeline for a version number -- so after that removal this
repo could not cut a vX.Y.Z release at all. Nothing downstream noticed
because existing releases stay downloadable, which is exactly why it
would have been found late.

Ported from build.yml into the full-matrix job:
  * `full` now also runs on a push to main. With the Docker pipeline gone
    it is the only thing that produces a release, so a merge has to build
    the whole matrix, not the 2-cell gate.
  * `reecetech/version-increment@2023.10.1` with `use_api: true`, then a
    real (non-pre) release with both tarballs.

Prereleases stay as they were and stay separate: a nixdev_* tag IS its
own version, so a downstream repo can be handed a fixed, immutable kernel
set to test against without consuming a version number.

The nixdev_* prefix now outlives its original reason -- it existed to
avoid colliding with build.yml's dev_* trigger. Kept anyway: nixdev_0.1.0
and nixdev_0.1.1 are published and pinned by downstream lockfiles.
Comments that described build.yml as still existing are corrected rather
than deleted, since they explain why the prefix is what it is.

README documents both paths, and how to move the version LINE: push a
bare marker tag at main's tip and let the patch increment continue from
it. Explicitly NOT by setting `increment: minor`, which is not
self-clearing and silently bumps the release after it too.
@lacraig2
lacraig2 merged commit b87fad4 into main Aug 15, 2026
15 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant