Pierre Rouanet f1915a218a Build gst-plugin-rtp, and check crate names before compiling
Second failure, and a self-inflicted one: `gst-plugin-rsrtp` is not a package
in gst-plugins-rs at 0.15.3 — or at 0.14.5. The crate is `gst-plugin-rtp`,
whose lib is named `gstrsrtp`, so the plugin filename and the package name
differ.

I took `gst-plugin-rsrtp` from reachy-mini-desktop-app README, which documents
a cargo cinstall line that cannot have worked at either tag. Read the crate
Cargo.toml, not a README — the source was one API call away.

The rockchip plugin and gst-plugin-webrtc both built fine, so the pin move in
the previous commit was right; this was the next thing along.

Also validates every crate name with `cargo pkgid` before compiling any of
them. cargo cinstall only checks a package name when it reaches it, so a typo
in the second crate surfaces after the first has spent three minutes building.
Now it costs a second.

Assisted-by: Claude:claude-opus-5[1m] shellcheck
2026-08-24 16:32:35 +02:00

263 lines
12 KiB
Bash
Executable File

#!/bin/sh
# Build the aarch64 GStreamer plugins the micro duck robot needs, from the pins in `pins.env`.
#
# Runs natively on aarch64 — in CI that is a `debian:trixie` container on an arm64 runner, which
# is what makes the output link against exactly the library versions the robot has rather than
# Ubuntu's or a cross sysroot's approximation of them. It also runs unchanged on a board, which
# is only useful for debugging: an RK3566 compiles the Rust half slowly enough that nobody should
# wait for it.
#
# sudo sh scripts/build.sh both plugins
# sudo sh scripts/build.sh rockchip just the MPP encoders
# sudo sh scripts/build.sh webrtc just webrtcsink/webrtcsrc
#
# Output lands in `dist/`: the .so files, SHA256SUMS, and MANIFEST recording which upstream commit
# each one came from. The manifest is not bookkeeping — it is the difference between a media bug
# somebody can reproduce and one they cannot, and it is what the third-party debs we rejected did
# not have.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
DIST="${ROOT}/dist"
# Debian's own plugin directory for this arch, which is where the .so wants to *end up* on a
# robot's GST_PLUGIN_PATH. Built here into a prefix we own so nothing is installed system-wide by
# a build.
STAGE="${ROOT}/.stage"
# shellcheck disable=SC1091 # sibling file, in this repository.
. "${ROOT}/pins.env"
say() { printf '\033[1m==>\033[0m %s\n' "$*"; }
warn() { printf '\033[33mwarning:\033[0m %s\n' "$*" >&2; }
die() { printf '\033[31merror:\033[0m %s\n' "$*" >&2; exit 1; }
WANT="${1:-both}"
case "$WANT" in
both|rockchip|webrtc) ;;
*) die "unknown target: ${WANT} (both, rockchip, webrtc)" ;;
esac
check_environment() {
[ "$(id -u)" = 0 ] || die "run as root — it installs build dependencies"
arch="$(uname -m)"
[ "$arch" = aarch64 ] \
|| die "this builds natively for aarch64 and this is ${arch}.
There is no cross-build here on purpose: linking against the robot's own Debian trixie
libraries is the reason CI uses an arm64 runner in a debian:trixie container."
command -v apt-get >/dev/null 2>&1 || die "expects a Debian userland"
}
apt_install() {
missing=""
for pkg in "$@"; do
dpkg -s "$pkg" >/dev/null 2>&1 || missing="$missing $pkg"
done
[ -n "$missing" ] || return 0
say "installing:$missing"
apt-get update -qq
# shellcheck disable=SC2086 # word-splitting the package list is the point
apt-get install -y -qq --no-install-recommends $missing \
|| die "apt failed installing:$missing"
}
# Rockchip's MPP and RGA, from Radxa's pool, runtime and headers together.
#
# `dpkg -i` resolves nothing here — these are direct downloads, not a configured apt source — so
# the full closure is named and installed in one call. Learning that one package at a time cost
# three rounds on a board: `rockchip-mpp-demos` needs `librockchip-vpu0` at an exact version, and
# the GStreamer plugin needs `librga2`.
install_rockchip_userspace() {
dpkg -s librockchip-mpp-dev >/dev/null 2>&1 && dpkg -s librga-dev >/dev/null 2>&1 && return 0
tmp="$(mktemp -d)"
# shellcheck disable=SC2064 # expand $tmp now, deliberately
trap "rm -rf '$tmp'" EXIT INT TERM
say "fetching Rockchip MPP ${MPP_VERSION} and RGA ${RGA_VERSION} from Radxa's pool"
for path in \
"m/mpp/librockchip-mpp1_${MPP_VERSION}_arm64.deb" \
"m/mpp/librockchip-vpu0_${MPP_VERSION}_arm64.deb" \
"m/mpp/librockchip-mpp-dev_${MPP_VERSION}_arm64.deb" \
"libr/librga/librga2_${RGA_VERSION}_arm64.deb" \
"libr/librga/librga-dev_${RGA_VERSION}_arm64.deb"
do
curl -fsSL -o "${tmp}/$(basename "$path")" "${RADXA_POOL}/${path}" \
|| die "cannot download ${RADXA_POOL}/${path}"
done
dpkg -i "${tmp}"/*.deb || die "dpkg -i failed on the Rockchip debs"
rm -rf "$tmp"
trap - EXIT INT TERM
}
build_rockchip() {
apt_install meson ninja-build build-essential pkg-config git curl ca-certificates \
libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libdrm-dev libglib2.0-dev
install_rockchip_userspace
# Verified, because meson's answer to a missing MPP is not an error:
# `gst/rockchipmpp/meson.build` ends in `if not mpp_dep.found() → subdir_done()`, so the whole
# plugin is *silently skipped* and the build succeeds having produced nothing.
for mod in rockchip_mpp librga; do
pkg-config --exists "$mod" \
|| die "pkg-config cannot find ${mod}. meson skips the plugin silently without it,
so this refuses here instead of shipping an empty release."
done
src="$(mktemp -d)"
say "gstreamer-rockchip @ ${GST_ROCKCHIP_REF}"
git clone -q --branch "$GST_ROCKCHIP_BRANCH" "$GST_ROCKCHIP_REPO" "${src}/s" \
|| die "cannot clone ${GST_ROCKCHIP_REPO}"
# Reset to the pin: `--depth 1` alone would take whatever the branch tip is today, which is
# the thing a pin exists to prevent.
git -C "${src}/s" checkout -q "$GST_ROCKCHIP_REF" \
|| die "${GST_ROCKCHIP_REF} is not on ${GST_ROCKCHIP_BRANCH}"
# `rkximage` and `kmssrc` are the X11 and KMS *sinks* in the same tree. A headless robot has
# no use for either, and they are why the prebuilt Radxa deb depends on libx11-6. Dropping
# them is the concrete thing building ourselves buys, beyond provenance.
say "configuring (rockchipmpp only; X11 and KMS sinks disabled)"
meson setup "${src}/b" "${src}/s" \
--prefix /usr --libdir lib --buildtype release \
-Drockchipmpp=enabled -Drga=enabled \
-Drkximage=disabled -Dkmssrc=disabled -Dvpxalphadec=disabled \
>"${src}/meson.log" 2>&1 || {
tail -40 "${src}/meson.log" >&2
die "meson setup failed; tail of its log above. The tree declares
meson_version >= 0.47 and was written against a far older meson, so a syntax rejection is the
failure to expect here. Installed: $(meson --version 2>/dev/null || echo unknown)."
}
ninja -C "${src}/b" >"${src}/ninja.log" 2>&1 || {
tail -40 "${src}/ninja.log" >&2
die "ninja failed; tail of its log above"
}
so="$(find "${src}/b" -name 'libgstrockchipmpp.so' -type f | head -1)"
[ -n "$so" ] || die "no libgstrockchipmpp.so was produced.
That is what a skipped subdir looks like rather than a compile error."
install -d "$DIST"
install -m 0644 "$so" "${DIST}/libgstrockchipmpp.so"
strip --strip-unneeded "${DIST}/libgstrockchipmpp.so"
printf 'libgstrockchipmpp.so %s %s\n' "$GST_ROCKCHIP_REPO" "$GST_ROCKCHIP_REF" \
>> "${DIST}/MANIFEST"
rm -rf "$src"
}
build_webrtc() {
# `libgstreamer-plugins-bad1.0-dev` is the load-bearing one: it carries
# `gstreamer-webrtc-1.0.pc` and `gstreamer-sdp-1.0.pc`, which is what the crate pkg-configs
# against. Everything else is what a Rust cdylib needs to link.
apt_install build-essential pkg-config git curl ca-certificates \
libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \
libgstreamer-plugins-bad1.0-dev libssl-dev libglib2.0-dev
# rustup rather than Debian's rustc. gst-plugins-rs tracks a recent toolchain and a distro
# rustc that is a few months behind fails on an edition or a lint, months after anybody
# remembers this choice was made.
if ! command -v cargo >/dev/null 2>&1; then
say "installing a Rust toolchain"
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --profile minimal --default-toolchain stable >/dev/null \
|| die "rustup install failed"
fi
# shellcheck disable=SC1091 # written by rustup, just above.
[ -f "$HOME/.cargo/env" ] && . "$HOME/.cargo/env"
command -v cargo-cbuild >/dev/null 2>&1 || {
say "installing cargo-c"
cargo install cargo-c --locked >/dev/null || die "cargo install cargo-c failed"
}
src="$(mktemp -d)"
say "gst-plugins-rs @ ${GST_PLUGINS_RS_REF}"
git clone -q --depth 1 --branch "$GST_PLUGINS_RS_REF" "$GST_PLUGINS_RS_REPO" "${src}/s" \
|| die "cannot clone ${GST_PLUGINS_RS_REPO} at ${GST_PLUGINS_RS_REF}"
install -d "$DIST" "$STAGE"
# Two crates, not one: the same stack wants `libgstrswebrtc.so` *and* `libgstrsrtp.so`.
#
# `gst-plugin-rtp`, whose lib is named `gstrsrtp` — the plugin filename and the crate name
# differ, which is how the wrong one gets used. Pollen's reachy-mini-desktop-app README
# documents `cargo cinstall -p gst-plugin-rsrtp`, and no such package exists in 0.14.5 or
# 0.15.3; taking that name on trust cost a build. Read the crate's Cargo.toml, not a README.
CRATES="gst-plugin-webrtc gst-plugin-rtp"
# Checked before anything is compiled. `cargo cinstall` validates the package name only when
# it gets to it, so a typo in the second crate is discovered after the first has spent three
# minutes building — which is exactly what happened.
bad=""
for crate in $CRATES; do
( cd "${src}/s" && cargo pkgid -p "$crate" >/dev/null 2>&1 ) || bad="$bad $crate"
done
[ -z "$bad" ] || die "not workspace members of gst-plugins-rs ${GST_PLUGINS_RS_REF}:${bad}
Package names move between releases. Check net/*/Cargo.toml at that tag."
for crate in $CRATES; do
say "cargo cinstall ${crate}"
( cd "${src}/s" && cargo cinstall -p "$crate" --prefix "$STAGE" --libdir lib --release ) \
>"${src}/${crate}.log" 2>&1 || {
tail -40 "${src}/${crate}.log" >&2
die "${crate} failed to build; tail of its log above"
}
done
found=0
for so in "${STAGE}"/lib/gstreamer-1.0/*.so; do
[ -e "$so" ] || continue
found=1
base="$(basename "$so")"
install -m 0644 "$so" "${DIST}/${base}"
strip --strip-unneeded "${DIST}/${base}"
printf '%s %s %s\n' "$base" "$GST_PLUGINS_RS_REPO" "$GST_PLUGINS_RS_REF" \
>> "${DIST}/MANIFEST"
done
[ "$found" = 1 ] || die "cargo cinstall produced no plugin under ${STAGE}/lib/gstreamer-1.0"
rm -rf "$src" "$STAGE"
}
# What was built, and enough to verify it independently.
finish() {
( cd "$DIST" && rm -f SHA256SUMS && sha256sum ./*.so > SHA256SUMS )
printf '\n'
say "dist/"
for f in "${DIST}"/*.so; do
printf ' %-28s %8s bytes\n' "$(basename "$f")" "$(stat -c '%s' "$f")"
done
printf '\n'
say "provenance"
sed 's/^/ /' "${DIST}/MANIFEST"
printf '\n'
say "elements"
# Loaded from dist/ exactly as a robot will load them, so this is the real question rather
# than a proxy. `mpph264enc` may be missing here even when built: registration probes MPP, and
# a container without /dev/mpp_service — or a robot whose node is still 0600 root:root —
# cannot answer. Absence in CI is expected; absence on a board with the udev rule is not.
for plug in rockchipmpp rswebrtc rsrtp; do
GST_PLUGIN_PATH="$DIST" gst-inspect-1.0 "$plug" 2>/dev/null \
| sed -n 's/^ \([a-z0-9]*\): / \1 /p' || true
done
[ -e /dev/mpp_service ] || warn "no /dev/mpp_service here, so the MPP encoders cannot register
in this environment. That is normal in CI: the .so still contains them, and a robot with the
udev rule will see them. Verify there, not here."
}
main() {
check_environment
rm -rf "$DIST" && install -d "$DIST"
: > "${DIST}/MANIFEST"
case "$WANT" in
rockchip) build_rockchip ;;
webrtc) build_webrtc ;;
both) build_rockchip; build_webrtc ;;
esac
apt_install gstreamer1.0-tools
finish
}
# Called on the last line so a truncated download defines functions and then does nothing, rather
# than running half a build.
main "$@"