#!/bin/sh
# Builds the ffmpeg and ffprobe the HyperDrop Media extension ships: LGPL
# only (no GPL and no nonfree component, so no software H.264 encoder),
# with dav1d for AV1 and Apple's VideoToolbox and AudioToolbox, static
# against dav1d and the libav* libraries, one universal binary each (arm64
# plus x86_64).
#
#   mediaext/ffmpeg/build.sh            # from anywhere; takes a while
#
# Needs, from Homebrew:  brew install nasm meson ninja pkg-config
# and Xcode's command line tools (clang, lipo, make). Without nasm the
# x86_64 half is built with --disable-x86asm (slower, still correct).
#
# Versions and tarball hashes come from versions.env beside this script. An
# hashes must be pinned before any download is used.
#
# Output:  mediaext/.build/ffmpeg/<FFMPEG_VERSION>/universal/{ffmpeg,ffprobe}
# Sources: mediaext/.build/ffmpeg/src/
# The build fails rather than ship one architecture. A copy of this file is
# published beside the Media page as website/extensions/media/build-ffmpeg.sh
# (kept identical by a test in mediaext/ffmpeg).
set -eu

HERE="$(cd "$(dirname "$0")" && pwd)"          # mediaext/ffmpeg
MEDIAEXT="$(cd "$HERE/.." && pwd)"              # mediaext
VERSIONS="$HERE/versions.env"
. "$VERSIONS"

BUILD="$MEDIAEXT/.build/ffmpeg"
SRC="$BUILD/src"
OUT="$BUILD/$FFMPEG_VERSION"
UNIVERSAL="$OUT/universal"
MIN_MACOS=26.0
JOBS="$(sysctl -n hw.ncpu 2>/dev/null || echo 4)"
NATIVE_ARCH="$(uname -m)"

# Include the complete dependency pins and recipe in the cache identity.
# These modes are also used by the bundle builder and need no build tools.
cache_key() { cat "$VERSIONS" "$HERE/build.sh" | shasum -a 256 | awk '{print $1}'; }
RECIPE_KEY="$(cache_key)"
case "${1:-}" in
  --cache-key) echo "$RECIPE_KEY"; exit 0 ;;
  --check-cache)
    [ "$(cat "$OUT/.recipe-sha256" 2>/dev/null || true)" = "$RECIPE_KEY" ] \
      && [ -x "$UNIVERSAL/ffmpeg" ] && [ -x "$UNIVERSAL/ffprobe" ] \
      || { echo "ffmpeg cache does not match versions.env and build.sh; rebuild it" >&2; exit 1; }
    exit 0 ;;
  "") ;;
  *) echo "usage: $0 [--cache-key|--check-cache]" >&2; exit 2 ;;
esac

# A version's output belongs to this build only. Never reuse a partial or
# stale recipe, and never remove source downloads or another version.
if [ "$(cat "$OUT/.recipe-sha256" 2>/dev/null || true)" != "$RECIPE_KEY" ]; then
  [ ! -L "$OUT" ] || { echo "refusing to replace a symlinked build directory: $OUT" >&2; exit 1; }
  rm -rf "$OUT"
fi

for tool in clang lipo make meson ninja pkg-config; do
  command -v "$tool" >/dev/null 2>&1 || { echo "missing $tool: brew install nasm meson ninja pkg-config" >&2; exit 1; }
done
HAVE_NASM=1
command -v nasm >/dev/null 2>&1 || { HAVE_NASM=0; echo "==> nasm not found: x86_64 is built with --disable-x86asm"; }

mkdir -p "$SRC" "$UNIVERSAL"

sha256_of() {
  if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | awk '{print $1}'
  elif command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | awk '{print $1}'
  else echo "no sha256 tool" >&2; exit 1; fi
}

# pin NAME FILE: verifies FILE against the reviewed pin in versions.env.
pin() {
  name="$1"; file="$2"
  actual="$(sha256_of "$file")"
  expected="$(eval "echo \${${name}_SHA256}")"
  if ! printf '%s\n' "$expected" | LC_ALL=C grep -Eq '^[0-9a-f]{64}$'; then
    echo "ERROR: ${name}_SHA256 must contain a reviewed SHA-256 pin" >&2
    exit 1
  elif [ "$expected" != "$actual" ]; then
    echo "ERROR: $file has SHA-256 $actual, versions.env says $expected" >&2
    exit 1
  else
    echo "    $(basename "$file") matches versions.env"
  fi
}

# fetch URL FILE NAME: downloads once, then pins.
fetch() {
  url="$1"; file="$2"; name="$3"
  if [ ! -s "$file" ]; then
    echo "==> downloading $url"
    curl -fL --retry 3 -o "$file.tmp" "$url"
    mv "$file.tmp" "$file"
  fi
  pin "$name" "$file"
}

FFMPEG_TAR="$SRC/ffmpeg-$FFMPEG_VERSION.tar.xz"
DAV1D_TAR="$SRC/dav1d-$DAV1D_VERSION.tar.xz"
fetch "https://ffmpeg.org/releases/ffmpeg-$FFMPEG_VERSION.tar.xz" "$FFMPEG_TAR" FFMPEG
fetch "https://downloads.videolan.org/pub/videolan/dav1d/$DAV1D_VERSION/dav1d-$DAV1D_VERSION.tar.xz" "$DAV1D_TAR" DAV1D

# meson_cpu_family ARCH: meson's name for the architecture.
meson_cpu_family() { case "$1" in arm64) echo aarch64;; *) echo "$1";; esac; }

# build_dav1d ARCH: a static libdav1d into $OUT/<arch>/prefix.
build_dav1d() {
  arch="$1"
  prefix="$OUT/$arch/prefix"
  if [ -f "$prefix/lib/pkgconfig/dav1d.pc" ]; then echo "==> dav1d $DAV1D_VERSION ($arch) already built"; return; fi
  echo "==> dav1d $DAV1D_VERSION ($arch)"
  srcdir="$OUT/$arch/dav1d-src"
  rm -rf "$srcdir"; mkdir -p "$srcdir"
  tar -xJf "$DAV1D_TAR" -C "$srcdir" --strip-components 1
  cross="$OUT/$arch/cross.ini"
  cat > "$cross" <<EOF
[binaries]
c = ['clang', '-arch', '$arch', '-mmacosx-version-min=$MIN_MACOS']
cpp = ['clang++', '-arch', '$arch', '-mmacosx-version-min=$MIN_MACOS']
objc = ['clang', '-arch', '$arch', '-mmacosx-version-min=$MIN_MACOS']
ar = 'ar'
strip = 'strip'
nasm = 'nasm'
pkg-config = 'pkg-config'

[host_machine]
system = 'darwin'
cpu_family = '$(meson_cpu_family "$arch")'
cpu = '$(meson_cpu_family "$arch")'
endian = 'little'
EOF
  asm=true
  if [ "$arch" = x86_64 ] && [ "$HAVE_NASM" = 0 ]; then asm=false; fi
  ( cd "$srcdir" && meson setup build --cross-file "$cross" --prefix "$prefix" --libdir lib \
      --default-library=static --buildtype=release -Denable_tools=false -Denable_tests=false \
      -Denable_examples=false -Denable_asm=$asm >/dev/null \
    && ninja -C build >/dev/null && ninja -C build install >/dev/null )
  [ -f "$prefix/lib/pkgconfig/dav1d.pc" ] || { echo "dav1d ($arch) did not install its .pc file" >&2; exit 1; }
}

# build_ffmpeg ARCH: static ffmpeg and ffprobe into $OUT/<arch>/prefix/bin.
build_ffmpeg() {
  arch="$1"
  prefix="$OUT/$arch/prefix"
  if [ -x "$prefix/bin/ffmpeg" ] && [ -x "$prefix/bin/ffprobe" ]; then echo "==> ffmpeg $FFMPEG_VERSION ($arch) already built"; return; fi
  echo "==> ffmpeg $FFMPEG_VERSION ($arch)"
  srcdir="$OUT/$arch/ffmpeg-src"
  rm -rf "$srcdir"; mkdir -p "$srcdir"
  tar -xJf "$FFMPEG_TAR" -C "$srcdir" --strip-components 1
  set -- \
    --prefix="$prefix" \
    --pkg-config-flags=--static \
    --disable-gpl --disable-nonfree \
    --disable-doc --disable-debug \
    --disable-protocols --enable-protocol=file,pipe,tcp,http \
    --disable-protocol=rtp,udp --disable-demuxer=concat,hls,dash \
    --disable-shared --enable-static \
    --disable-ffplay --disable-sdl2 --disable-xlib --disable-libxcb \
    --enable-videotoolbox --enable-audiotoolbox --enable-libdav1d \
    --extra-cflags="-arch $arch -mmacosx-version-min=$MIN_MACOS" \
    --extra-ldflags="-arch $arch -mmacosx-version-min=$MIN_MACOS"
  if [ "$arch" != "$NATIVE_ARCH" ]; then
    set -- "$@" --enable-cross-compile --arch="$arch" --target-os=darwin --cc="clang -arch $arch"
  fi
  if [ "$arch" = x86_64 ] && [ "$HAVE_NASM" = 0 ]; then
    set -- "$@" --disable-x86asm
  fi
  ( cd "$srcdir" \
    && PKG_CONFIG_LIBDIR="$prefix/lib/pkgconfig" PKG_CONFIG_PATH="" ./configure "$@" \
    && make -j"$JOBS" >/dev/null && make install >/dev/null )
  [ -x "$prefix/bin/ffmpeg" ] && [ -x "$prefix/bin/ffprobe" ] || { echo "ffmpeg ($arch) did not produce both binaries" >&2; exit 1; }
}

for arch in arm64 x86_64; do
  build_dav1d "$arch"
  build_ffmpeg "$arch"
done

echo "==> lipo"
for bin in ffmpeg ffprobe; do
  a="$OUT/arm64/prefix/bin/$bin"; x="$OUT/x86_64/prefix/bin/$bin"
  [ -x "$a" ] && [ -x "$x" ] || { echo "ERROR: $bin is missing for one architecture; both are required" >&2; exit 1; }
  lipo -create "$a" "$x" -output "$UNIVERSAL/$bin"
  lipo -info "$UNIVERSAL/$bin" | grep -q "arm64" && lipo -info "$UNIVERSAL/$bin" | grep -q "x86_64" \
    || { echo "ERROR: $UNIVERSAL/$bin is not universal" >&2; exit 1; }
done

echo "==> configuration"
"$UNIVERSAL/ffmpeg" -hide_banner -version | sed -n '1,3p'
if "$UNIVERSAL/ffmpeg" -hide_banner -version | grep -qE -- '--enable-(gpl|nonfree)'; then
  echo "ERROR: the built ffmpeg reports a GPL or nonfree configuration" >&2; exit 1
fi
actual_protocols="$("$UNIVERSAL/ffmpeg" -hide_banner -protocols | sed -n 's/^  \([a-z0-9_]*\)$/\1/p' | sort -u | tr '\n' ' ')"
[ "$actual_protocols" = "file http pipe tcp " ] \
  || { echo "ERROR: unexpected ffmpeg protocols: $actual_protocols" >&2; exit 1; }
printf '%s\n' "$RECIPE_KEY" > "$OUT/.recipe-sha256"
echo "built $UNIVERSAL/ffmpeg and ffprobe ($FFMPEG_VERSION, dav1d $DAV1D_VERSION)"
