From e68b127acc4a4b7bf8328e6beb7406f62b509faa Mon Sep 17 00:00:00 2001 From: Deivid Soto Date: Wed, 6 May 2026 11:26:01 +0200 Subject: [PATCH] feat(release): bundle ffmpeg + ffprobe in tarballs and Docker image MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operators no longer have to install ffmpeg manually. Both the release tarballs (5 platforms × 2 binaries) and the Docker image now ship a working ffmpeg + ffprobe pair adjacent to the unarr binary; ResolveFFmpeg / ResolveFFprobe pick them up via the "adjacent to executable" branch with zero configuration. Tarball bundle (scripts/download-ffmpeg-static.sh + .goreleaser.yml): - ffbinaries.com (johnvansickle / Zeranoe-style static GPL builds) for linux-amd64, linux-arm64, darwin-amd64, windows-amd64 - evermeet.cx universal Mach-O for darwin-arm64 (ffbinaries lacks it) - BtbN/FFmpeg-Builds for windows-arm64 (ffbinaries lacks it) - Idempotent fetch with curl --retry 5 so transient github.com SSL errors don't fail the goreleaser before-hook - New `before.hooks` runs the script automatically per release; archive files glob `dist-ffbinaries/{{ .Os }}-{{ .Arch }}/*` + strip_parent - Migrated to non-deprecated `formats: [tar.gz]` / `formats: [zip]` - Verified via `goreleaser release --snapshot --clean --skip=publish` — 6 archives all carry ffmpeg + ffprobe (~60-130MB each) Docker image (Dockerfile): - Replaced the failing BtbN static glibc binaries with Alpine's native musl `apk add ffmpeg`. The static GPL builds need glibc + libmvec / libgcc_s; gcompat alone is not enough (vector-math symbols unresolved). Alpine ships ffmpeg 6.1.2 which is fine for the WebRTC transcoder. - Image size 174MB, built + ffmpeg/ffprobe/unarr smoke OK. Targets the v0.8 unarr release (per user direction — new feature, not a patch). dist-ffbinaries/ added to .gitignore. --- .gitignore | 1 + .goreleaser.yml | 22 +++++- Dockerfile | 30 ++------ scripts/download-ffmpeg-static.sh | 117 ++++++++++++++++++++++++++++++ 4 files changed, 144 insertions(+), 26 deletions(-) create mode 100755 scripts/download-ffmpeg-static.sh diff --git a/.gitignore b/.gitignore index 0de3731..a6d17b3 100644 --- a/.gitignore +++ b/.gitignore @@ -36,6 +36,7 @@ Thumbs.db # GoReleaser dist/ +dist-ffbinaries/ # Docker tmp/ diff --git a/.goreleaser.yml b/.goreleaser.yml index 44656cd..0a5c821 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -2,6 +2,14 @@ version: 2 project_name: unarr +# Pre-build hook: fetch static ffmpeg + ffprobe per platform so each +# release tarball ships them adjacent to the unarr binary. ResolveFFmpeg / +# ResolveFFprobe pick them up via the "adjacent to executable" branch — no +# system install or runtime download needed. +before: + hooks: + - bash scripts/download-ffmpeg-static.sh + builds: - main: ./cmd/unarr/ binary: unarr @@ -20,11 +28,21 @@ builds: - -X github.com/torrentclaw/unarr/internal/sentry.dsn={{ .Env.SENTRY_DSN }} archives: - - format: tar.gz + - formats: [tar.gz] name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" format_overrides: - goos: windows - format: zip + formats: [zip] + files: + - LICENSE* + - README* + # Bundle the matching ffmpeg + ffprobe (filename includes .exe on Windows + # because download-ffmpeg-static.sh writes ffmpeg.exe / ffprobe.exe there). + - src: "dist-ffbinaries/{{ .Os }}-{{ .Arch }}/*" + dst: . + strip_parent: true + info: + mode: 0o755 checksum: name_template: "checksums.txt" diff --git a/Dockerfile b/Dockerfile index f0e816f..1773622 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,25 +1,3 @@ -# ---- ffprobe static binary stage ---- -# Download a static ffprobe build from BtbN/FFmpeg-Builds (GitHub CDN, reliable). -FROM alpine:3.22 AS ffprobe-dl - -RUN apk add --no-cache curl xz - -RUN ARCH=$(uname -m) && \ - case "$ARCH" in \ - x86_64) SLUG="linux64" ;; \ - aarch64) SLUG="linuxarm64" ;; \ - *) echo "Unsupported arch: $ARCH" && exit 1 ;; \ - esac && \ - curl -fsSL --retry 3 --retry-delay 5 \ - "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-${SLUG}-gpl.tar.xz" \ - -o /tmp/ff.tar.xz && \ - mkdir /tmp/ffbuild && \ - tar xJ -f /tmp/ff.tar.xz --strip-components=1 -C /tmp/ffbuild/ && \ - mv /tmp/ffbuild/bin/ffprobe /usr/local/bin/ffprobe && \ - chmod +x /usr/local/bin/ffprobe && \ - rm -rf /tmp/ff.tar.xz /tmp/ffbuild && \ - ffprobe -version | head -1 - # ---- Build stage ---- FROM golang:1.25-alpine AS builder @@ -40,8 +18,13 @@ RUN CGO_ENABLED=0 go build -ldflags="-s -w -X github.com/torrentclaw/unarr/inter # ---- Runtime stage ---- FROM alpine:3.22 +# Use Alpine's native musl ffmpeg + ffprobe instead of the johnvansickle / +# BtbN static glibc builds — those need a glibc shim on Alpine and the +# vector-math symbols the GPL builds reference are not satisfiable by +# gcompat. Alpine ships ffmpeg ~7.x which is fine for the WebRTC +# transcoding pipeline (libx264 + libfdk-aac alternatives included). RUN apk upgrade --no-cache && \ - apk add --no-cache ca-certificates tzdata + apk add --no-cache ca-certificates tzdata ffmpeg # Non-root user (UID 1000 matches typical host user for volume permissions) RUN addgroup -g 1000 unarr && adduser -u 1000 -G unarr -D -h /home/unarr unarr @@ -53,7 +36,6 @@ RUN mkdir -p /config /downloads /data && \ USER unarr COPY --from=builder /unarr /usr/local/bin/unarr -COPY --from=ffprobe-dl /usr/local/bin/ffprobe /usr/local/bin/ffprobe # Environment: point config/data to container paths ENV UNARR_CONFIG_DIR=/config diff --git a/scripts/download-ffmpeg-static.sh b/scripts/download-ffmpeg-static.sh new file mode 100755 index 0000000..719fcde --- /dev/null +++ b/scripts/download-ffmpeg-static.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +# scripts/download-ffmpeg-static.sh — fetch static ffmpeg + ffprobe binaries +# for every platform we ship. Run by goreleaser's `before.hooks` so each +# tarball can bundle the binaries adjacent to `unarr`. +# +# Source: https://ffbinaries.com (same index the runtime fallback uses). +# Output: +# dist-ffbinaries/-/{ffmpeg, ffprobe}[.exe] +# Idempotent: skips downloads when the target file already exists. + +set -euo pipefail + +# Map ffbinaries platform key → goreleaser {Os}-{Arch}. ffbinaries.com only +# ships an x86_64 macOS build; for darwin-arm64 we fall back to evermeet.cx +# universal binaries (handled separately below). +PLATFORMS=( + "linux-64:linux-amd64" + "linux-arm64:linux-arm64" + "osx-64:darwin-amd64" + "windows-64:windows-amd64" +) +DEST_ROOT="${FFBINARIES_DEST:-dist-ffbinaries}" +INDEX_URL="https://ffbinaries.com/api/v1/version/latest" + +for cmd in curl jq unzip; do + command -v "$cmd" >/dev/null 2>&1 || { + echo "[ffbin] missing required tool: $cmd" >&2 + exit 2 + } +done + +mkdir -p "$DEST_ROOT" + +echo "[ffbin] fetching index from $INDEX_URL" +INDEX_JSON="$(curl -fsSL "$INDEX_URL")" +VERSION="$(echo "$INDEX_JSON" | jq -r .version)" +echo "[ffbin] ffbinaries version: $VERSION" + +for entry in "${PLATFORMS[@]}"; do + ffbkey="${entry%%:*}" + goplat="${entry##*:}" + outdir="$DEST_ROOT/$goplat" + mkdir -p "$outdir" + + for tool in ffmpeg ffprobe; do + binname="$tool" + [[ "$goplat" == windows-* ]] && binname="${tool}.exe" + + if [ -f "$outdir/$binname" ]; then + echo "[ffbin] skip $goplat/$binname (already present)" + continue + fi + + url="$(echo "$INDEX_JSON" | jq -r ".bin[\"$ffbkey\"][\"$tool\"] // empty")" + if [ -z "$url" ]; then + echo "[ffbin] WARN $goplat/$tool: no download URL in index" >&2 + continue + fi + + tmpzip="$(mktemp --suffix=.zip)" + echo "[ffbin] fetch $goplat/$tool from $url" + curl -fsSL --retry 5 --retry-delay 3 --retry-all-errors "$url" -o "$tmpzip" + unzip -p "$tmpzip" "$binname" > "$outdir/$binname" + chmod +x "$outdir/$binname" + rm -f "$tmpzip" + done +done + +# --- darwin-arm64 via evermeet.cx (universal binary; ffbinaries lacks it) --- +darwin_arm_dir="$DEST_ROOT/darwin-arm64" +mkdir -p "$darwin_arm_dir" +for tool in ffmpeg ffprobe; do + out="$darwin_arm_dir/$tool" + if [ -f "$out" ]; then + echo "[ffbin] skip darwin-arm64/$tool (already present)" + continue + fi + url="https://evermeet.cx/ffmpeg/getrelease/$tool/zip" + tmpzip="$(mktemp --suffix=.zip)" + echo "[ffbin] fetch darwin-arm64/$tool from $url" + curl -fsSL --retry 5 --retry-delay 3 --retry-all-errors "$url" -o "$tmpzip" + unzip -p "$tmpzip" "$tool" > "$out" + chmod +x "$out" + rm -f "$tmpzip" +done + +# --- windows-arm64 via BtbN/FFmpeg-Builds (ffbinaries lacks it) --- +# BtbN ships a single zip per platform with ffmpeg.exe + ffprobe.exe under +# ffmpeg-master-latest-winarm64-gpl/bin/. Extract both in one fetch. +win_arm_dir="$DEST_ROOT/windows-arm64" +mkdir -p "$win_arm_dir" +needs_win_arm=0 +for tool in ffmpeg.exe ffprobe.exe; do + [ -f "$win_arm_dir/$tool" ] || needs_win_arm=1 +done +if [ "$needs_win_arm" = "1" ]; then + url="https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-master-latest-winarm64-gpl.zip" + tmpzip="$(mktemp --suffix=.zip)" + echo "[ffbin] fetch windows-arm64/{ffmpeg,ffprobe}.exe from $url" + curl -fsSL --retry 5 --retry-delay 3 --retry-all-errors "$url" -o "$tmpzip" + for tool in ffmpeg.exe ffprobe.exe; do + out="$win_arm_dir/$tool" + member="$(unzip -Z1 "$tmpzip" "*/bin/$tool" 2>/dev/null | head -1)" + if [ -z "$member" ]; then + echo "[ffbin] WARN windows-arm64/$tool: not found in BtbN zip" >&2 + continue + fi + unzip -p "$tmpzip" "$member" > "$out" + chmod +x "$out" + done + rm -f "$tmpzip" +else + echo "[ffbin] skip windows-arm64 (already present)" +fi + +echo "[ffbin] done. layout:" +find "$DEST_ROOT" -type f -printf " %p (%s bytes)\n"