fix(packaging): build pywhispercpp at the portable x86-64 baseline (#959)

* fix(packaging): build pywhispercpp at the portable x86-64 baseline

Every packaged channel shipped a libggml-cpu compiled for AVX2+FMA:
the manylinux PyPI wheel itself carries -mavx2 -mfma -mbmi2 code, so
any package that bundles it dies with SIGILL when whisper.cpp loads a
model on a pre-Haswell CPU (the reporter's i5-3337U). GGML_NATIVE=OFF
alone is not the portable baseline either; while the native default
is on, ggml turns every GGML_* SIMD option on, so each is pinned off.

- Snap: force the sdist (PIP_NO_BINARY) with the flag floor via
  CMAKE_ARGS in the part's build-environment.
- Flatpak: extend the module's CMAKE_ARGS with the same floor.
- Native .deb/.rpm: the build still vendors the wheels, then rebuilds
  libggml-cpu alone from the same hash-pinned sdist and swaps it into
  the vendored copies, asserting no AVX/FMA survives; the container
  image gains cmake, g++, make and binutils.
- AppImage: extract the pinned-sdist reinstall into a shared helper
  and route every CPU-only fallback of the Vulkan path through a new
  baseline rebuild at the same floor; the Vulkan compile keeps its
  own flags via the same helper. Keeping the wheel on those paths was
  the bug's exact binary.

Fixes #957

* style(tests): apply black/isort to the new packaging guards

* fix(native): keep the wheel's libggml providers, alias its base/gomp sonames

Swapping the whole libggml set for the baseline build broke the import
smoke: the wheel's auditwheel-mangled core lib doubles as the
extension's __cxa_call_terminate provider, and its NEEDED entries are
the mangled names. Swap only libggml-cpu, and satisfy the baseline
library's own unmangled NEEDEDs (libggml-base.so.0, libgomp.so.1) with
aliases of the vendored copies, which carry no generated SIMD and are
the same upstream version. Fail closed when the vendored set has
restructured instead of silently shipping the AVX2 wheel.

* fix(packaging): fail builds that lose the speech engine, unblock the ISA assert

Greptile findings on the baseline work:

- A failed baseline rebuild in the AppImage returned success after
  remove_appdir_pywhispercpp, so the bundle would ship with no speech
  engine at all while logging that the wheel was kept. Propagate the
  failure through every CPU-only fallback so the build stops instead.
- The native libggml-cpu assertion used grep -q under pipefail, where
  grep's early exit SIGPIPEs objdump and the pipeline reports a miss,
  letting an AVX2 library through. Read the full disassembly instead.

* fix(installer): build pywhispercpp from sdist on the CPU path

install_cpu_pywhispercpp let pip pick the manylinux wheel, whose
libggml-cpu is compiled for AVX2+FMA and SIGILLs on pre-Haswell CPUs
(#957). Pass --no-binary pywhispercpp like the Vulkan/CUDA paths so the
sdist compiles against this machine's own instruction set.

* fix(installer): install C++ toolchain on distros that ship without one

Forcing the pywhispercpp sdist on the CPU path needs a compiler, and the
fedora, mageia, alpine, void and solus package maps did not request one.
Add gcc-c++/make (rpm), build-base (alpine), base-devel (void) and
gcc/g++/make (solus) so source installs compile on minimal systems.

---------

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
This commit is contained in:
devin-ai-integration[bot]
2026-10-16 09:24:15 -07:00
committed by GitHub
co-authored by Jatin K Malik
parent 08e3e397a5
commit ed0ed8ba49
13 changed files with 304 additions and 50 deletions
+5 -5
View File
@@ -55,7 +55,7 @@ load_distro_package_map() {
;;
fedora)
SYSTEM_PACKAGES=(python3-pip python3-gobject gtk3 ibus-devel gobject-introspection-devel python3-devel portaudio-devel cairo-devel python3-virtualenv pkg-config cmake wget curl unzip vulkan-tools vulkan-loader-devel glslc patchelf xclip xsel wl-clipboard)
SYSTEM_PACKAGES=(python3-pip python3-gobject gtk3 ibus-devel gobject-introspection-devel python3-devel portaudio-devel cairo-devel python3-virtualenv pkg-config cmake gcc-c++ make wget curl unzip vulkan-tools vulkan-loader-devel glslc patchelf xclip xsel wl-clipboard)
APPINDICATOR_PACKAGES=(libayatana-appindicator-gtk3 libappindicator-gtk3)
XDOTOOL_PACKAGES=(xdotool)
WTYPE_PACKAGES=(wtype)
@@ -94,28 +94,28 @@ load_distro_package_map() {
;;
alpine)
SYSTEM_PACKAGES=(py3-gobject3 py3-pip gtk+3.0 py3-cairo portaudio-dev cairo-dev py3-virtualenv pkgconf cmake wget curl unzip shaderc patchelf vulkan-tools xclip xsel wl-clipboard)
SYSTEM_PACKAGES=(py3-gobject3 py3-pip gtk+3.0 py3-cairo portaudio-dev cairo-dev py3-virtualenv pkgconf cmake build-base wget curl unzip shaderc patchelf vulkan-tools xclip xsel wl-clipboard)
XDOTOOL_PACKAGES=(xdotool)
WTYPE_PACKAGES=(wtype)
YDOTOOL_PACKAGES=(ydotool)
;;
void)
SYSTEM_PACKAGES=(python3-pip python3-gobject gtk+3 libappindicator-gtk3 gobject-introspection portaudio-devel cairo-devel python3-devel pkg-config cmake wget curl unzip shaderc patchelf Vulkan-Tools xclip xsel wl-clipboard)
SYSTEM_PACKAGES=(python3-pip python3-gobject gtk+3 libappindicator-gtk3 gobject-introspection portaudio-devel cairo-devel python3-devel pkg-config cmake base-devel wget curl unzip shaderc patchelf Vulkan-Tools xclip xsel wl-clipboard)
XDOTOOL_PACKAGES=(xdotool)
WTYPE_PACKAGES=(wtype)
YDOTOOL_PACKAGES=(ydotool)
;;
solus)
SYSTEM_PACKAGES=(python3-pip python3-gobject gtk3 libappindicator gobject-introspection-devel portaudio-devel cairo-devel python3-virtualenv pkg-config cmake wget curl unzip shaderc patchelf vulkan-tools xclip xsel wl-clipboard)
SYSTEM_PACKAGES=(python3-pip python3-gobject gtk3 libappindicator gobject-introspection-devel portaudio-devel cairo-devel python3-virtualenv pkg-config cmake gcc g++ make wget curl unzip shaderc patchelf vulkan-tools xclip xsel wl-clipboard)
XDOTOOL_PACKAGES=(xdotool)
WTYPE_PACKAGES=(wtype)
YDOTOOL_PACKAGES=(ydotool)
;;
mageia)
SYSTEM_PACKAGES=(python3-pip python3-gobject gtk3 ibus-devel gobject-introspection-devel python3-devel portaudio-devel cairo-devel python3-virtualenv pkg-config cmake wget curl unzip vulkan-tools vulkan-loader-devel glslc patchelf xclip xsel wl-clipboard)
SYSTEM_PACKAGES=(python3-pip python3-gobject gtk3 ibus-devel gobject-introspection-devel python3-devel portaudio-devel cairo-devel python3-virtualenv pkg-config cmake gcc-c++ make wget curl unzip vulkan-tools vulkan-loader-devel glslc patchelf xclip xsel wl-clipboard)
XDOTOOL_PACKAGES=(xdotool)
WTYPE_PACKAGES=(wtype)
YDOTOOL_PACKAGES=(ydotool)
+4 -1
View File
@@ -1774,8 +1774,11 @@ install_cpu_pywhispercpp() {
local PYWHISPERCPP_CMAKE_ARGS
PYWHISPERCPP_CMAKE_ARGS=$(get_pywhispercpp_cmake_args)
# The PyPI wheel's libggml-cpu is compiled for AVX2+FMA and SIGILLs on
# pre-Haswell CPUs (#957), so the CPU path builds the pinned sdist like
# the GPU paths do; -march=native then matches this machine exactly.
CMAKE_ARGS="${CMAKE_ARGS:+$CMAKE_ARGS }$PYWHISPERCPP_CMAKE_ARGS" \
pip_reinstall_pywhispercpp "$PIP_LOG_FILE"
pip_reinstall_pywhispercpp "$PIP_LOG_FILE" --no-binary pywhispercpp
}
pip_reinstall_pywhispercpp() {
+63 -23
View File
@@ -427,6 +427,58 @@ copy_whisper_native_libs_to_usr_lib() {
done < <(find "$APPDIR/usr" \( -name 'libggml*.so*' -o -name 'libwhisper.so*' \) ! -path '*/usr/lib/*' 2>/dev/null || true)
}
# ggml's instruction-set floor for every CPU-side pywhispercpp build this
# script ships; CMAKE_ARGS inlines it below so the native-cache id keeps
# hashing it. GGML_NATIVE is ggml's default and compiles libggml-cpu with
# -march=native, tuned to whichever runner the build lands on: a runner with
# AVX-512 ships a libggml-cpu.so that dies with SIGILL on any CPU without it
# (#938), and the native cache then serves the same binary to every later
# build. GGML_NATIVE=OFF alone is not the portable baseline either — while
# the native default is on, ggml turns every GGML_* SIMD option on and still
# compiles -mavx2 -mfma, dying the same way on pre-Haswell CPUs (#957). Each
# option is pinned off for the plain x86-64 baseline; the Vulkan path is
# unaffected because its shaders run on the GPU.
PORTABLE_GGML_CMAKE_ARGS="-DGGML_NATIVE=OFF -DGGML_SSE42=OFF -DGGML_AVX=OFF -DGGML_AVX2=OFF -DGGML_BMI2=OFF -DGGML_FMA=OFF -DGGML_F16C=OFF"
# Reinstall the same pinned sdist the bundle already carries, hashes and all:
# a source build is still a download, and this one ends up in ctypes.cdll.
# GGML_VULKAN=1 in the caller's env selects the Vulkan build; without it the
# compile is CPU-only and any -DVulkan_INCLUDE_DIR is dead.
_install_pinned_pywhispercpp() {
local pip_log="$1"
awk '/^pywhispercpp==/ {found=1; print; next}
found && /^[[:space:]]/ {print; next}
found {exit}' "$REPO_ROOT/requirements/vad.txt" > "$WORKDIR/pywhispercpp.txt"
# Hosts that default cc/c++ to clang (this Cloud Agent image) fail the
# CMAKE C++ compiler test: clang does not search gcc's libstdc++ path.
CC="${CC:-gcc}" CXX="${CXX:-g++}" \
GGML_VULKAN="${GGML_VULKAN:-0}" \
CMAKE_ARGS="-DCMAKE_INSTALL_RPATH=\$ORIGIN -DCMAKE_BUILD_WITH_INSTALL_RPATH=ON $PORTABLE_GGML_CMAKE_ARGS${VULKAN_INCLUDE_DIR:+ -DVulkan_INCLUDE_DIR=$VULKAN_INCLUDE_DIR}" \
uv_install --verbose --require-hashes --no-deps \
--reinstall-package pywhispercpp --no-binary pywhispercpp \
-r "$WORKDIR/pywhispercpp.txt" >"$pip_log" 2>&1
}
# CPU-only fallback for the Vulkan path. The PyPI wheel the bundle started
# with is the same AVX2+FMA build the SIGILL reports come from, so keeping it
# is no fallback at all on pre-Haswell CPUs — rebuild the sdist at the
# baseline instead.
_compile_pywhispercpp_baseline() {
local pip_log="$WORKDIR/pywhispercpp-baseline.log"
export CMAKE_BUILD_PARALLEL_LEVEL="${CMAKE_BUILD_PARALLEL_LEVEL:-$(nproc)}"
ensure_build_tools
echo "== Rebuilding pywhispercpp at the portable CPU baseline =="
remove_appdir_pywhispercpp
if ! _install_pinned_pywhispercpp "$pip_log"; then
# The wheel is already gone, so "continue" would ship an AppImage with
# no speech engine at all; fail the build instead.
echo "Baseline pywhispercpp rebuild failed; refusing to ship without the speech engine." >&2
tail -n 40 "$pip_log" 2>/dev/null | sed 's/^/ /' >&2 || true
return 1
fi
copy_whisper_native_libs_to_usr_lib
}
# The miss path of rebuild_pywhispercpp_vulkan. require_vulkan, cache_dir, and
# site are locals of that caller; bash shows them to this function.
_compile_pywhispercpp_vulkan() {
@@ -437,7 +489,8 @@ _compile_pywhispercpp_vulkan() {
echo "VOCALINUX_APPIMAGE_REQUIRE_VULKAN=1: refusing to ship a CPU-only AppImage." >&2
exit 1
fi
echo "Warning: AppImage will use the CPU-only pywhispercpp wheel." >&2
echo "Warning: AppImage will use a CPU-only baseline pywhispercpp build." >&2
_compile_pywhispercpp_baseline || return 1
return 0
fi
@@ -448,7 +501,8 @@ _compile_pywhispercpp_vulkan() {
if [ "$require_vulkan" = "1" ]; then
exit 1
fi
echo "Warning: continuing with the CPU-only pywhispercpp wheel." >&2
echo "Warning: continuing with a CPU-only baseline pywhispercpp build." >&2
_compile_pywhispercpp_baseline || return 1
return 0
fi
@@ -456,25 +510,7 @@ _compile_pywhispercpp_vulkan() {
remove_appdir_pywhispercpp
local pip_log="$WORKDIR/pywhispercpp-vulkan.log"
export CMAKE_BUILD_PARALLEL_LEVEL="${CMAKE_BUILD_PARALLEL_LEVEL:-$(nproc)}"
# Reinstall the same pinned sdist the bundle already carries, hashes and all:
# a source build is still a download, and this one ends up in ctypes.cdll.
awk '/^pywhispercpp==/ {found=1; print; next}
found && /^[[:space:]]/ {print; next}
found {exit}' "$REPO_ROOT/requirements/vad.txt" > "$WORKDIR/pywhispercpp.txt"
# Hosts that default cc/c++ to clang (this Cloud Agent image) fail the
# CMAKE C++ compiler test: clang does not search gcc's libstdc++ path.
# GGML_NATIVE is ggml's default and compiles libggml-cpu with -march=native,
# tuned to whichever runner the build lands on. A runner with AVX-512 ships a
# libggml-cpu.so that dies with SIGILL on any CPU without it (#938), and the
# native cache then serves the same binary to every later build. OFF builds
# the portable baseline the PyPI wheel already ships; the Vulkan path is
# unaffected because its shaders run on the GPU.
if ! CC="${CC:-gcc}" CXX="${CXX:-g++}" \
GGML_VULKAN=1 \
CMAKE_ARGS="-DCMAKE_INSTALL_RPATH=\$ORIGIN -DCMAKE_BUILD_WITH_INSTALL_RPATH=ON -DGGML_NATIVE=OFF -DVulkan_INCLUDE_DIR=$VULKAN_INCLUDE_DIR" \
uv_install --verbose --require-hashes --no-deps \
--reinstall-package pywhispercpp --no-binary pywhispercpp \
-r "$WORKDIR/pywhispercpp.txt" >"$pip_log" 2>&1; then
if ! GGML_VULKAN=1 _install_pinned_pywhispercpp "$pip_log"; then
# The tail is the Python traceback wrapping the build and says nothing about
# the compile, so lead with the compiler's own errors.
echo "pywhispercpp Vulkan rebuild failed." >&2
@@ -485,7 +521,8 @@ _compile_pywhispercpp_vulkan() {
if [ "$require_vulkan" = "1" ]; then
exit 1
fi
echo "Warning: continuing with the CPU-only pywhispercpp wheel." >&2
echo "Warning: continuing with a CPU-only baseline pywhispercpp build." >&2
_compile_pywhispercpp_baseline || return 1
return 0
fi
@@ -496,7 +533,10 @@ _compile_pywhispercpp_vulkan() {
tail -n 80 "$pip_log" 2>/dev/null | sed 's/^/ /' >&2 || true
exit 1
fi
echo "Warning: AppImage will use CPU-only pywhispercpp." >&2
# The GGML_VULKAN=1 compile is already the portable baseline even when no
# Vulkan library comes out of it, so this build is safe to keep.
echo "Warning: AppImage will use CPU-only baseline pywhispercpp." >&2
copy_whisper_native_libs_to_usr_lib
return 0
fi
echo " found $vk_lib"
+6 -3
View File
@@ -29,9 +29,12 @@ modules:
# GGML_NATIVE is ggml's default and compiles libggml-cpu with
# -march=native, so the package inherits whichever instruction set the
# Flatpak builder has. An AVX-512 builder ships a binary that SIGILLs on
# any CPU without it (#938). OFF ships the portable baseline the PyPI
# wheel carries.
- PYWHISPERCPP_VERSION=1.5.0 NO_REPAIR=1 GGML_VULKAN=1 CMAKE_ARGS="-DGGML_NATIVE=OFF" pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
# any CPU without it (#938). GGML_NATIVE=OFF alone is not the portable
# baseline either: while the native default is on, ggml turns every
# GGML_* SIMD option on and still compiles -mavx2 -mfma, dying the same
# way on pre-Haswell CPUs (#957). Each option is pinned off for the
# plain x86-64 baseline.
- PYWHISPERCPP_VERSION=1.5.0 NO_REPAIR=1 GGML_VULKAN=1 CMAKE_ARGS="-DGGML_NATIVE=OFF -DGGML_SSE42=OFF -DGGML_AVX=OFF -DGGML_AVX2=OFF -DGGML_BMI2=OFF -DGGML_FMA=OFF -DGGML_F16C=OFF" pip3 install --verbose --exists-action=i --no-index --find-links="file://${PWD}"
--prefix=${FLATPAK_DEST} "pywhispercpp" --no-build-isolation
# whisper.cpp installs its shared libraries next to the extension module.
# Symlink them onto the default loader path so the extension resolves them
+57
View File
@@ -102,6 +102,63 @@ for VENDOR_WHEEL in "$TOOLS"/wheels/*/*.whl; do
python3 -m zipfile -e "$VENDOR_WHEEL" "$PAYLOAD/usr/lib/vocalinux/vendor/"
done
# --- portable libggml-cpu -------------------------------------------------
# The pinned pywhispercpp wheels bundle a libggml-cpu compiled for AVX2+FMA,
# which dies with SIGILL at model load on pre-Haswell CPUs (#957). Rebuild
# the ggml libraries from the same pinned sdist at the plain x86-64 baseline —
# GGML_NATIVE=OFF alone still compiles -mavx2 -mfma, because ggml turns every
# GGML_* SIMD option on while its native default is on — and swap them into
# the vendored copies. libwhisper and the Python extension carry no generated
# SIMD, so only the libggml set needs replacing.
SDIST_DIR="$TOOLS/pywhispercpp-sdist"
mkdir -p "$SDIST_DIR"
pip3 download --require-hashes --no-deps --no-binary pywhispercpp \
-r "$PINS_VENDOR" -d "$SDIST_DIR" >/dev/null
tar -xzf "$SDIST_DIR"/pywhispercpp-*.tar.gz -C "$SDIST_DIR"
WHISPER_GGML="$(echo "$SDIST_DIR"/pywhispercpp-*/whisper.cpp/ggml)"
[ -d "$WHISPER_GGML" ] || fail "whisper.cpp/ggml missing from pywhispercpp sdist"
# ggml.pc.in is generated upstream and absent from the sdist; it only feeds
# the standalone install rules this build never runs.
touch "$WHISPER_GGML/ggml.pc.in"
cmake -S "$WHISPER_GGML" -B "$TOOLS/ggml-build" \
-DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=ON \
-DGGML_NATIVE=OFF -DGGML_SSE42=OFF -DGGML_AVX=OFF -DGGML_AVX2=OFF \
-DGGML_BMI2=OFF -DGGML_FMA=OFF -DGGML_F16C=OFF \
-DGGML_BUILD_TESTS=OFF -DGGML_BUILD_EXAMPLES=OFF
cmake --build "$TOOLS/ggml-build" --target ggml-cpu -j"$(nproc)"
BASELINE_CPU="$(find "$TOOLS/ggml-build" -name 'libggml-cpu.so.*' -type f | sort -V | tail -1)"
[ -f "$BASELINE_CPU" ] || fail "baseline libggml-cpu build produced no library"
# objdump, not a feature check: the SIGILL is ISA-level, so assert no 256-bit
# or FMA instructions survive in the library that ships. No grep -q: under
# pipefail an early exit SIGPIPEs objdump and the pipeline reports a miss.
if objdump -d "$BASELINE_CPU" | grep -E 'ymm|vfmadd|vfnmadd|vfmsub|vzeroupper' >/dev/null; then
fail "baseline libggml-cpu still carries AVX/FMA instructions"
fi
# The vendored wheel files carry auditwheel-mangled basenames
# (libggml-cpu-<hash>.so.0.9.8) that the extension's NEEDED entries point at,
# so overwrite the cpu library in place and keep its filename. Only cpu is
# swapped: the wheel's other libggml files carry no generated SIMD, and the
# mangled core lib doubles as the extension's __cxa_call_terminate provider —
# replacing it with a locally built copy drops that export.
swapped=0
while IFS= read -r vendored; do
cp "$BASELINE_CPU" "$vendored"
swapped=$((swapped + 1))
done < <(find "$PAYLOAD/usr/lib/vocalinux/vendor" -name 'libggml-cpu*.so*')
[ "$swapped" -gt 0 ] || fail "found no vendored libggml-cpu to replace"
# The baseline library's own NEEDED entries use the unmangled sonames
# (libggml-base.so.0, libgomp.so.1), so alias the vendored copies — the same
# upstream version, and base/gomp carry no generated SIMD anyway — under
# those names inside pywhispercpp.libs.
while IFS= read -r libs_dir; do
vendored_base="$(find "$libs_dir" -name 'libggml-base*.so*' | head -1)"
[ -n "$vendored_base" ] || fail "no vendored libggml-base in $libs_dir"
cp "$vendored_base" "$libs_dir/libggml-base.so.0"
vendored_gomp="$(find "$libs_dir" -name 'libgomp*.so*' | head -1)"
[ -n "$vendored_gomp" ] || fail "no vendored libgomp in $libs_dir"
cp "$vendored_gomp" "$libs_dir/libgomp.so.1"
done < <(find "$PAYLOAD/usr/lib/vocalinux/vendor" -type d -name 'pywhispercpp.libs')
# --- launchers -----------------------------------------------------------
# Same environment install.sh's wrapper establishes: the vendored
# pywhispercpp.libs on LD_LIBRARY_PATH, user site-packages off so they cannot
+3 -1
View File
@@ -5,7 +5,9 @@ set -euo pipefail
export DEBIAN_FRONTEND=noninteractive
apt-get update
# cmake, g++, make and binutils rebuild the vendored libggml-cpu from the
# pinned pywhispercpp sdist at the portable baseline in build.sh.
apt-get install -y --no-install-recommends \
ca-certificates curl python3 python3-pip xz-utils
binutils ca-certificates cmake curl g++ make python3 python3-pip xz-utils
exec bash "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/build.sh" "$@"
+12 -6
View File
@@ -121,7 +121,9 @@ distributions:
fedora:
# cairo-devel is needed because the pinned pycairo builds from source.
system: [python3-pip, python3-gobject, gtk3, ibus-devel, gobject-introspection-devel, python3-devel, portaudio-devel, cairo-devel, python3-virtualenv, pkg-config, cmake, wget, curl, unzip, vulkan-tools, vulkan-loader-devel, glslc, patchelf, xclip, xsel, wl-clipboard]
# gcc-c++/make compile the pywhispercpp sdist (the wheel SIGILLs on old CPUs)
# and the evdev/PyAudio sdists; python3-devel only guarantees gcc.
system: [python3-pip, python3-gobject, gtk3, ibus-devel, gobject-introspection-devel, python3-devel, portaudio-devel, cairo-devel, python3-virtualenv, pkg-config, cmake, gcc-c++, make, wget, curl, unzip, vulkan-tools, vulkan-loader-devel, glslc, patchelf, xclip, xsel, wl-clipboard]
appindicator: [libayatana-appindicator-gtk3, libappindicator-gtk3]
text_input:
xdotool: [xdotool]
@@ -165,7 +167,8 @@ distributions:
alpine:
# cairo-dev is needed because the pinned pycairo builds from source.
system: [py3-gobject3, py3-pip, gtk+3.0, py3-cairo, portaudio-dev, cairo-dev, py3-virtualenv, pkgconf, cmake, wget, curl, unzip, shaderc, patchelf, vulkan-tools, xclip, xsel, wl-clipboard]
# build-base compiles the pywhispercpp sdist and the evdev/PyAudio sdists.
system: [py3-gobject3, py3-pip, gtk+3.0, py3-cairo, portaudio-dev, cairo-dev, py3-virtualenv, pkgconf, cmake, build-base, wget, curl, unzip, shaderc, patchelf, vulkan-tools, xclip, xsel, wl-clipboard]
text_input:
xdotool: [xdotool]
wtype: [wtype]
@@ -173,7 +176,8 @@ distributions:
void:
# cairo-devel is needed because the pinned pycairo builds from source.
system: [python3-pip, python3-gobject, gtk+3, libappindicator-gtk3, gobject-introspection, portaudio-devel, cairo-devel, python3-devel, pkg-config, cmake, wget, curl, unzip, shaderc, patchelf, Vulkan-Tools, xclip, xsel, wl-clipboard]
# base-devel compiles the pywhispercpp sdist and the evdev/PyAudio sdists.
system: [python3-pip, python3-gobject, gtk+3, libappindicator-gtk3, gobject-introspection, portaudio-devel, cairo-devel, python3-devel, pkg-config, cmake, base-devel, wget, curl, unzip, shaderc, patchelf, Vulkan-Tools, xclip, xsel, wl-clipboard]
text_input:
xdotool: [xdotool]
wtype: [wtype]
@@ -181,15 +185,17 @@ distributions:
solus:
# cairo-devel is needed because the pinned pycairo builds from source.
system: [python3-pip, python3-gobject, gtk3, libappindicator, gobject-introspection-devel, portaudio-devel, cairo-devel, python3-virtualenv, pkg-config, cmake, wget, curl, unzip, shaderc, patchelf, vulkan-tools, xclip, xsel, wl-clipboard]
# gcc/g++/make compile the pywhispercpp sdist and the evdev/PyAudio sdists.
system: [python3-pip, python3-gobject, gtk3, libappindicator, gobject-introspection-devel, portaudio-devel, cairo-devel, python3-virtualenv, pkg-config, cmake, gcc, g++, make, wget, curl, unzip, shaderc, patchelf, vulkan-tools, xclip, xsel, wl-clipboard]
text_input:
xdotool: [xdotool]
wtype: [wtype]
ydotool: [ydotool]
mageia:
# The existing installer deliberately follows Fedora's package inventory.
system: [python3-pip, python3-gobject, gtk3, ibus-devel, gobject-introspection-devel, python3-devel, portaudio-devel, cairo-devel, python3-virtualenv, pkg-config, cmake, wget, curl, unzip, vulkan-tools, vulkan-loader-devel, glslc, patchelf, xclip, xsel, wl-clipboard]
# The existing installer deliberately follows Fedora's package inventory,
# including gcc-c++/make for the pywhispercpp sdist compile.
system: [python3-pip, python3-gobject, gtk3, ibus-devel, gobject-introspection-devel, python3-devel, portaudio-devel, cairo-devel, python3-virtualenv, pkg-config, cmake, gcc-c++, make, wget, curl, unzip, vulkan-tools, vulkan-loader-devel, glslc, patchelf, xclip, xsel, wl-clipboard]
text_input:
xdotool: [xdotool]
wtype: [wtype]
+8
View File
@@ -105,6 +105,14 @@ parts:
source-type: local
build-environment:
- PIP_DISABLE_PIP_VERSION_CHECK: "1"
# pywhispercpp's PyPI wheel ships a libggml-cpu compiled for AVX2+FMA,
# which dies with SIGILL at model load on pre-Haswell CPUs (#957), so
# the pinned sdist is built instead of installing that wheel.
# GGML_NATIVE=OFF alone is not the portable baseline either: while
# ggml's native default is on it turns every GGML_* SIMD option on, so
# each one is pinned off for the plain x86-64 baseline.
- PIP_NO_BINARY: pywhispercpp
- CMAKE_ARGS: "-DGGML_NATIVE=OFF -DGGML_SSE42=OFF -DGGML_AVX=OFF -DGGML_AVX2=OFF -DGGML_BMI2=OFF -DGGML_FMA=OFF -DGGML_F16C=OFF"
build-packages:
- build-essential
- cmake
+38 -6
View File
@@ -23,6 +23,19 @@ WORKFLOWS = REPO_ROOT / ".github" / "workflows"
#: Workflows that build an AppImage, and so decide which distros it runs on.
APPIMAGE_WORKFLOWS = ("unified-pipeline.yml", "release.yml", "nightly.yml")
#: ggml SIMD options pinned off for the portable x86-64 baseline in every
#: packaged pywhispercpp build (#957). GGML_NATIVE=OFF alone leaves each of
#: these on while ggml's native default is on.
PORTABLE_GGML_OPTIONS = (
"GGML_NATIVE",
"GGML_SSE42",
"GGML_AVX",
"GGML_AVX2",
"GGML_BMI2",
"GGML_FMA",
"GGML_F16C",
)
def _pins() -> dict:
"""name -> (kind, value, source) from tool_checksums.txt."""
@@ -255,14 +268,33 @@ def test_vulkan_rebuild_does_not_borrow_the_builders_instruction_set() -> None:
"""GGML_NATIVE is ggml's default and puts -march=native on libggml-cpu, so
the AppImage inherits whichever instruction set the CI runner has. A runner
with AVX-512 ships a binary that SIGILLs on any CPU without it (#938), and
the shared native cache then serves the same one to every later build. OFF
matches the portable baseline the PyPI wheel falls back to."""
the shared native cache then serves the same one to every later build.
OFF alone is not the portable baseline either: while the native default is
on, ggml turns every GGML_* SIMD option on, still compiling -mavx2 -mfma
and dying on pre-Haswell CPUs (#957), so each option must be pinned off."""
text = BUILD_SH.read_text(encoding="utf-8")
command_lines = [line for line in text.splitlines() if not line.lstrip().startswith("#")]
cmake_args = re.findall(r'CMAKE_ARGS="([^"]*)"', "\n".join(command_lines))
assert any("-DGGML_NATIVE=OFF" in args for args in cmake_args), (
"the Vulkan rebuild must not compile with -march=native; the AppImage"
" runs on CPUs the runner's instruction set does not describe"
cmake_args = " ".join(re.findall(r'CMAKE_ARGS="([^"]*)"', "\n".join(command_lines)))
for flag in PORTABLE_GGML_OPTIONS:
assert f"-D{flag}=OFF" in cmake_args, (
f"the Vulkan rebuild does not pin {flag} off; the AppImage runs on"
" CPUs the runner's instruction set does not describe"
)
def test_cpu_only_fallback_rebuilds_at_the_portable_baseline() -> None:
"""The PyPI pywhispercpp wheel is itself the AVX2+FMA build that SIGILLs on
pre-Haswell CPUs (#957), so "keep the wheel" is no fallback: every CPU-only
exit of the Vulkan path must rebuild the sdist at the baseline instead."""
text = BUILD_SH.read_text(encoding="utf-8")
assert "_compile_pywhispercpp_baseline" in text, (
"no CPU-only baseline rebuild exists; the Vulkan fallbacks still keep" " the AVX2+FMA wheel"
)
command_lines = [line for line in text.splitlines() if not line.lstrip().startswith("#")]
calls = sum("_compile_pywhispercpp_baseline" in line for line in command_lines)
assert calls >= 3, (
"expected the baseline rebuild on every CPU-only fallback path, found"
f" only {calls - 1} call site(s)"
)
+24 -5
View File
@@ -28,6 +28,19 @@ from packaging.specifiers import SpecifierSet
from packaging.utils import canonicalize_name
from packaging.version import Version
#: ggml SIMD options pinned off for the portable x86-64 baseline in every
#: packaged pywhispercpp build (#957). GGML_NATIVE=OFF alone leaves each of
#: these on while ggml's native default is on.
PORTABLE_GGML_OPTIONS = (
"GGML_NATIVE",
"GGML_SSE42",
"GGML_AVX",
"GGML_AVX2",
"GGML_BMI2",
"GGML_FMA",
"GGML_F16C",
)
REPO_ROOT = Path(__file__).resolve().parents[1]
PYPROJECT = REPO_ROOT / "pyproject.toml"
EXPORT = REPO_ROOT / "requirements" / "runtime.txt"
@@ -221,8 +234,10 @@ def test_the_version_injection_names_the_version_being_built() -> None:
def test_the_vulkan_build_does_not_borrow_the_builders_instruction_set() -> None:
"""GGML_NATIVE is ggml's default and puts -march=native on libggml-cpu, so
the Flatpak inherits whichever instruction set the builder has, and the
result SIGILLs on any CPU without it (#938). OFF ships the portable
baseline the PyPI wheel carries."""
result SIGILLs on any CPU without it (#938). OFF alone is not the portable
baseline either: while the native default is on, ggml turns every GGML_*
SIMD option on, still compiling -mavx2 -mfma and dying on pre-Haswell CPUs
(#957), so each option must be pinned off for the plain x86-64 baseline."""
module = re.search(
r"^ - name: python3-pywhispercpp$(.*?)(?=^ - name: |\Z)",
DEPS_YAML.read_text(encoding="utf-8"),
@@ -232,9 +247,13 @@ def test_the_vulkan_build_does_not_borrow_the_builders_instruction_set() -> None
command_lines = [
line for line in module.group(1).splitlines() if not line.lstrip().startswith("#")
]
assert any(
"-DGGML_NATIVE=OFF" in line and "pip3 install" in line for line in command_lines
), "the Flatpak Vulkan build must not compile with -march=native"
install_line = next((line for line in command_lines if "pip3 install" in line), "")
assert install_line, "the pywhispercpp pip3 install line is gone"
for flag in PORTABLE_GGML_OPTIONS:
assert f"-D{flag}=OFF" in install_line, (
f"the Flatpak build does not pin {flag} off; the package runs on"
" CPUs the builder's instruction set does not describe"
)
def test_the_manifest_env_agrees_with_the_module() -> None:
@@ -130,6 +130,9 @@ def test_missing_pins_fail_before_pip(tmp_path: Path, kind: str) -> None:
@pytest.mark.parametrize("backend", ["cpu", "vulkan", "cuda"])
def test_rebuild_uses_only_locked_pywhispercpp(tmp_path: Path, backend: str) -> None:
"""Every backend builds the pinned sdist. The PyPI wheel's libggml-cpu is
compiled for AVX2+FMA and SIGILLs on pre-Haswell CPUs (#957), so the CPU
path must compile like the GPU paths do, never install the wheel."""
command = (
"install_cpu_pywhispercpp log"
if backend == "cpu"
@@ -141,6 +144,10 @@ def test_rebuild_uses_only_locked_pywhispercpp(tmp_path: Path, backend: str) ->
assert {"--require-hashes", "--no-deps", "--no-build-isolation", "--force-reinstall"} <= set(
call
)
no_binary = call[call.index("--no-binary") + 1]
assert (
no_binary == "pywhispercpp"
), "installing the wheel ships the AVX2 build that SIGILLs on old CPUs"
selected = (tmp_path / "pywhispercpp.txt").read_text()
assert re.findall(r"^([\w.-]+)==", selected, re.M) == ["pywhispercpp"]
lock = tomllib.loads((ROOT / "uv.lock").read_text())
+43
View File
@@ -338,3 +338,46 @@ def test_pipeline_gates_the_packages() -> None:
for distro in re.findall(r"- (\S+)", matrix.group(1)):
family = distro.split(":")[0].split("/")[-1]
assert family in smoke, f"smoke-test.sh has no recipe for {distro}"
#: ggml SIMD options pinned off for the portable x86-64 baseline in every
#: packaged pywhispercpp build (#957). GGML_NATIVE=OFF alone leaves each of
#: these on while ggml's native default is on.
PORTABLE_GGML_OPTIONS = (
"GGML_NATIVE",
"GGML_SSE42",
"GGML_AVX",
"GGML_AVX2",
"GGML_BMI2",
"GGML_FMA",
"GGML_F16C",
)
def test_vendored_libggml_cpu_is_rebuilt_at_the_portable_baseline() -> None:
"""The pinned pywhispercpp wheels build.sh vendors carry a libggml-cpu
compiled for AVX2+FMA, which dies with SIGILL at model load on pre-Haswell
CPUs (#957). The package must rebuild just that library from the same
pinned sdist at the plain x86-64 baseline and swap it into the vendored
copies; without a toolchain in the container image that build cannot run."""
build = BUILD_SH.read_text(encoding="utf-8")
assert (
"--no-binary pywhispercpp" in build
), "the baseline libggml-cpu must come from the hash-pinned sdist, not a wheel"
command_lines = [line for line in build.splitlines() if not line.lstrip().startswith("#")]
cmake_args = "\n".join(
line for line in command_lines if "-DGGML_" in line or "cmake -S" in line
)
for flag in PORTABLE_GGML_OPTIONS:
assert (
f"-D{flag}=OFF" in cmake_args
), f"the vendored libggml-cpu rebuild does not pin {flag} off"
assert "libggml-cpu" in build and "find" in build, (
"the rebuild must swap into the vendored copies in place so the"
" wheels' NEEDED entries still resolve"
)
container = CONTAINER_BUILD_SH.read_text(encoding="utf-8")
for tool in ("cmake", "g++", "binutils"):
assert (
tool in container
), f"container-build.sh installs no {tool}; the baseline rebuild fails"
+34
View File
@@ -196,3 +196,37 @@ def test_snap_promote_dispatch_input_never_reaches_the_shell() -> None:
assert "${{" not in line, f"a run: step interpolates a template expression: {line.strip()}"
assert "TAG: ${{ inputs.tag }}" in text
assert "REVISION: ${{ steps.candidate.outputs.revision }}" in text
#: ggml SIMD options pinned off for the portable x86-64 baseline in every
#: packaged pywhispercpp build (#957). GGML_NATIVE=OFF alone leaves each of
#: these on while ggml's native default is on.
PORTABLE_GGML_OPTIONS = (
"GGML_NATIVE",
"GGML_SSE42",
"GGML_AVX",
"GGML_AVX2",
"GGML_BMI2",
"GGML_FMA",
"GGML_F16C",
)
def test_snap_builds_pywhispercpp_at_the_portable_baseline() -> None:
"""pywhispercpp's PyPI wheel ships a libggml-cpu compiled for AVX2+FMA,
which dies with SIGILL at model load on pre-Haswell CPUs (#957). The
python plugin installs that wheel by default, so the recipe must force an
sdist build with every ggml SIMD option pinned off — GGML_NATIVE=OFF
alone leaves them all on."""
doc = yaml.safe_load(SNAPCRAFT_YAML.read_text(encoding="utf-8"))
env: dict = {}
for entry in doc["parts"]["vocalinux"].get("build-environment") or []:
env.update(entry)
assert (
env.get("PIP_NO_BINARY") == "pywhispercpp"
), "without --no-binary the snap installs the AVX2+FMA wheel (#957)"
cmake_args = env.get("CMAKE_ARGS") or ""
for flag in PORTABLE_GGML_OPTIONS:
assert (
f"-D{flag}=OFF" in cmake_args
), f"the snap's pywhispercpp build does not pin {flag} off"