#!/bin/bash # # Reproducible build of the FFmpeg libraries VidViewer embeds to convert # legacy, patent-free codecs (MPEG-4 Part 2 / Xvid / DivX 4-6, VP8, Theora, # Sorenson Spark / H.263, Vorbis) into cached MP4 proxies, and to decode # Dolby TrueHD / MLP audio in MKV (the one owner-approved codec that isn't # patent-free; DTS stays out). The H.264 / HEVC parsers only split frames for # passthrough remuxing (no H.264 / HEVC decoder is built). # # scripts/ffmpeg/build.sh /path/to/ffmpeg-9.0.2.tar.xz # # Produces Vendor/FFmpeg/{include,lib} plus CONFIGURE.txt, COMPONENTS.txt, # COPYING.LGPLv2.1 and LICENSE.md. SOURCE.md and THIRD_PARTY_NOTICE.txt are # hand-written and left untouched. # # The result is: # * LGPL 2.1 or later: no --enable-gpl / --enable-version3 / --enable-nonfree # (checked at runtime via av*_license() — the build fails otherwise); # * decoder/demuxer-only: no encoders, muxers, filters, devices, programs, # network code, swscale or swresample; # * hermetic: the script re-runs itself under `env -i` with a system-only # PATH, and configure gets --disable-autodetect, so nothing from Homebrew # (or /usr/local, or the caller's CFLAGS / PKG_CONFIG_PATH) is detected or # linked. The only external libraries are libSystem and the system zlib; # * arm64, macOS 13.0 minimum, three dylibs whose install names are # @rpath/lib..dylib, ready to embed in Contents/Frameworks. # # Environment (all optional): # FFMPEG_BUILD_ROOT parent dir for the temporary build tree [$TMPDIR] # FFMPEG_OUT_DIR output dir [/Vendor/FFmpeg] # JOBS make -j [hw.ncpu] # KEEP_BUILD=1 keep the build tree (default: deleted on exit) # DEVELOPER_DIR pick a specific Xcode (passed through) set -euo pipefail FFMPEG_VERSION="9.0.2" FFMPEG_SHA256="8c3850283eb25fa026482078a04051e0be17347b09ef81a0849bec15a96e002e" MIN_MACOS="13.0" # --- Re-exec with a clean environment --------------------------------------- if [[ -z "${VV_FFMPEG_CLEAN_ENV:-}" ]]; then script="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")" exec /usr/bin/env -i \ VV_FFMPEG_CLEAN_ENV=1 \ HOME="$HOME" \ PATH=/usr/bin:/bin:/usr/sbin:/sbin \ LANG=C LC_ALL=C \ TMPDIR="${TMPDIR:-/tmp}" \ ${FFMPEG_BUILD_ROOT:+FFMPEG_BUILD_ROOT="$FFMPEG_BUILD_ROOT"} \ ${FFMPEG_OUT_DIR:+FFMPEG_OUT_DIR="$FFMPEG_OUT_DIR"} \ ${JOBS:+JOBS="$JOBS"} \ ${KEEP_BUILD:+KEEP_BUILD="$KEEP_BUILD"} \ ${DEVELOPER_DIR:+DEVELOPER_DIR="$DEVELOPER_DIR"} \ /bin/bash "$script" "$@" fi die() { echo "build.sh: error: $*" >&2; exit 1; } log() { echo "==> $*"; } [[ $# -eq 1 ]] || die "usage: $0 /path/to/ffmpeg-${FFMPEG_VERSION}.tar.xz" TARBALL="$(cd "$(dirname "$1")" && pwd)/$(basename "$1")" [[ -f "$TARBALL" ]] || die "tarball not found: $1" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" OUT_DIR="${FFMPEG_OUT_DIR:-$REPO_ROOT/Vendor/FFmpeg}" JOBS="${JOBS:-$(sysctl -n hw.ncpu)}" export MACOSX_DEPLOYMENT_TARGET="$MIN_MACOS" [[ "$(uname -m)" == "arm64" ]] || die "build on an Apple Silicon Mac (native arm64)" # --- Verify the source ------------------------------------------------------- log "Verifying $TARBALL" actual="$(/usr/bin/shasum -a 256 "$TARBALL" | awk '{print $1}')" [[ "$actual" == "$FFMPEG_SHA256" ]] || die "sha256 mismatch: expected $FFMPEG_SHA256, got $actual" echo " sha256 OK ($actual)" if [[ -f "$TARBALL.asc" ]] && command -v gpg >/dev/null 2>&1; then gpg --verify "$TARBALL.asc" "$TARBALL" || die "PGP signature check failed" else echo " (PGP signature not checked here: no gpg on the system PATH; the" echo " pinned sha256 is the gate. See Vendor/FFmpeg/SOURCE.md.)" fi SDK_VERSION="$(xcrun --sdk macosx --show-sdk-version)" CLANG_VERSION="$(clang --version | head -1)" log "Toolchain: $CLANG_VERSION, macOS SDK $SDK_VERSION" # --- Temporary build tree ---------------------------------------------------- BUILD_PARENT="${FFMPEG_BUILD_ROOT:-${TMPDIR:-/tmp}}" mkdir -p "$BUILD_PARENT" WORK="$(mktemp -d "$BUILD_PARENT/vv-ffmpeg-build.XXXXXX")" cleanup() { if [[ "${KEEP_BUILD:-0}" == "1" ]]; then echo "build tree kept at $WORK" else rm -rf "$WORK" fi } trap cleanup EXIT log "Extracting into $WORK" tar -xf "$TARBALL" -C "$WORK" SRC="$WORK/ffmpeg-$FFMPEG_VERSION" [[ -x "$SRC/configure" ]] || die "unexpected tarball layout" # --- Configure --------------------------------------------------------------- # Built in-tree so __FILE__ strings stay relative, and with no --prefix (the # install goes through DESTDIR) so no machine-specific path ends up in the # configuration string compiled into the libraries. # # --enable-pthreads: pthreads is on configure's autodetect list, so # --disable-autodetect would otherwise leave the decoders single-threaded. # --enable-zlib: the system /usr/lib/libz (Matroska zlib-compressed tracks, # QuickTime compressed movie headers). # -dead_strip_dylibs: configure always probes CoreFoundation / CoreMedia / # CoreVideo and adds them to libavutil's link line; nothing in this # configuration uses them, so their load commands are dropped. DECODERS="mpeg4,h263,h263i,h263p,flv,vp8,theora,vorbis,truehd,mlp" DEMUXERS="avi,matroska,ogg,flv,mov,mpegps" PARSERS="mpeg4video,h263,vp8,vorbis,mpegaudio,ac3,aac,flac,opus,mlp,h264,hevc" BSFS="mpeg4_unpack_bframes,aac_adtstoasc" PROTOCOLS="file" CONFIGURE_ARGS=( --arch=arm64 --cc=clang "--extra-cflags=-mmacosx-version-min=$MIN_MACOS" "--extra-ldflags=-mmacosx-version-min=$MIN_MACOS -Wl,-headerpad_max_install_names -Wl,-dead_strip_dylibs" --install-name-dir=@rpath --enable-shared --disable-static --disable-autodetect --enable-pthreads --enable-zlib --disable-programs --disable-doc --disable-network --disable-avdevice --disable-avfilter --disable-swscale --disable-swresample --disable-iamf --disable-debug --disable-everything "--enable-decoder=$DECODERS" "--enable-demuxer=$DEMUXERS" "--enable-parser=$PARSERS" "--enable-bsf=$BSFS" "--enable-protocol=$PROTOCOLS" ) log "Configuring" (cd "$SRC" && ./configure "${CONFIGURE_ARGS[@]}") > "$WORK/configure.out" 2>&1 || { tail -40 "$WORK/configure.out"; tail -60 "$SRC/ffbuild/config.log"; die "configure failed"; } grep -q '^License: LGPL version 2.1 or later$' "$WORK/configure.out" || { cat "$WORK/configure.out"; die "configure did not settle on LGPL 2.1+"; } if grep -qi "warning" "$WORK/configure.out"; then echo " configure warnings:"; grep -i -A2 "warning" "$WORK/configure.out" | sed 's/^/ /' fi # --- Build + stage ----------------------------------------------------------- log "Building (make -j$JOBS)" make -C "$SRC" -j"$JOBS" > "$WORK/make.out" 2>&1 || { tail -60 "$WORK/make.out"; die "make failed"; } if grep -q "warning: object file .* was built for newer" "$WORK/make.out"; then die "an object was built for a macOS newer than $MIN_MACOS" fi STAGE="$WORK/stage" make -C "$SRC" install DESTDIR="$STAGE" > "$WORK/install.out" 2>&1 || { tail -40 "$WORK/install.out"; die "make install failed"; } PREFIX_DIR="$STAGE/usr/local" # Assemble the vendor tree: real dylibs named after their install names # (lib..dylib), no symlinks, no pkg-config files. NEW="$WORK/out" mkdir -p "$NEW/lib" cp -R "$PREFIX_DIR/include" "$NEW/include" LIBS=() for name in avutil avcodec avformat; do real="$(cd "$PREFIX_DIR/lib" && ls lib$name.*.*.*.dylib)" id="$(otool -D "$PREFIX_DIR/lib/$real" | sed -n 2p)" [[ "$id" == @rpath/lib$name.*.dylib ]] || die "$real has install name '$id'" file="${id#@rpath/}" cp "$PREFIX_DIR/lib/$real" "$NEW/lib/$file" LIBS+=("$file") done # Swift / Clang module map over the public API the converter uses. No `link` # directive: the Xcode target links the dylibs explicitly. cat > "$NEW/include/module.modulemap" <<'EOF' // Generated by scripts/ffmpeg/build.sh — `import CFFmpeg` from Swift, with // Vendor/FFmpeg/include on the import / header search path. module CFFmpeg [system] { header "libavutil/avutil.h" header "libavutil/channel_layout.h" header "libavutil/display.h" header "libavutil/error.h" header "libavutil/imgutils.h" header "libavutil/mathematics.h" header "libavutil/opt.h" header "libavutil/pixdesc.h" header "libavutil/samplefmt.h" header "libavcodec/avcodec.h" header "libavcodec/bsf.h" header "libavformat/avformat.h" export * } EOF cp "$SRC/COPYING.LGPLv2.1" "$NEW/COPYING.LGPLv2.1" cp "$SRC/LICENSE.md" "$NEW/LICENSE.md" # Shell-quoted so the first line of CONFIGURE.txt can be pasted as-is. quoted_args() { local a out="" for a in "$@"; do if [[ "$a" == *[[:space:]]* ]]; then out+=" '$a'"; else out+=" $a"; fi done printf '%s' "$out" } { echo "./configure$(quoted_args "${CONFIGURE_ARGS[@]}")" echo echo "# FFmpeg $FFMPEG_VERSION (sha256 $FFMPEG_SHA256)" echo "# Built by scripts/ffmpeg/build.sh with: $CLANG_VERSION, macOS SDK $SDK_VERSION" echo "# Environment: env -i, PATH=/usr/bin:/bin:/usr/sbin:/sbin, MACOSX_DEPLOYMENT_TARGET=$MIN_MACOS" echo "# (configure's \"pkg-config not found\" warning is expected: the build PATH" echo "# deliberately has no pkg-config, and nothing here needs one.)" echo "# configure summary:" sed -n '/^install prefix/,$p' "$WORK/configure.out" | sed 's/^/# /' } > "$NEW/CONFIGURE.txt" # --- Verify ------------------------------------------------------------------ log "Verifying the libraries" for lib in "${LIBS[@]}"; do path="$NEW/lib/$lib" archs="$(lipo -archs "$path")" [[ "$archs" == "arm64" ]] || die "$lib archs: $archs" minos="$(otool -l "$path" | awk '/LC_BUILD_VERSION/{f=1} f&&$1=="minos"{print $2; exit}')" [[ "$minos" == "$MIN_MACOS" ]] || die "$lib minos $minos (want $MIN_MACOS)" # Dependencies: only the OS and our own libraries. while read -r dep; do case "$dep" in /usr/lib/libSystem.B.dylib|/usr/lib/libz.1.dylib) ;; @rpath/libavutil.*.dylib|@rpath/libavcodec.*.dylib|@rpath/libavformat.*.dylib) ;; *) die "$lib links unexpected library: $dep" ;; esac done < <(otool -L "$path" | tail -n +2 | awk '{print $1}') if strings -a "$path" | grep -qiE 'homebrew|/opt/local|/usr/local'; then die "$lib contains a reference to a non-system prefix" fi # arm64 code must be signed to load. The linker's ad-hoc signature # survives install's `strip -x` with current Xcode; re-sign ad-hoc if a # toolchain ever leaves it invalid. Xcode re-signs with the app's # identity when it embeds the dylib ("Code Sign On Copy"). codesign -v "$path" 2>/dev/null || codesign -f -s - "$path" >/dev/null 2>&1 codesign -v "$path" || die "$lib: invalid code signature" echo " $lib $(stat -f%z "$path") bytes minos $minos deps OK" done log "Runtime check (licenses, components)" clang -mmacosx-version-min="$MIN_MACOS" -Wall -O1 -I"$NEW/include" \ "$SCRIPT_DIR/ffcheck.c" "${LIBS[@]/#/$NEW/lib/}" -Wl,-rpath,"$NEW/lib" -o "$WORK/ffcheck" "$WORK/ffcheck" > "$NEW/COMPONENTS.txt" || { cat "$NEW/COMPONENTS.txt"; die "runtime check failed"; } sed 's/^/ /' "$NEW/COMPONENTS.txt" # --- Install into the vendor dir --------------------------------------------- # Replace only the generated parts; SOURCE.md / THIRD_PARTY_NOTICE.txt stay. log "Installing into $OUT_DIR" mkdir -p "$OUT_DIR" for item in include lib COPYING.LGPLv2.1 LICENSE.md CONFIGURE.txt COMPONENTS.txt; do rm -rf "${OUT_DIR:?}/$item" mv "$NEW/$item" "$OUT_DIR/$item" done du -sh "$OUT_DIR/lib"/* "$OUT_DIR/include" | sed 's/^/ /' log "Done (FFmpeg $FFMPEG_VERSION, LGPL 2.1+)"