Files
plezy/linux/packaging/build-libmpv_test.sh
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

270 lines
9.9 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=build-libmpv.sh
source "$SCRIPT_DIR/build-libmpv.sh"
fail() {
echo "FAIL: $*" >&2
exit 1
}
assert_absent() {
[ ! -e "$1" ] || fail "unexpected path remains: $1"
}
init_repository() {
mkdir -p "$1"
git -C "$1" init --quiet
git -C "$1" config user.name "Plezy provenance test"
git -C "$1" config user.email "provenance-test@invalid.example"
}
temporary="$(mktemp -d)"
trap 'rm -rf "$temporary"' EXIT
fixture="$temporary/source.bin"
destination="$temporary/download/output.bin"
printf 'reviewed native input\n' >"$fixture"
expected="$(sha256_file "$fixture")"
download_verified "file://$fixture" "$expected" "$destination"
cmp -s "$fixture" "$destination" || fail "verified download changed bytes"
printf 'reviewed native inpuu\n' >"$fixture"
rm -f "$destination"
if download_verified "file://$fixture" "$expected" "$destination"; then
fail "changed archive was accepted"
fi
assert_absent "$destination"
if compgen -G "$destination.tmp.*" >/dev/null; then
fail "failed download left a temporary file"
fi
repository="$temporary/repository"
checkout="$temporary/checkout"
init_repository "$repository"
printf 'first\n' >"$repository/input.txt"
git -C "$repository" add input.txt
git -C "$repository" commit --quiet -m first
git -C "$repository" tag release
approved_commit="$(git -C "$repository" rev-parse HEAD)"
checkout_verified_ref "file://$repository" release "$approved_commit" "$checkout"
[ "$(git -C "$checkout" rev-parse HEAD)" = "$approved_commit" ] ||
fail "verified checkout selected the wrong commit"
# A dead primary must fall through to the mirror, because the whole point is
# that one unreachable host cannot stop the build. This runs while the tag still
# points at the approved commit; the moved-tag case is below.
unreachable="file://$temporary/definitely-not-a-repository"
checkout_verified_ref "$unreachable" release "$approved_commit" "$checkout" "file://$repository"
[ "$(git -C "$checkout" rev-parse HEAD)" = "$approved_commit" ] ||
fail "mirror fallback selected the wrong commit"
# And the mirror answers to the same pin. A mirror serving a different tree is
# the one thing a fallback must never quietly accept.
mirror_repository="$temporary/mirror"
init_repository "$mirror_repository"
printf 'substituted\n' >"$mirror_repository/input.txt"
git -C "$mirror_repository" add input.txt
git -C "$mirror_repository" commit --quiet -m substituted
git -C "$mirror_repository" tag release
if checkout_verified_ref "$unreachable" release "$approved_commit" "$checkout" "file://$mirror_repository"; then
fail "mirror serving another commit was accepted"
fi
assert_absent "$checkout"
printf 'second\n' >"$repository/input.txt"
git -C "$repository" commit --quiet -am second
git -C "$repository" tag --force release >/dev/null
if checkout_verified_ref "file://$repository" release "$approved_commit" "$checkout"; then
fail "moved tag was accepted"
fi
assert_absent "$checkout"
# ─── The build plan ─────────────────────────────────────────────────────────
# Everything above checks how sources arrive. What they are then configured
# with decides whether the feature works at all, and it fails quietly: a libmpv
# built without -Dwayland=enabled still compiles, still links and still plays,
# it just has no Wayland backend, so the render context cannot accept
# MPV_RENDER_PARAM_WL_DISPLAY, vaapi finds no device and hardware decoding
# drops to a copy-back path. Nothing crashes, so nothing else notices. Run
# main() for real against stub build tools and assert the argument vectors they
# were handed.
stub_bin="$temporary/stub-bin"
stub_extra="$temporary/stub-extra"
records="$temporary/records"
mkdir -p "$stub_bin" "$stub_extra" "$records" "$temporary/tmp"
# Deliberately unlike the pinned versions: every path below is derived from the
# manifest, so a stub that matched by accident would prove nothing.
ffmpeg_version="9.9.9"
shaderc_version="6.6.6"
libplacebo_version="7.7.7"
mpv_version="8.8.8"
# Each stub records the vector it was called with, one argument per line, plus
# the directory it ran in - which is what tells mpv's meson call apart from
# libplacebo's. An argument containing a newline would corrupt the record, and
# none of these ever does: they are literal flags and mktemp -d paths.
make_stub() {
local directory="$1" name="$2" extra="${3:-}"
cat >"$directory/$name" <<STUB
#!/usr/bin/env bash
set -euo pipefail
record="\$(mktemp "$records/record.XXXXXX")"
{ printf '%s\n' "$name" "\$(pwd)"; [ \$# -eq 0 ] || printf '%s\n' "\$@"; } >"\$record"
$extra
STUB
chmod +x "$directory/$name"
}
payload="$temporary/payload.bin"
printf 'stub archive payload\n' >"$payload"
payload_sha256="$(sha256_file "$payload")"
curl_extra='
output=""
previous=""
for argument in "$@"; do
if [ "$previous" = "--output" ]; then output="$argument"; fi
previous="$argument"
done
[ -n "$output" ] || { echo "stub curl: no --output argument" >&2; exit 1; }
cp -- "'"$payload"'" "$output"
'
# The unmapped case is fatal on purpose: a new archive in the build plan has to
# come here and be described rather than silently extract to nothing.
tar_extra='
archive="$(basename -- "${!#}")"
case "$archive" in
ffmpeg.tar.xz) directory="ffmpeg-'"$ffmpeg_version"'" ;;
mpv.tar.gz) directory="mpv-'"$mpv_version"'" ;;
*) echo "stub tar: unexpected archive $archive" >&2; exit 1 ;;
esac
mkdir -p "$directory"
cp -- "'"$stub_extra"'/configure" "$directory/configure"
chmod +x "$directory/configure"
'
# ffmpeg runs ./configure out of its own tarball, so that one is planted by the
# tar stub rather than found on PATH.
make_stub "$stub_extra" configure
make_stub "$stub_bin" curl "$curl_extra"
make_stub "$stub_bin" tar "$tar_extra"
for tool in make cmake meson ninja; do
make_stub "$stub_bin" "$tool"
done
# git stays real, pointed at local repositories: the checkout is pinned by
# commit, and a stub that answered rev-parse would be asserting its own input.
shaderc_repository="$temporary/shaderc-source"
init_repository "$shaderc_repository"
mkdir -p "$shaderc_repository/utils"
printf '#!/bin/sh\nexit 0\n' >"$shaderc_repository/utils/git-sync-deps"
chmod +x "$shaderc_repository/utils/git-sync-deps"
git -C "$shaderc_repository" add utils/git-sync-deps
# Windows checkouts do not track the mode bit, and the build plan executes it.
git -C "$shaderc_repository" update-index --chmod=+x utils/git-sync-deps
git -C "$shaderc_repository" commit --quiet -m shaderc
git -C "$shaderc_repository" tag release
shaderc_commit="$(git -C "$shaderc_repository" rev-parse HEAD)"
libplacebo_repository="$temporary/libplacebo-source"
init_repository "$libplacebo_repository"
printf 'stub libplacebo\n' >"$libplacebo_repository/meson.build"
git -C "$libplacebo_repository" add meson.build
git -C "$libplacebo_repository" commit --quiet -m libplacebo
git -C "$libplacebo_repository" tag release
libplacebo_commit="$(git -C "$libplacebo_repository" rev-parse HEAD)"
manifest="$temporary/native-inputs.json"
cat >"$manifest" <<JSON
{
"inputs": {
"ffmpeg": {
"version": "$ffmpeg_version",
"url": "https://stub.invalid/ffmpeg.tar.xz",
"sha256": "$payload_sha256"
},
"shaderc": {
"version": "$shaderc_version",
"url": "file://$shaderc_repository",
"ref": "release",
"commit": "$shaderc_commit"
},
"libplacebo": {
"version": "$libplacebo_version",
"url": "file://$libplacebo_repository",
"ref": "release",
"commit": "$libplacebo_commit"
},
"mpv": {
"version": "$mpv_version",
"url": "https://stub.invalid/mpv.tar.gz",
"sha256": "$payload_sha256"
}
}
}
JSON
# JOBS keeps nproc out of it; TMPDIR keeps main()'s own mktemp -d inside the
# directory this test already cleans up.
build_log="$temporary/build.log"
if ! (
export NATIVE_INPUTS_MANIFEST="$manifest"
export PATH="$stub_bin:$PATH"
export TMPDIR="$temporary/tmp"
export PREFIX="$temporary/prefix"
export JOBS=1
bash "$SCRIPT_DIR/build-libmpv.sh"
) >"$build_log" 2>&1; then
cat "$build_log" >&2
fail "the stubbed build plan did not run to completion"
fi
recorded_call() {
local program="$1" directory="$2" candidate
for candidate in "$records"/record.*; do
[ -f "$candidate" ] || continue
if [ "$(sed -n 1p "$candidate")" = "$program" ] &&
[ "$(basename -- "$(sed -n 2p "$candidate")")" = "$directory" ]; then
printf '%s\n' "$candidate"
return 0
fi
done
return 1
}
# -Fxq, so this matches a whole recorded argument. A substring search would
# accept -Dwayland=enabled inside a comment, which is exactly the hole here.
assert_argument() {
local record="$1" argument="$2" description="$3"
tail -n +3 "$record" | grep -Fxq -- "$argument" ||
fail "$description does not pass $argument"
}
mpv_meson="$(recorded_call meson "mpv-$mpv_version")" ||
fail "the build plan never ran meson in the mpv source tree"
[ "$(sed -n 3p "$mpv_meson")" = "setup" ] ||
fail "mpv's first meson call is no longer 'setup'"
assert_argument "$mpv_meson" "-Dwayland=enabled" "mpv's meson setup"
assert_argument "$mpv_meson" "-Dx11=disabled" "mpv's meson setup"
assert_argument "$mpv_meson" "-Dvaapi=enabled" "mpv's meson setup"
assert_argument "$mpv_meson" "-Dgl=enabled" "mpv's meson setup"
# vaapi has to reach ffmpeg too, or mpv's hwdec has no decoder behind it.
ffmpeg_configure="$(recorded_call configure "ffmpeg-$ffmpeg_version")" ||
fail "the build plan never configured ffmpeg"
assert_argument "$ffmpeg_configure" "--enable-vaapi" "ffmpeg's configure"
# libplacebo runs meson as well, and its call carries no Wayland flag: finding
# it separately is what proves the directory above really discriminated.
recorded_call meson "libplacebo-v$libplacebo_version" >/dev/null ||
fail "the build plan never ran meson in the libplacebo source tree"
echo "Linux native acquisition and build plan verification passed"