From c1d204f86faf694cf1dae10dbb5fcacf00e69006 Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Sat, 1 Aug 2026 16:43:40 +0200 Subject: [PATCH] fix(build): fall back to a mirror when ffmpeg.org refuses the runner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The macOS x64 leg of v1.8.0-rc.6 died on the ffmpeg tarball fetch, twice, twenty minutes apart: curl: (35) Recv failure: Connection reset by peer This is not congestion and not a bad pin. In both runs the arm64 leg on `macos-latest` fetched the same tarball from the same host with the same cold cache and succeeded, while the x64 leg on `macos-15-intel` was reset about a second after starting. An immediate reset that reproduces on one runner pool and never on the other is an egress-level block, so no amount of retrying clears it — a first attempt at this shipped only `--retry-all-errors` and would not have fixed the build. So the source list grows a second entry. Debian's `.orig.tar.xz` is the upstream tarball unmodified — verified byte-identical to the pinned sha256 — and deb.debian.org is CDN-backed. Each source is tried in turn and must both download and match the checksum; the checksum is what makes a second origin safe to trust, and it gates every source equally. Nothing is downgraded: ffmpeg.org stays first, and a mirror that lacks a future version is skipped rather than fatal. Three flags carry their own reasons: - `--retry-all-errors`, because curl only auto-retries what it classes as transient (timeouts, 429, 5xx) — not a reset, not a handshake failure, which are exactly the errors seen here. Verified against a refused connection: `--retry 2 --retry-delay 1` gives up after 0s, adding `--retry-all-errors` spends 2s retrying. - `--connect-timeout 20`, because a throttled origin hangs rather than refuses. Measured: after a few rapid fetches ffmpeg.org left a connect sitting for 75s before failing. Times four attempts, that is five minutes burned before the second source is even tried. - `-f`, so an HTTP error page is not written to the tarball and resurfaced as a checksum mismatch, which reads like a moved pin rather than a bad response. Exercised by extracting the function and running it against real endpoints: canonical alone succeeds; a dead first source falls through to Debian; a source serving the wrong bytes (ffmpeg 8.1.1) is rejected on checksum and the next one is used; and with every source broken it throws listing each attempt with its reason — curl status or the actual hash. The sibling `scripts/fetch-ffmpeg.mjs` has the same single-source shape at line 359 but is left alone: it is the Windows/Linux path, pulls from GitHub releases rather than ffmpeg.org, and uses node's fetch rather than curl. --- scripts/fetch-ffmpeg-macos.mjs | 94 ++++++++++++++++++++++++++++++---- 1 file changed, 84 insertions(+), 10 deletions(-) diff --git a/scripts/fetch-ffmpeg-macos.mjs b/scripts/fetch-ffmpeg-macos.mjs index 14330ac24..6104a2d96 100644 --- a/scripts/fetch-ffmpeg-macos.mjs +++ b/scripts/fetch-ffmpeg-macos.mjs @@ -40,6 +40,89 @@ function run(cmd, args, opts = {}) { } } +/** + * Where the pinned tarball can be fetched, in order of preference. + * + * ffmpeg.org is canonical and stays first, but it cannot be the only one: + * GitHub's `macos-15-intel` runner pool cannot reach it. On the v1.8.0-rc.6 + * build the x64 leg died twice, twenty minutes apart, on + * `curl: (35) Recv failure: Connection reset by peer` about a second after + * starting — while the arm64 leg on `macos-latest` fetched the same tarball + * from the same host in the same runs and succeeded both times. An immediate + * reset that reproduces on one runner pool and never on the other is an + * egress-level block, not congestion, so retrying alone does not clear it. + * + * Debian's `.orig.tar.xz` is the upstream tarball unmodified — verified + * byte-identical to TARBALL_SHA256 below — and deb.debian.org is CDN-backed. + * It is a fallback, not a replacement: the checksum is what makes trusting a + * second origin safe, and it gates every source equally. + * + * When the pin moves, a mirror may not carry the new version yet. That is not + * a failure mode to design around — the list is tried in order and a source + * that 404s is simply skipped, with every attempt reported if none works. + */ +const TARBALL_URLS = [ + `https://ffmpeg.org/releases/ffmpeg-${VERSION}.tar.xz`, + `https://deb.debian.org/debian/pool/main/f/ffmpeg/ffmpeg_${VERSION}.orig.tar.xz`, +]; + +/** + * Fetches the pinned tarball to `dest`, trying each source until one both + * downloads and matches the checksum. + * + * `--retry-all-errors` rather than a plain `--retry`: curl only auto-retries + * what it classes as transient (timeouts, 429, 5xx), which does not include a + * connection reset or a TLS handshake failure — precisely the errors seen here. + * `-f` keeps an HTTP error page from being written to the tarball and + * resurfacing as a checksum mismatch, which reads like a moved pin. + */ +function downloadTarball(dest) { + const failures = []; + for (const url of TARBALL_URLS) { + console.log(`Downloading ffmpeg ${VERSION} from ${new URL(url).host}…`); + // --connect-timeout bounds the fallback, and is not decoration: a throttled + // origin does not refuse, it hangs. Measured against ffmpeg.org after a few + // rapid fetches, a single connect sat for 75s before failing — times four + // attempts, that is five minutes of a build spent before the second source + // is even tried. 20s is far above any healthy handshake. + const r = spawnSync( + "curl", + [ + "-fsSL", + "--connect-timeout", + "20", + "--retry", + "3", + "--retry-delay", + "2", + "--retry-all-errors", + "-o", + dest, + url, + ], + { stdio: "inherit" }, + ); + if (r.status !== 0) { + failures.push(` ${url}\n curl exited with ${r.status}`); + continue; + } + const actual = crypto.createHash("sha256").update(fs.readFileSync(dest)).digest("hex"); + if (actual !== TARBALL_SHA256) { + // Not fatal on its own — a mirror may carry a repacked tarball. It is + // reported in full, so a genuinely moved pin is still legible as + // "every source disagreed the same way" rather than "network down". + failures.push(` ${url}\n checksum ${actual}`); + continue; + } + console.log("Checksum OK."); + return; + } + throw new Error( + `Could not obtain ffmpeg ${VERSION} from any source.\n` + + `Expected sha256 ${TARBALL_SHA256}\n${failures.join("\n")}`, + ); +} + /** The binary's own licence banner — the only claim worth trusting. */ function isLgpl(dir) { const bin = path.join(dir, "bin", "ffmpeg"); @@ -68,16 +151,7 @@ if (fs.existsSync(path.join(DEST, "include"))) { const work = fs.mkdtempSync(path.join(os.tmpdir(), "openscreen-ffmpeg-")); const tarball = path.join(work, `ffmpeg-${VERSION}.tar.xz`); -console.log(`Downloading ffmpeg ${VERSION}…`); -run("curl", ["-sSL", "-o", tarball, `https://ffmpeg.org/releases/ffmpeg-${VERSION}.tar.xz`]); - -const actual = crypto.createHash("sha256").update(fs.readFileSync(tarball)).digest("hex"); -if (actual !== TARBALL_SHA256) { - throw new Error( - `Checksum mismatch for the ffmpeg tarball.\n expected ${TARBALL_SHA256}\n got ${actual}`, - ); -} -console.log("Checksum OK."); +downloadTarball(tarball); run("tar", ["-xJf", tarball, "-C", work]); const src = path.join(work, `ffmpeg-${VERSION}`);