git apply silently skips 'diff --git' patches and exits 0, so a source tree ends up half-patched
scope: generic · severity: trap · confidence: proven · subsystem: build
Symptom — a source tree you patched in a loop that reported success builds into a binary missing some of the changes. Nothing failed. The loop checked every exit code and every one was 0. The tree is patched in part: the fixes you happened to test are there, the ones you did not are not, and the build is a plausible-looking regression that costs whatever it costs to build.
On taimen this was 5h18m of webkit, and the resulting package had the CSS-filter
fix (so video freezes looked fixed) but not frame-done-at-fence,
pipeline-compositor-frames-in-flight, cull-quads-outside-the-scissor-box or
video-flush-keeps-the-last-frame – the compositor patches. Measuring the
WEBKIT_COMPOSITOR_MAX_FRAMES_IN_FLIGHT knob on it would have set an
environment variable that no code reads and reported a clean null.
Cause — git apply run inside a git work tree treats a patch carrying a
diff --git header plus an index <sha>..<sha> line as a git patch, and
resolves its paths against the repository, not the current directory. When those
paths are not tracked here – an upstream tarball unpacked into a gitignored
scratch directory is the usual case – it declines the file, prints
Skipped patch 'Source/…'. on stdout, and exits 0. A plain diff -u /
diff -ru patch has no index line, takes the ordinary path, and applies. So a
mixed patch set splits cleanly in two and only half of it lands.
Two further ways to get a false pass on the same tree:
git apply --check -R(the “is it already applied?” test) returns 0 for a patch that was never applied, for the same reason. It is not a presence test.git apply --recountwidens it: it skipped 17 of 18 patches on a tree where the same patches without--recountapplied 10.
What to do —
-
Use
patch(1), notgit apply, on a tree that is not a git checkout. That is what abuild does, and it is why the aport build of the same 18 patches was correct while the hand-patched tree was not. Notepatchis often absent: it is not on a stock Fedora host and not in the porthole sandbox image, which is how the substitution gets made in the first place. -
Treat
Skipped patchas a failure. An exit code is not enough:out=$(git apply -p1 -v "$p" 2>&1) || failprintf '%s' "$out" | grep -q '^Skipped patch' && fail -
Verify by content, never by the applier’s exit code. Pick one distinctive added symbol per patch and grep the tree, or diff the built binary’s symbol table against a known-good build – four functions present in one and missing in the other is what exposed this one, in about a minute:
nm -C --defined-only lib.debug | sed -n 's/^[0-9a-f]* [tT] //p' | sort -u -
An aport is the source of truth. A
--src/ out-of-tree copy is a convenience that can drift from it silently; the-taimen-v7-2-tree-was-ten-venus-patches-behind-its-own-aport-series is the same failure with a different mechanism.
