Files
plezy/linux/packaging/build-libmpv.sh
T
edde746 e9a213807f ci(linux): check the runner's libraries reach the package metadata
The plane added three runtime libraries that bundle-libs.sh deliberately does not
bundle, so they have to be declared per distro by hand - and two hand-maintained
lists drifting apart is the failure this guard exists to prevent.

check_linux_package_deps.py parses the runner's CMake for every pkg-config module
it links, follows target_link_libraries to prove each one actually reaches the
binary, and requires a package name for it in every distro's depends list. It
fails closed on the shapes a naive parser gets wrong: a pkg_check_modules call
naming several modules, options preceding the module name, and version
constraints like mpv>=0.40 that would otherwise be read as a package nobody
ships.

The smoke job builds the three packages and reads the dependencies back out of
the artifacts, deriving what to expect from build-packages.py rather than
restating it - so a library is declared once and verified everywhere. That job is
off by default, which is exactly why it must not carry its own copy of the list.

The Linux native job names libwayland-dev and libegl-dev instead of riding
GTK's and epoxy's transitive dev dependencies, matching the CMake comment's own
rationale. In CI the host-dependency guard runs once: the named step covers the
staged bundle, and build-packages.py's internal run - which exists for by-hand
packaging - is skipped. The smoke job also drops patchelf, which nothing
invokes.
2026-08-10 08:48:14 +02:00

311 lines
9.2 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
NATIVE_INPUTS_MANIFEST="${NATIVE_INPUTS_MANIFEST:-$SCRIPT_DIR/native-inputs.json}"
manifest_value() {
python3 - "$NATIVE_INPUTS_MANIFEST" "$1" "$2" <<'PY'
import json
import sys
with open(sys.argv[1], encoding="utf-8") as source:
manifest = json.load(source)
value = manifest["inputs"][sys.argv[2]][sys.argv[3]]
if not isinstance(value, str) or not value:
raise SystemExit(f"invalid manifest value: {sys.argv[2]}.{sys.argv[3]}")
print(value)
PY
}
# For keys that are genuinely optional, where absence means "not offered"
# rather than a broken manifest. Pinned values never come through here: a
# missing checksum or commit has to stay fatal.
manifest_optional() {
python3 - "$NATIVE_INPUTS_MANIFEST" "$1" "$2" <<'PY'
import json
import sys
with open(sys.argv[1], encoding="utf-8") as source:
manifest = json.load(source)
value = manifest["inputs"][sys.argv[2]].get(sys.argv[3], "")
if not isinstance(value, str):
raise SystemExit(f"invalid manifest value: {sys.argv[2]}.{sys.argv[3]}")
print(value)
PY
}
FFMPEG_VERSION="$(manifest_value ffmpeg version)"
FFMPEG_URL="$(manifest_value ffmpeg url)"
FFMPEG_SHA256="$(manifest_value ffmpeg sha256)"
SHADERC_VERSION="$(manifest_value shaderc version)"
SHADERC_URL="$(manifest_value shaderc url)"
SHADERC_REF="$(manifest_value shaderc ref)"
SHADERC_COMMIT="$(manifest_value shaderc commit)"
LIBPLACEBO_VERSION="$(manifest_value libplacebo version)"
LIBPLACEBO_URL="$(manifest_value libplacebo url)"
LIBPLACEBO_MIRROR="$(manifest_optional libplacebo mirror)"
LIBPLACEBO_REF="$(manifest_value libplacebo ref)"
LIBPLACEBO_COMMIT="$(manifest_value libplacebo commit)"
MPV_VERSION="$(manifest_value mpv version)"
MPV_URL="$(manifest_value mpv url)"
MPV_SHA256="$(manifest_value mpv sha256)"
sha256_file() {
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$1" | cut -d ' ' -f 1
else
shasum -a 256 "$1" | cut -d ' ' -f 1
fi
}
download_verified() {
local url="$1"
local expected_sha256="$2"
local destination="$3"
local temporary
local actual_sha256
if [[ ! "$expected_sha256" =~ ^[0-9a-f]{64}$ ]]; then
echo "Invalid SHA-256 pin for $url" >&2
return 1
fi
mkdir -p "$(dirname "$destination")"
temporary="$(mktemp "${destination}.tmp.XXXXXX")"
# Retries cover the transfer only. A checksum mismatch below is never retried:
# that is a tampered or moved artefact, not a flaky connection, and trying
# again would only turn a loud failure into an intermittent one.
if ! curl \
--fail \
--location \
--silent \
--show-error \
--retry 3 \
--retry-connrefused \
--retry-delay 5 \
--connect-timeout 30 \
--proto '=https,file' \
--tlsv1.2 \
--output "$temporary" \
"$url"; then
rm -f "$temporary"
return 1
fi
actual_sha256="$(sha256_file "$temporary")"
if [ "$actual_sha256" != "$expected_sha256" ]; then
echo "SHA-256 mismatch for $url" >&2
echo "Expected: $expected_sha256" >&2
echo "Actual: $actual_sha256" >&2
rm -f "$temporary" "$destination"
return 1
fi
mv "$temporary" "$destination"
}
checkout_verified_ref() {
local url="$1"
local ref="$2"
local expected_commit="$3"
local destination="$4"
local mirror="${5:-}"
local actual_commit
local source
local attempt
if [[ ! "$expected_commit" =~ ^[0-9a-f]{40}$ ]]; then
echo "Invalid Git commit pin for $url at $ref" >&2
return 1
fi
# Retries and the mirror cover the transfer, never the verification. The commit
# pin below is checked identically whichever source answered, so a mirror can
# only supply the same tree or fail - it cannot substitute another one.
#
# This exists because code.videolan.org, the only source fetched over git,
# refused connections for well over two minutes at a time across several CI
# runs and took every build with it.
rm -rf "$destination"
for source in "$url" ${mirror:+"$mirror"}; do
for attempt in 1 2 3; do
if git clone --quiet --depth 1 --branch "$ref" --no-checkout \
"$source" "$destination"; then
break 2
fi
rm -rf "$destination"
# No point pausing before giving up on this source.
if [ "$attempt" -lt 3 ]; then sleep $((attempt * 5)); fi
done
echo "Could not clone $source at $ref after 3 attempts" >&2
done
if [ ! -d "$destination" ]; then
echo "No source produced $ref for $url" >&2
return 1
fi
actual_commit="$(git -C "$destination" rev-parse 'HEAD^{commit}')"
if [ "$actual_commit" != "$expected_commit" ]; then
echo "Git ref mismatch for $ref" >&2
echo "Expected: $expected_commit" >&2
echo "Actual: $actual_commit" >&2
rm -rf "$destination"
return 1
fi
git -C "$destination" checkout --quiet --detach "$expected_commit"
}
cleanup_srcdir=""
cleanup() {
if [ -n "$cleanup_srcdir" ]; then
rm -rf -- "$cleanup_srcdir"
fi
}
main() {
local prefix="${PREFIX:-$(pwd)/libmpv-prefix}"
local jobs="${JOBS:-$(nproc)}"
local srcdir
mkdir -p "$prefix"
prefix="$(realpath "$prefix")"
export PKG_CONFIG_PATH="$prefix/lib/pkgconfig:$prefix/lib/$(uname -m)-linux-gnu/pkgconfig:${PKG_CONFIG_PATH:-}"
srcdir="$(mktemp -d)"
cleanup_srcdir="$srcdir"
trap cleanup EXIT
cd "$srcdir"
echo "==> Sources in $srcdir"
echo "==> Install prefix: $prefix"
echo ""
# ─── Step 1: ffmpeg (static libraries) ─────────────────────────────────────
echo "==> Building ffmpeg $FFMPEG_VERSION (static, decoder-only)..."
download_verified "$FFMPEG_URL" "$FFMPEG_SHA256" "$srcdir/ffmpeg.tar.xz"
tar -xJf "$srcdir/ffmpeg.tar.xz"
cd "ffmpeg-${FFMPEG_VERSION}"
./configure \
--prefix="$prefix" \
--enable-gpl \
--enable-version3 \
--enable-static \
--disable-shared \
--enable-pic \
--disable-programs \
--disable-doc \
--disable-encoders \
--disable-muxers \
--enable-muxer=spdif \
--disable-devices \
--disable-bsfs \
--enable-bsf=aac_adtstoasc,av1_metadata,extract_extradata,h264_metadata,h264_mp4toannexb,hevc_metadata,hevc_mp4toannexb,vp9_metadata \
--disable-filters \
--enable-filter=aformat,aresample,format,null,scale \
--enable-gnutls \
--enable-vaapi \
--disable-vdpau \
--disable-debug \
--disable-stripping
make -j"$jobs"
make install
cd "$srcdir"
echo ""
echo "==> ffmpeg done."
echo ""
# ─── Step 2: shaderc (static library) ───────────────────────────────────────
echo "==> Building shaderc $SHADERC_VERSION (static)..."
checkout_verified_ref \
"$SHADERC_URL" "$SHADERC_REF" "$SHADERC_COMMIT" \
"$srcdir/shaderc-v${SHADERC_VERSION}"
cd "shaderc-v${SHADERC_VERSION}"
./utils/git-sync-deps
cmake -S . -B build \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_INSTALL_PREFIX="$prefix" \
-DSHADERC_SKIP_TESTS=ON \
-DSHADERC_SKIP_EXAMPLES=ON \
-DSHADERC_SKIP_COPYRIGHT_CHECK=ON \
-DBUILD_SHARED_LIBS=OFF \
-DCMAKE_POSITION_INDEPENDENT_CODE=ON
cmake --build build -j"$jobs"
cmake --install build
cd "$srcdir"
echo ""
echo "==> shaderc done."
echo ""
# ─── Step 3: libplacebo (static library) ───────────────────────────────────
echo "==> Building libplacebo $LIBPLACEBO_VERSION (static)..."
checkout_verified_ref \
"$LIBPLACEBO_URL" "$LIBPLACEBO_REF" "$LIBPLACEBO_COMMIT" \
"$srcdir/libplacebo-v${LIBPLACEBO_VERSION}" "$LIBPLACEBO_MIRROR"
cd "libplacebo-v${LIBPLACEBO_VERSION}"
git submodule update --init --recursive
meson setup build \
--prefix="$prefix" \
--default-library=static \
-Dvulkan=disabled \
-Dd3d11=disabled \
-Ddemos=false \
-Dtests=false
ninja -C build -j"$jobs"
ninja -C build install
cd "$srcdir"
echo ""
echo "==> libplacebo done."
echo ""
# ─── Step 4: mpv (shared libmpv) ───────────────────────────────────────────
echo "==> Building mpv $MPV_VERSION (shared libmpv only)..."
download_verified "$MPV_URL" "$MPV_SHA256" "$srcdir/mpv.tar.gz"
tar -xzf "$srcdir/mpv.tar.gz"
cd "mpv-${MPV_VERSION}"
# The runner's only video path is a Wayland subsurface, and it hands mpv
# MPV_RENDER_PARAM_WL_DISPLAY so VAAPI can find the device instead of falling
# back to software decoding. A libmpv built without Wayland cannot use that.
# VDPAU goes with X11 - it has no Wayland backend at all.
meson setup build \
--prefix="$prefix" \
-Dlibmpv=true \
-Dcplayer=false \
-Dbuild-date=false \
-Dlua=enabled \
-Djavascript=enabled \
-Dcplugins=disabled \
-Dmanpage-build=disabled \
-Djack=disabled \
-Dvulkan=disabled \
-Dd3d11=disabled \
-Dgl=enabled \
-Dvaapi=enabled \
-Dalsa=enabled \
-Dpulse=enabled \
-Dpipewire=enabled \
-Dvdpau=disabled \
-Dwayland=enabled \
-Dx11=disabled
ninja -C build -j"$jobs"
ninja -C build install
echo ""
echo "==> mpv done."
echo ""
echo "==> libmpv build complete. Output in $prefix"
}
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
main "$@"
fi