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
80 changes: 80 additions & 0 deletions .github/scripts/stage-linux-whisper.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env bash
#
# Stage a Linux whisper.cpp build into ./staging for packaging.
#
# Shared by the linux-vulkan and linux-cuda jobs of
# build-whispercpp-release.yml. Run from the whisper.cpp build root (the
# directory holding ./build).
#
# Why this is not just `cp build/bin/libggml*.so staging/` — that was the
# original one-liner, and it shipped three separate defects for four
# releases (SubtitleEdit issue #13680):
#
# 1. The glob never matched libwhisper.so*, so the library whisper-cli is
# linked against was simply absent from the archive.
# 2. cmake leaves a symlink chain in build/bin
# (libggml.so -> libggml.so.0 -> libggml.so.0.9.4). A bare `cp`
# dereferences those, so the file landed under the *unversioned* name
# while its SONAME — and whisper-cli's DT_NEEDED entry — said
# libggml.so.0. The loader looks up the SONAME, so it never matched.
# 3. The RUNPATH cmake bakes in points at the CI build directory
# (/home/runner/work/...), which does not exist on a user's machine,
# and the loader does not search the working directory.
#
# Shipping the symlink chain as-is would not fix (2): Subtitle Edit unpacks
# these archives with .NET's ZipArchive, whose zip path writes a symlink
# entry out as an ordinary file containing the target path. So every library
# is staged as a real file named after its SONAME.

set -euo pipefail

# build/bin is where whisper.cpp puts both binaries and libraries; older /
# differently-configured trees split them, so accept the alternatives too.
lib_dirs=(build/bin build/lib build/src build/ggml/src)

mkdir -p staging
cp build/bin/whisper-cli staging/

soname_of() {
# Empty output when the ELF has no DT_SONAME (true of the dlopen'd ggml
# backend plugins: libggml-vulkan.so, libggml-cuda.so, libggml-cpu-*.so).
readelf -d "$1" 2>/dev/null | sed -n 's/.*SONAME.*\[\(.*\)\].*/\1/p'
}

shopt -s nullglob
staged=0
for dir in "${lib_dirs[@]}"; do
for src in "$dir"/libwhisper.so* "$dir"/libggml*.so*; do
# Collapse the symlink chain to the one real ELF behind it, so each
# library is staged exactly once no matter how many aliases point at it.
real=$(readlink -f "$src")
[ -f "$real" ] || continue

soname=$(soname_of "$real")
name=${soname:-$(basename "$real")}

if [ ! -e "staging/$name" ]; then
cp -L "$real" "staging/$name"
staged=$((staged + 1))
fi
done
done

if [ "$staged" -eq 0 ]; then
echo "::error::No whisper/ggml shared libraries found under: ${lib_dirs[*]}"
exit 1
fi

# Point every ELF at its own directory. patchelf writes DT_RUNPATH, which is
# NOT inherited by transitive dependencies — libggml.so.0 does not benefit
# from whisper-cli's RUNPATH when it looks for libggml-base.so.0 — so the
# libraries need this just as much as the executable does.
for f in staging/whisper-cli staging/*.so*; do
patchelf --set-rpath '$ORIGIN' "$f"
done

echo "Staged $staged shared libraries:"
ls -la staging
echo
echo "whisper-cli dynamic section:"
readelf -d staging/whisper-cli | grep -E 'NEEDED|RUNPATH|RPATH'
69 changes: 69 additions & 0 deletions .github/scripts/verify-linux-whisper.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
#!/usr/bin/env bash
#
# Regression guard for the Linux whisper.cpp archives (SubtitleEdit #13680).
#
# whispercpp-184, -186 and -191 all shipped a whisper-cli that dies on
# startup with "error while loading shared libraries: libwhisper.so.1"
# because the packaging step never copied the library. Nothing in the build
# noticed, because the build machine had the libraries sitting in build/bin
# the whole time.
#
# So verify the way a user's machine sees it: copy the staged payload to a
# fresh location, run the loader from an unrelated working directory with an
# empty LD_LIBRARY_PATH, and require every bundled library to resolve on the
# strength of the $ORIGIN RPATH alone.
#
# Libraries we deliberately do NOT ship stay out of scope: libvulkan.so.1
# comes from the user's GPU driver, and libcuda/libcudart/libcublas come
# from their CUDA install. Only lib{whisper,ggml}* must resolve from within
# the archive.

set -euo pipefail

verify_dir=$(mktemp -d)
cp -a staging/. "$verify_dir/"

# Anywhere except the library directory — if the working directory were on
# the search path, a broken RPATH would still look fine here.
cd /

status=0

for f in "$verify_dir"/*; do
# staging holds only ELF files at this point (the silero .bin model is
# added after this step), but skip anything else defensively.
readelf -h "$f" >/dev/null 2>&1 || continue

name=$(basename "$f")
out=$(LD_LIBRARY_PATH= ldd "$f" 2>/dev/null || true)

missing=$(echo "$out" | awk '/not found/ {print $1}' | grep -E '^lib(whisper|ggml)' || true)
if [ -n "$missing" ]; then
echo "::error::$name cannot resolve bundled libraries: $(echo "$missing" | tr '\n' ' ')"
echo "$out"
status=1
fi
done

# whisper-cli is the one the user actually launches, and an empty NEEDED
# list would sail through the loop above. Assert it really did bind to the
# bundled libraries rather than to nothing at all.
cli_out=$(LD_LIBRARY_PATH= ldd "$verify_dir/whisper-cli" 2>/dev/null || true)
for required in libwhisper.so libggml.so; do
if ! echo "$cli_out" | grep -q "$required"; then
echo "::error::whisper-cli does not link against $required — check the build configuration"
status=1
fi
done

echo "whisper-cli resolved dependencies (LD_LIBRARY_PATH cleared, cwd=/):"
echo "$cli_out"

rm -rf "$verify_dir"

if [ "$status" -ne 0 ]; then
echo "::error::Linux archive would ship broken — refusing to publish."
exit 1
fi

echo "OK: every bundled whisper/ggml library resolves via \$ORIGIN."
52 changes: 38 additions & 14 deletions .github/workflows/build-whispercpp-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ name: Build whisper.cpp release zips
# whisper-cuda-linux64.zip, which intentionally does NOT include the VAD
# model (also matches the v184 manifest).
#
# Linux packaging (see SubtitleEdit issue #13680): the two Linux jobs build
# with BUILD_SHARED_LIBS=ON, so whisper-cli is only half the payload — it
# links against libwhisper.so.1 and libggml.so.0 at runtime. Those must be
# staged under the SONAME the loader asks for, as REAL files rather than the
# symlink chain cmake leaves in build/bin, because Subtitle Edit unpacks
# these archives with .NET's ZipArchive, which writes a symlink entry out as
# an ordinary file containing the target path. Every ELF also gets its RPATH
# rewritten to $ORIGIN so the loader looks next to the binary; the build-dir
# RUNPATH cmake bakes in is meaningless on a user's machine. The `Verify`
# step is the regression guard — whispercpp-184/186/191 all shipped Linux
# archives with no libwhisper.so at all and nothing caught it.
#
# Dispatch manually with workflow_dispatch. Defaults target v1.9.1; bump
# `whisper_ref` / `release_tag` for future releases.

Expand All @@ -30,11 +42,11 @@ on:
release_tag:
description: 'Tag for the resulting support-files release'
required: true
default: 'whispercpp-191'
default: 'whispercpp-191-r2'
release_name:
description: 'Display name for the release'
required: true
default: 'Whisper CPP v1.9.1'
default: 'Whisper CPP v1.9.1 (r2 - Linux shared library fix)'
silero_url:
description: 'URL of ggml-silero-v6.2.0.zip (the VAD model bundled into 4 of the 5 archives)'
required: true
Expand Down Expand Up @@ -186,6 +198,14 @@ jobs:
repository: ggml-org/whisper.cpp
ref: ${{ inputs.whisper_ref }}

# Second, into a subfolder, for the shared Linux staging/verify scripts.
# Must come after the whisper.cpp checkout — that one owns the workspace
# root and cleans it.
- name: Check out support-files scripts
uses: actions/checkout@v5
with:
path: .se-support

- name: Install Vulkan SDK + tools
# ubuntu-latest is now noble (24.04), so use the noble repo path —
# the jammy variant pulls libyaml-cpp0.7 which isn't in noble.
Expand All @@ -194,7 +214,7 @@ jobs:
sudo wget -qO /etc/apt/sources.list.d/lunarg-vulkan-noble.list \
https://packages.lunarg.com/vulkan/lunarg-vulkan-noble.list
sudo apt-get update
sudo apt-get install -y vulkan-sdk libvulkan-dev glslang-tools libsdl2-dev cmake
sudo apt-get install -y vulkan-sdk libvulkan-dev glslang-tools libsdl2-dev cmake patchelf binutils

- name: Configure
# GGML_BACKEND_DL is required by GGML_CPU_ALL_VARIANTS — it switches
Expand All @@ -214,11 +234,10 @@ jobs:
run: cmake --build build --config Release --target whisper-cli -j $(nproc)

- name: Stage release bits
run: |
mkdir staging
cp build/bin/whisper-cli staging/
cp build/bin/libggml*.so staging/ 2>/dev/null || cp build/lib/libggml*.so staging/
ls -la staging
run: bash .se-support/.github/scripts/stage-linux-whisper.sh

- name: Verify bundled libraries resolve
run: bash .se-support/.github/scripts/verify-linux-whisper.sh

- uses: actions/download-artifact@v4
with: { name: silero, path: staging }
Expand Down Expand Up @@ -251,14 +270,20 @@ jobs:
repository: ggml-org/whisper.cpp
ref: ${{ inputs.whisper_ref }}

# See the note in linux-vulkan: must follow the whisper.cpp checkout.
- name: Check out support-files scripts
uses: actions/checkout@v5
with:
path: .se-support

- name: Install CUDA toolkit 12.4
uses: Jimver/cuda-toolkit@v0.2.19
with:
cuda: '12.4.0'
method: network

- name: Install build deps
run: sudo apt-get update && sudo apt-get install -y cmake build-essential
run: sudo apt-get update && sudo apt-get install -y cmake build-essential patchelf binutils

- name: Configure
# GGML_BACKEND_DL is required by GGML_CPU_ALL_VARIANTS — see notes in
Expand All @@ -277,11 +302,10 @@ jobs:
run: cmake --build build --config Release --target whisper-cli -j $(nproc)

- name: Stage release bits (no silero in this zip per v184 layout)
run: |
mkdir staging
cp build/bin/whisper-cli staging/
cp build/bin/libggml*.so staging/ 2>/dev/null || cp build/lib/libggml*.so staging/
ls -la staging
run: bash .se-support/.github/scripts/stage-linux-whisper.sh

- name: Verify bundled libraries resolve
run: bash .se-support/.github/scripts/verify-linux-whisper.sh

- name: Pack whisper-cuda-linux64.zip
run: |
Expand Down
Loading