# ffmpeg_portable — the pinned LGPL ffmpeg + ffprobe we bundle

One recipe, two binaries, both platforms. `build_portable_ffmpeg.py` compiles
FFmpeg **8.1.2** (upstream tarball, SHA-256 pinned and cross-checked against
Homebrew's independent pin) into minimal, static, **LGPL-only** `ffmpeg` and
`ffprobe` binaries and stages them under `dist/<platform>/` (gitignored). It
replaces the old practice of copying the build machine's tools, which are
**GPL** builds (Homebrew / gyan.dev: `--enable-gpl --enable-libx264
--enable-libx265`) plus, on macOS, a ~33 MB dylib closure we must not
redistribute. See `docs/legal/third-party-licenses.md`.

Run it **once per build machine** (network + a few minutes of compile):

```sh
# macOS (clang; nasm optional on x86_64)
python3 packaging/ffmpeg_portable/build_portable_ffmpeg.py
```

`packaging/build_macos_dmg.py` and `packaging/build_windows_exe.py` prefer
`dist/` over anything on PATH when they bundle `tools/ffprobe` and
`tools/ffmpeg`. Override per tool with
`KONFORMISTA_FFPROBE_PORTABLE=/path/to/ffprobe` /
`KONFORMISTA_FFMPEG_PORTABLE=…` (set to `system` to force the legacy
system-copy path; a missing override path is a hard error). If `dist/` is
missing the packagers fall back to the system copy — typically GPL — and the
provenance stamp (`tools/FFMPEG-PROVENANCE.txt` / `FFPROBE-PROVENANCE.txt`)
plus a loud build warning call it out. Don't ship a release that warns.

## What's in the build (and why)

LGPL core only: no `--enable-gpl`, no `--enable-nonfree`, no external
libraries (zlib comes from `/usr/lib` on macOS, a pinned static build on
Windows). Every native demuxer/decoder/parser stays in (probing + decoding
acquisition codecs); the write side is trimmed to the union of the product's
three ffmpeg call sites:

- `mjpeg` encoder + `image2` muxer — VFX reference stills
  (`premiere_bridge/vfx_pull_turnover.py`)
- `pcm_*` encoders + `amerge` — grade-pullback audio extraction
  (`premiere_bridge/grade_pullback_mux.py`)
- `prores_ks` encoder + `color`/`fps`/`setpts`/`overlay` — sparse-master
  assembly (`premiere_bridge/grade_pullback_assembler.py`)

plus `mov`/`mp4` muxers, `file`/`pipe` protocols, and the conversion filters
the ffmpeg CLI auto-inserts. No network, no devices, no hwaccels.
`MACOSX_DEPLOYMENT_TARGET=11.0`, same floor as the pyexpat build.

Verification before install — the script aborts if any check fails: `-L` must
report the GNU **Lesser** GPL, the configuration must carry no
`--enable-gpl`/`--enable-nonfree`/`--enable-lib*` markers, `otool -L` must
show only system linkage (macOS), the component inventory must include every
required decoder/encoder/filter, the binary must correctly probe synthesized
YUV4MPEG/WAV samples plus `fixtures/sample_prores_pcm.mov` (ProRes + PCM +
timecode — the exact fields the app reads), and the product's three pipelines
above must run end-to-end against the fresh binaries.

## Windows (`build_ffmpeg_windows.py`)

The win64 sibling — the FFmpeg pin, the component set and the verification
helpers are **imported from `build_portable_ffmpeg.py`**, so the platforms
cannot drift apart. Two ways to produce
`dist/win32-amd64/{ffmpeg.exe,ffprobe.exe}`:

```sh
# cross-compile from macOS/Linux (brew install mingw-w64 nasm)
python3 packaging/ffmpeg_portable/build_ffmpeg_windows.py

# or natively on Windows in an MSYS2 MINGW64/UCRT64 shell
# (pacman -S make tar mingw-w64-x86_64-toolchain mingw-w64-x86_64-nasm)
python3 packaging/ffmpeg_portable/build_ffmpeg_windows.py --native
```

Windows has no system zlib, so a pinned zlib release (sha256 cross-checked
against Homebrew's independent pin) is built and linked statically;
`--extra-ldflags=-static` folds libgcc/winpthread in too, leaving exes that
import Windows system DLLs only. Threads come from `w32threads`.

Verification differs where it must: the license blurb and configuration line
are compile-time string literals, so each exe is classified from its bytes
(LGPL text present, no GPL/nonfree/external-lib markers) and the PE import
table is checked with objdump — no execution needed. A native Windows build
also runs the full behavioral gauntlet from the shared recipe; a cross build
installs with `behavioral_verified: false` in its buildinfo — copy the repo to
a Windows machine and run `--verify-only` before shipping.

`packaging/build_windows_exe.py` refuses shared-build layouts (ffmpeg runtime
DLLs next to a "portable" exe), and `build_macos_dmg.py` refuses a "portable"
binary with foreign dylib linkage.

CI (`build-windows.yml`) builds the exes natively in an MSYS2 step (cached on
the recipe files' hash, so the compile runs only when the pin or component
set changes), re-verifies them with `--verify-only` on every run — the native
runner executes the full behavioral gauntlet, so CI artifacts are
`behavioral_verified` — pins the packager to them via the
`KONFORMISTA_*_PORTABLE` overrides (a missing exe is a hard error, never a
silent GPL fallback), and fails the job unless both provenance stamps read
`origin: portable` + `license classification: lgpl`.

## Bumping the pin

1. Update `FFMPEG_VERSION` + `FFMPEG_SHA256` in `build_portable_ffmpeg.py`
   (download the tarball, `shasum -a 256`, and cross-check the hash against an
   independent pin, e.g. Homebrew's formula for the same release). Both
   platforms share the pin — bump once.
2. Rebuild on each build machine; the verification gauntlet must pass.
3. `pytest tests/test_ffmpeg_portable_recipe.py tests/test_macos_dmg_packaging.py
   tests/test_windows_exe_packaging.py` — the recipe tests re-assert the LGPL
   flag invariants and, when `dist/` binaries exist, re-verify them end-to-end.
