#!/bin/bash # # Compile les deux seuls outils dont le player a besoin — `ffmpeg` et `ffprobe` — # et les dépose dans SurfLingoPlayer/Resources/Bin/. # # --------------------------------------------------------------------------- # Pourquoi ce script existe # --------------------------------------------------------------------------- # # Jusqu'ici l'app ne livrait aucun binaire : `SubtitleExtractor` cherchait # `ffmpeg` dans le bundle, ne le trouvait jamais, et retombait sur # /opt/homebrew/bin/ffmpeg. Deux conséquences, l'une visible et l'autre pas. # # La visible : sur un Mac sans Homebrew — c'est-à-dire chez la quasi-totalité # des acheteurs — les sous-titres INTÉGRÉS aux fichiers .mkv ne fonctionnaient # pas du tout, et le message d'échec disait « brew install ffmpeg », en anglais. # # L'invisible : /opt/homebrew/bin appartient à l'utilisateur, pas à root. # N'importe quel processus tournant sous son compte pouvait y déposer un faux # `ffmpeg`, que SurfLingo lançait ensuite sur chaque vidéo ouverte. Un # sous-processus n'hérite ni de notre signature ni du Hardened Runtime : il n'y # avait rien pour l'arrêter. # # Le binaire embarqué règle les deux d'un coup. Il est scellé par la signature # du bundle : le modifier casse la signature, et Gatekeeper refuse de lancer # l'app. # # --------------------------------------------------------------------------- # Ce que ce binaire sait faire, et rien d'autre # --------------------------------------------------------------------------- # # `--disable-everything` part de zéro et on rallume à la main ce que le player # utilise réellement : ouvrir un conteneur vidéo, y lire la liste des pistes, # extraire une piste de sous-titres en texte. C'est tout. # # Ce n'est pas de l'économie de mégaoctets, c'est de la surface d'attaque en # moins. Un ffmpeg de Homebrew embarque des centaines de décodeurs — chacun # étant du C qui lit un format hostile, et l'histoire des CVE de FFmpeg est # d'abord celle des décodeurs exotiques. Ceux-là ne sont pas dans notre binaire : # ils ne peuvent pas être atteints. # # `--disable-network` retire toute la pile réseau. Notre ffmpeg ne sait ouvrir # qu'un fichier local ; il ne sait pas ce qu'est une URL. C'est la garantie la # plus forte du lot, et elle est gratuite. # # Piège à connaître : le muxeur de sous-titres SubRip s'appelle « srt », et le # protocole réseau SRT (Secure Reliable Transport) aussi. Nous activons le # premier (--enable-muxer=srt) et jamais le second — de toute façon exclu par # --disable-network. Ne pas confondre les deux en relisant cette liste. # # --------------------------------------------------------------------------- # La licence, et pourquoi la configuration ci-dessous n'est pas négociable # --------------------------------------------------------------------------- # # FFmpeg est LGPL v2.1+ **tant qu'on ne demande pas autre chose**. Deux options # de configure changeraient cela, et aucune n'apparaît ici : # # --enable-gpl → bascule le binaire en GPL v2+, qui exigerait de publier # le code source de SurfLingo Player lui-même. # --enable-nonfree → produit un binaire non redistribuable, point final. # # `--disable-autodetect` verrouille la promesse : sans lui, configure lierait # silencieusement ce qu'il trouve sur la machine de compilation — dont des # bibliothèques GPL — et le résultat dépendrait de ce qui traîne sur ce Mac. # Avec lui, seule zlib (licence permissive, fournie par macOS) est liée, et # elle l'est explicitement. # # Restent trois obligations LGPL, toutes tenues : # 1. citer FFmpeg dans l'app → écran « À propos » # 2. livrer le texte de la licence → Resources/Licenses/, copié plus bas # 3. fournir la source correspondante → le tarball vérifié est conservé dans # $CACHE_DIR et doit être publié sur surflingo.app/licences, avec ce # script — c'est lui qui documente la configuration exacte. # # Le binaire n'est pas modifié : c'est une compilation d'une release officielle, # à options de configure documentées. Nous ne redistribuons donc pas une version # altérée de FFmpeg. # # Usage : # ./scripts/build-ffmpeg.sh # version épinglée dans ffmpeg.pin # FFMPEG_VERSION=9.0.2 ./scripts/build-ffmpeg.sh --epingler # set -euo pipefail cd "$(dirname "$0")/.." PIN_FILE="scripts/ffmpeg.pin" DEST_DIR="SurfLingoPlayer/Resources/Bin" LICENSE_DIR="SurfLingoPlayer/Resources/Licenses" CACHE_DIR="build-tools/ffmpeg" DEPLOYMENT_TARGET="13.0" # Empreinte de la clé de signature des releases FFmpeg. # # Vérifiée à deux sources indépendantes le 5 septembre 2026 : la page # https://ffmpeg.org/download.html, et la signature du tarball 9.0.1 elle-même. # Elle est écrite ici en dur parce que c'est tout l'intérêt : importer une clé # depuis le réseau et lui faire confiance parce qu'elle vient d'arriver ne # vérifie rien du tout. FFMPEG_KEY_FINGERPRINT="FCF986EA15E6E293A5644F10B4322F04D67658D8" EPINGLER=false [ "${1:-}" = "--epingler" ] && EPINGLER=true # --------------------------------------------------------------------------- # 0. Version demandée # --------------------------------------------------------------------------- if [ -f "$PIN_FILE" ]; then # shellcheck disable=SC1090 source "$PIN_FILE" fi FFMPEG_VERSION="${FFMPEG_VERSION:-${PINNED_VERSION:-}}" if [ -z "$FFMPEG_VERSION" ]; then echo "❌ Aucune version épinglée. Lancer : FFMPEG_VERSION=9.0.1 $0 --epingler" >&2 exit 1 fi echo "==> FFmpeg $FFMPEG_VERSION" for tool in gpg curl make clang lipo; do command -v "$tool" >/dev/null 2>&1 || { echo "❌ « $tool » est introuvable." >&2 [ "$tool" = "gpg" ] && echo " Installer GnuPG : brew install gnupg" >&2 exit 1 } done mkdir -p "$CACHE_DIR" "$DEST_DIR" "$LICENSE_DIR" TARBALL="$CACHE_DIR/ffmpeg-$FFMPEG_VERSION.tar.xz" SIGNATURE="$TARBALL.asc" # --------------------------------------------------------------------------- # 1. Téléchargement # --------------------------------------------------------------------------- if [ ! -f "$TARBALL" ]; then echo "==> Téléchargement de la source…" curl -fsSL -o "$TARBALL" "https://ffmpeg.org/releases/ffmpeg-$FFMPEG_VERSION.tar.xz" fi if [ ! -f "$SIGNATURE" ]; then curl -fsSL -o "$SIGNATURE" "https://ffmpeg.org/releases/ffmpeg-$FFMPEG_VERSION.tar.xz.asc" fi # --------------------------------------------------------------------------- # 2. Vérification de la signature # --------------------------------------------------------------------------- # C'est la seule vérification qui compte. FFmpeg ne publie ni .sha256 ni .md5 à # côté de ses tarballs (vérifié : les deux rendent 404) — la signature GPG EST # le mécanisme officiel. Un sha256 seul ne prouverait rien de plus que « le # fichier n'a pas changé depuis que je l'ai téléchargé », y compris s'il avait # été altéré avant. echo "==> Vérification de la signature GPG…" if ! gpg --list-keys "$FFMPEG_KEY_FINGERPRINT" >/dev/null 2>&1; then echo " Import de la clé de release FFmpeg…" curl -fsSL https://ffmpeg.org/ffmpeg-devel.asc | gpg --import 2>&1 | sed 's/^/ /' fi # --status-fd permet de lire le verdict de gpg par machine plutôt que d'essayer # de reconnaître une phrase dans sa sortie, qui est traduite dans la langue du # système et change d'une version à l'autre. GPG_STATUS=$(gpg --status-fd 1 --verify "$SIGNATURE" "$TARBALL" 2>/dev/null || true) if ! grep -q "^\[GNUPG:\] VALIDSIG $FFMPEG_KEY_FINGERPRINT" <<<"$GPG_STATUS"; then echo "❌ SIGNATURE INVALIDE ou signée par une autre clé que celle de FFmpeg." >&2 echo " Attendu : $FFMPEG_KEY_FINGERPRINT" >&2 echo " Ne pas compiler cette archive. La supprimer : rm $TARBALL" >&2 exit 1 fi echo " ✅ signature valide (clé $FFMPEG_KEY_FINGERPRINT)" # --------------------------------------------------------------------------- # 3. Épinglage # --------------------------------------------------------------------------- # Le sha256 vient APRÈS la vérification GPG, et il ne la remplace pas : il rend # les compilations suivantes reproductibles et détecte une archive de cache # abîmée sans redemander à gpg. SOURCE_SHA=$(shasum -a 256 "$TARBALL" | cut -d' ' -f1) if [ -n "${PINNED_SHA256:-}" ] && [ "${PINNED_VERSION:-}" = "$FFMPEG_VERSION" ]; then if [ "$SOURCE_SHA" != "$PINNED_SHA256" ]; then echo "❌ L'archive ne correspond pas à l'empreinte épinglée." >&2 echo " attendu : $PINNED_SHA256" >&2 echo " obtenu : $SOURCE_SHA" >&2 exit 1 fi elif [ "$EPINGLER" = true ]; then cat > "$PIN_FILE" <&2 exit 1 fi # --------------------------------------------------------------------------- # 4. Compilation, une architecture après l'autre # --------------------------------------------------------------------------- SRC_DIR="$CACHE_DIR/ffmpeg-$FFMPEG_VERSION" if [ ! -d "$SRC_DIR" ]; then echo "==> Extraction…" # tar d'Apple décompresse .xz nativement (libarchive) : pas besoin de xz. tar -xf "$TARBALL" -C "$CACHE_DIR" fi # Les options sont ici plutôt que dans la boucle : c'est la liste qu'on relit # quand on se demande ce que ce binaire sait faire, et c'est elle qu'il faut # publier avec les sources au titre de la LGPL. CONFIGURE_OPTIONS=( # --- Rien par défaut --- --disable-everything --disable-autodetect # n'attrape aucune bibliothèque du système --disable-network # aucun protocole distant, aucune URL --disable-avdevice # ni caméra ni carte son --disable-doc --disable-debug --disable-shared --enable-static --enable-small --disable-x86asm # évite d'exiger nasm sur la machine de build ; # on ne décode aucune image, le gain serait nul # --- Les deux outils, et pas ffplay --- --disable-programs --enable-ffmpeg --enable-ffprobe # --- Lire un fichier local, rien de plus --- --enable-protocol=file --enable-zlib # certains .mkv compressent leurs pistes de # sous-titres ; sans zlib elles sont illisibles # --- Les conteneurs qu'un utilisateur ouvre vraiment --- --enable-demuxer=matroska,mov,mpegts,avi,asf,flv,ogg,mpeg,mpegvideo # --- Les sous-titres TEXTE, seuls convertibles en SRT --- # Les sous-titres image (PGS du Blu-ray, VobSub du DVD) sont absents à # dessein : les convertir en texte demanderait une reconnaissance optique que # le player ne fait pas. Les activer n'ajouterait que du code à attaquer. --enable-decoder=subrip,srt,ass,ssa,movtext,webvtt,text --enable-encoder=subrip,srt,ass,webvtt,text --enable-muxer=srt,ass,webvtt # --- Deux filtres de passage --- # ffmpeg refuse de construire son graphe sans eux, même quand rien ne le # traverse. --enable-filter=null,anull ) BUILT_ARCHS=() for ARCH in arm64 x86_64; do BUILD_DIR="$CACHE_DIR/build-$ARCH" # Accolades obligatoires : le bash 3.2 d'Apple, en locale UTF-8, avale le # caractère « … » dans le nom de la variable et s'arrête sur # « ARCH…: unbound variable ». echo "==> Compilation ${ARCH}…" mkdir -p "$BUILD_DIR" ( cd "$BUILD_DIR" if [ ! -f config.h ]; then CROSS=() # configure ne devine pas la cible quand elle diffère de la machine : # sans --enable-cross-compile il testerait les binaires produits en les # exécutant, et s'arrêterait sur « C compiler test failed ». if [ "$ARCH" != "$(uname -m)" ]; then CROSS=(--enable-cross-compile --arch="$ARCH" --target-os=darwin) fi # `${CROSS[@]+"${CROSS[@]}"}` et non `"${CROSS[@]}"` : sous `set -u`, le # bash 3.2 d'Apple traite l'expansion d'un tableau VIDE comme une variable # non définie et s'arrête. Le cas se produit à chaque compilation native, # c'est-à-dire toujours pour l'une des deux architectures. "../ffmpeg-$FFMPEG_VERSION/configure" \ "${CONFIGURE_OPTIONS[@]}" \ ${CROSS[@]+"${CROSS[@]}"} \ --cc="clang -arch $ARCH" \ --extra-cflags="-arch $ARCH -mmacosx-version-min=$DEPLOYMENT_TARGET" \ --extra-ldflags="-arch $ARCH -mmacosx-version-min=$DEPLOYMENT_TARGET" \ > configure.log 2>&1 || { tail -20 configure.log; exit 1; } fi make -j"$(sysctl -n hw.ncpu)" ffmpeg ffprobe > make.log 2>&1 || { tail -30 make.log; exit 1; } ) BUILT_ARCHS+=("$BUILD_DIR") done # --------------------------------------------------------------------------- # 5. Binaire universel # --------------------------------------------------------------------------- # Une seule app pour les Mac Intel et Apple Silicon : c'est ce que fait déjà # le reste du bundle, et livrer un ffmpeg mono-architecture ferait échouer # l'extraction sur la moitié du parc — silencieusement, puisque le binaire # serait bien présent. echo "==> Assemblage universel…" for tool in ffmpeg ffprobe; do lipo -create \ "${BUILT_ARCHS[0]}/$tool" \ "${BUILT_ARCHS[1]}/$tool" \ -output "$DEST_DIR/$tool" chmod +x "$DEST_DIR/$tool" echo " $tool : $(du -h "$DEST_DIR/$tool" | cut -f1) — $(lipo -archs "$DEST_DIR/$tool")" done # --------------------------------------------------------------------------- # 6. Licence et traçabilité # --------------------------------------------------------------------------- # Le texte de la LGPL part dans le bundle : l'obligation est de le livrer AVEC # le binaire, pas de le publier quelque part. cp "$SRC_DIR/COPYING.LGPLv2.1" "$LICENSE_DIR/ffmpeg-COPYING.LGPLv2.1.txt" # Le manifeste dit quelle source a produit ces binaires et avec quelles options. # Il sert à deux choses : répondre à une demande de conformité LGPL sans avoir à # se souvenir, et permettre à verifier-ffmpeg.sh de comparer la version # embarquée aux releases publiées. cat > "$DEST_DIR/ffmpeg-build.json" <