#!/usr/bin/env python3
"""Build the pinned, minimal, LGPL-only static ffmpeg + ffprobe we bundle.

Why this exists: the packaged apps ship ffprobe (codec probing) and ffmpeg
(MXF audio extraction, sparse-master assembly, VFX reference stills). Copying
whatever Homebrew/gyan.dev build is on the build machine drags in a GPL
configuration (x264/x265/--enable-gpl) plus a ~33 MB dylib closure, which we
must not redistribute with a proprietary app — see
docs/legal/third-party-licenses.md. This recipe builds both tools from a
pinned upstream release with:

  * no --enable-gpl / --enable-nonfree, no external libraries (LGPL v2.1+)
  * static ff-libraries (single self-contained binary, system libs only)
  * every native demuxer/decoder/parser (probing + decoding acquisition
    codecs: ProRes, DNxHD/HR, H.264/HEVC, MPEG-2, DV, ...)
  * the write side trimmed to the union of every ffmpeg invocation in the
    product — encoders mjpeg + pcm_*, muxers mov/mp4/image2, and
    the filters those graphs name or auto-insert; nothing else
  * no network, no devices, no hwaccels, no docs

Run it once per build machine (output is gitignored):

    python3 packaging/ffmpeg_portable/build_portable_ffmpeg.py

It downloads the pinned tarball (cached in build/), verifies its SHA-256,
compiles, self-verifies the result (LGPL banner, component inventory, a
system-libs-only dylib closure, and a behavioral gauntlet that probes
synthesized samples + the committed ProRes fixture and re-runs each of the
product's three ffmpeg pipelines against the fresh binaries), and stages:

    packaging/ffmpeg_portable/dist/<platform>/ffmpeg
    packaging/ffmpeg_portable/dist/<platform>/ffprobe
    packaging/ffmpeg_portable/dist/<platform>/BUILDINFO.json

packaging/build_macos_dmg.py and build_windows_exe.py prefer these binaries
over anything on PATH; when dist/ is missing they fall back to the system
copy and the FFMPEG/FFPROBE-PROVENANCE.txt stamp warns that a non-LGPL build
was bundled.

Windows: use packaging/ffmpeg_portable/build_ffmpeg_windows.py — it imports
this module's pin, component set and verification helpers (the platforms
cannot drift apart) and cross-compiles both .exes from macOS/Linux with
mingw-w64, or builds natively inside an MSYS2 shell with --native.
"""

from __future__ import annotations

import hashlib
import json
import os
import platform
import shutil
import struct
import subprocess
import sys
import tempfile
import urllib.request
from pathlib import Path

FFMPEG_VERSION = "8.1.2"
FFMPEG_URL = f"https://ffmpeg.org/releases/ffmpeg-{FFMPEG_VERSION}.tar.xz"
# SHA-256 of the upstream release tarball, pinned from ffmpeg.org on 2026-07-10
# and cross-checked against Homebrew's independently pinned checksum for the
# same URL (homebrew-core Formula/f/ffmpeg.rb @ 8.1.2). Bump both together.
FFMPEG_SHA256 = "464beb5e7bf0c311e68b45ae2f04e9cc2af88851abb4082231742a74d97b524c"

# Oldest macOS the binaries must run on. The build box tracks current macOS;
# without an explicit floor the binary inherits the box's SDK minimum and dies
# on older systems (the bundled-pyexpat lesson: packaging/pyexpat_portable).
MACOS_DEPLOYMENT_TARGET = "11.0"

# The write side is the union of every ffmpeg invocation in the product (the
# ONLY encode paths in the codebase):
#   premiere_bridge/grade_pullback_mux.py       — "-c:a copy" single-stream, or
#       amerge of N mono streams re-encoded to pcm_* into "-f mov"
#   premiere_bridge/vfx_pull_turnover.py        — 1-frame -q:v JPG stills
#       (mjpeg into image2)
# The old sparse-master assembly encoded prores_ks off a color/fps/setpts/
# overlay filtergraph; that path was removed — the graded master is now a
# byte-copy mux (grade_pullback_mux), so NO video encoder ships. Keeping the
# prores DECODER (below) is unaffected; this recipe just no longer emits ProRes.
VIDEO_ENCODERS = ("mjpeg",)
AUDIO_ENCODERS = ("pcm_*",)  # configure expands the glob against native pcm encoders
MUXERS = ("mov", "mp4", "image2")
PROTOCOLS = ("file", "pipe")
FILTERS = (
    # graph endpoints + the conversions the ffmpeg CLI auto-inserts for ANY
    # decode->encode run (scale/format to reach mjpeg's pixel format,
    # aresample/aformat to reach pcm sample formats)
    "abuffer", "abuffersink", "buffer", "buffersink",
    "aformat", "format", "aresample", "scale",
    # grade_pullback_mux: N mono streams -> one N-channel track
    "amerge",
    # no-op endpoints ([out] passes through null) + -ss/-t guards
    "anull", "null", "atrim", "trim",
)

# Post-build inventory checks: decoding acquisition codecs is the point of
# keeping all native decoders, so fail loudly if a headline one went missing.
REQUIRED_DECODERS = ("prores", "dnxhd", "h264", "hevc", "mpeg2video", "mjpeg", "dvvideo", "pcm_s24le")
REQUIRED_DEMUXERS = ("mxf", "mov,mp4,m4a,3gp,3g2,mj2", "image2")
REQUIRED_ENCODERS = ("mjpeg", "pcm_s24le", "pcm_s16le")
REQUIRED_FILTERS = ("amerge", "scale", "aresample")
FORBIDDEN_COMPONENT_SUBSTRINGS = ("libx264", "libx265")

# Anything matching these in a `-version` configuration line (or, for cross
# builds, anywhere in the binary's bytes — the configuration is a compile-time
# literal) means the build is not the LGPL one this recipe promises.
# --enable-lib* is blanket: this recipe links zero external libraries
# (--enable-zlib names the OS/pinned zlib, not an --enable-lib* flag).
GPL_CONFIGURE_MARKERS = ("--enable-gpl", "--enable-nonfree", "--enable-lib")

# ffmpeg/ffprobe -L's license text is selected by the preprocessor (fftools/
# opt_common.c), so exactly one blurb is baked into the binary and its bytes
# classify the build without running it. The literal wraps as "...GNU Lesser
# General Public\nLicense...", hence no trailing "License" here.
LGPL_LICENSE_BLURB = b"GNU Lesser General Public"

# System dylibs every macOS ships; anything outside these prefixes means the
# binary is not self-contained.
ALLOWED_LINKAGE_PREFIXES = ("/usr/lib/", "/System/Library/")

HERE = Path(__file__).resolve().parent
BUILD_DIR = HERE / "build"
DIST_DIR = HERE / "dist"
FIXTURES_DIR = HERE / "fixtures"


def platform_key(*, for_platform: str | None = None, for_machine: str | None = None) -> str:
    """Directory key for one OS+CPU flavour, e.g. darwin-arm64, win32-amd64."""
    target = for_platform or sys.platform
    machine = (for_machine or platform.machine()).lower()
    return f"{target}-{machine}"


def executable_name(tool: str, *, for_platform: str | None = None) -> str:
    return f"{tool}.exe" if (for_platform or sys.platform) == "win32" else tool


def configure_args(
    *,
    for_platform: str | None = None,
    for_machine: str | None = None,
    have_nasm: bool = True,
) -> list[str]:
    """The pinned configure invocation's platform-independent core (no --prefix;
    we never install).

    Kept as a pure function so tests can assert the licensing-critical shape
    (static, no GPL/nonfree, no autodetected external libs, trimmed write side)
    and so build_ffmpeg_windows.py builds from the SAME component set — it
    appends only toolchain flags (--target-os/--cross-prefix/zlib paths).
    """
    target = for_platform or sys.platform
    machine = (for_machine or platform.machine()).lower()
    args = [
        "--disable-shared",
        "--enable-static",
        # No silently-absorbed external libs from the build box; everything we
        # DO want (threads, zlib) is requested explicitly below so configure
        # hard-fails instead of quietly dropping it.
        "--disable-autodetect",
        "--disable-doc",
        "--disable-ffplay",
        "--disable-network",
        "--disable-devices",
        "--disable-hwaccels",
        "--disable-debug",
        "--disable-encoders",
        f"--enable-encoder={','.join(VIDEO_ENCODERS + AUDIO_ENCODERS)}",
        "--disable-muxers",
        f"--enable-muxer={','.join(MUXERS)}",
        "--disable-filters",
        f"--enable-filter={','.join(FILTERS)}",
        "--disable-protocols",
        f"--enable-protocol={','.join(PROTOCOLS)}",
        # mov demuxer needs zlib for compressed moov atoms; on macOS this links
        # /usr/lib's copy, on win64 the pinned static zlib from the windows
        # recipe satisfies it.
        "--enable-zlib",
    ]
    # pthreads sits in configure's AUTODETECT_LIBS, so --disable-autodetect
    # would silently build a SINGLE-THREADED ffmpeg. Request threads
    # explicitly; configure now errors if they can't be enabled.
    args.append("--enable-w32threads" if target == "win32" else "--enable-pthreads")
    if target == "darwin":
        floor = f"-mmacosx-version-min={MACOS_DEPLOYMENT_TARGET}"
        args += [f"--extra-cflags={floor}", f"--extra-ldflags={floor}"]
    if machine in {"x86_64", "amd64"} and not have_nasm:
        # x86 asm needs nasm/yasm at build time; slower-but-correct beats a
        # configure failure on a bare build box. arm64 needs no assembler.
        # The check keys on the TARGET arch (cross builds pass for_machine).
        args.append("--disable-x86asm")
    return args


def run(cmd: list[str], **kwargs) -> subprocess.CompletedProcess:
    shown = cmd if len(cmd) < 10 else [*cmd[:9], f"... ({len(cmd)} args)"]
    print("+", " ".join(str(part) for part in shown))
    return subprocess.run(cmd, check=True, **kwargs)


def require_tool(name: str) -> None:
    if shutil.which(name) is None:
        raise SystemExit(f"Required tool not found: {name}")


def _sha256(path: Path) -> str:
    digest = hashlib.sha256()
    with path.open("rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()


def tool_output(binary: Path, args: list[str]) -> str:
    result = subprocess.run(
        [str(binary), "-hide_banner", *args], check=True, capture_output=True, text=True,
    )
    return result.stdout


def download_source(build_root: Path) -> Path:
    """Fetch the pinned tarball (cached) and verify its sha256 before use."""
    build_root.mkdir(parents=True, exist_ok=True)
    tarball = build_root / f"ffmpeg-{FFMPEG_VERSION}.tar.xz"
    if tarball.exists() and _sha256(tarball) == FFMPEG_SHA256:
        print(f"Using cached {tarball.name} (sha256 verified).")
        return tarball
    print(f"Downloading {FFMPEG_URL} ...")
    with urllib.request.urlopen(FFMPEG_URL, timeout=120) as response, tarball.open("wb") as out:
        shutil.copyfileobj(response, out)
    actual = _sha256(tarball)
    if actual != FFMPEG_SHA256:
        tarball.unlink(missing_ok=True)
        raise SystemExit(
            f"ffmpeg tarball checksum mismatch:\n  expected {FFMPEG_SHA256}\n  actual   {actual}\n"
            "Refusing to build from an unverified source."
        )
    print(f"Downloaded {tarball.name} (sha256 verified).")
    return tarball


def extract_source(tarball: Path, build_root: Path) -> Path:
    source_dir = build_root / f"ffmpeg-{FFMPEG_VERSION}"
    if source_dir.exists():
        shutil.rmtree(source_dir)
    run(["tar", "-xf", str(tarball), "-C", str(build_root)])
    if not (source_dir / "configure").exists():
        raise SystemExit(f"Extracted tree has no configure script: {source_dir}")
    return source_dir


# --------------------------------------------------------------------------- #
# Verification: nothing leaves this script unproven. License text, configure
# markers, dynamic linkage, component inventory and the product's actual
# pipelines are all checked against the fresh binaries before install.

def _run_capture(cmd: list[str]) -> str:
    result = subprocess.run(cmd, check=False, capture_output=True, text=True, timeout=120)
    return (result.stdout or "") + (result.stderr or "")


def verify_license(binary: Path) -> str:
    """`-L` must say LGPL and `-version` must carry no GPL/external-lib flags.
    Returns the tool's version line for BUILDINFO."""
    license_text = " ".join(_run_capture([str(binary), "-L"]).split())
    if "GNU Lesser General Public License" not in license_text:
        raise SystemExit(f"{binary.name} -L does not report LGPL:\n{license_text}")

    version_text = _run_capture([str(binary), "-version"])
    for marker in GPL_CONFIGURE_MARKERS:
        if marker in version_text:
            raise SystemExit(f"{binary.name} -version configuration carries {marker!r}:\n{version_text}")
    for required in ("--disable-autodetect", "--disable-network"):
        if required not in version_text:
            raise SystemExit(f"{binary.name} -version configuration is missing {required!r}:\n{version_text}")
    return version_text.splitlines()[0].strip() if version_text.strip() else ""


def verify_static_linkage_macos(binary: Path) -> None:
    """otool -L may show only /usr/lib + /System frameworks: static means no
    vendored dylib closure, which is the whole point of this build."""
    output = _run_capture(["otool", "-L", str(binary)])
    foreign = [
        line.strip().split(" ", 1)[0]
        for line in output.splitlines()[1:]
        if line.strip() and not line.strip().split(" ", 1)[0].startswith(ALLOWED_LINKAGE_PREFIXES)
    ]
    if foreign:
        raise SystemExit(f"{binary.name} is not self-contained; foreign linkage:\n  " + "\n  ".join(foreign))


def verify_component_inventory(ffmpeg: Path) -> str:
    """Interrogate the fresh ffmpeg; abort on any licensing/component drift.
    Returns the configuration line for BUILDINFO."""
    version_text = tool_output(ffmpeg, ["-version"])
    configuration = next(
        (line.split(":", 1)[1].strip() for line in version_text.splitlines()
         if line.strip().startswith("configuration:")),
        "",
    )
    inventories = {
        "-decoders": REQUIRED_DECODERS,
        "-demuxers": REQUIRED_DEMUXERS,
        "-encoders": REQUIRED_ENCODERS,
        "-muxers": MUXERS,
        "-filters": REQUIRED_FILTERS,
    }
    for flag, required in inventories.items():
        listing = tool_output(ffmpeg, [flag])
        for name in required:
            if f" {name} " not in listing and f" {name}\n" not in listing:
                raise SystemExit(f"built ffmpeg is missing required component {name!r} ({flag})")
        for forbidden in FORBIDDEN_COMPONENT_SUBSTRINGS:
            if forbidden in listing:
                raise SystemExit(f"built ffmpeg unexpectedly contains {forbidden!r} ({flag})")
    if "--enable-pthreads" not in configuration and "--enable-w32threads" not in configuration:
        raise SystemExit("built ffmpeg has no threading — check the pthreads/w32threads flag")
    return configuration


def _write_sample_y4m(path: Path) -> None:
    # Hand-written YUV4MPEG2: 64x36 @ 24fps, 3 frames of 4:2:0 zeros. No encoder
    # needed, yet it exercises demuxer -> decoder -> fps/resolution reporting.
    header = b"YUV4MPEG2 W64 H36 F24:1 Ip A1:1 C420jpeg\n"
    frame = b"FRAME\n" + bytes(64 * 36 + 2 * (32 * 18))
    path.write_bytes(header + frame * 3)


def _write_sample_wav(path: Path) -> None:
    # Minimal RIFF/WAVE: 48 kHz mono 16-bit, 0.1 s of silence.
    sample_rate, channels, bits = 48000, 1, 16
    data = bytes(4800 * channels * (bits // 8))
    block_align = channels * bits // 8
    header = b"RIFF" + struct.pack("<I", 36 + len(data)) + b"WAVEfmt " + struct.pack(
        "<IHHIIHH", 16, 1, channels, sample_rate, sample_rate * block_align, block_align, bits
    ) + b"data" + struct.pack("<I", len(data))
    path.write_bytes(header + data)


def _probe_json(ffprobe: Path, media: Path, *, select: str | None = None) -> list[dict]:
    # Same invocation shape as the app: -v error, -show_entries, JSON writer
    # (premiere_bridge/resolve_drt_writer.py::_ffprobe_output).
    cmd = [str(ffprobe), "-v", "error"]
    if select:
        cmd += ["-select_streams", select]
    cmd += [
        "-show_entries",
        "stream=codec_type,codec_name,codec_tag_string,pix_fmt,width,height,"
        "r_frame_rate,avg_frame_rate,sample_rate,channels,bits_per_sample,duration"
        ":stream_tags=timecode",
        "-of",
        "json",
        str(media),
    ]
    result = subprocess.run(cmd, check=False, capture_output=True, text=True, timeout=60)
    if result.returncode != 0:
        raise SystemExit(f"ffprobe failed on {media.name}:\n{result.stderr}")
    return json.loads(result.stdout).get("streams") or []


def _probe_default_writer(ffprobe: Path, media: Path, select: str, entries: str) -> dict[str, str]:
    # The `-of default=nw=1` key=value path desktop_app/services.py relies on.
    result = subprocess.run(
        [str(ffprobe), "-v", "error", "-select_streams", select, "-show_entries", entries, "-of", "default=nw=1", str(media)],
        check=False,
        capture_output=True,
        text=True,
        timeout=60,
    )
    if result.returncode != 0:
        raise SystemExit(f"ffprobe (default writer) failed on {media.name}:\n{result.stderr}")
    info: dict[str, str] = {}
    for line in result.stdout.splitlines():
        if "=" in line:
            key, value = line.split("=", 1)
            info[key.strip()] = value.strip()
    return info


def _expect(context: str, info: dict, expectations: dict) -> None:
    for key, wanted in expectations.items():
        actual = info.get(key)
        if actual != wanted:
            raise SystemExit(f"{context}: expected {key}={wanted!r}, got {actual!r}\nfull: {info}")


def verify_probe_output(ffprobe: Path) -> None:
    """Probe synthesized samples + the committed ProRes fixture and assert the
    exact codec/fps/resolution/audio fields the app consumes."""
    with tempfile.TemporaryDirectory() as tmp_name:
        tmp = Path(tmp_name)
        y4m = tmp / "sample.y4m"
        wav = tmp / "sample.wav"
        _write_sample_y4m(y4m)
        _write_sample_wav(wav)

        video = _probe_json(ffprobe, y4m, select="v:0")
        if not video:
            raise SystemExit("no video stream reported for the y4m sample")
        _expect(
            "y4m video",
            video[0],
            {"codec_name": "rawvideo", "width": 64, "height": 36, "r_frame_rate": "24/1", "pix_fmt": "yuv420p"},
        )
        # ...and through the default writer, the way probe_media_codec reads it.
        flat = _probe_default_writer(ffprobe, y4m, "v:0", "stream=codec_name,width,height,r_frame_rate")
        _expect(
            "y4m video (default writer)",
            flat,
            {"codec_name": "rawvideo", "width": "64", "height": "36", "r_frame_rate": "24/1"},
        )

        audio = _probe_default_writer(ffprobe, wav, "a:0", "stream=codec_name,sample_rate,channels")
        _expect("wav audio", audio, {"codec_name": "pcm_s16le", "sample_rate": "48000", "channels": "1"})

    fixture = FIXTURES_DIR / "sample_prores_pcm.mov"
    if fixture.is_file():
        streams = _probe_json(ffprobe, fixture)
        by_type = {stream.get("codec_type"): stream for stream in streams}
        _expect(
            "fixture video",
            by_type.get("video") or {},
            {
                "codec_name": "prores",
                "codec_tag_string": "apco",
                "pix_fmt": "yuv422p10le",
                "width": 64,
                "height": 36,
                "r_frame_rate": "24/1",
            },
        )
        _expect(
            "fixture audio",
            by_type.get("audio") or {},
            {"codec_name": "pcm_s24le", "sample_rate": "48000", "channels": 1, "bits_per_sample": 24},
        )
        video_tags = (by_type.get("video") or {}).get("tags") or {}
        if video_tags.get("timecode") != "01:00:00:00":
            raise SystemExit(f"fixture timecode tag missing/wrong: {video_tags}")
    else:
        print(f"NOTE: {fixture} missing; skipped the ProRes/MOV fixture probe.")

    extra = os.environ.get("KONFORMISTA_FFPROBE_VERIFY_MEDIA", "").strip()
    if extra:
        streams = _probe_json(ffprobe, Path(extra).expanduser())
        if not any(stream.get("codec_name") for stream in streams):
            raise SystemExit(f"verification media {extra} probed with no codec_name")
        print(f"Probed {extra}: {[stream.get('codec_name') for stream in streams]}")


def _run_ffmpeg(ffmpeg: Path, args: list[str], context: str) -> None:
    result = subprocess.run(
        [str(ffmpeg), "-y", "-hide_banner", "-loglevel", "error", *args],
        check=False, capture_output=True, text=True, timeout=120,
    )
    if result.returncode != 0:
        raise SystemExit(f"{context} failed with the built ffmpeg:\n{result.stderr}")


def verify_product_pipelines(ffmpeg: Path, ffprobe: Path) -> None:
    """Re-run each of the product's three ffmpeg pipelines against the fresh
    binaries — the exact command shapes from vfx_pull_turnover.extract_still,
    grade_pullback_mux._extract_audio_ffmpeg and grade_pullback_assembler's
    filtergraph — so a missing encoder/filter fails HERE, not on a user render.
    """
    with tempfile.TemporaryDirectory() as tmp_name:
        tmp = Path(tmp_name)
        y4m = tmp / "sample.y4m"
        wav = tmp / "sample.wav"
        _write_sample_y4m(y4m)
        _write_sample_wav(wav)

        # vfx_pull_turnover.extract_still: 1-frame -q:v JPG (mjpeg into image2)
        still = tmp / "still.jpg"
        _run_ffmpeg(
            ffmpeg,
            ["-ss", "0.00000", "-i", str(y4m), "-frames:v", "1", "-q:v", "3", str(still)],
            "VFX still extraction (mjpeg)",
        )
        if not still.is_file() or still.stat().st_size == 0:
            raise SystemExit("VFX still extraction produced no output")

        # grade_pullback_mux: N mono streams amerge'd into one N-channel PCM mov
        audio_mov = tmp / "audio.mov"
        _run_ffmpeg(
            ffmpeg,
            ["-i", str(wav), "-i", str(wav),
             "-filter_complex", "[0:a][1:a]amerge=inputs=2[a]", "-map", "[a]",
             "-c:a", "pcm_s16le", "-f", "mov", str(audio_mov)],
            "audio amerge mux (pcm into mov)",
        )
        merged = _probe_default_writer(ffprobe, audio_mov, "a:0", "stream=codec_name,channels")
        _expect("amerge mux audio", merged, {"codec_name": "pcm_s16le", "channels": "2"})


def verify_build(bindir: Path) -> str:
    """The full on-box gauntlet for freshly built binaries. Returns the ffmpeg
    configuration line for BUILDINFO. (Cross builds can't execute their output;
    build_ffmpeg_windows.py byte-scans instead and defers this to --verify-only
    on a real Windows machine.)"""
    ffmpeg = bindir / executable_name("ffmpeg")
    ffprobe = bindir / executable_name("ffprobe")

    for binary in (ffmpeg, ffprobe):
        verify_license(binary)
        if sys.platform == "darwin":
            verify_static_linkage_macos(binary)
    configuration = verify_component_inventory(ffmpeg)
    verify_probe_output(ffprobe)
    verify_product_pipelines(ffmpeg, ffprobe)
    return configuration


def buildinfo_payload(
    *, configuration: str, version_line: str, binaries: dict[str, Path],
    target_key: str, behavioral_verified: bool, recipe: str, extra: dict | None = None,
) -> dict:
    payload = {
        "ffmpeg_version": FFMPEG_VERSION,
        "source_url": FFMPEG_URL,
        "source_sha256": FFMPEG_SHA256,
        "target": target_key,
        "license": "LGPL-2.1-or-later",
        "version_line": version_line,
        "configuration": configuration,
        "behavioral_verified": behavioral_verified,
        "recipe": recipe,
        "binaries": {name: _sha256(path) for name, path in sorted(binaries.items())},
    }
    payload.update(extra or {})
    return payload


def main() -> int:
    if sys.platform == "win32":
        raise SystemExit(
            "On Windows use packaging/ffmpeg_portable/build_ffmpeg_windows.py --native "
            "(MSYS2 MinGW-w64 shell); this script drives the macOS/clang build."
        )
    for tool in ("clang" if sys.platform == "darwin" else "cc", "make", "tar"):
        require_tool(tool)

    tarball = download_source(BUILD_DIR)
    source_dir = extract_source(tarball, BUILD_DIR)
    build_dir = BUILD_DIR / f"native-{platform_key()}"
    build_dir.mkdir(parents=True, exist_ok=True)

    have_nasm = bool(shutil.which("nasm") or shutil.which("yasm"))
    env = os.environ.copy()
    if sys.platform == "darwin":
        env["MACOSX_DEPLOYMENT_TARGET"] = MACOS_DEPLOYMENT_TARGET

    run(
        [str(source_dir / "configure"), *configure_args(have_nasm=have_nasm)],
        cwd=build_dir, env=env,
    )
    jobs = str(os.cpu_count() or 4)
    run(["make", f"-j{jobs}", "ffmpeg", "ffprobe"], cwd=build_dir, env=env)

    configuration = verify_build(build_dir)

    out_dir = DIST_DIR / platform_key()
    out_dir.mkdir(parents=True, exist_ok=True)
    staged: dict[str, Path] = {}
    for tool in ("ffmpeg", "ffprobe"):
        name = executable_name(tool)
        target = out_dir / name
        shutil.copy2(build_dir / name, target)
        target.chmod(0o755)
        staged[name] = target
    version_line = tool_output(staged[executable_name("ffmpeg")], ["-version"]).splitlines()[0]
    payload = buildinfo_payload(
        configuration=configuration,
        version_line=version_line,
        binaries=staged,
        target_key=platform_key(),
        behavioral_verified=True,
        recipe="packaging/ffmpeg_portable/build_portable_ffmpeg.py",
        extra={"macos_deployment_target": MACOS_DEPLOYMENT_TARGET} if sys.platform == "darwin" else None,
    )
    (out_dir / "BUILDINFO.json").write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
    for target in staged.values():
        size_mb = target.stat().st_size / (1024 * 1024)
        print(f"staged {target} ({size_mb:.1f} MB)")
    print("packaging/build_macos_dmg.py will now bundle these instead of system copies.")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
