diff --git a/install.d/package_map.sh b/install.d/package_map.sh index a3a76e4..fb8a7f6 100644 --- a/install.d/package_map.sh +++ b/install.d/package_map.sh @@ -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) diff --git a/install.sh b/install.sh index ed9468b..c80231e 100755 --- a/install.sh +++ b/install.sh @@ -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() { diff --git a/packaging/appimage/build.sh b/packaging/appimage/build.sh index 62dec9e..ce7ed09 100755 --- a/packaging/appimage/build.sh +++ b/packaging/appimage/build.sh @@ -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" diff --git a/packaging/flatpak/python3-dependencies.yaml b/packaging/flatpak/python3-dependencies.yaml index c6b484a..d78de19 100644 --- a/packaging/flatpak/python3-dependencies.yaml +++ b/packaging/flatpak/python3-dependencies.yaml @@ -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 diff --git a/packaging/native/build.sh b/packaging/native/build.sh index 4da4ecd..6efbc81 100755 --- a/packaging/native/build.sh +++ b/packaging/native/build.sh @@ -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-.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 diff --git a/packaging/native/container-build.sh b/packaging/native/container-build.sh index 064d59b..95c5717 100755 --- a/packaging/native/container-build.sh +++ b/packaging/native/container-build.sh @@ -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" "$@" diff --git a/scripts/distro-package-map.yaml b/scripts/distro-package-map.yaml index d911dd7..5d3f0b1 100644 --- a/scripts/distro-package-map.yaml +++ b/scripts/distro-package-map.yaml @@ -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] diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 911612d..3adeeab 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -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 diff --git a/tests/test_appimage_packaging.py b/tests/test_appimage_packaging.py index 3b5fc69..2d653b2 100644 --- a/tests/test_appimage_packaging.py +++ b/tests/test_appimage_packaging.py @@ -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)" ) diff --git a/tests/test_flatpak_packaging.py b/tests/test_flatpak_packaging.py index c97a682..48d3763 100644 --- a/tests/test_flatpak_packaging.py +++ b/tests/test_flatpak_packaging.py @@ -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: diff --git a/tests/test_installer_pinned_dependencies.py b/tests/test_installer_pinned_dependencies.py index c90bfeb..6d95c9e 100644 --- a/tests/test_installer_pinned_dependencies.py +++ b/tests/test_installer_pinned_dependencies.py @@ -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()) diff --git a/tests/test_native_packaging.py b/tests/test_native_packaging.py index c73d458..3175655 100644 --- a/tests/test_native_packaging.py +++ b/tests/test_native_packaging.py @@ -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" diff --git a/tests/test_snap_packaging.py b/tests/test_snap_packaging.py index 09a60e4..2cd1d7e 100644 --- a/tests/test_snap_packaging.py +++ b/tests/test_snap_packaging.py @@ -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"