#!/bin/bash # tools/build_ffmpeg_lgpl.sh — compila el ffmpeg/ffprobe (y la libopus) que viajan en el DMG. # # s70: build LGPL propio porque el ffmpeg de Homebrew es `--enable-gpl --enable-version3`. # # Lote A de la auditoría de lanzamiento (PORT-001, PORT-002), 23-sep-2026. Dos fallos del # build anterior que este script corrige: # # 1. El ffmpeg «LGPL» se enlazaba DINÁMICAMENTE con libavcodec.62.dylib & cía. Esas # librerías tenían el mismo nombre que las de PyAV (que faster-whisper arrastraba), y # PyInstaller se quedó con las de PyAV: en todos los DMG de 0.9.2 a 0.9.16 el ffmpeg # empaquetado ejecutaba libavcodec LGPLv3 con libx264 y libx265 (GPL) dentro. Ahora # ffmpeg y ffprobe son ESTÁTICOS: no dependen de ninguna librería que no sea del # sistema, así que no hay nada que PyInstaller pueda sustituir. El script lo comprueba # con otool -L al final, y tools/bundle_gates.py lo vuelve a comprobar sobre la copia # que va DENTRO de la app. # 2. Todo salía con minos 26 (el macOS de la máquina de build) y además enlazaba lame y # opus de Homebrew (también minos 26). Ahora opus y lame se compilan aquí desde sus # fuentes, y todo con MACOSX_DEPLOYMENT_TARGET=14.0 (la app declara macOS 14). # # LICENCIA DEL RESULTADO # ffmpeg sin --enable-gpl / --enable-version3 / --enable-nonfree = LGPL 2.1 or later. # LAME (libmp3lame) = LGPL 2.0 or later. libopus = BSD 3-Clause. # ffmpeg y ffprobe son PROGRAMAS aparte: RadioCheck los ejecuta como procesos, no los # enlaza. Enlazados estáticamente sólo contienen código LGPL (FFmpeg, LAME) y BSD (opus); # quien quiera sustituirlos puede recompilarlos con esta misma receta y reemplazar los # dos ejecutables. # Los avisos (textos de licencia + procedencia + sha256 de las fuentes) se generan en # third_party/ffmpeg-lgpl/ y RadioCheck.spec los copia a Contents/Resources/licenses. # # FUENTES (LGPL 2.1 §4: el código fuente completo, «incluidos los scripts que controlan # la compilación», disponible desde el mismo sitio que el binario). Revisión del lote A: # - Las fuentes EXACTAS se guardan en third_party/sources/ (gitignored, persistente: # no se borra entre builds). Si una ya está ahí, no se vuelve a bajar. # - Esta receta se copia a third_party/ffmpeg-lgpl/ (viaja DENTRO de la app, en # Resources/licenses) y a third_party/sources/. # - third_party/ffmpeg-lgpl/SOURCES-ffmpeg.sha256 lista cada fichero con su sha256. # tools/publish_dmg.sh sube exactamente esa lista a # https://dl.radiocheckapp.com/source// junto al DMG, y falla si falta algo. # # libopus además se instala como dylib (third_party/native/shared/libopus.0.dylib): la # usan discord.py y opuslib (ctypes) para la voz de Discord. Antes se copiaba la de Homebrew. # # Uso: bash tools/build_ffmpeg_lgpl.sh # Salida: third_party/ffmpeg-lgpl/{bin/ffmpeg,bin/ffprobe,BUILD_INFO.txt,licencias, # build_ffmpeg_lgpl.sh,SOURCES-ffmpeg.sha256} # third_party/sources/{ffmpeg,lame,opus tarballs, build_ffmpeg_lgpl.sh} # third_party/native/{lib,include} (opus + lame estáticos, sólo para enlazar ffmpeg) # third_party/native/shared/libopus.0.dylib set -euo pipefail cd "$(dirname "$0")/.." REPO="$PWD" MINOS="14.0" # = LSMinimumSystemVersion en RadioCheck.spec FFMPEG_VERSION="8.0.3" # rama 8.0 (la que ya usábamos), último parche OPUS_VERSION="1.6.1" LAME_VERSION="3.100" # sha256 de las fuentes. opus: coincide con el de la fórmula de Homebrew y con el # SHA256SUMS de xiph. lame 3.100: el publicado por Homebrew/Debian desde 2017. # ffmpeg: se verifica con la FIRMA GPG oficial (clave de releases de FFmpeg). OPUS_SHA256="6ffcb593207be92584df15b32466ed64bbec99109f007c82205f0194572411a1" LAME_SHA256="ddfe36cab873794038ae2c1210557ad34857a4b6bdc515785d1da9e175b1da1e" FFMPEG_KEY_FPR="FCF986EA15E6E293A5644F10B4322F04D67658D8" # FFmpeg release signing key PREFIX="$REPO/third_party/ffmpeg-lgpl" NATIVE="$REPO/third_party/native" BUILD="$REPO/third_party/.build" # árbol de compilación y logs (desechable) SRC="$REPO/third_party/sources" # fuentes EXACTAS que se publican (persistente) # Prefijo neutro: ffmpeg incrusta --prefix (y su datadir) en el binario y en -buildconf. # Con la ruta real salía /Users//... dentro de la app. Se instala con # DESTDIR en un directorio temporal y de ahí se copian los dos programas. FF_PREFIX="/opt/radiocheck-ffmpeg" FF_URL="https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.xz" OPUS_URL="https://ftp.osuosl.org/pub/xiph/releases/opus/opus-${OPUS_VERSION}.tar.gz" LAME_URL="https://downloads.sourceforge.net/project/lame/lame/${LAME_VERSION}/lame-${LAME_VERSION}.tar.gz" echo "── ffmpeg ${FFMPEG_VERSION} estático LGPL 2.1 · opus ${OPUS_VERSION} · lame ${LAME_VERSION} · macOS ≥ ${MINOS}" command -v pkg-config >/dev/null || { echo "❌ falta pkg-config → brew install pkgconf"; exit 1; } command -v gpg >/dev/null || { echo "❌ falta gpg (verifica la firma de ffmpeg) → brew install gnupg"; exit 1; } # Nada de Homebrew ni de variables del usuario colándose en el enlace. unset LIBRARY_PATH CPATH C_INCLUDE_PATH CPPFLAGS CFLAGS LDFLAGS PKG_CONFIG_PATH export MACOSX_DEPLOYMENT_TARGET="$MINOS" export PKG_CONFIG_LIBDIR="$NATIVE/lib/pkgconfig" # SÓLO nuestros .pc TFLAGS="-mmacosx-version-min=${MINOS} -O2" mkdir -p "$BUILD" "$SRC"; cd "$BUILD" bajar() { # url fichero → $SRC/fichero (reutiliza la copia de .build/ de builds antiguos) if [ ! -f "$SRC/$2" ] && [ -f "$BUILD/$2" ]; then mv "$BUILD/$2" "$SRC/$2"; fi [ -f "$SRC/$2" ] || { echo "→ descargando $2"; curl -fsSL -o "$SRC/$2.part" "$1" && mv "$SRC/$2.part" "$SRC/$2"; } } sha_ok() { # fichero sha [ "$(shasum -a 256 "$SRC/$1" | cut -d' ' -f1)" = "$2" ] || { echo "❌ sha256 de $1 no coincide"; exit 1; } } bajar "$OPUS_URL" "opus-${OPUS_VERSION}.tar.gz"; sha_ok "opus-${OPUS_VERSION}.tar.gz" "$OPUS_SHA256" bajar "$LAME_URL" "lame-${LAME_VERSION}.tar.gz"; sha_ok "lame-${LAME_VERSION}.tar.gz" "$LAME_SHA256" bajar "$FF_URL" "ffmpeg-${FFMPEG_VERSION}.tar.xz" bajar "$FF_URL.asc" "ffmpeg-${FFMPEG_VERSION}.tar.xz.asc" bajar "https://ffmpeg.org/ffmpeg-devel.asc" "ffmpeg-devel.asc" GNUPGHOME="$(mktemp -d)"; export GNUPGHOME gpg -q --import "$SRC/ffmpeg-devel.asc" 2>/dev/null gpg --status-fd 1 --verify "$SRC/ffmpeg-${FFMPEG_VERSION}.tar.xz.asc" "$SRC/ffmpeg-${FFMPEG_VERSION}.tar.xz" 2>/dev/null \ | grep -q "VALIDSIG ${FFMPEG_KEY_FPR}" \ || { echo "❌ la firma de ffmpeg-${FFMPEG_VERSION}.tar.xz no es de la clave de releases de FFmpeg"; exit 1; } rm -rf "$GNUPGHOME"; unset GNUPGHOME FF_SHA256="$(shasum -a 256 "$SRC/ffmpeg-${FFMPEG_VERSION}.tar.xz" | cut -d' ' -f1)" echo " ✓ fuentes verificadas (sha256 opus/lame, firma GPG ffmpeg)" rm -rf "$NATIVE"; mkdir -p "$NATIVE/shared" # ── opus (BSD): estática para ffmpeg + dylib para discord.py/opuslib ───────────── echo "→ opus" rm -rf "opus-${OPUS_VERSION}"; tar xf "$SRC/opus-${OPUS_VERSION}.tar.gz" ( cd "opus-${OPUS_VERSION}" CFLAGS="$TFLAGS" LDFLAGS="-mmacosx-version-min=${MINOS}" ./configure --prefix="$NATIVE" \ --enable-static --enable-shared --disable-doc --disable-extra-programs \ > "$BUILD/opus-configure.log" 2>&1 || { tail -25 "$BUILD/opus-configure.log"; exit 1; } make -j"$(sysctl -n hw.ncpu)" > "$BUILD/opus-make.log" 2>&1 || { tail -25 "$BUILD/opus-make.log"; exit 1; } make install > /dev/null 2>&1 ) # La dylib fuera de lib/: si el enlazador la viera, ffmpeg enlazaría con ella y no con la .a cp -L "$NATIVE/lib/libopus.0.dylib" "$NATIVE/shared/libopus.0.dylib" rm -f "$NATIVE"/lib/libopus*.dylib install_name_tool -id "@rpath/libopus.0.dylib" "$NATIVE/shared/libopus.0.dylib" 2>/dev/null codesign -f -s - "$NATIVE/shared/libopus.0.dylib" 2>/dev/null # install_name_tool invalida la firma ad hoc # ── lame (LGPL): sólo la librería, estática, sin decodificador ni programa ───────── echo "→ lame" rm -rf "lame-${LAME_VERSION}"; tar xf "$SRC/lame-${LAME_VERSION}.tar.gz" ( cd "lame-${LAME_VERSION}" # 3.100 exporta un símbolo que ya no existe (lame_init_old); sólo afecta al enlace # dinámico, pero se quita igual para que el build no dependa de eso. sed -i '' -e '/lame_init_old/d' include/libmp3lame.sym CFLAGS="$TFLAGS" LDFLAGS="-mmacosx-version-min=${MINOS}" ./configure --prefix="$NATIVE" \ --enable-static --disable-shared --disable-frontend --disable-decoder \ --disable-gtktest --disable-dependency-tracking \ > "$BUILD/lame-configure.log" 2>&1 || { tail -25 "$BUILD/lame-configure.log"; exit 1; } make -j"$(sysctl -n hw.ncpu)" > "$BUILD/lame-make.log" 2>&1 || { tail -25 "$BUILD/lame-make.log"; exit 1; } make install > /dev/null 2>&1 ) # ── ffmpeg estático, LGPL 2.1 ───────────────────────────────────────────────────── echo "→ ffmpeg configure" rm -rf "ffmpeg-${FFMPEG_VERSION}"; tar xf "$SRC/ffmpeg-${FFMPEG_VERSION}.tar.xz" cd "ffmpeg-${FFMPEG_VERSION}" # Sin --enable-gpl, sin --enable-version3, sin --enable-nonfree = LGPL 2.1+. # Sólo audio: nada de x264/x265/vídeo, que además es de donde viene el GPL. # --disable-autodetect: nada que no pidamos aquí (ni zlib, ni iconv, ni VideoToolbox). # Rutas de lame/opus RELATIVAS (configure y make corren en este directorio, que está en # third_party/.build/ffmpeg-X): así -buildconf no lleva la ruta de quien compila. ./configure \ --prefix="$FF_PREFIX" \ --enable-static --disable-shared --pkg-config-flags=--static \ --disable-doc --disable-debug --disable-autodetect \ --disable-programs --enable-ffmpeg --enable-ffprobe \ --disable-avdevice --disable-devices --disable-network \ --enable-libmp3lame --enable-libopus \ --enable-audiotoolbox \ --extra-cflags="-I../../native/include -mmacosx-version-min=${MINOS}" \ --extra-ldflags="-L../../native/lib -mmacosx-version-min=${MINOS}" \ > "$BUILD/configure.log" 2>&1 || { tail -25 "$BUILD/configure.log"; exit 1; } echo "→ make (esto tarda unos minutos)" make -j"$(sysctl -n hw.ncpu)" > "$BUILD/make.log" 2>&1 || { tail -25 "$BUILD/make.log"; exit 1; } STAGE="$BUILD/ffmpeg-stage"; rm -rf "$STAGE" make install DESTDIR="$STAGE" > /dev/null 2>&1 # Sólo viajan los dos programas: las .a, cabeceras y .pc no sirven dentro de la app. rm -rf "$PREFIX"; mkdir -p "$PREFIX/bin" cp "$STAGE$FF_PREFIX/bin/ffmpeg" "$STAGE$FF_PREFIX/bin/ffprobe" "$PREFIX/bin/" rm -rf "$STAGE" # ── verificación: si algo de esto falla, NO se puede publicar ───────────────────── for t in ffmpeg ffprobe; do B="$PREFIX/bin/$t" CONF="$("$B" -hide_banner -buildconf 2>&1)" if echo "$CONF" | grep -qE -- "--enable-(gpl|nonfree|version3)|libx26[45]|libfdk"; then echo "❌ $t salió con GPL/nonfree/version3"; echo "$CONF"; exit 1 fi "$B" -hide_banner -L 2>&1 | grep -q "GNU Lesser General Public" \ || { echo "❌ $t no se declara LGPL"; exit 1; } # Estático de verdad: sólo librerías del sistema. if otool -L "$B" | tail -n +2 | awk '{print $1}' | grep -vE '^(/usr/lib/|/System/Library/)'; then echo "❌ $t depende de librerías que no son del sistema (arriba)"; exit 1 fi MIN="$(otool -l "$B" | awk '/LC_BUILD_VERSION/{f=1} f&&/minos/{print $2; exit}')" [ "$MIN" = "$MINOS" ] || { echo "❌ $t tiene minos $MIN (se esperaba $MINOS)"; exit 1; } # Nada de la ruta de quien compila dentro del binario (prefijo neutro, rutas relativas). if strings -a "$B" | grep -qF "$REPO"; then echo "❌ $t lleva dentro la ruta local $REPO"; strings -a "$B" | grep -F "$REPO" | head -3; exit 1 fi done MIN="$(otool -l "$NATIVE/shared/libopus.0.dylib" | awk '/LC_BUILD_VERSION/{f=1} f&&/minos/{print $2; exit}')" [ "$MIN" = "$MINOS" ] || { echo "❌ libopus tiene minos $MIN"; exit 1; } echo "✅ ffmpeg/ffprobe estáticos, LGPL 2.1, sólo librerías del sistema, minos ${MINOS}" # ── avisos de licencia y procedencia (van a Contents/Resources/licenses) ────────── cp LICENSE.md "$PREFIX/LICENSE-ffmpeg.md" cp COPYING.LGPLv2.1 "$PREFIX/COPYING.LGPLv2.1" cp "$BUILD/lame-${LAME_VERSION}/COPYING" "$PREFIX/COPYING-lame.txt" cp "$BUILD/opus-${OPUS_VERSION}/COPYING" "$PREFIX/COPYING-opus.txt" BUILDCONF="$("$PREFIX/bin/ffmpeg" -hide_banner -buildconf 2>&1 | sed 's/^/ /')" cat > "$PREFIX/BUILD_INFO.txt" </ (for example .../source/0.9.17/ffmpeg-${FFMPEG_VERSION}.tar.xz). SOURCE_CODE.txt, next to this file, gives the full address of every file for this copy of RadioCheck, and SOURCES-ffmpeg.sha256 lists each one with its sha256: ffmpeg-${FFMPEG_VERSION}.tar.xz sha256 ${FF_SHA256} upstream ${FF_URL} (signature verified against the FFmpeg release key ${FFMPEG_KEY_FPR}) lame-${LAME_VERSION}.tar.gz sha256 ${LAME_SHA256} upstream ${LAME_URL} opus-${OPUS_VERSION}.tar.gz sha256 ${OPUS_SHA256} upstream ${OPUS_URL} Build recipe: build_ffmpeg_lgpl.sh, next to this file and hosted with the sources. The sources are used unmodified; the only edit is removing the obsolete lame_init_old line from lame's export list, which the recipe does. If a link ever stops working, write to hello@radiocheckapp.com and we will send the same files. Replacing them: build the same (or modified) sources with build_ffmpeg_lgpl.sh, or any LGPL-compatible FFmpeg build, and replace the two executables in Contents/Frameworks (then re-sign the app ad hoc: codesign --force --deep -s - RadioCheck.app). Built: $(date -u +%Y-%m-%dT%H:%M:%SZ) on macOS $(sw_vers -productVersion), $(clang --version | head -1) ffmpeg -buildconf: ${BUILDCONF} EOF rm -f "$PREFIX/LICENSE.md" # nombre antiguo: evita dos copias con distinto nombre # ── receta y lista de fuentes (LGPL §4: viajan con el binario y se publican junto al DMG) ── install -m 644 "$REPO/tools/build_ffmpeg_lgpl.sh" "$PREFIX/build_ffmpeg_lgpl.sh" install -m 644 "$REPO/tools/build_ffmpeg_lgpl.sh" "$SRC/build_ffmpeg_lgpl.sh" ( cd "$SRC" && shasum -a 256 "ffmpeg-${FFMPEG_VERSION}.tar.xz" "ffmpeg-${FFMPEG_VERSION}.tar.xz.asc" \ "lame-${LAME_VERSION}.tar.gz" "opus-${OPUS_VERSION}.tar.gz" build_ffmpeg_lgpl.sh ) \ > "$PREFIX/SOURCES-ffmpeg.sha256" echo "── fuentes a publicar (third_party/sources/, lista en SOURCES-ffmpeg.sha256):" sed 's/^/ /' "$PREFIX/SOURCES-ffmpeg.sha256" echo "── binarios:"; ls -la "$PREFIX/bin" "$NATIVE/shared" "$PREFIX/bin/ffmpeg" -hide_banner -version | head -1