Arch shipped ffmpeg 2:9.0-5 on 2026-08-08 and every soname moved with it (libavutil .60→.61, libavcodec .62→.63, libavfilter .11→.12, libavdevice .62→.63, libswscale .9→.10). The packaged punktfunk-host was built against FFmpeg 8, so a plain pacman -Syu left it unable to start at all: exit 127 before main(), in a systemd restart loop.
Two commits, split so the packaging fix can ship on its own.
b1e05258 — the root cause was ours, and it was a packaging bug
packaging/arch/PKGBUILD declared depends=('ffmpeg' …) with no bound. That mattered because pacman is the only one of our five packaging formats that does not derive dependencies from ELF DT_NEEDED — rpm auto-generates libavcodec.so.62()(64bit), dpkg-shlibdeps emits libavcodec62, nix pins the closure, bootc bakes the image. All five were audited; Arch was the only exposed surface.
The symptom lies about the cause, which is worth knowing for next time. punktfunk-web is a separate bun service with zero libav linkage, so it keeps serving :47992 perfectly while :47990 has nothing listening — it reads as "the mgmt API is broken" rather than "the host is not running", and the host's own log is empty because the process never reaches its first line. ldd /usr/bin/punktfunk-host | grep "not found" is the one-line diagnosis.
The fix is soname deps, not a version pin. Arch's ffmpeg declares provides=(libavcodec.so=63-64 …), and makepkg rewrites each bare libfoo.so in depends into libfoo.so=<soname>-<arch> by reading the built binary's DT_NEEDED. The bound then tracks whatever FFmpeg the builder linked, permanently, with nothing to hand-maintain — a written-in ffmpeg<2:9 would have gone stale on the very next major. pacman now refuses the ffmpeg upgrade instead of bricking the install.
Verified empirically with a throwaway package before relying on it: bare libavcodec.so → libavcodec.so=63-64, and an unlinked lib (libavformat.so) is left bare and satisfied by any ffmpeg, so listing all seven is free.
Not bundling the way the .deb does: that exists only because Ubuntu 24.04 LTS is frozen on FFmpeg 6.1 and can never satisfy the dep, whereas rolling Arch always ships a current one (and vendoring a second copy of a system library would not survive AUR review).
The new arch.yml step asserts the expansion actually happened. If it ever stops — Arch dropping the soname provides, someone tidying the entries out of depends — the dep silently degrades to an unversioned name that any ffmpeg satisfies, i.e. exactly the state that caused this, and it is invisible in a green build until a box bricks weeks later.
deeb8b67 — build against FFmpeg 9
ffmpeg-next 8.1.0 genuinely could not accept FFmpeg 9: ffmpeg-sys-next's version probe covered avcodec majors 56..62 (the range is exclusive of its end), so libavcodec 63 was outside what it could bind. 9.0.0 widens it to 56..63.
No API drift to fix. A crate major is a ceiling, not a target — one source tree still spans FFmpeg 7.x/61, 8.x/62 and 9.x/63 via per-version cfgs, and every wrapper symbol the NVENC-libav, VAAPI and amf-qsv backends name survives 8.1.0 → 9.0.0 unchanged. (The whole-file diff between the two crate versions is a wholesale reformat; don't read the diffstat as churn.)
The three hand-written #[repr(C)] hwcontext mirrors are the parts no compiler checks, so they were re-read against the real headers rather than trusted. AVCUDADeviceContext and AVD3D11VAFramesContext are byte-identical across 7.1/8/9; AVD3D11VADeviceContext gained two trailing UINTs in FFmpeg 8 that 7.1 lacks — and Windows builds against BtbN n7.1, so "completing" that mirror to match 8/9 would have made it more wrong for the version we actually ship. It deliberately stops at the common prefix, and the assertions now say what they do and do not buy you: they pin our layout, not libav's.
The CI image re-key is what actually ships this. arch.yml deliberately runs no -Syu ("the image's snapshot IS the build environment"), so the builder stayed frozen on ffmpeg 8 no matter what Arch shipped, and a canary built from that snapshot could not satisfy the new soname dep. Re-keying ci/ (content hash = git rev-parse HEAD:ci) rebuilds it against ffmpeg 9.
⚠️arch.yml and docker.yml are separate workflows with no needs: between them, so the first push after merge can race: arch.yml may pull the stale :latest and build an ffmpeg-8 canary that then refuses to install (declaring libavcodec.so=62-64). Confirm the builders job republished the image, then re-run arch.yml.
Deliberately not moved: the noble .deb bundles its own FFmpeg 8 behind an rpath and strips the libav sonames from its Depends (zero exposure), and Windows bundles BtbN DLLs into the signed installer — BtbN publishes no FFmpeg 9 build at all (only n7.1/n8.1), so matching Arch isn't even available. Moving either would re-qualify an encode stack to buy nothing. Both documented in place.
Verification
On 192.168.1.21 (CachyOS, system ffmpeg 2:9.0-5, RTX 5070 Ti):
built package records libavcodec.so=63-64, libavutil.so=61-64, libavfilter.so=12-64, libavdevice.so=63-64, libswscale.so=10-64 — exactly matching DT_NEEDED
installs on a real ffmpeg-9 box (proving the new deps resolve); ldd fully resolved, no "not found"
ffmpeg-8 compat shim removed; service runs with NRestarts=0 and answers HTTP 401 on :47990
cargo fmt / clippy clean; 67 pf-encode tests pass
live synthetic encode drove real NVENC hardware through FFmpeg 9's libavcodec to a decodable 1080p HEVC stream (180/180 frames, FEC loopback 0 mismatches), with libavcodec.so.63 and libnvidia-encode both mapped into the encoding process
Not verified: the VAAPI live legs on FFmpeg 9 (.21 is NVIDIA, so those tests self-skip — they need an AMD/Intel box or a Deck), and the Windows amf-qsv leg was not compiled, though the changes there are comment-only.
Arch shipped `ffmpeg 2:9.0-5` on 2026-08-08 and every soname moved with it (libavutil .60→.61, libavcodec .62→.63, libavfilter .11→.12, libavdevice .62→.63, libswscale .9→.10). The packaged `punktfunk-host` was built against FFmpeg 8, so a plain `pacman -Syu` left it unable to start at all: **exit 127 before `main()`**, in a systemd restart loop.
Two commits, split so the packaging fix can ship on its own.
## `b1e05258` — the root cause was ours, and it was a packaging bug
`packaging/arch/PKGBUILD` declared `depends=('ffmpeg' …)` with no bound. That mattered because **pacman is the only one of our five packaging formats that does not derive dependencies from ELF `DT_NEEDED`** — rpm auto-generates `libavcodec.so.62()(64bit)`, `dpkg-shlibdeps` emits `libavcodec62`, nix pins the closure, bootc bakes the image. All five were audited; **Arch was the only exposed surface.**
**The symptom lies about the cause,** which is worth knowing for next time. `punktfunk-web` is a separate bun service with zero libav linkage, so it keeps serving `:47992` perfectly while `:47990` has nothing listening — it reads as "the mgmt API is broken" rather than "the host is not running", and the host's own log is empty because the process never reaches its first line. `ldd /usr/bin/punktfunk-host | grep "not found"` is the one-line diagnosis.
The fix is **soname deps, not a version pin**. Arch's ffmpeg declares `provides=(libavcodec.so=63-64 …)`, and makepkg rewrites each bare `libfoo.so` in `depends` into `libfoo.so=<soname>-<arch>` by reading the built binary's `DT_NEEDED`. The bound then tracks whatever FFmpeg the builder linked, permanently, with nothing to hand-maintain — a written-in `ffmpeg<2:9` would have gone stale on the very next major. pacman now refuses the ffmpeg upgrade instead of bricking the install.
Verified empirically with a throwaway package before relying on it: bare `libavcodec.so` → `libavcodec.so=63-64`, and an unlinked lib (`libavformat.so`) is left bare and satisfied by any ffmpeg, so listing all seven is free.
Not bundling the way the `.deb` does: that exists only because Ubuntu 24.04 LTS is frozen on FFmpeg 6.1 and can never satisfy the dep, whereas rolling Arch always ships a current one (and vendoring a second copy of a system library would not survive AUR review).
The new arch.yml step asserts the expansion actually happened. If it ever stops — Arch dropping the soname `provides`, someone tidying the entries out of `depends` — the dep silently degrades to an unversioned name that any ffmpeg satisfies, i.e. exactly the state that caused this, and it is invisible in a green build until a box bricks weeks later.
## `deeb8b67` — build against FFmpeg 9
`ffmpeg-next` 8.1.0 genuinely **could not** accept FFmpeg 9: ffmpeg-sys-next's version probe covered avcodec majors 56..62 (the range is exclusive of its end), so libavcodec 63 was outside what it could bind. 9.0.0 widens it to 56..63.
**No API drift to fix.** A crate major is a *ceiling, not a target* — one source tree still spans FFmpeg 7.x/61, 8.x/62 and 9.x/63 via per-version cfgs, and every wrapper symbol the NVENC-libav, VAAPI and amf-qsv backends name survives 8.1.0 → 9.0.0 unchanged. (The whole-file diff between the two crate versions is a wholesale reformat; don't read the diffstat as churn.)
The three hand-written `#[repr(C)]` hwcontext mirrors are the parts no compiler checks, so they were re-read against the real headers rather than trusted. `AVCUDADeviceContext` and `AVD3D11VAFramesContext` are byte-identical across 7.1/8/9; **`AVD3D11VADeviceContext` gained two trailing `UINT`s in FFmpeg 8 that 7.1 lacks** — and Windows builds against BtbN n7.1, so "completing" that mirror to match 8/9 would have made it *more* wrong for the version we actually ship. It deliberately stops at the common prefix, and the assertions now say what they do and do not buy you: they pin *our* layout, not libav's.
**The CI image re-key is what actually ships this.** arch.yml deliberately runs no `-Syu` ("the image's snapshot IS the build environment"), so the builder stayed frozen on ffmpeg 8 no matter what Arch shipped, and a canary built from that snapshot could not satisfy the new soname dep. Re-keying `ci/` (content hash = `git rev-parse HEAD:ci`) rebuilds it against ffmpeg 9.
⚠️ **arch.yml and docker.yml are separate workflows with no `needs:` between them**, so the first push after merge can race: arch.yml may pull the stale `:latest` and build an ffmpeg-8 canary that then *refuses to install* (declaring `libavcodec.so=62-64`). Confirm the builders job republished the image, then re-run arch.yml.
**Deliberately not moved:** the noble `.deb` bundles its own FFmpeg 8 behind an rpath and strips the libav sonames from its `Depends` (zero exposure), and Windows bundles BtbN DLLs into the signed installer — BtbN publishes no FFmpeg 9 build at all (only n7.1/n8.1), so matching Arch isn't even available. Moving either would re-qualify an encode stack to buy nothing. Both documented in place.
## Verification
On 192.168.1.21 (CachyOS, system `ffmpeg 2:9.0-5`, RTX 5070 Ti):
- built package records `libavcodec.so=63-64`, `libavutil.so=61-64`, `libavfilter.so=12-64`, `libavdevice.so=63-64`, `libswscale.so=10-64` — exactly matching `DT_NEEDED`
- installs on a real ffmpeg-9 box (proving the new deps resolve); `ldd` fully resolved, **no "not found"**
- ffmpeg-8 compat shim removed; service runs with `NRestarts=0` and answers **HTTP 401** on `:47990`
- `cargo fmt` / clippy clean; 67 pf-encode tests pass
- **live** synthetic encode drove real NVENC hardware through FFmpeg 9's libavcodec to a decodable 1080p HEVC stream (180/180 frames, FEC loopback 0 mismatches), with `libavcodec.so.63` and `libnvidia-encode` both mapped into the encoding process
**Not verified:** the VAAPI live legs on FFmpeg 9 (.21 is NVIDIA, so those tests self-skip — they need an AMD/Intel box or a Deck), and the Windows `amf-qsv` leg was not compiled, though the changes there are comment-only.
`depends=('ffmpeg' ...)` carried no version bound, and pacman is the only one of our
packaging formats that does not derive dependencies from ELF DT_NEEDED — rpm
auto-generates `libavcodec.so.62()(64bit)`, dpkg-shlibdeps emits `libavcodec62`, nix
pins the closure. So when Arch shipped ffmpeg 2:9.0-5 on 2026-08-08 and every soname
moved (libavutil .60->.61, libavcodec .62->.63, libavfilter .11->.12, libavdevice
.62->.63, libswscale .9->.10), a plain `pacman -Syu` walked every Arch/CachyOS install
straight across the break. The result is not a crash we can log: the dynamic loader
cannot start the binary at all, so it is exit 127 *before* main() in a systemd restart
loop, and because punktfunk-web is a separate bun service with no libav linkage it keeps
serving happily while :47990 has nothing listening — which reads as "the mgmt API is
broken" rather than "the host is not running". `ldd /usr/bin/punktfunk-host | grep
"not found"` is the one-line diagnosis.
Depend on the sonames instead of the package. Arch's ffmpeg declares the matching
`provides=(libavcodec.so=63-64 ...)`, and makepkg rewrites each bare `libfoo.so` listed
in depends into `libfoo.so=<soname>-<arch>` by reading the built binary's DT_NEEDED, so
the bound tracks whatever FFmpeg the builder linked against with nothing to hand-maintain
across the next bump. pacman now refuses the ffmpeg upgrade rather than bricking the
install. A hand-written `ffmpeg<2:9` would have gone stale on the very next major; not
bundling FFmpeg the way the .deb does, because that exists only because Ubuntu 24.04 LTS
is frozen on 6.1 and can never satisfy the dep, while rolling Arch always ships a current
one.
Verified on a real ffmpeg-9 box (192.168.1.21): the built package records
libavcodec.so=63-64, libavutil.so=61-64, libavfilter.so=12-64, libavdevice.so=63-64 and
libswscale.so=10-64, exactly matching DT_NEEDED, with the two libs --as-needed drops left
bare and satisfied by any ffmpeg.
The new arch.yml step asserts that expansion actually happened. If it ever stops — Arch
dropping the soname provides, someone tidying the entries out of depends — the dep
silently degrades to an unversioned name that any ffmpeg satisfies, which is exactly the
state that caused this, and it is invisible in a green build until a box bricks weeks later.
ffmpeg-next 8.1.0 could not accept FFmpeg 9 at all: ffmpeg-sys-next's version probe
covered avcodec majors 56..62 (the range is exclusive of its end), so libavcodec 63 fell
outside what it knew how to bind. 9.0.0 widens that to 56..63, which is what actually
unblocks Arch. Bump both pins — the unconditional Linux dep and the optional Windows
amf-qsv one — and the lock with them.
No API drift to fix. The crate major is a CEILING, not a target: one source tree still
spans FFmpeg 7.x/libavcodec 61, 8.x/62 and 9.x/63 via per-version cfgs, and every wrapper
symbol the NVENC-libav, VAAPI and amf-qsv backends name survives 8.1.0 -> 9.0.0
unchanged. The three hand-written #[repr(C)] hwcontext mirrors are the parts no compiler
checks, so they were re-read against the real headers rather than trusted:
AVCUDADeviceContext and AVD3D11VAFramesContext are byte-identical across 7.1/8/9, and
AVD3D11VADeviceContext gained two trailing UINTs in 8 that 7.1 lacks — which is why that
mirror deliberately stops at the common prefix, and why its assertions now say what they
do and do not buy you. They pin our layout, not libav's; a green build is not evidence.
The CI image is the step that makes this reach users. arch.yml deliberately runs no -Syu
("the image's snapshot IS the build environment"), so the builder stayed frozen on ffmpeg
8 no matter what Arch shipped, and a canary built from that snapshot could not satisfy the
soname dep the PKGBUILD now derives. Re-keying ci/ rebuilds it against ffmpeg 9.
Ubuntu and Windows deliberately stay put: the noble .deb bundles its own FFmpeg 8 behind
an rpath and strips the libav sonames from its Depends, and Windows bundles BtbN DLLs into
the signed installer — neither is exposed to the break, BtbN publishes no FFmpeg 9 build,
and moving either would re-qualify an encode stack to buy nothing.
Verified end to end on 192.168.1.21 (CachyOS, system ffmpeg 2:9.0-5, RTX 5070 Ti): host
builds clean and links libavcodec.so.63/libavutil.so.61/libavfilter.so.12/libswscale.so.10
with no unresolved sonames; the ffmpeg-8 compat shim is gone and the service runs with
NRestarts=0 and answers 401 on :47990; pf-encode's 67 tests pass; and a live synthetic
encode drives real NVENC hardware through FFmpeg 9's libavcodec to a decodable 1080p HEVC
stream (180/180 frames, FEC loopback 0 mismatches) with libavcodec.so.63 and
libnvidia-encode both mapped into the encoding process.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Arch shipped
ffmpeg 2:9.0-5on 2026-08-08 and every soname moved with it (libavutil .60→.61, libavcodec .62→.63, libavfilter .11→.12, libavdevice .62→.63, libswscale .9→.10). The packagedpunktfunk-hostwas built against FFmpeg 8, so a plainpacman -Syuleft it unable to start at all: exit 127 beforemain(), in a systemd restart loop.Two commits, split so the packaging fix can ship on its own.
b1e05258— the root cause was ours, and it was a packaging bugpackaging/arch/PKGBUILDdeclareddepends=('ffmpeg' …)with no bound. That mattered because pacman is the only one of our five packaging formats that does not derive dependencies from ELFDT_NEEDED— rpm auto-generateslibavcodec.so.62()(64bit),dpkg-shlibdepsemitslibavcodec62, nix pins the closure, bootc bakes the image. All five were audited; Arch was the only exposed surface.The symptom lies about the cause, which is worth knowing for next time.
punktfunk-webis a separate bun service with zero libav linkage, so it keeps serving:47992perfectly while:47990has nothing listening — it reads as "the mgmt API is broken" rather than "the host is not running", and the host's own log is empty because the process never reaches its first line.ldd /usr/bin/punktfunk-host | grep "not found"is the one-line diagnosis.The fix is soname deps, not a version pin. Arch's ffmpeg declares
provides=(libavcodec.so=63-64 …), and makepkg rewrites each barelibfoo.soindependsintolibfoo.so=<soname>-<arch>by reading the built binary'sDT_NEEDED. The bound then tracks whatever FFmpeg the builder linked, permanently, with nothing to hand-maintain — a written-inffmpeg<2:9would have gone stale on the very next major. pacman now refuses the ffmpeg upgrade instead of bricking the install.Verified empirically with a throwaway package before relying on it: bare
libavcodec.so→libavcodec.so=63-64, and an unlinked lib (libavformat.so) is left bare and satisfied by any ffmpeg, so listing all seven is free.Not bundling the way the
.debdoes: that exists only because Ubuntu 24.04 LTS is frozen on FFmpeg 6.1 and can never satisfy the dep, whereas rolling Arch always ships a current one (and vendoring a second copy of a system library would not survive AUR review).The new arch.yml step asserts the expansion actually happened. If it ever stops — Arch dropping the soname
provides, someone tidying the entries out ofdepends— the dep silently degrades to an unversioned name that any ffmpeg satisfies, i.e. exactly the state that caused this, and it is invisible in a green build until a box bricks weeks later.deeb8b67— build against FFmpeg 9ffmpeg-next8.1.0 genuinely could not accept FFmpeg 9: ffmpeg-sys-next's version probe covered avcodec majors 56..62 (the range is exclusive of its end), so libavcodec 63 was outside what it could bind. 9.0.0 widens it to 56..63.No API drift to fix. A crate major is a ceiling, not a target — one source tree still spans FFmpeg 7.x/61, 8.x/62 and 9.x/63 via per-version cfgs, and every wrapper symbol the NVENC-libav, VAAPI and amf-qsv backends name survives 8.1.0 → 9.0.0 unchanged. (The whole-file diff between the two crate versions is a wholesale reformat; don't read the diffstat as churn.)
The three hand-written
#[repr(C)]hwcontext mirrors are the parts no compiler checks, so they were re-read against the real headers rather than trusted.AVCUDADeviceContextandAVD3D11VAFramesContextare byte-identical across 7.1/8/9;AVD3D11VADeviceContextgained two trailingUINTs in FFmpeg 8 that 7.1 lacks — and Windows builds against BtbN n7.1, so "completing" that mirror to match 8/9 would have made it more wrong for the version we actually ship. It deliberately stops at the common prefix, and the assertions now say what they do and do not buy you: they pin our layout, not libav's.The CI image re-key is what actually ships this. arch.yml deliberately runs no
-Syu("the image's snapshot IS the build environment"), so the builder stayed frozen on ffmpeg 8 no matter what Arch shipped, and a canary built from that snapshot could not satisfy the new soname dep. Re-keyingci/(content hash =git rev-parse HEAD:ci) rebuilds it against ffmpeg 9.⚠️ arch.yml and docker.yml are separate workflows with no
needs:between them, so the first push after merge can race: arch.yml may pull the stale:latestand build an ffmpeg-8 canary that then refuses to install (declaringlibavcodec.so=62-64). Confirm the builders job republished the image, then re-run arch.yml.Deliberately not moved: the noble
.debbundles its own FFmpeg 8 behind an rpath and strips the libav sonames from itsDepends(zero exposure), and Windows bundles BtbN DLLs into the signed installer — BtbN publishes no FFmpeg 9 build at all (only n7.1/n8.1), so matching Arch isn't even available. Moving either would re-qualify an encode stack to buy nothing. Both documented in place.Verification
On 192.168.1.21 (CachyOS, system
ffmpeg 2:9.0-5, RTX 5070 Ti):libavcodec.so=63-64,libavutil.so=61-64,libavfilter.so=12-64,libavdevice.so=63-64,libswscale.so=10-64— exactly matchingDT_NEEDEDlddfully resolved, no "not found"NRestarts=0and answers HTTP 401 on:47990cargo fmt/ clippy clean; 67 pf-encode tests passlibavcodec.so.63andlibnvidia-encodeboth mapped into the encoding processNot verified: the VAAPI live legs on FFmpeg 9 (.21 is NVIDIA, so those tests self-skip — they need an AMD/Intel box or a Deck), and the Windows
amf-qsvleg was not compiled, though the changes there are comment-only.`depends=('ffmpeg' ...)` carried no version bound, and pacman is the only one of our packaging formats that does not derive dependencies from ELF DT_NEEDED — rpm auto-generates `libavcodec.so.62()(64bit)`, dpkg-shlibdeps emits `libavcodec62`, nix pins the closure. So when Arch shipped ffmpeg 2:9.0-5 on 2026-08-08 and every soname moved (libavutil .60->.61, libavcodec .62->.63, libavfilter .11->.12, libavdevice .62->.63, libswscale .9->.10), a plain `pacman -Syu` walked every Arch/CachyOS install straight across the break. The result is not a crash we can log: the dynamic loader cannot start the binary at all, so it is exit 127 *before* main() in a systemd restart loop, and because punktfunk-web is a separate bun service with no libav linkage it keeps serving happily while :47990 has nothing listening — which reads as "the mgmt API is broken" rather than "the host is not running". `ldd /usr/bin/punktfunk-host | grep "not found"` is the one-line diagnosis. Depend on the sonames instead of the package. Arch's ffmpeg declares the matching `provides=(libavcodec.so=63-64 ...)`, and makepkg rewrites each bare `libfoo.so` listed in depends into `libfoo.so=<soname>-<arch>` by reading the built binary's DT_NEEDED, so the bound tracks whatever FFmpeg the builder linked against with nothing to hand-maintain across the next bump. pacman now refuses the ffmpeg upgrade rather than bricking the install. A hand-written `ffmpeg<2:9` would have gone stale on the very next major; not bundling FFmpeg the way the .deb does, because that exists only because Ubuntu 24.04 LTS is frozen on 6.1 and can never satisfy the dep, while rolling Arch always ships a current one. Verified on a real ffmpeg-9 box (192.168.1.21): the built package records libavcodec.so=63-64, libavutil.so=61-64, libavfilter.so=12-64, libavdevice.so=63-64 and libswscale.so=10-64, exactly matching DT_NEEDED, with the two libs --as-needed drops left bare and satisfied by any ffmpeg. The new arch.yml step asserts that expansion actually happened. If it ever stops — Arch dropping the soname provides, someone tidying the entries out of depends — the dep silently degrades to an unversioned name that any ffmpeg satisfies, which is exactly the state that caused this, and it is invisible in a green build until a box bricks weeks later.ffmpeg-next 8.1.0 could not accept FFmpeg 9 at all: ffmpeg-sys-next's version probe covered avcodec majors 56..62 (the range is exclusive of its end), so libavcodec 63 fell outside what it knew how to bind. 9.0.0 widens that to 56..63, which is what actually unblocks Arch. Bump both pins — the unconditional Linux dep and the optional Windows amf-qsv one — and the lock with them. No API drift to fix. The crate major is a CEILING, not a target: one source tree still spans FFmpeg 7.x/libavcodec 61, 8.x/62 and 9.x/63 via per-version cfgs, and every wrapper symbol the NVENC-libav, VAAPI and amf-qsv backends name survives 8.1.0 -> 9.0.0 unchanged. The three hand-written #[repr(C)] hwcontext mirrors are the parts no compiler checks, so they were re-read against the real headers rather than trusted: AVCUDADeviceContext and AVD3D11VAFramesContext are byte-identical across 7.1/8/9, and AVD3D11VADeviceContext gained two trailing UINTs in 8 that 7.1 lacks — which is why that mirror deliberately stops at the common prefix, and why its assertions now say what they do and do not buy you. They pin our layout, not libav's; a green build is not evidence. The CI image is the step that makes this reach users. arch.yml deliberately runs no -Syu ("the image's snapshot IS the build environment"), so the builder stayed frozen on ffmpeg 8 no matter what Arch shipped, and a canary built from that snapshot could not satisfy the soname dep the PKGBUILD now derives. Re-keying ci/ rebuilds it against ffmpeg 9. Ubuntu and Windows deliberately stay put: the noble .deb bundles its own FFmpeg 8 behind an rpath and strips the libav sonames from its Depends, and Windows bundles BtbN DLLs into the signed installer — neither is exposed to the break, BtbN publishes no FFmpeg 9 build, and moving either would re-qualify an encode stack to buy nothing. Verified end to end on 192.168.1.21 (CachyOS, system ffmpeg 2:9.0-5, RTX 5070 Ti): host builds clean and links libavcodec.so.63/libavutil.so.61/libavfilter.so.12/libswscale.so.10 with no unresolved sonames; the ffmpeg-8 compat shim is gone and the service runs with NRestarts=0 and answers 401 on :47990; pf-encode's 67 tests pass; and a live synthetic encode drives real NVENC hardware through FFmpeg 9's libavcodec to a decodable 1080p HEVC stream (180/180 frames, FEC loopback 0 mismatches) with libavcodec.so.63 and libnvidia-encode both mapped into the encoding process.