Kernel release 6.18.38-rt-cuckoo+: reproducible build, guarded installer

Rebuilt with KBUILD_BUILD_USER/HOST/TIMESTAMP pinned. Without them the kernel
bakes the builder's user@hostname and wall-clock build time into /proc/version,
which publishes operator identity in a public artifact and makes the build
unreproducible. Pinned, anyone can rebuild and diff the bytes — the only trust
story available for an unsigned community kernel.

The installer refuses to run on anything but a Pi 4, refuses a mismatched
image/modules pair (vermagic failure means no /dev/pps0 on a kernel that
otherwise boots fine — silent and miserable to debug), verifies SHA256SUMS,
never touches kernel8.img, and leaves rollback as deleting one config.txt line.

Boot-tested on the Pi: Stratum 1, 273 ns offset, PPS handler confirmed NOT
threaded. Binaries ship as release assets, not in git.
This commit is contained in:
Ryan Malloy 2026-07-14 09:52:17 -06:00
parent 6b6d80ded3
commit 3b505644b2
15 changed files with 333 additions and 62 deletions

5
.gitignore vendored
View File

@ -4,3 +4,8 @@ __pycache__/
dist/
*.egg-info/
node_modules/
# Kernel release binaries — 32 MB. These are published as Gitea release assets,
# not committed. Rebuild them with kernel/build.sh; verify with SHA256SUMS.
kernel/release/*.img.gz
kernel/release/*.tar.gz

View File

@ -0,0 +1,57 @@
/* Self-hosted: this dashboard runs on a LAN and must work with no internet.
* Latin subset only the latin-ext/greek/vietnamese blocks Google serves
* would triple the payload for glyphs a status page will never render.
*
* Atkinson Hyperlegible is the Braille Institute's legibility face. It is
* here for a functional reason, not a brand one: it disambiguates 0/O, 1/l/I
* and rn/m, which is exactly what you need when reading `+66 ns` or
* `2607:f740:e::d3` at a glance from across a room.
*/
@font-face {
font-family: "Atkinson Hyperlegible";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("/static/fonts/atkinson-400.woff2") format("woff2");
}
@font-face {
font-family: "Atkinson Hyperlegible";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url("/static/fonts/atkinson-700.woff2") format("woff2");
}
@font-face {
font-family: "Atkinson Hyperlegible Mono";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("/static/fonts/atkinson-mono-400.woff2") format("woff2");
}
@font-face {
font-family: "Atkinson Hyperlegible Mono";
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("/static/fonts/atkinson-mono-600.woff2") format("woff2");
}
@font-face {
font-family: "Libre Baskerville";
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("/static/fonts/baskerville-400.woff2") format("woff2");
}
@font-face {
font-family: "Libre Baskerville";
font-style: normal;
font-weight: 700;
font-display: swap;
src: url("/static/fonts/baskerville-700.woff2") format("woff2");
}

View File

@ -171,6 +171,13 @@
<div class="foot-meta">
<span id="foot-version">gpsntp-dashboard</span>
<!-- Anyone reading "PPS locked · ±219 ns" and wondering what that means,
or why this box is a Pi and not an appliance, deserves one click to
the write-up rather than a search. -->
<a class="foot-docs" href="https://cuckoo.warehack.ing" rel="noopener">
How this clock works
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="M4 8h7M8 5l3 3-3 3"/></svg>
</a>
<span id="foot-updated"></span>
</div>
</footer>

View File

@ -1,13 +1,34 @@
@import url("/static/fonts/fonts.css");
/* Supported Systems house style, applied as WARM SHELL / COOL DATA.
*
* The chrome background, panels, type, brand accent comes from
* supported.systems: warm near-black, cream text, Libre Baskerville display,
* Atkinson Hyperlegible body, brass accent.
*
* The INSTRUMENTS do not. Cyan, green, amber and red stay exactly as they were,
* because on this page those colors carry information: which constellation a
* satellite belongs to, whether a source is selected, whether the clock is
* healthy. Recoloring them to brass would trade meaning for brand, which on a
* status page is a bad trade at any exchange rate.
*
* So: --brand is for chrome, --accent is for live data. They are different
* variables on purpose. Don't merge them.
*/
:root {
--bg: #0a0e13;
--bg-2: #0e141c;
--panel: #121a23;
--panel-2: #16212d;
--border: #21303f;
--border-soft: #1a2632;
--text: #e7eef5;
--muted: #8a99a8;
--faint: #57687a;
/* shell — warm (supported.systems) */
--bg: #171717;
--bg-2: #1b1a18;
--panel: #232120;
--panel-2: #2b2825;
--border: #3d382f;
--border-soft: #322e28;
--text: #eeece8;
--muted: #a8a196;
--faint: #6f6a61;
--brand: #d9a441; /* brass. chrome, links, brand marks. NOT data. */
/* instruments — cool (semantic; do not rebrand) */
--accent: #22d3ee;
--good: #34d399;
--warn: #f5a524;
@ -20,8 +41,15 @@
--c-sbas: #a3e635;
--c-qzss: #2dd4bf;
--c-other: #94a3b8;
--shadow: 0 1px 0 rgba(255, 255, 255, 0.03), 0 12px 30px -18px rgba(0, 0, 0, 0.9);
--mono: ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Code", Menlo, monospace;
/* Atkinson Hyperlegible: the Braille Institute's legibility face. Chosen for
* function before brand it disambiguates 0/O, 1/l/I, rn/m, which is exactly
* what a glanceable status page needs. */
--sans: "Atkinson Hyperlegible", system-ui, -apple-system, sans-serif;
--serif: "Libre Baskerville", Georgia, serif;
--mono: "Atkinson Hyperlegible Mono", ui-monospace, "SF Mono", Menlo, monospace;
}
* { box-sizing: border-box; }
@ -29,11 +57,13 @@
body {
margin: 0;
background:
radial-gradient(1200px 600px at 80% -10%, rgba(34, 211, 238, 0.06), transparent 60%),
radial-gradient(900px 500px at 0% 0%, rgba(52, 211, 153, 0.05), transparent 55%),
/* the warm bench light from supported.systems */
radial-gradient(1100px 620px at 50% -8%, rgba(217, 164, 65, 0.07), transparent 62%),
/* one cool wash, bottom-left, so the instruments feel like instruments */
radial-gradient(900px 500px at 0% 100%, rgba(34, 211, 238, 0.04), transparent 58%),
var(--bg);
color: var(--text);
font: 15px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
font: 15px/1.5 var(--sans);
-webkit-font-smoothing: antialiased;
min-height: 100vh;
}
@ -47,12 +77,16 @@ svg { width: 1.15em; height: 1.15em; fill: none; stroke: currentColor; stroke-wi
gap: 16px; padding: 16px 20px;
border-bottom: 1px solid var(--border-soft);
position: sticky; top: 0; z-index: 5;
background: linear-gradient(var(--bg), rgba(10, 14, 19, 0.86));
background: linear-gradient(var(--bg), rgba(23, 23, 23, 0.86));
backdrop-filter: blur(8px);
}
.brand { display: flex; align-items: center; gap: 13px; }
.brand-icon { width: 30px; height: 30px; color: var(--accent); }
.brand h1 { font-size: 19px; margin: 0; letter-spacing: 0.3px; font-family: var(--mono); }
/* The dish is brand chrome, not a reading — brass, not cyan. */
.brand-icon { width: 30px; height: 30px; color: var(--brand); }
/* Libre Baskerville, as on supported.systems. The wordmark is the one place a
serif belongs on this page; everything that reports a NUMBER stays mono. */
.brand h1 { font-size: 20px; margin: 0; letter-spacing: 0.2px; font-family: var(--serif);
font-weight: 700; }
.brand-sub { margin: 0; color: var(--muted); font-size: 12.5px; }
.conn { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--muted);
@ -75,7 +109,7 @@ main { max-width: 1180px; margin: 0 auto; padding: 20px; display: grid; gap: 18p
.panel-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
.panel-head h2 { font-size: 14px; margin: 0; color: var(--muted); font-weight: 600;
display: flex; align-items: center; gap: 8px; text-transform: uppercase; letter-spacing: 0.6px; }
.panel-head h2 svg { color: var(--accent); }
.panel-head h2 svg { color: var(--brand); }
/* ---------- hero ---------- */
.hero { display: flex; align-items: center; justify-content: space-between; gap: 24px; flex-wrap: wrap; }
@ -104,7 +138,7 @@ main { max-width: 1180px; margin: 0 auto; padding: 20px; display: grid; gap: 18p
.card { background: var(--panel); border: 1px solid var(--border-soft); border-radius: 14px; padding: 14px 15px; }
.card-head { display: flex; align-items: center; gap: 7px; color: var(--muted); font-size: 12px;
text-transform: uppercase; letter-spacing: 0.4px; }
.card-head svg { width: 15px; height: 15px; color: var(--accent); }
.card-head svg { width: 15px; height: 15px; color: var(--brand); }
.card-value { font-family: var(--mono); font-size: 25px; font-weight: 650; margin: 7px 0 3px; letter-spacing: 0.3px; }
.card-value.good { color: var(--good); }
.card-value.warn { color: var(--warn); }
@ -179,8 +213,13 @@ main { max-width: 1180px; margin: 0 auto; padding: 20px; display: grid; gap: 18p
/* ---------- footer ---------- */
.foot { max-width: 1180px; margin: 0 auto; padding: 14px 20px 30px; }
.foot-meta { display: flex; justify-content: space-between; margin-top: 14px;
color: var(--faint); font-size: 12px; font-family: var(--mono); }
.foot-meta { display: flex; justify-content: space-between; align-items: center; gap: 12px;
margin-top: 14px; color: var(--faint); font-size: 12px; font-family: var(--mono); }
.foot-docs { display: inline-flex; align-items: center; gap: 5px; color: var(--faint);
text-decoration: none; transition: color .2s; }
.foot-docs:hover { color: var(--brand); }
.foot-docs svg { width: 13px; height: 13px; stroke-width: 1.5; transition: transform .2s; }
.foot-docs:hover svg { transform: translateX(2px); }
/* ---------- the maker's plate ----------
* Clockmakers signed the backplate the brass face only a repairer sees once
@ -199,7 +238,7 @@ main { max-width: 1180px; margin: 0 auto; padding: 20px; display: grid; gap: 18p
var(--panel);
transition: color .2s, border-color .2s;
}
.ss-plate__link:hover { color: var(--text); border-color: var(--accent); }
.ss-plate__link:hover { color: var(--text); border-color: var(--brand); }
.ss-plate__logo { flex: 0 0 auto; width: 46px; height: auto; opacity: .85;
transition: opacity .2s; }
@ -209,9 +248,9 @@ main { max-width: 1180px; margin: 0 auto; padding: 20px; display: grid; gap: 18p
font-family: var(--mono); font-size: 12px; font-weight: 600;
text-transform: uppercase; letter-spacing: .14em; }
.ss-plate__body { display: block; font-size: 13px; line-height: 1.55; max-width: 62ch; }
.ss-plate__name { color: var(--accent); }
.ss-plate__name { color: var(--brand); }
.ss-plate__cta { display: inline-flex; align-items: center; gap: 5px; margin-top: 8px;
font-size: 12px; color: var(--accent); }
font-size: 12px; color: var(--brand); }
.ss-plate__cta svg { width: 14px; height: 14px; transition: transform .2s; }
.ss-plate__link:hover .ss-plate__cta svg { transform: translateX(2px); }

View File

@ -56,4 +56,4 @@ deploy: ## Pull main + rebuild the docs container on the prod host
@echo "==> deploying $(DEPLOY_HOST):$(DEPLOY_PATH)"
ssh -A $(DEPLOY_HOST) "cd $(DEPLOY_PATH) && git fetch origin main && git reset --hard origin/main && cd docs-site && make prod"
@echo "==> sanity check"
@curl -s -o /dev/null -w " HTTP %{http_code} %{url_effective}\n" "https://cuckoo.warehack.ing/explanation/bug-detection/"
@curl -s -o /dev/null -w " HTTP %{http_code} %{url_effective}\n" "https://cuckoo.warehack.ing/explanation/preempt-rt-made-it-worse/"

View File

@ -1,6 +1,6 @@
---
title: Downloads — prebuilt RT kernel
description: A patched PREEMPT_RT kernel for the Raspberry Pi 4, so you don't need a cross-compile toolchain.
description: A patched PREEMPT_RT kernel for the Raspberry Pi 4, boot-tested and benchmarked, so you don't need a cross-compile toolchain.
sidebar:
order: 5
---
@ -9,28 +9,37 @@ The only expensive part of [the patch](/reference/the-patch/) is the toolchain.
Building natively on a Pi 4 takes hours; cross-compiling needs an x86 box and a
setup session. So here's the artifact.
**Current release: `6.18.38-rt-cuckoo+`** — Raspberry Pi's `rpi-6.18.y`,
`bcm2711_defconfig`, `PREEMPT_RT`, plus the `IRQF_NO_THREAD` patch.
:::danger[Read this before you download]
- **Raspberry Pi 4 / arm64 only.** `bcm2711_defconfig`. It will not boot a Pi 5 or
a Pi 3.
- **Unsigned, community-built.** We built this on a workstation. There is no chain
of trust here beyond "we published the exact recipe and the checksums." If that
isn't good enough for your environment — and for some environments it correctly
isn't — [build it yourself](/how-to/cross-compile-rt-kernel/). It's forty
minutes.
- **Verify the checksums.** They're in `SHA256SUMS`.
- **Raspberry Pi 4 / arm64 only.** The installer refuses to run on anything else,
because a `bcm2711` kernel on a Pi 5 doesn't boot — and a black screen on a
headless box is exactly the outcome we're trying to prevent.
- **Unsigned, community-built.** There is no chain of trust here beyond published
checksums and a reproducible recipe. For some environments that is correctly
not good enough. [Build it yourself](/how-to/cross-compile-rt-kernel/) — it's
forty minutes, and `kernel/build.sh` in the repo does the whole thing.
- **Verify the checksums.** The installer does it for you if `SHA256SUMS` is
present. Don't skip it.
:::
## Artifacts
Published on the [releases page](https://git.supported.systems/warehack.ing/cuckoo-escapement/releases):
On the [releases page](https://git.supported.systems/warehack.ing/cuckoo-escapement/releases):
| File | What |
| File | |
|---|---|
| `kernel-rt-<ver>.img.gz` | The kernel image, gzipped (Pi OS's own format) |
| `rt-modules-<ver>.tar.gz` | Matching modules — **must** be installed with the image |
| `install-rt-kernel.sh` | Installer. Adds a *new* image, never replaces `kernel8.img` |
| `kernel-rt-6.18.38-rt-cuckoo+.img.gz` | The kernel, gzipped (Pi OS's own format) — 9.8 MB |
| `rt-modules-6.18.38-rt-cuckoo+.tar.gz` | Matching modules — 22 MB. **Must** be installed with the image. |
| `install-rt-kernel.sh` | Installer. Adds a *new* image; never replaces `kernel8.img`. |
| `SHA256SUMS` | Checksums |
```
97533ea81ec56ef090908560e88bccfb2a3f7dfcc3d33a5a1cbe4bbf15163b55 kernel-rt-6.18.38-rt-cuckoo+.img.gz
6af126711eef2ae1651ac48e19353e6830f12fb0ef9a7e265f54c27206a4f0eb rt-modules-6.18.38-rt-cuckoo+.tar.gz
```
## Install
```bash
@ -39,34 +48,68 @@ sudo ./install-rt-kernel.sh
sudo reboot
```
The installer:
Then confirm the patch is actually live. This is the check that matters:
1. Untars the modules into `/lib/modules/`
2. Writes the image as `/boot/firmware/kernel-rt.img`**`kernel8.img` is left
alone**
3. Appends one line, `kernel=kernel-rt.img`, to `config.txt`
```console
$ uname -a
Linux … 6.18.38-rt-cuckoo+ #1 SMP PREEMPT_RT … aarch64
**Rollback is deleting that one line.** Mount the SD card's FAT partition on any
machine, remove it, and the stock kernel boots. That's deliberate: you should never
have to make a physical trip to a headless box because of a kernel you got from a
website.
## What's in it
Raspberry Pi's `rpi-6.12.y` tree, `bcm2711_defconfig`, plus exactly two changes:
```bash
scripts/config --enable PREEMPT_RT
# + the IRQF_NO_THREAD patch in drivers/pps/clients/pps-gpio.c
$ ps -eo comm | grep irq/.*pps
(nothing)
```
Nothing else. The full recipe is in
[Cross-compile an RT kernel](/how-to/cross-compile-rt-kernel/), and you should be
able to reproduce this byte-for-byte modulo build timestamps.
**That empty output is the whole point.** If an `irq/NN-pps@…` thread appears,
the handler is being force-threaded and [your timestamp is being taken behind the
scheduler](/explanation/preempt-rt-made-it-worse/).
## Rollback is one line
The installer appends exactly one line to `config.txt` and never touches
`kernel8.img`. If the Pi doesn't come back: pull the SD card, open `config.txt`
on any machine (it's a FAT partition — a Windows laptop will do), delete the
`kernel=kernel-rt.img` line, put the card back. The stock kernel boots.
It also leaves a `config.txt.before-rt` next to it.
## What we actually tested
Not "it compiled." This exact artifact, installed by that exact script, on a Pi 4:
| | |
|---|---|
| Boots | yes |
| `uname` | `6.18.38-rt-cuckoo+ SMP PREEMPT_RT` |
| PPS handler threaded | **no** — patch confirmed live |
| chrony | Stratum 1, ref PPS |
| System offset | 273 ns |
| Raw PPS jitter | 952 ns σ over 62 s |
:::note[One window is not a benchmark]
That 952 ns is a single 62-second sample, on a fresh boot under a clear sky, and
it is *better* than the 2568 ns in [the measurements](/reference/measurements/).
We report it because it's what we saw — not as a revision of the headline
numbers. Those came from an [A/B/A protocol](/how-to/benchmark-pps-jitter/), and
one lucky window doesn't overturn them. Sky conditions move this figure around
more than most software changes do. Measure your own board.
:::
## Reproducing it
```bash
git clone https://git.supported.systems/warehack.ing/cuckoo-escapement
cd cuckoo-escapement/kernel && ./build.sh
```
`KBUILD_BUILD_USER`, `KBUILD_BUILD_HOST` and `KBUILD_BUILD_TIMESTAMP` are pinned
in that script. Without them a kernel bakes the builder's `user@hostname` and the
wall-clock build time into `/proc/version` — which publishes whoever built it and
makes the output unreproducible. Pinned, you should be able to rebuild this and
compare bytes. For an unsigned kernel from a website, that's the only trust story
we can honestly offer.
:::note[Pinned to a tested version]
The published download always points at a kernel we have **actually booted and
benchmarked** on a Pi 4 — not simply the newest upstream. Shipping a stranger an
unvalidated kernel for a machine they may not be able to physically reach is not
something we're willing to do.
The published download always points at a kernel we have **booted and benchmarked
on a Pi 4** — never simply the newest upstream. Shipping a stranger an unvalidated
kernel, for a machine they may not be able to physically reach, is not something
we're willing to do.
:::

33
kernel/build.sh Executable file
View File

@ -0,0 +1,33 @@
#!/usr/bin/env bash
# Reproducible RT kernel for the Cuckoo Escapement release.
#
# KBUILD_BUILD_{USER,HOST,TIMESTAMP} are the whole point of this rebuild: without
# them the kernel bakes the builder's `user@hostname` and the wall-clock build
# time into /proc/version, which (a) publishes operator identity and (b) makes
# the artifact unreproducible. Pinning all three means anyone can rebuild this
# and diff the bytes.
set -euo pipefail
cd "$HOME/src"
PATCH="$(cd "$(dirname "$0")" && pwd)/0001-pps-gpio-keep-timestamp-in-hard-irq-under-PREEMPT_RT.patch"
[ -d linux ] || git clone --depth=1 --branch rpi-6.18.y https://github.com/raspberrypi/linux
cd linux
export ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-
export KBUILD_BUILD_USER=cuckoo
export KBUILD_BUILD_HOST=escapement
export KBUILD_BUILD_TIMESTAMP='Mon Jan 1 00:00:00 UTC 2024'
make bcm2711_defconfig
./scripts/config --enable EXPERT --enable PREEMPT_RT \
--set-str LOCALVERSION "-rt-cuckoo" --disable LOCALVERSION_AUTO
make olddefconfig
git apply --check "$PATCH" 2>/dev/null && git apply "$PATCH" && echo "PATCH APPLIED" \
|| grep -q IRQF_NO_THREAD drivers/pps/clients/pps-gpio.c && echo "PATCH ALREADY PRESENT"
grep -q "IRQF_NO_THREAD" drivers/pps/clients/pps-gpio.c || { echo "FATAL: patch missing"; exit 1; }
make -j"$(nproc)" Image modules dtbs
echo "BUILD OK: $(cat include/config/kernel.release)"

View File

@ -0,0 +1,2 @@
97533ea81ec56ef090908560e88bccfb2a3f7dfcc3d33a5a1cbe4bbf15163b55 kernel-rt-6.18.38-rt-cuckoo+.img.gz
6af126711eef2ae1651ac48e19353e6830f12fb0ef9a7e265f54c27206a4f0eb rt-modules-6.18.38-rt-cuckoo+.tar.gz

View File

@ -0,0 +1,85 @@
#!/usr/bin/env bash
#
# Install the Cuckoo Escapement PREEMPT_RT kernel on a Raspberry Pi 4.
#
# The design goal here is ONE THING: you must never need physical access to the
# SD card because of something this script did. So:
#
# - kernel8.img (the stock kernel) is never touched
# - the RT kernel installs under a NEW filename
# - the ONLY change to config.txt is a single appended line
#
# If it doesn't boot: pull the card, open config.txt on any machine (it's a FAT
# partition, so a Windows laptop will do), delete that one line, put it back.
# The stock kernel returns. That's the entire rollback.
#
# Run it FROM the directory holding the release files.
set -euo pipefail
IMG_NAME="kernel-rt.img"
BOOT="/boot/firmware"
die() { printf '\nERROR: %s\n' "$1" >&2; exit 1; }
[[ $EUID -eq 0 ]] || die "run with sudo"
# --- Refuse to run on hardware this kernel was not built for. A bcm2711 kernel
# --- on a Pi 5 does not boot, and the failure mode is a black screen on a
# --- headless box, which is exactly what we promised wouldn't happen.
MODEL="$(tr -d '\0' < /proc/device-tree/model 2>/dev/null || echo unknown)"
case "$MODEL" in
*"Raspberry Pi 4"*|*"Raspberry Pi Compute Module 4"*|*"Raspberry Pi 400"*) ;;
*) die "this kernel is bcm2711 (Pi 4) only — found: $MODEL" ;;
esac
[[ "$(uname -m)" == "aarch64" ]] || die "need a 64-bit OS (uname -m says $(uname -m))"
[[ -d "$BOOT" ]] || die "$BOOT not found — is this Raspberry Pi OS?"
IMG_GZ="$(ls -1 kernel-rt-*.img.gz 2>/dev/null | head -1)" || true
MODS="$(ls -1 rt-modules-*.tar.gz 2>/dev/null | head -1)" || true
[[ -n "${IMG_GZ:-}" && -n "${MODS:-}" ]] || die "run this from the directory containing the release files"
# --- The image and the modules MUST come from the same build. A mismatched pair
# --- means modprobe fails on vermagic — and since pps-gpio is a module, that
# --- means no /dev/pps0 and no refclock, on a kernel that otherwise boots fine.
# --- Silent, and miserable to debug. Check it up front.
IMG_VER="${IMG_GZ#kernel-rt-}"; IMG_VER="${IMG_VER%.img.gz}"
MOD_VER="${MODS#rt-modules-}"; MOD_VER="${MOD_VER%.tar.gz}"
[[ "$IMG_VER" == "$MOD_VER" ]] \
|| die "image ($IMG_VER) and modules ($MOD_VER) are from different builds"
if [[ -f SHA256SUMS ]]; then
echo "==> verifying checksums"
sha256sum -c SHA256SUMS || die "checksum mismatch — do not install this"
else
echo "!! no SHA256SUMS present — skipping verification (not recommended)"
fi
echo "==> installing modules for $MOD_VER"
tar -C / -xzf "$MODS"
depmod -a "$MOD_VER"
echo "==> installing kernel as $BOOT/$IMG_NAME (kernel8.img left alone)"
zcat "$IMG_GZ" > "$BOOT/$IMG_NAME"
if grep -qE "^kernel=$IMG_NAME" "$BOOT/config.txt"; then
echo "==> config.txt already selects $IMG_NAME"
else
cp "$BOOT/config.txt" "$BOOT/config.txt.before-rt"
printf '\n# Cuckoo Escapement RT kernel. DELETE THIS LINE to boot the stock kernel.\nkernel=%s\n' \
"$IMG_NAME" >> "$BOOT/config.txt"
echo "==> appended 'kernel=$IMG_NAME' to config.txt (backup: config.txt.before-rt)"
fi
cat <<EOF
Done. Reboot, then confirm:
uname -a # expect $MOD_VER ... SMP PREEMPT_RT
ps -eo comm | grep irq/.*pps # expect NOTHING — the handler must not be
# a thread. If you see one, the patch isn't
# in this kernel and your jitter will be bad.
To roll back: delete the 'kernel=$IMG_NAME' line from $BOOT/config.txt
(or restore config.txt.before-rt) and reboot.
EOF