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"