@@ -1421,46 +1421,60 @@ jobs:
14211421 # A bare GitHub runner has no libav* under /usr/include, so a
14221422 # descriptor that loses its vendored headers to the host's would still
14231423 # be green here. That is precisely how compat.ffmpeg's -idirafter hole
1424- # survived: nothing in CI had a system ffmpeg to lose to. Installing
1425- # the dev headers is what turns this leg into a real test of include
1426- # ORDER rather than of include EXISTENCE.
1424+ # survived: nothing in CI had a system ffmpeg to lose to. Headers of
1425+ # FFmpeg's own names under /usr/include are what turn this leg into a
1426+ # test of include ORDER rather than of include EXISTENCE.
1427+ #
1428+ # They are planted, not installed: each public header of the release
1429+ # compat.ffmpeg names, as a file that stops the compile, naming itself.
1430+ # A reach of the host's header is then an error at the line that made
1431+ # it, where an installed header of another FFmpeg compiled silently or
1432+ # failed somewhere else. And nothing is downloaded from Ubuntu's mirror,
1433+ # whose ffmpeg closure (~100 MiB at the 2026-11-06 rates) outlasted
1434+ # every timeout on ubuntu-26.04 runners.
14271435 #
14281436 # Deliberately only on the llvm leg. The default leg reaches its
14291437 # compiler through --sysroot, so it is immune by construction and
1430- # installing there would change an existing job's environment for no
1438+ # planting there would change an existing job's environment for no
14311439 # coverage. Keeping the blast radius to the new leg means a red here
14321440 # is always about the thing this leg was added to watch.
1433- - name : Install host ffmpeg dev headers (llvm leg only)
1441+ - name : Plant host ffmpeg headers that stop the compile (llvm leg only)
14341442 if : matrix.platform == 'linux' && matrix.toolchain == 'llvm'
14351443 shell : bash
1436- # Only Ubuntu's own archive is refreshed: a bare `apt-get update` also
1437- # walks every third-party source the runner image carries (PPAs,
1438- # vendor repos), and one slow mirror among them held this step for
1439- # minutes. Bounded timeouts and retries, and no recommended packages.
14401444 run : |
1441- apt_opts=(-o Acquire::Retries=3 -o Acquire::http::Timeout=20 -o Acquire::https::Timeout=20)
1442- if [[ -f /etc/apt/sources.list.d/ubuntu.sources ]]; then
1443- mkdir -p "$RUNNER_TEMP/apt-ubuntu"
1444- cp /etc/apt/sources.list.d/ubuntu.sources "$RUNNER_TEMP/apt-ubuntu/"
1445- apt_opts+=(-o Dir::Etc::SourceList=/dev/null -o "Dir::Etc::SourceParts=$RUNNER_TEMP/apt-ubuntu")
1446- fi
1447- # IPv4: on 2026-11-06 the install stalled without output for the
1448- # whole timeout on several runners while the index update had taken
1449- # seconds. The install now reports its progress, waits for a held
1450- # dpkg lock rather than stalling on it, and is tried three times.
1451- apt_opts+=(-o Acquire::ForceIPv4=true -o DPkg::Lock::Timeout=120)
1452- time sudo timeout 180 apt-get update -qq "${apt_opts[@]}"
1453- for attempt in 1 2 3; do
1454- if time sudo timeout 240 apt-get install -y -q --no-install-recommends "${apt_opts[@]}" \
1455- libavutil-dev libavcodec-dev libavformat-dev \
1456- libavfilter-dev libavdevice-dev libswscale-dev libswresample-dev; then
1457- break
1458- fi
1459- [[ $attempt == 3 ]] && { echo "::error::the ffmpeg dev headers did not install"; exit 1; }
1460- echo "::warning::apt-get install failed (attempt $attempt); retrying"
1461- sudo timeout 120 apt-get update -qq "${apt_opts[@]}" || true
1462- done
1463- dpkg -l | grep -E 'libav(util|codec)-dev|libswscale-dev'
1445+ python3 - <<'PY'
1446+ import hashlib, io, re, subprocess, tarfile, urllib.request
1447+ from pathlib import Path
1448+ lua = Path("pkgs/c/compat.ffmpeg.lua").read_text()
1449+ entry = re.search(r'linux\s*=\s*\{\s*\["([^"]+)"\]\s*=\s*\{.*?GLOBAL\s*=\s*"([^"]+)".*?CN\s*=\s*"([^"]+)".*?sha256\s*=\s*"([0-9a-f]{64})"', lua, re.S)
1450+ version, urls, sha = entry.group(1), (entry.group(2), entry.group(3)), entry.group(4)
1451+ data = None
1452+ for url in urls:
1453+ try:
1454+ data = urllib.request.urlopen(url, timeout=120).read()
1455+ except OSError as e:
1456+ print(f"::warning::{url}: {e}")
1457+ continue
1458+ if hashlib.sha256(data).hexdigest() == sha:
1459+ break
1460+ data = None
1461+ if data is None:
1462+ raise SystemExit(f"::error::the FFmpeg {version} source matching the descriptor could not be read")
1463+ libs = ("libavutil", "libavcodec", "libavformat", "libavfilter", "libavdevice", "libswscale", "libswresample")
1464+ planted = 0
1465+ with tarfile.open(fileobj=io.BytesIO(data)) as tar:
1466+ for name in tar.getnames():
1467+ parts = name.split("/")
1468+ if len(parts) == 3 and parts[1] in libs and parts[2].endswith(".h"):
1469+ target = Path("/usr/include") / parts[1] / parts[2]
1470+ text = f'#error "host <{parts[1]}/{parts[2]}> reached: a vendored FFmpeg header was not found first"\n'
1471+ subprocess.run(["sudo", "mkdir", "-p", str(target.parent)], check=True)
1472+ subprocess.run(["sudo", "tee", str(target)], input=text.encode(), check=True, stdout=subprocess.DEVNULL)
1473+ planted += 1
1474+ if planted < 100:
1475+ raise SystemExit(f"::error::only {planted} FFmpeg headers were planted")
1476+ print(f"planted {planted} FFmpeg {version} headers under /usr/include")
1477+ PY
14641478
14651479 # ── Select the toolchain for this leg ─────────────────────────────
14661480 # No version pin: `llvm` resolves to whatever the toolchain family's
0 commit comments