421 lines
14 KiB
Bash
421 lines
14 KiB
Bash
#!/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"
|