Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions scripts/build-whisper-stt.sh
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,47 @@ libparakeet*.so|libparakeet*.so.*|\
relocate_macos_rpaths "${OUT_DIR}"
fi

# Linux: stage GCC's OpenMP runtime beside what links it.
#
# Every binary staged above links libgomp.so.1, and nothing shipped it. The
# deb/rpm/pacman declare it since 1.9.2, but the AppImage has no dependency
# mechanism at all, so on a machine without it the whole STT stack dies in
# ld.so before main() and transcription shows an end user a developer error.
#
# It belongs HERE rather than in stage-whisper-stt.sh, and that distinction is
# the whole point: this script runs on the machine that COMPILES these
# binaries, and build-whisper-stt.yml pins that to ubuntu-22.04, matching the
# floor before-pack.cjs enforces. Copying it at packaging time instead took it
# from whoever happened to run the build — and a 24.04 desktop's libgomp needs
# GLIBC_2.38, so a developer there could no longer package at all. Staged here
# it travels inside the whisper artifact, so every consumer gets the 22.04 copy
# whatever their own distro is.
#
# libgomp is the only system library this stack may bundle. The AppImage
# project's excludelist names the two it must not — libgbm.so.1 is "part of
# mesa" and speaks to the host's DRM stack, libasound.so.2 loads the host's
# ALSA plugins — and libgomp, a self-contained runtime, is absent from it.
#
# Resolved through the binary rather than a hardcoded /usr/lib path so arm64
# needs no second case, and copied under its soname because that is the
# DT_NEEDED the loader looks for; the file on disk is libgomp.so.1.0.0. No
# patchelf is needed: these binaries already carry RUNPATH=$ORIGIN.
if [[ "${OS_ARCH}" == linux-* ]]; then
local gomp
gomp="$(ldd "${OUT_DIR}/${out_bin_name}" 2>/dev/null | awk '/libgomp\.so\.1/ {print $3; exit}')"
if [[ -z "${gomp}" || ! -f "${gomp}" ]]; then
echo "FATAL: libgomp.so.1 is not resolvable for ${out_bin_name}." >&2
echo " Install it (libgomp1 on Debian/Ubuntu, libgomp on Fedora/Arch)." >&2
echo " Without it the AppImage ships an STT stack that cannot load," >&2
echo " which is silent until a user tries to transcribe." >&2
exit 1
fi
# Already ours: ldd resolved it through $ORIGIN on a re-run of this script.
if [[ "$(cd "$(dirname "${gomp}")" && pwd)" != "$(cd "${OUT_DIR}" && pwd)" ]]; then
cp -v "${gomp}" "${OUT_DIR}/libgomp.so.1"
fi
fi

echo "[whisper-stt] built ${variant_name} -> ${OUT_DIR}/${out_bin_name}"
ls -la "${OUT_DIR}"
}
Expand Down
49 changes: 49 additions & 0 deletions scripts/stage-whisper-stt.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,54 @@ ARTIFACT="whisper-stt-${TAG}"
DEST="electron/native/bin/${TAG}"
REPO="${GITHUB_REPOSITORY:-getopenscreen/openscreen}"

# Asserts that GCC's OpenMP runtime travelled with the binaries, on Linux only.
#
# Every ELF of this stack links libgomp.so.1, and 1.9.1 shipped none of them a
# way to find it: the deb/rpm/pacman did not declare it (1.9.2 does) and the
# AppImage has no dependency mechanism at all, so on a machine without it
# whisper-stt-server dies in ld.so before main() and transcription reports a
# developer error to end users. Declaring it fixes three formats out of four;
# bundling the library fixes the fourth, which cannot be fixed any other way.
#
# This only CHECKS. build-whisper-stt.sh does the copying, on the machine that
# compiles these binaries, and build-whisper-stt.yml pins that to ubuntu-22.04.
# Copying it here instead would take it from whoever runs the packaging, and a
# 24.04 desktop's libgomp needs GLIBC_2.38 — which before-pack.cjs then refuses,
# so a developer on a current distro could not package at all. Provenance is the
# whole point: the copy that ships must come from the same machine, and the same
# glibc floor, as the binaries that load it.
#
# Nothing else is needed to make it resolve: these binaries already carry
# `RUNPATH=$ORIGIN:$ORIGIN/bin`, so a copy beside them wins over the system one.
assert_openmp_runtime_staged() {
case "${TAG}" in linux-*) ;; *) return 0 ;; esac
[ -f "${DEST}/libgomp.so.1" ] && return 0

cat >&2 <<EOF

FATAL: libgomp.so.1 is missing from ${DEST}.

Every binary of this stack links it, and the AppImage has no way to declare a
dependency, so shipping without it means transcription dies in ld.so on any
machine that lacks it — silently, until a user tries to transcribe.

It is staged by scripts/build-whisper-stt.sh and travels inside the
whisper-stt artifact. An artifact built before that existed does not carry it.
Re-run the workflow against this branch, then re-run this build:

gh workflow run build-whisper-stt.yml --repo ${REPO}
EOF
exit 1
}

# A locally built binary wins: `npm run build:whisper-binaries` puts one here,
# and a developer testing a change should not have it silently replaced by CI's.
if compgen -G "${DEST}/whisper-stt-server*" > /dev/null; then
echo "whisper-stt-server already present in ${DEST} — leaving it alone."
# Checked on this path too: it is the one a developer takes, and a local
# whisper build predating the staging change would otherwise package an
# AppImage whose STT stack cannot load.
assert_openmp_runtime_staged
exit 0
fi

Expand Down Expand Up @@ -70,6 +114,11 @@ BIN="$(find "${DEST}" -maxdepth 1 -name 'whisper-stt-server*' -print -quit)"
[ -n "${BIN}" ] || { echo "FATAL: no whisper-stt-server binary in ${DEST}" >&2; exit 1; }
[ "${TAG#win32}" = "${TAG}" ] && chmod +x "${BIN}"

# Before the load check below, not after: if libgomp did not travel with the
# artifact, the check would otherwise pass by resolving the build machine's copy
# and prove nothing about what ships.
assert_openmp_runtime_staged

# The existence check above is not enough: 1.8.0-rc.1..rc.3 staged a binary that
# was present and unrunnable. cpp-httplib had linked OpenSSL, the two
# libssl/libcrypto DLLs were never in the artifact, and Windows killed the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,13 @@ The container is the point, not an implementation detail — the runner has more

`rpm` and `pacman` are verified too, and they are the ones with no other safety net: nobody installs them often enough to report a gap quickly, and the package names genuinely differ. `libgomp.so.1` is `libgomp1` on Debian and `libgomp` on both Fedora and Arch, where it was split out of `gcc-libs` — a guess would have been wrong.

The AppImage is deliberately not covered. It has no dependency mechanism, so there is no declaration to verify against and every system soname is missing by construction. It stays exposed, which is what `d3d_linux::diagnose` naming the Mesa package is for.
The AppImage is deliberately not covered by this check. What the check verifies is that everything a package needs is either declared or shipped, and the AppImage declares nothing — there is no manifest to verify against, so it would report every library the format legitimately expects from the host. That is the AppImage model working as intended, not a defect, and a check that cannot distinguish the two says nothing.

The exposure is instead handled at the source, which is the layer to prefer anyway. Of the three sonames 1.9.2 chased, exactly one may be bundled, and `scripts/build-whisper-stt.sh` now does: `libgomp.so.1` is a self-contained runtime, it is absent from [the AppImage project's excludelist](https://github.com/AppImage/pkg2appimage/blob/master/excludelist), and the STT binaries already carry `RUNPATH=$ORIGIN:$ORIGIN/bin`, so a copy beside them is found before the system one — no `patchelf`, no `AppRun` wrapper. The other two are on that excludelist and say why: `libgbm.so.1` is "part of mesa" and speaks to the host's DRM stack, `libasound.so.2` loads the host's ALSA plugins and configuration. A bundled copy of either is worse than none.

**Which script does the copying is the interesting part.** It belongs to the build, not to packaging, because provenance is what makes the bundled copy correct: `build-whisper-stt.yml` pins its Linux leg to `ubuntu-22.04`, the same floor `before-pack.cjs` enforces, so the library that ships comes from the same machine and the same glibc as the binaries that load it, and it travels inside the whisper artifact to every consumer. Copying it at packaging time instead would take it from whoever ran the build — and a 24.04 desktop's `libgomp` needs `GLIBC_2.38`, which the symbol-version guard then rejects, leaving a developer on a current distro unable to package at all. `scripts/stage-whisper-stt.sh` only asserts it arrived, and says to re-run the whisper workflow if it did not.

What remains host-supplied for the AppImage is the GTK/GLib/NSS stack, which no AppImage bundles — theme engines, GIO modules and pixbuf loaders all resolve against the host. `libvulkan.so.1` is already bundled at the AppImage root by electron-builder itself. For the Vulkan *driver*, which cannot be bundled, `d3d_linux::diagnose` names the Mesa package instead.

### Testing without the build machine's advantages

Expand Down
Loading