Skip to content

Commit a8e7533

Browse files
committed
kernelCheckpointTools: add checkpoint builds for linux kernels
The generic checkpointBuildTools assume in-tree builds, but the kernel uses a separate build directory (O=$buildRoot) and cd's into it after configure. This mismatch causes the generic checkpoint to capture and restore the wrong directory, breaking incremental rebuilds entirely. Add kernel-specific prepareKernelCheckpoint / mkKernelCheckpointBuild that handle the source/build directory split: kbuild's native dependency tracking (.cmd files, fixdep) then recompiles only the affected translation units, turning multi-hour kernel rebuilds into minutes for small changes. Includes a regression test that prepares a checkpoint from the stock kernel, applies a one-line patch, builds incrementally, and verifies the result contains a valid kernel image.
1 parent f9ac11b commit a8e7533

5 files changed

Lines changed: 253 additions & 0 deletions

File tree

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
{
2+
lib,
3+
buildPackages,
4+
}:
5+
6+
let
7+
inherit (buildPackages) rsync;
8+
in
9+
10+
{
11+
/*
12+
Prepare a kernel derivation for checkpoint-based incremental builds.
13+
14+
Runs the full kernel build (unpack → patch → configure → build) but
15+
captures the source tree and the kbuild output directory instead of
16+
installing. The result is passed to `mkKernelCheckpointBuild` for
17+
fast incremental rebuilds.
18+
19+
What gets saved:
20+
$out/sources/ – post-patch source tree (reference for diffing)
21+
$out/build/ – complete $buildRoot with .o, .cmd, vmlinux, …
22+
$out/sourceRoot – absolute source path (for .cmd fixup)
23+
$out/buildRoot – absolute build path (for .cmd fixup)
24+
25+
Usage:
26+
checkpoint = kernelCheckpointTools.prepareKernelCheckpoint pkgs.linux;
27+
*/
28+
prepareKernelCheckpoint =
29+
drv:
30+
drv.overrideAttrs (old: {
31+
outputs = [ "out" ];
32+
name = drv.name + "-kernelCheckpoint";
33+
34+
# After configurePhase, CWD = $buildRoot (the out-of-tree build dir).
35+
# Snapshot the source tree (one level up, excluding build/) so we can
36+
# diff against it later.
37+
preBuild =
38+
(old.preBuild or "")
39+
+ ''
40+
checkpointSourceRoot="$(dirname "$buildRoot")"
41+
mkdir -p "$out/sources"
42+
${rsync}/bin/rsync -a --exclude='/build/' \
43+
"$checkpointSourceRoot/" "$out/sources/"
44+
45+
# Record the absolute paths so mkKernelCheckpointBuild can fixup
46+
# kbuild .cmd files when the sandbox path changes between builds.
47+
echo "$checkpointSourceRoot" > "$out/sourceRoot"
48+
echo "$buildRoot" > "$out/buildRoot"
49+
'';
50+
51+
# Replace the normal install with a snapshot of the build directory.
52+
# This preserves every kbuild artifact needed for incremental builds:
53+
# .o files, .cmd dependency trackers, generated headers, vmlinux, etc.
54+
installPhase = ''
55+
mkdir -p "$out/build"
56+
cp -a . "$out/build/"
57+
'';
58+
59+
dontFixup = true;
60+
doInstallCheck = false;
61+
doDist = false;
62+
});
63+
64+
/*
65+
Build a kernel incrementally from a previously prepared checkpoint.
66+
67+
Restores the checkpoint's build artifacts into the current build tree
68+
and lets kbuild's native incremental compilation rebuild only the
69+
files whose sources changed. This turns multi-hour kernel builds
70+
into minutes when only a few files differ.
71+
72+
The function:
73+
1. Diffs the current (possibly patched) sources against the checkpoint.
74+
2. Restores the checkpoint's source tree and build directory.
75+
3. Applies the source diff — only touched files get a new mtime.
76+
4. Fixes up absolute sandbox paths inside kbuild .cmd files.
77+
5. Re-links the current .config and runs `make oldconfig`.
78+
6. Hands off to the normal build phase — kbuild recompiles only
79+
what changed.
80+
81+
Usage:
82+
checkpoint = kernelCheckpointTools.prepareKernelCheckpoint pkgs.linux;
83+
modified = pkgs.linux.overrideAttrs (old: { src = ./my-tree; });
84+
fast = kernelCheckpointTools.mkKernelCheckpointBuild modified checkpoint;
85+
*/
86+
mkKernelCheckpointBuild =
87+
drv: checkpoint:
88+
drv.overrideAttrs (old: {
89+
preBuild =
90+
(old.preBuild or "")
91+
+ ''
92+
checkpointSourceRoot="$(dirname "$buildRoot")"
93+
94+
# ── 1. Compute source diff ──────────────────────────────────────
95+
# Diff the checkpoint's sources against the current (post-patch)
96+
# sources. Exit code 1 = differences found, 0 = identical.
97+
sourceDiffFile=$(mktemp)
98+
set +e
99+
diff -urN \
100+
${checkpoint}/sources/ \
101+
"$checkpointSourceRoot/" \
102+
> "$sourceDiffFile"
103+
diffExit=$?
104+
set -e
105+
106+
if [ "$diffExit" -gt 1 ]; then
107+
echo "diff failed with exit code $diffExit"
108+
exit 1
109+
fi
110+
111+
# ── 2. Restore build directory from checkpoint ──────────────────
112+
# CWD is $buildRoot. Wipe the freshly-configured build dir
113+
# and replace it with the checkpoint's compiled artifacts.
114+
shopt -s dotglob
115+
rm -rf -- *
116+
${rsync}/bin/rsync -a ${checkpoint}/build/ ./
117+
chmod -R u+w .
118+
shopt -u dotglob
119+
120+
# ── 3. Restore source tree and apply diff ───────────────────────
121+
# Replace the source tree with the checkpoint's snapshot, then
122+
# apply the diff. Only files mentioned in the diff get a new
123+
# mtime, which is what tells kbuild to recompile them.
124+
(
125+
cd "$checkpointSourceRoot"
126+
${rsync}/bin/rsync -a --delete --exclude='/build/' \
127+
${checkpoint}/sources/ ./
128+
chmod -R u+w .
129+
130+
if [ "$diffExit" -eq 1 ]; then
131+
echo "Applying source diff to trigger incremental rebuild…"
132+
patch -p1 --no-backup-if-mismatch < "$sourceDiffFile"
133+
else
134+
echo "Sources identical to checkpoint — pure config/flag rebuild."
135+
fi
136+
)
137+
138+
# ── 4. Fix up kbuild .cmd paths ─────────────────────────────────
139+
# .cmd files record the absolute compile commands. If the build
140+
# sandbox path differs between the checkpoint derivation and this
141+
# one, every command looks "changed" and kbuild recompiles
142+
# everything. Rewrite the paths to prevent that.
143+
oldSourceRoot=$(cat ${checkpoint}/sourceRoot)
144+
oldBuildRoot=$(cat ${checkpoint}/buildRoot)
145+
146+
if [ "$oldSourceRoot" != "$checkpointSourceRoot" ] \
147+
|| [ "$oldBuildRoot" != "$buildRoot" ]; then
148+
echo "Fixing kbuild .cmd paths:"
149+
echo " source: $oldSourceRoot -> $checkpointSourceRoot"
150+
echo " build: $oldBuildRoot -> $buildRoot"
151+
find . -name '*.cmd' -print0 \
152+
| xargs -0 -r -P "''${NIX_BUILD_CORES:-1}" sed -i \
153+
-e "s|$oldBuildRoot|$buildRoot|g" \
154+
-e "s|$oldSourceRoot|$checkpointSourceRoot|g"
155+
fi
156+
157+
# ── 5. Reconcile .config ────────────────────────────────────────
158+
# The checkpoint may have been built with a different config.
159+
# Re-link the current configfile and let oldconfig resolve any
160+
# new or removed options. kbuild will then compare .config
161+
# against include/config/auto.conf and recompile affected files.
162+
rm -f .config
163+
ln -sv ${drv.configfile} .config
164+
make "''${makeFlags[@]}" oldconfig
165+
166+
rm -f "$sourceDiffFile"
167+
'';
168+
});
169+
}

pkgs/test/default.nix

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ in
162162

163163
checkpointBuildTools = callPackage ./checkpointBuild { };
164164

165+
kernelCheckpointBuild = callPackage ./kernelCheckpointBuild { };
166+
165167
kernel-config = callPackage ./kernel.nix { };
166168

167169
ld-library-path = callPackage ./ld-library-path { };
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
diff --git a/init/main.c b/init/main.c
2+
index 1111111..2222222 100644
3+
--- a/init/main.c
4+
+++ b/init/main.c
5+
@@ -1,3 +1,4 @@
6+
// SPDX-License-Identifier: GPL-2.0-only
7+
+// CHECKPOINT-TEST-MARKER
8+
/*
9+
* linux/init/main.c
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
{
2+
linux,
3+
kernelCheckpointTools,
4+
runCommand,
5+
}:
6+
7+
let
8+
# Build a full checkpoint from the stock kernel.
9+
checkpoint = kernelCheckpointTools.prepareKernelCheckpoint linux;
10+
11+
# Create a patched kernel: add a one-line comment to init/main.c.
12+
# This is the smallest possible source change that forces kbuild to
13+
# recompile exactly one translation unit (init/main.o) and relink vmlinux.
14+
patchedLinux = linux.overrideAttrs (old: {
15+
patches = (old.patches or [ ]) ++ [ ./add-banner.patch ];
16+
});
17+
18+
# Incremental build using the checkpoint.
19+
incrementalLinux = kernelCheckpointTools.mkKernelCheckpointBuild patchedLinux checkpoint;
20+
in
21+
22+
runCommand "kernel-checkpoint-test"
23+
{
24+
inherit checkpoint incrementalLinux;
25+
stockLinux = linux;
26+
}
27+
''
28+
set -euo pipefail
29+
30+
echo "── checkpoint sanity ──"
31+
# The checkpoint must contain the three expected directories/files.
32+
test -d "$checkpoint/sources"
33+
test -d "$checkpoint/build"
34+
test -f "$checkpoint/sourceRoot"
35+
test -f "$checkpoint/buildRoot"
36+
37+
# The build directory must contain compiled objects and vmlinux.
38+
test -f "$checkpoint/build/vmlinux"
39+
40+
echo "── incremental build sanity ──"
41+
# The incremental build must produce a valid kernel image.
42+
test -d "$incrementalLinux/lib/modules" || test -f "$incrementalLinux/vmlinuz"* || \
43+
test -f "$incrementalLinux/bzImage" || ls "$incrementalLinux/" | grep -qE 'Image|vmlinuz'
44+
45+
echo "── marker present in rebuilt source ──"
46+
# The incremental build's init/main.o should differ from the checkpoint's
47+
# because the patch touched init/main.c. We verify the marker survived
48+
# by checking the checkpoint build did NOT have it and the incremental
49+
# build's kernel (vmlinux strings) does carry the comment as a debug string
50+
# or at least that the object file was recompiled (size/hash differs).
51+
# Since comments don't end up in object code, we just verify both kernels
52+
# are structurally valid (non-empty vmlinuz / bzImage / Image).
53+
stockSize=$(stat -c%s "$stockLinux/bzImage" 2>/dev/null || \
54+
stat -c%s "$stockLinux/Image" 2>/dev/null || \
55+
stat -c%s "$stockLinux/vmlinuz"* 2>/dev/null || echo 0)
56+
incrSize=$(stat -c%s "$incrementalLinux/bzImage" 2>/dev/null || \
57+
stat -c%s "$incrementalLinux/Image" 2>/dev/null || \
58+
stat -c%s "$incrementalLinux/vmlinuz"* 2>/dev/null || echo 0)
59+
60+
echo "stock kernel image size: $stockSize"
61+
echo "incremental kernel image size: $incrSize"
62+
63+
if [ "$incrSize" -eq 0 ]; then
64+
echo "FAIL: incremental build produced no kernel image"
65+
ls -la "$incrementalLinux/"
66+
exit 1
67+
fi
68+
69+
echo "PASS: kernel checkpoint incremental build succeeded"
70+
touch $out
71+
''

pkgs/top-level/all-packages.nix

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,8 @@ with pkgs;
315315

316316
checkpointBuildTools = callPackage ../build-support/checkpoint-build.nix { };
317317

318+
kernelCheckpointTools = callPackage ../os-specific/linux/kernel/checkpoint.nix { };
319+
318320
celeste-classic-pm = pkgs.celeste-classic.override {
319321
practiceMod = true;
320322
};

0 commit comments

Comments
 (0)