#!/usr/bin/env bash # SPDX-License-Identifier: GPL-3.0-or-later set -euo pipefail usage() { cat <<'EOF' Usage: scripts/build-reviewed-ffmpeg-core.sh st|mt SOURCE_ARCHIVE EMSDK_ROOT [JOBS] Builds one private, staged FFmpeg WebAssembly core from the exact 0.12.10 Corresponding Source archive. The only upstream source change is ffmpeg.wasm commit b409e36475bc21f0451b5b1e1d126fa82871439a: `-sSTACK_SIZE=5MB`, which fixes upstream issues #591 and #775. Output remains below .core-build/reviewed-stack5m and is never copied into the application automatically. Re-running resumes at verified component stamps. EOF } if [[ $# -lt 3 || $# -gt 4 ]]; then usage >&2 exit 2 fi MODE=$1 SOURCE_ARCHIVE=$2 EMSDK_ROOT=$3 JOBS=${4:-2} if [[ "$MODE" != "st" && "$MODE" != "mt" ]]; then echo "Mode must be st or mt." >&2 exit 2 fi if [[ ! "$JOBS" =~ ^[1-8]$ ]]; then echo "JOBS must be an integer from 1 to 8." >&2 exit 2 fi SCRIPT_DIRECTORY=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P) REPOSITORY_ROOT=$(cd -- "$SCRIPT_DIRECTORY/.." && pwd -P) SOURCE_ARCHIVE=$(realpath -- "$SOURCE_ARCHIVE") EMSDK_ROOT=$(realpath -- "$EMSDK_ROOT") EXPECTED_ARCHIVE_SHA256=2ab505f72be883d1257bc71cbb93880d1ade5bc029792621cb87b4ecc068399a EXPECTED_EMCC_SHA256=b0c9551f9155fa9c028efa24591acdfd30f02f32fb247cf9266f5f0f2ede7589 EXPECTED_CMAKE_SHA256=ade9326f83f4996b17e2cb1bbfe4848df30a237ef5769cd3c00239b9f0d3ec58 EXPECTED_SOURCE_ROOT=ffmpeg-core-0.12.10-reviewed-stack5m.1-corresponding-source SOURCE_DATE_EPOCH=1736208000 if [[ ! -f "$SOURCE_ARCHIVE" || -L "$SOURCE_ARCHIVE" ]]; then echo "SOURCE_ARCHIVE must be a regular, non-symlink file." >&2 exit 2 fi if [[ ! -d "$EMSDK_ROOT" || -L "$EMSDK_ROOT" ]]; then echo "EMSDK_ROOT must be a real directory." >&2 exit 2 fi actual_archive_sha256=$(sha256sum -- "$SOURCE_ARCHIVE" | cut -d' ' -f1) if [[ "$actual_archive_sha256" != "$EXPECTED_ARCHIVE_SHA256" ]]; then echo "Corresponding Source archive SHA-256 mismatch." >&2 exit 1 fi # shellcheck disable=SC1091 source "$EMSDK_ROOT/emsdk_env.sh" >/dev/null export PATH="$REPOSITORY_ROOT/scripts/build-tools:$PATH" if [[ -z "${AV_REVIEWED_CMAKE_BIN:-}" ]]; then echo "AV_REVIEWED_CMAKE_BIN must identify the exact CMake 3.27.9 binary directory." >&2 exit 2 fi AV_REVIEWED_CMAKE_BIN=$(realpath -- "$AV_REVIEWED_CMAKE_BIN") if [[ ! -d "$AV_REVIEWED_CMAKE_BIN" || -L "$AV_REVIEWED_CMAKE_BIN" ]]; then echo "AV_REVIEWED_CMAKE_BIN must be a real directory." >&2 exit 2 fi if [[ $(sha256sum -- "$AV_REVIEWED_CMAKE_BIN/cmake" | cut -d' ' -f1) != "$EXPECTED_CMAKE_SHA256" ]]; then echo "CMake binary SHA-256 mismatch; exact PyPI CMake 3.27.9 is required." >&2 exit 1 fi export PATH="$AV_REVIEWED_CMAKE_BIN:$PATH" EMCC=$(realpath -- "$EMSDK_ROOT/upstream/emscripten/emcc") actual_emcc_sha256=$(sha256sum -- "$EMCC" | cut -d' ' -f1) if [[ "$actual_emcc_sha256" != "$EXPECTED_EMCC_SHA256" ]]; then echo "Emscripten compiler SHA-256 mismatch; activate exact version 3.1.40." >&2 exit 1 fi if ! "$EMCC" --version | head -1 | grep -Fq \ "3.1.40 (5c27e79dd0a9c4e27ef2326841698cdd4f6b5784)"; then echo "Emscripten identity mismatch; exact version 3.1.40 is required." >&2 exit 1 fi if [[ $(cmake --version | head -1) != "cmake version 3.27.9" ]]; then echo "CMake identity mismatch; exact version 3.27.9 is required." >&2 exit 1 fi BUILD_PARENT="$REPOSITORY_ROOT/.core-build/reviewed-stack5m" MODE_ROOT="$BUILD_PARENT/$MODE" EXTRACT_ROOT="$MODE_ROOT/source" SOURCE_ROOT="$EXTRACT_ROOT/$EXPECTED_SOURCE_ROOT/sources" INSTALL_DIR="$MODE_ROOT/install" OUTPUT_DIR="$MODE_ROOT/output" STAMP_DIR="$MODE_ROOT/stamps" case "$MODE_ROOT" in "$REPOSITORY_ROOT"/.core-build/reviewed-stack5m/st | \ "$REPOSITORY_ROOT"/.core-build/reviewed-stack5m/mt) ;; *) echo "Refusing an unexpected build directory: $MODE_ROOT" >&2 exit 1 ;; esac mkdir -p -- "$MODE_ROOT" "$STAMP_DIR" if [[ ! -f "$STAMP_DIR/source-prepared" ]]; then if [[ -e "$EXTRACT_ROOT" ]]; then echo "Incomplete source staging already exists at $EXTRACT_ROOT." >&2 echo "Remove only that private mode directory to restart it." >&2 exit 1 fi mkdir -p -- "$EXTRACT_ROOT" tar -xf "$SOURCE_ARCHIVE" -C "$EXTRACT_ROOT" lock="$EXTRACT_ROOT/$EXPECTED_SOURCE_ROOT/SOURCE_LOCK.json" node -e ' const fs = require("node:fs"); const lock = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); const expected = new Map([ ["ffmpeg.wasm", "71aa99d37c02a7b4c435275ca9ef50e612f6efa1"], ["FFmpeg", "4729204c17f756e186d622060088371d10b34f7e"], ["Opus", "e85ed7726db5d677c9c0677298ea0cb9c65bdd23"], ]); for (const [name, revision] of expected) { const entry = lock.sources.find((candidate) => candidate.name === name); if (!entry || entry.revision !== revision) { throw new Error(`Unexpected ${name} source identity.`); } } ' "$lock" wasm_build_script="$SOURCE_ROOT/ffmpeg.wasm/build/ffmpeg-wasm.sh" if grep -Fq -- "-sSTACK_SIZE=" "$wasm_build_script"; then echo "Base archive unexpectedly already selects a stack size." >&2 exit 1 fi sed -i \ '/-sUSE_SDL=2/a\ -sSTACK_SIZE=5MB # upstream b409e364: support libopus' \ "$wasm_build_script" if [[ $(grep -Fc -- "-sSTACK_SIZE=5MB" "$wasm_build_script") -ne 1 ]]; then echo "Could not apply the reviewed one-line stack patch exactly once." >&2 exit 1 fi touch "$STAMP_DIR/source-prepared" fi # The upstream container uses unbounded `make -j`. Keep local builds bounded # without changing compiled source, and allow a resumed build to select a new # bounded job count. while IFS= read -r -d '' build_script; do sed -E -i \ -e "s/ -j([1-8])?$/ -j$JOBS/" \ -e "s/ -j([1-8])? / -j$JOBS /g" \ "$build_script" done < <(find "$SOURCE_ROOT/ffmpeg.wasm/build" -type f -name '*.sh' -print0) # The upstream container does not provide native NASM. Some development hosts # do; allowing CMake to discover it would produce native x86 objects that # cannot be linked into WebAssembly. Make the container's effective choice # explicit and deterministic. X265_BUILD_SCRIPT="$SOURCE_ROOT/ffmpeg.wasm/build/x265.sh" if ! grep -Fq -- "-DENABLE_ASSEMBLY=OFF" "$X265_BUILD_SCRIPT"; then sed -i \ '/-DENABLE_LIBNUMA=OFF/a\ -DENABLE_ASSEMBLY=OFF' \ "$X265_BUILD_SCRIPT" fi if [[ $(grep -Fc -- "-DENABLE_ASSEMBLY=OFF" "$X265_BUILD_SCRIPT") -ne 1 ]]; then echo "Could not disable native x265 assembly exactly once." >&2 exit 1 fi # Upstream builds each dependency in an isolated container stage. A cumulative # local prefix would otherwise let FreeType discover the already-built zlib and # let HarfBuzz discover FreeType. Their resulting private dependencies are not # present in FFmpeg's pkg-config probes and, on a developer workstation, optional # libraries such as libpng may even resolve to host-native /usr objects. Make the # isolated-stage dependency choices explicit. FREETYPE_BUILD_SCRIPT="$SOURCE_ROOT/ffmpeg.wasm/build/freetype2.sh" if ! grep -Fq -- "--with-zlib=no" "$FREETYPE_BUILD_SCRIPT"; then sed -i \ '/--without-harfbuzz/a\ --with-zlib=no\n --with-bzip2=no\n --with-png=no\n --with-brotli=no' \ "$FREETYPE_BUILD_SCRIPT" fi for freetype_flag in \ "--without-harfbuzz" \ "--with-zlib=no" \ "--with-bzip2=no" \ "--with-png=no" \ "--with-brotli=no"; do if [[ $(grep -Fc -- "$freetype_flag" "$FREETYPE_BUILD_SCRIPT") -ne 1 ]]; then echo "Could not select isolated FreeType dependency flags exactly once." >&2 exit 1 fi done HARFBUZZ_BUILD_SCRIPT="$SOURCE_ROOT/ffmpeg.wasm/build/harfbuzz.sh" for harfbuzz_flag in \ "--with-freetype=no" \ "--with-glib=no" \ "--with-gobject=no" \ "--with-cairo=no" \ "--with-icu=no" \ "--with-graphite2=no" \ "--with-uniscribe=no" \ "--with-directwrite=no" \ "--with-coretext=no"; do if ! grep -Fq -- "$harfbuzz_flag" "$HARFBUZZ_BUILD_SCRIPT"; then sed -i \ "/--enable-static/a\\ $harfbuzz_flag" \ "$HARFBUZZ_BUILD_SCRIPT" fi if [[ $(grep -Fc -- "$harfbuzz_flag" "$HARFBUZZ_BUILD_SCRIPT") -ne 1 ]]; then echo "Could not select isolated HarfBuzz dependency flags exactly once." >&2 exit 1 fi done mkdir -p -- "$INSTALL_DIR" "$OUTPUT_DIR" export INSTALL_DIR export EM_PKG_CONFIG_PATH="$EMSDK/upstream/emscripten/system/lib/pkgconfig:$INSTALL_DIR/lib/pkgconfig" export EM_TOOLCHAIN_FILE="$EMSDK/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake" export PKG_CONFIG_PATH="$INSTALL_DIR/lib/pkgconfig:$EM_PKG_CONFIG_PATH" export LANG=C export LC_ALL=C export TZ=UTC export SOURCE_DATE_EPOCH export ZERO_AR_DATE=1 MODE_FLAGS="-O3 -msimd128" if [[ "$MODE" == "st" ]]; then export FFMPEG_ST=yes unset FFMPEG_MT || true else MODE_FLAGS="$MODE_FLAGS -sUSE_PTHREADS -pthread" # HarfBuzz's pinned upstream build script reads this value directly under # `set -u`, while the other scripts use its presence to disable threading. # An exported empty value keeps both behaviours correct for the MT build. export FFMPEG_ST= export FFMPEG_MT=yes fi export CFLAGS="-I$INSTALL_DIR/include $MODE_FLAGS" export CXXFLAGS="$CFLAGS" export LDFLAGS="-L$INSTALL_DIR/lib $CFLAGS" FFMPEG_WASM="$SOURCE_ROOT/ffmpeg.wasm" build_component() { local stamp=$1 local directory=$2 local script=$3 if [[ -f "$STAMP_DIR/$stamp" ]]; then echo "[reviewed-core:$MODE] reuse $stamp" return fi echo "[reviewed-core:$MODE] build $stamp" ( cd -- "$SOURCE_ROOT/$directory" if [[ "$stamp" == "libvpx" ]]; then # libvpx embeds its configure command in the linked library. A relative # prefix keeps that shipped identity independent of the checkout path # while still installing into this mode's private prefix. INSTALL_DIR=../../../../install \ CFLAGS="-I../../../../install/include $MODE_FLAGS" \ CXXFLAGS="-I../../../../install/include $MODE_FLAGS" \ LDFLAGS="-L../../../../install/lib -I../../../../install/include $MODE_FLAGS" \ bash "$FFMPEG_WASM/build/$script" else bash "$FFMPEG_WASM/build/$script" fi ) touch "$STAMP_DIR/$stamp" } if [[ ! -f "$STAMP_DIR/sdl2" ]]; then if [[ "$MODE" == "st" ]]; then embuilder build sdl2 else embuilder build sdl2-mt fi touch "$STAMP_DIR/sdl2" fi build_component x264 x264 x264.sh build_component x265 x265 x265.sh build_component libvpx libvpx libvpx.sh build_component lame lame lame.sh build_component ogg ogg ogg.sh build_component theora theora theora.sh build_component opus opus opus.sh build_component vorbis vorbis vorbis.sh build_component zlib zlib zlib.sh build_component libwebp libwebp libwebp.sh build_component freetype2 freetype2 freetype2.sh build_component fribidi fribidi fribidi.sh build_component harfbuzz harfbuzz harfbuzz.sh build_component libass libass libass.sh build_component zimg zimg zimg.sh # Configuration probes from Autotools projects must never smuggle host-native # include/library paths into the reviewed WebAssembly link. if grep -R -E -n -- \ '/usr/(lib|include)|/lib/(x86_64|i[3-6]86)-linux-gnu' \ "$INSTALL_DIR/lib/pkgconfig" "$INSTALL_DIR/lib"/*.la; then echo "A staged dependency recorded a host-native build path." >&2 exit 1 fi if emnm "$INSTALL_DIR/lib/libfreetype.a" | grep -F -- " U png_" >/dev/null; then echo "FreeType unexpectedly retained an unbundled libpng dependency." >&2 exit 1 fi if emnm "$INSTALL_DIR/lib/libfreetype.a" | grep -E -- ' U (inflate|BZ2_|Brotli)' >/dev/null; then echo "FreeType unexpectedly retained a cross-stage compression dependency." >&2 exit 1 fi FFMPEG_SOURCE="$SOURCE_ROOT/FFmpeg" # FFmpeg records --extra-cflags in `ffmpeg -buildconf`. Use a path relative to # its source root so the shipped runtime identity is independent of the # maintainer's checkout location. Dependency builds above retain their # absolute private staging prefix; only the final FFmpeg configuration is # embedded in the distributed core. FFMPEG_INSTALL_RELATIVE=../../../../install export CFLAGS="-I$FFMPEG_INSTALL_RELATIVE/include $MODE_FLAGS" export CXXFLAGS="$CFLAGS" export LDFLAGS="-L$INSTALL_DIR/lib $CFLAGS" if [[ ! -f "$STAMP_DIR/ffmpeg" ]]; then echo "[reviewed-core:$MODE] build FFmpeg" ( cd -- "$FFMPEG_SOURCE" bash "$FFMPEG_WASM/build/ffmpeg.sh" \ --enable-gpl \ --enable-libx264 \ --enable-libx265 \ --enable-libvpx \ --enable-libmp3lame \ --enable-libtheora \ --enable-libvorbis \ --enable-libopus \ --enable-zlib \ --enable-libwebp \ --enable-libfreetype \ --enable-libfribidi \ --enable-libass \ --enable-libzimg ) touch "$STAMP_DIR/ffmpeg" fi if [[ ! -f "$STAMP_DIR/core" ]]; then echo "[reviewed-core:$MODE] link ffmpeg-core with 5 MiB stack" mkdir -p -- "$FFMPEG_SOURCE/src" cp -a -- "$FFMPEG_WASM/src/bind" "$FFMPEG_SOURCE/src/" cp -a -- "$FFMPEG_WASM/src/fftools" "$FFMPEG_SOURCE/src/" cp -- "$FFMPEG_WASM/build/ffmpeg-wasm.sh" "$FFMPEG_SOURCE/build-reviewed-core.sh" ( cd -- "$FFMPEG_SOURCE" bash ./build-reviewed-core.sh \ -lx264 \ -lx265 \ -lvpx \ -lmp3lame \ -logg \ -ltheora \ -lvorbis \ -lvorbisenc \ -lvorbisfile \ -lopus \ -lz \ -lwebpmux \ -lwebp \ -lsharpyuv \ -lfreetype \ -lfribidi \ -lharfbuzz \ -lass \ -lzimg \ -sEXPORT_ES6 \ -o "$OUTPUT_DIR/ffmpeg-core.js" ) touch "$STAMP_DIR/core" fi expected_files=(ffmpeg-core.js ffmpeg-core.wasm) if [[ "$MODE" == "mt" ]]; then expected_files+=(ffmpeg-core.worker.js) fi for expected_file in "${expected_files[@]}"; do if [[ ! -s "$OUTPUT_DIR/$expected_file" || -L "$OUTPUT_DIR/$expected_file" ]]; then echo "Missing staged output: $OUTPUT_DIR/$expected_file" >&2 exit 1 fi if grep -aFq -- "$REPOSITORY_ROOT" "$OUTPUT_DIR/$expected_file" || grep -aFq -- "$EMSDK_ROOT" "$OUTPUT_DIR/$expected_file"; then echo "Staged output leaks a maintainer-local build path: $expected_file" >&2 exit 1 fi done ( cd -- "$OUTPUT_DIR" sha256sum -- "${expected_files[@]}" >SHA256SUMS ) echo "[reviewed-core:$MODE] staged outputs" cat "$OUTPUT_DIR/SHA256SUMS"