feat(release): bundle ffmpeg + ffprobe in tarballs and Docker image
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.
This commit is contained in:
parent
727ab19468
commit
e68b127acc
4 changed files with 144 additions and 26 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -36,6 +36,7 @@ Thumbs.db
|
||||||
|
|
||||||
# GoReleaser
|
# GoReleaser
|
||||||
dist/
|
dist/
|
||||||
|
dist-ffbinaries/
|
||||||
|
|
||||||
# Docker
|
# Docker
|
||||||
tmp/
|
tmp/
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,14 @@ version: 2
|
||||||
|
|
||||||
project_name: unarr
|
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:
|
builds:
|
||||||
- main: ./cmd/unarr/
|
- main: ./cmd/unarr/
|
||||||
binary: unarr
|
binary: unarr
|
||||||
|
|
@ -20,11 +28,21 @@ builds:
|
||||||
- -X github.com/torrentclaw/unarr/internal/sentry.dsn={{ .Env.SENTRY_DSN }}
|
- -X github.com/torrentclaw/unarr/internal/sentry.dsn={{ .Env.SENTRY_DSN }}
|
||||||
|
|
||||||
archives:
|
archives:
|
||||||
- format: tar.gz
|
- formats: [tar.gz]
|
||||||
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
|
||||||
format_overrides:
|
format_overrides:
|
||||||
- goos: windows
|
- 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:
|
checksum:
|
||||||
name_template: "checksums.txt"
|
name_template: "checksums.txt"
|
||||||
|
|
|
||||||
30
Dockerfile
30
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 ----
|
# ---- Build stage ----
|
||||||
FROM golang:1.25-alpine AS builder
|
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 ----
|
# ---- Runtime stage ----
|
||||||
FROM alpine:3.22
|
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 && \
|
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)
|
# 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
|
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
|
USER unarr
|
||||||
|
|
||||||
COPY --from=builder /unarr /usr/local/bin/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
|
# Environment: point config/data to container paths
|
||||||
ENV UNARR_CONFIG_DIR=/config
|
ENV UNARR_CONFIG_DIR=/config
|
||||||
|
|
|
||||||
117
scripts/download-ffmpeg-static.sh
Executable file
117
scripts/download-ffmpeg-static.sh
Executable file
|
|
@ -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/<goos>-<goarch>/{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"
|
||||||
Loading…
Add table
Add a link
Reference in a new issue