build(rpm): let the spec build a client-only aarch64 RPM

The blocker was never ExclusiveArch — it was that %build builds host +
tray + client together and %install lays down the host unconditionally,
with no way to express "client only". `%bcond_without host` (default ON,
so an x86_64 build is unchanged) now gates the host binary, the tray, the
headless-session data, the firewalld services, the main %post and the bare
%files section. Omitting %files for the MAIN package is the load-bearing
part: it is what stops rpm emitting an empty `punktfunk` alongside
punktfunk-client.

build-rpm.sh exposes it as PF_WITHOUT_HOST=1 and skips the libcuda stub
regeneration and the libcuda leak check, neither of which means anything
without the host.

Verified two ways. `rpmspec -P` shows the default expansion still carrying
3 host install lines, the tray build and one bare %files, while
--without host carries none of those and one %files client. And a real
rpmbuild in a native aarch64 Fedora 43 container produced
punktfunk-client-*.aarch64.rpm with correct aarch64 sonames
(libc.so.6(GLIBC_2.17), libSDL3.so.0, libavcodec.so.61) and no main
package.

Not a cross-compile: %build runs cargo for the build machine's arch, so
this wants an arm64 builder. No CI leg yet — deliberate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-25 14:32:45 +02:00
co-authored by Claude Opus 5
parent cac82d13df
commit 1c76b8c7b4
3 changed files with 64 additions and 9 deletions
+21
View File
@@ -123,3 +123,24 @@ docker run --rm -v "$PWD:/src" -w /src punktfunk-fedora-rpm \
A plain `rpmbuild`/COPR build with no `pf_version`/`pf_release` defines produces `0.3.0-1` (the A plain `rpmbuild`/COPR build with no `pf_version`/`pf_release` defines produces `0.3.0-1` (the
spec defaults). spec defaults).
### aarch64 — the client RPM
The **client** builds for aarch64; the **host** does not (its encode stack is NVENC/QSV/AMF, all
x86). `PF_WITHOUT_HOST=1` drops the host binary, the tray, the headless-session data, the
firewalld services and the main package's `%files`, leaving exactly one RPM: `punktfunk-client`.
Omitting the main `%files` is what keeps rpm from emitting an empty `punktfunk` next to it.
This is **not** a cross-compile — `%build` runs cargo for the host architecture, so run it on an
arm64 machine (or an emulated arm64 container, which is very slow):
```sh
docker build --platform linux/arm64 -f ci/fedora-rpm.Dockerfile -t punktfunk-fedora-rpm-arm64 ci
docker run --rm --platform linux/arm64 -v "$PWD:/src" -w /src punktfunk-fedora-rpm-arm64 \
bash -lc 'git config --global --add safe.directory /src && \
PF_VERSION=0.0.1 PF_WITHOUT_HOST=1 bash packaging/rpm/build-rpm.sh'
# -> dist/punktfunk-client-0.0.1-1.fcNN.aarch64.rpm
```
`PF_WITHOUT_HOST=1` works on x86_64 too, if you only want the client RPM. The flag is orthogonal
to the architecture; it is just that aarch64 has no other option.
+16 -5
View File
@@ -19,6 +19,11 @@ WEB_OPT=()
# requirement as web; default off so a bare `rpmbuild`/COPR still works. # requirement as web; default off so a bare `rpmbuild`/COPR still works.
SCRIPTING_OPT=() SCRIPTING_OPT=()
[ "${PF_WITH_SCRIPTING:-0}" = "1" ] && SCRIPTING_OPT=(--with scripting) [ "${PF_WITH_SCRIPTING:-0}" = "1" ] && SCRIPTING_OPT=(--with scripting)
# PF_WITHOUT_HOST=1 drops the host binary, the tray and the main package, leaving only
# punktfunk-client. This is what an aarch64 build uses: the client is portable, the host's encode
# stack (NVENC/QSV/AMF) is x86. Harmless on x86_64 too, if you only want the client RPM.
HOST_OPT=()
[ "${PF_WITHOUT_HOST:-0}" = "1" ] && HOST_OPT=(--without host)
ROOTDIR="$(cd "$(dirname "$0")/../.." && pwd)" ROOTDIR="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$ROOTDIR" cd "$ROOTDIR"
@@ -39,7 +44,7 @@ git archive --format=tar.gz --prefix="punktfunk-${PF_VERSION}/" \
# undefined cuStreamCreateWithPriority/cuMemcpy2DAsync_v2/…; this regen supersedes it.) Defining # undefined cuStreamCreateWithPriority/cuMemcpy2DAsync_v2/…; this regen supersedes it.) Defining
# extra unused symbols is harmless; a missing one fails the link. Only when /usr/lib64 is writable # extra unused symbols is harmless; a missing one fails the link. Only when /usr/lib64 is writable
# (CI image runs as root) — COPR/mock provides the real cuda-cudart-devel stub instead. # (CI image runs as root) — COPR/mock provides the real cuda-cudart-devel stub instead.
if [ "$(id -u)" = 0 ] && [ -d /usr/lib64 ]; then if [ "${PF_WITHOUT_HOST:-0}" != "1" ] && [ "$(id -u)" = 0 ] && [ -d /usr/lib64 ]; then
CU_SYMS="$(grep -rhoE '\bcu[A-Z][A-Za-z0-9_]*' crates/punktfunk-host/src/ | sort -u || true)" CU_SYMS="$(grep -rhoE '\bcu[A-Z][A-Za-z0-9_]*' crates/punktfunk-host/src/ | sort -u || true)"
if [ -n "$CU_SYMS" ]; then if [ -n "$CU_SYMS" ]; then
STUB_C="$(mktemp --suffix=.c)" STUB_C="$(mktemp --suffix=.c)"
@@ -55,7 +60,7 @@ fi
# resolves them from RPMs. Our builder image provides the toolchain via rustup (so # resolves them from RPMs. Our builder image provides the toolchain via rustup (so
# rust-toolchain.toml's pinned channel works) and the -devel libs via dnf, neither of which # rust-toolchain.toml's pinned channel works) and the -devel libs via dnf, neither of which
# rpmbuild's RPM-level check sees — skip it; a genuinely missing dep fails the compile/link. # rpmbuild's RPM-level check sees — skip it; a genuinely missing dep fails the compile/link.
rpmbuild -bb --nodeps "${WEB_OPT[@]}" "${SCRIPTING_OPT[@]}" \ rpmbuild -bb --nodeps "${WEB_OPT[@]}" "${SCRIPTING_OPT[@]}" "${HOST_OPT[@]}" \
--define "_topdir $TOP" \ --define "_topdir $TOP" \
--define "pf_version ${PF_VERSION}" \ --define "pf_version ${PF_VERSION}" \
--define "pf_release ${PF_RELEASE}" \ --define "pf_release ${PF_RELEASE}" \
@@ -63,6 +68,12 @@ rpmbuild -bb --nodeps "${WEB_OPT[@]}" "${SCRIPTING_OPT[@]}" \
mkdir -p dist mkdir -p dist
find "$TOP/RPMS" -name '*.rpm' -exec cp -v {} dist/ \; find "$TOP/RPMS" -name '*.rpm' -exec cp -v {} dist/ \;
echo "== Requires (must NOT contain libcuda) ==" # The libcuda leak check applies to the HOST package (the only thing that links the driver); a
rpm -qp --requires dist/punktfunk-${PF_VERSION}-*.rpm 2>/dev/null | grep -iE 'cuda|nvidia' \ # client-only build has no such package to inspect.
&& echo " !! NVIDIA/CUDA leak !!" || echo " clean" if [ "${PF_WITHOUT_HOST:-0}" = "1" ]; then
echo "== client-only build: no host RPM, libcuda check not applicable =="
else
echo "== Requires (must NOT contain libcuda) =="
rpm -qp --requires dist/punktfunk-${PF_VERSION}-*.rpm 2>/dev/null | grep -iE 'cuda|nvidia' \
&& echo " !! NVIDIA/CUDA leak !!" || echo " clean"
fi
+27 -4
View File
@@ -31,10 +31,10 @@ URL: https://git.unom.io/unom/punktfunk
# COPR SCM builds provide the checkout; for a tarball build, drop a git archive here: # COPR SCM builds provide the checkout; for a tarball build, drop a git archive here:
Source0: %{name}-%{version}.tar.gz Source0: %{name}-%{version}.tar.gz
# punktfunk-host is Linux-only and links system FFmpeg/PipeWire/Opus. x86_64 only for now: encode # punktfunk-host is Linux-only and links system FFmpeg/PipeWire/Opus. The HOST is x86_64 only —
# is NVENC (desktop NVIDIA) and no aarch64 build is produced/published by CI — claiming aarch64 # its encode stack is NVENC/QSV/AMF — but the CLIENT builds and runs fine on aarch64, so the spec
# here would advertise an arch we never ship. Re-add aarch64 once there's an arm64 build leg. # accepts both arches and `--without host` (below) selects the client-only build.
ExclusiveArch: x86_64 ExclusiveArch: x86_64 aarch64
# The zerocopy FFI links the NVIDIA driver's libcuda.so.1; rpm's auto-dep generator would turn # The zerocopy FFI links the NVIDIA driver's libcuda.so.1; rpm's auto-dep generator would turn
# that into a hard Requires on libcuda.so.1 (and we never want to pin the driver — NVENC/EGL come # that into a hard Requires on libcuda.so.1 (and we never want to pin the driver — NVENC/EGL come
@@ -55,6 +55,14 @@ ExclusiveArch: x86_64
# Debian punktfunk-scripting .deb. # Debian punktfunk-scripting .deb.
%bcond_with scripting %bcond_with scripting
# The HOST half of this spec (the punktfunk package itself + the tray). ON by default, so an
# ordinary x86_64 build is unchanged. `--without host` drops the host binary, the tray, the
# headless-session data, the firewalld services and the main %%files section entirely, leaving
# only punktfunk-client — which is what an aarch64 build produces, since the host's encode stack
# (NVENC/QSV/AMF) is x86 and the client's is not. Omitting the main %%files is what stops rpm
# from emitting an empty `punktfunk` package alongside the client.
%bcond_without host
# --- Build toolchain --------------------------------------------------------- # --- Build toolchain ---------------------------------------------------------
BuildRequires: cargo BuildRequires: cargo
BuildRequires: rust BuildRequires: rust
@@ -211,14 +219,21 @@ export PUNKTFUNK_BUILD_VERSION="%{version}-%{release}"
# with real RFI (clean P-frame recovery anchor via DPB reference slots; design/linux-vulkan-video-encode.md). # with real RFI (clean P-frame recovery anchor via DPB reference slots; design/linux-vulkan-video-encode.md).
# Pure Rust `ash` (no new lib / no link-time dep); default on for HEVC (PUNKTFUNK_VULKAN_ENCODE=0 opts # Pure Rust `ash` (no new lib / no link-time dep); default on for HEVC (PUNKTFUNK_VULKAN_ENCODE=0 opts
# back to libav VAAPI), and a failed open falls back to VAAPI so unsupported devices degrade gracefully. # back to libav VAAPI), and a failed open falls back to VAAPI so unsupported devices degrade gracefully.
%if %{with host}
cargo build --release --locked --features punktfunk-host/nvenc,punktfunk-host/vulkan-encode \ cargo build --release --locked --features punktfunk-host/nvenc,punktfunk-host/vulkan-encode \
-p punktfunk-host -p punktfunk-client-linux -p punktfunk-client-session -p punktfunk-host -p punktfunk-client-linux -p punktfunk-client-session
%else
# Client-only (aarch64): no host crate, so none of the encode features apply.
cargo build --release --locked -p punktfunk-client-linux -p punktfunk-client-session
%endif
# The status tray in its OWN cargo invocation — load-bearing, not tidiness. Cargo unifies features # The status tray in its OWN cargo invocation — load-bearing, not tidiness. Cargo unifies features
# across everything in one build, so co-building the tray with the host pulls the host's # across everything in one build, so co-building the tray with the host pulls the host's
# ashpd -> zbus/tokio onto the tray's shared zbus; the tray (ksni async-io + blocking, no tokio # ashpd -> zbus/tokio onto the tray's shared zbus; the tray (ksni async-io + blocking, no tokio
# runtime by design) then panics at startup ("there is no reactor running, must be called from the # runtime by design) then panics at startup ("there is no reactor running, must be called from the
# context of a Tokio 1.x runtime"). Built alone, its zbus stays on async-io. (Same split the .deb does.) # context of a Tokio 1.x runtime"). Built alone, its zbus stays on async-io. (Same split the .deb does.)
%if %{with host}
cargo build --release --locked -p punktfunk-tray cargo build --release --locked -p punktfunk-tray
%endif
%if %{with web} %if %{with web}
# Management web console: build the Nitro SSR bundle with bun (the `bun` preset + our Bun.serve # Management web console: build the Nitro SSR bundle with bun (the `bun` preset + our Bun.serve
@@ -243,6 +258,7 @@ fi
%endif %endif
%install %install
%if %{with host}
# Binary # Binary
install -Dm0755 target/release/punktfunk-host %{buildroot}%{_bindir}/punktfunk-host install -Dm0755 target/release/punktfunk-host %{buildroot}%{_bindir}/punktfunk-host
@@ -294,6 +310,7 @@ for sz in 22x22 48x48; do
install -Dm0644 "$png" %{buildroot}%{_datadir}/icons/hicolor/$sz/apps/"$(basename "$png")" install -Dm0644 "$png" %{buildroot}%{_datadir}/icons/hicolor/$sz/apps/"$(basename "$png")"
done done
done done
%endif
# --- client subpackage --- # --- client subpackage ---
install -Dm0755 target/release/punktfunk-client %{buildroot}%{_bindir}/punktfunk-client install -Dm0755 target/release/punktfunk-client %{buildroot}%{_bindir}/punktfunk-client
@@ -311,6 +328,7 @@ install -Dm0644 scripts/70-punktfunk-client.rules \
install -Dm0644 scripts/99-punktfunk-client-net.conf \ install -Dm0644 scripts/99-punktfunk-client-net.conf \
%{buildroot}%{_prefix}/lib/sysctl.d/99-punktfunk-client-net.conf %{buildroot}%{_prefix}/lib/sysctl.d/99-punktfunk-client-net.conf
%if %{with host}
# Headless session helpers + example config + OpenAPI doc (reference material). # Headless session helpers + example config + OpenAPI doc (reference material).
install -d %{buildroot}%{_datadir}/%{name}/headless install -d %{buildroot}%{_datadir}/%{name}/headless
install -Dm0755 scripts/headless/run-headless-kde.sh %{buildroot}%{_datadir}/%{name}/headless/run-headless-kde.sh install -Dm0755 scripts/headless/run-headless-kde.sh %{buildroot}%{_datadir}/%{name}/headless/run-headless-kde.sh
@@ -344,6 +362,7 @@ install -Dm0644 packaging/linux/punktfunk-native.xml \
# Web console opener (TCP 47992) — only meaningful with the web subpackage, opened deliberately. # Web console opener (TCP 47992) — only meaningful with the web subpackage, opened deliberately.
install -Dm0644 packaging/linux/punktfunk-web.xml \ install -Dm0644 packaging/linux/punktfunk-web.xml \
%{buildroot}%{_prefix}/lib/firewalld/services/punktfunk-web.xml %{buildroot}%{_prefix}/lib/firewalld/services/punktfunk-web.xml
%endif
%if %{with web} %if %{with web}
# --- web console subpackage (punktfunk-web) --- # --- web console subpackage (punktfunk-web) ---
@@ -383,6 +402,7 @@ chmod 0755 %{buildroot}%{_bindir}/punktfunk-scripting
install -Dm0644 scripts/punktfunk-scripting.service %{buildroot}%{_userunitdir}/punktfunk-scripting.service install -Dm0644 scripts/punktfunk-scripting.service %{buildroot}%{_userunitdir}/punktfunk-scripting.service
%endif %endif
%if %{with host}
%files %files
%license LICENSE-MIT LICENSE-APACHE THIRD-PARTY-NOTICES.txt %license LICENSE-MIT LICENSE-APACHE THIRD-PARTY-NOTICES.txt
%doc README.md packaging/README.md %doc README.md packaging/README.md
@@ -407,6 +427,7 @@ install -Dm0644 scripts/punktfunk-scripting.service %{buildroot}%{_userunitdir}/
%config(noreplace) /etc/gamescope-session-plus/sessions.d/steam %config(noreplace) /etc/gamescope-session-plus/sessions.d/steam
%dir %{_datadir}/%{name} %dir %{_datadir}/%{name}
%{_datadir}/%{name}/* %{_datadir}/%{name}/*
%endif
%files client %files client
%license LICENSE-MIT LICENSE-APACHE THIRD-PARTY-NOTICES.txt %license LICENSE-MIT LICENSE-APACHE THIRD-PARTY-NOTICES.txt
@@ -450,6 +471,7 @@ udevadm trigger --subsystem-match=hidraw 2>/dev/null || :
# rpm-ostree it takes effect on the next boot into the layered deployment). # rpm-ostree it takes effect on the next boot into the layered deployment).
sysctl -p %{_prefix}/lib/sysctl.d/99-punktfunk-client-net.conf >/dev/null 2>&1 || : sysctl -p %{_prefix}/lib/sysctl.d/99-punktfunk-client-net.conf >/dev/null 2>&1 || :
%if %{with host}
%post %post
# Reload udev so /dev/uinput picks up the new rule without a reboot (best-effort). # Reload udev so /dev/uinput picks up the new rule without a reboot (best-effort).
udevadm control --reload-rules 2>/dev/null || : udevadm control --reload-rules 2>/dev/null || :
@@ -474,6 +496,7 @@ if command -v punktfunk-host >/dev/null 2>&1; then
echo "$conflict" echo "$conflict"
fi fi
fi fi
%endif
%if %{with web} %if %{with web}
%post web %post web