#!/bin/bash # build-universal-ffmpeg.sh — a self-contained, LGPL, universal ffmpeg. # # SUPERSEDES build-lgpl-ffmpeg.sh, which produced a binary that dynamically # linked four Homebrew dylibs (SDL2, lzma, LAME, X11). Those paths exist only on # the build machine, so the binary crashed on launch anywhere else — and under # hardened runtime it failed even locally, because Homebrew dylibs are unsigned. # # This build fixes three things at once: # 1. SELF-CONTAINED — LAME is compiled from source and linked statically, and # every optional dependency that dragged in a dylib is disabled. The result # links ONLY against macOS system frameworks. # 2. UNIVERSAL — arm64 + x86_64, lipo'd together, so it runs natively on both # Apple Silicon and Intel. # 3. STILL LGPL — no --enable-gpl, no --enable-version3, no libx264/libx265. set -euo pipefail FFMPEG_VER="${FFMPEG_VERSION:-7.1.1}" LAME_VER="${LAME_VERSION:-3.100}" HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" WORK="$HERE/work-universal" OUT="$HERE/dist" MIN="11.0" # oldest macOS we support mkdir -p "$WORK" "$OUT" cd "$WORK" fetch () { # url, tarball [ -f "$2" ] || { echo "→ fetching $2"; curl -fL --retry 3 -o "$2" "$1"; } } fetch "https://downloads.sourceforge.net/project/lame/lame/$LAME_VER/lame-$LAME_VER.tar.gz" "lame-$LAME_VER.tar.gz" # ---- nasm ------------------------------------------------------------------ # ffmpeg's x86 SIMD paths need an assembler. Rather than --disable-x86asm, which # ships a knowingly slower binary to every Intel user, build nasm from source # into our own prefix. No admin rights, no Homebrew, ~20 seconds. NASM_VER="2.16.03" TOOLS="$WORK/tools" if [ ! -x "$TOOLS/bin/nasm" ]; then fetch "https://www.nasm.us/pub/nasm/releasebuilds/$NASM_VER/nasm-$NASM_VER.tar.xz" "nasm-$NASM_VER.tar.xz" echo "→ building nasm" rm -rf "nasm-src"; mkdir -p nasm-src tar xf "nasm-$NASM_VER.tar.xz" -C nasm-src --strip-components=1 ( cd nasm-src ./configure --prefix="$TOOLS" > "$WORK/nasm.log" 2>&1 || { tail -20 "$WORK/nasm.log"; exit 1; } make -j"$(sysctl -n hw.ncpu)" >> "$WORK/nasm.log" 2>&1 make install >> "$WORK/nasm.log" 2>&1 || { mkdir -p "$TOOLS/bin"; cp nasm "$TOOLS/bin/"; } ) fi export PATH="$TOOLS/bin:$PATH" echo "→ assembler: $(command -v nasm) $(nasm -v 2>/dev/null | head -1)" fetch "https://ffmpeg.org/releases/ffmpeg-$FFMPEG_VER.tar.xz" "ffmpeg-$FFMPEG_VER.tar.xz" build_arch () { local ARCH="$1" # arm64 | x86_64 local PREFIX="$WORK/prefix-$ARCH" local CFLAGS="-arch $ARCH -mmacosx-version-min=$MIN" local LDFLAGS="-arch $ARCH -mmacosx-version-min=$MIN" mkdir -p "$PREFIX" # ---- LAME, static, from source ----------------------------------------- # Homebrew's LAME is x86_64-only on an Intel Mac and ships a .dylib the linker # prefers over the .a. Compiling it ourselves gives a static lib per arch. if [ ! -f "$PREFIX/lib/libmp3lame.a" ]; then echo "→ [$ARCH] lame" rm -rf "lame-$ARCH"; mkdir -p "lame-$ARCH" tar xf "lame-$LAME_VER.tar.gz" -C "lame-$ARCH" --strip-components=1 ( cd "lame-$ARCH" # lame 3.100 ships a symbol its own headers no longer declare sed -i '' '/lame_init_old/d' include/libmp3lame.sym 2>/dev/null || true # LAME 3.100 predates Apple Silicon and its config.sub rejects the # "arm64" triple outright. aarch64 is the GNU-canonical spelling for the # same machine, and the old config.sub does recognise it. local HOSTTRIPLE="$ARCH-apple-darwin" [ "$ARCH" = "arm64" ] && HOSTTRIPLE="aarch64-apple-darwin" ./configure --prefix="$PREFIX" --host="$HOSTTRIPLE" \ --disable-shared --enable-static --disable-frontend --disable-dependency-tracking \ CFLAGS="$CFLAGS" LDFLAGS="$LDFLAGS" > "$WORK/lame-$ARCH.log" 2>&1 \ || { tail -25 "$WORK/lame-$ARCH.log"; exit 1; } make -j"$(sysctl -n hw.ncpu)" >> "$WORK/lame-$ARCH.log" 2>&1 make install >> "$WORK/lame-$ARCH.log" 2>&1 ) fi # ---- ffmpeg ------------------------------------------------------------- echo "→ [$ARCH] ffmpeg" rm -rf "ffmpeg-$ARCH"; mkdir -p "ffmpeg-$ARCH" tar xf "ffmpeg-$FFMPEG_VER.tar.xz" -C "ffmpeg-$ARCH" --strip-components=1 ( cd "ffmpeg-$ARCH" local CROSS="" [ "$ARCH" != "$(uname -m)" ] && CROSS="--enable-cross-compile --arch=$ARCH --target-os=darwin" # Every --disable below removes a dependency that previously pulled in a # Homebrew dylib, or a subsystem an audio converter has no use for. ./configure \ --prefix="$PREFIX" \ $CROSS \ --cc="clang -arch $ARCH" \ --extra-cflags="$CFLAGS -I$PREFIX/include" \ --extra-ldflags="$LDFLAGS -L$PREFIX/lib" \ --enable-libmp3lame \ --pkg-config-flags="--static" \ --enable-static --disable-shared \ --disable-sdl2 \ --disable-lzma \ --disable-libxcb --disable-xlib \ --disable-iconv \ --disable-programs --enable-ffmpeg \ --disable-doc --disable-debug --disable-network \ --disable-videotoolbox --disable-audiotoolbox \ > "$WORK/ffmpeg-$ARCH.log" 2>&1 || { tail -30 "$WORK/ffmpeg-$ARCH.log"; exit 1; } make -j"$(sysctl -n hw.ncpu)" >> "$WORK/ffmpeg-$ARCH.log" 2>&1 \ || { tail -30 "$WORK/ffmpeg-$ARCH.log"; exit 1; } cp ffmpeg "$WORK/ffmpeg-bin-$ARCH" ) } build_arch x86_64 build_arch arm64 echo "→ lipo" lipo -create "$WORK/ffmpeg-bin-x86_64" "$WORK/ffmpeg-bin-arm64" -output "$OUT/ffmpeg" chmod +x "$OUT/ffmpeg" cp "$WORK/ffmpeg-x86_64/COPYING.LGPLv2.1" "$OUT/LICENSE-ffmpeg-LGPLv2.1.txt" 2>/dev/null || true # ---- gates: refuse to hand back a binary that fails any of these ----------- echo echo "→ verifying" echo "architectures: $(lipo -archs "$OUT/ffmpeg")" lipo -archs "$OUT/ffmpeg" | grep -q arm64 || { echo "✗ missing arm64"; exit 1; } lipo -archs "$OUT/ffmpeg" | grep -q x86_64 || { echo "✗ missing x86_64"; exit 1; } EXTERNAL="$(otool -L "$OUT/ffmpeg" | tail -n +2 | awk '{print $1}' \ | grep -vE '^/usr/lib/|^/System/Library/' || true)" if [ -n "$EXTERNAL" ]; then echo "✗ NOT SELF-CONTAINED — would crash on another Mac:"; echo "$EXTERNAL" | sed 's/^/ /' exit 1 fi echo "✓ links only system libraries" if "$OUT/ffmpeg" -version 2>&1 | grep -q -- "--enable-gpl"; then echo "✗ GPL build"; exit 1; fi "$OUT/ffmpeg" -L 2>&1 | tr '\n' ' ' | grep -q "GNU Lesser General Public License" \ || { echo "✗ not LGPL"; exit 1; } echo "✓ LGPL v2.1" for c in libmp3lame aac flac pcm_s16le; do "$OUT/ffmpeg" -hide_banner -encoders 2>/dev/null | grep -qw "$c" \ && echo "✓ encoder $c" || { echo "✗ encoder $c missing"; exit 1; } done echo echo "✓ universal, self-contained, LGPL: $OUT/ffmpeg"