From e0eae4d8fee1502aac9f31914576f9902a5b8be4 Mon Sep 17 00:00:00 2001 From: nomoreshow <45514669+nomoreshow@users.noreply.github.com> Date: Fri, 4 Sep 2026 00:34:55 +0300 Subject: [PATCH] Ship a managed Vulkan whisper-server for Linux x64 --- .github/workflows/whisper-vulkan.yml | 189 ++++++++++++++++++ README.md | 5 +- README.tr.md | 3 + dikte/ggml.py | 60 ++++-- packaging/whisper-vulkan/Dockerfile.build | 44 ++++ .../whisper-vulkan/Dockerfile.runtime-cpu | 6 + .../whisper-vulkan/Dockerfile.runtime-vulkan | 7 + packaging/whisper-vulkan/build-package.sh | 128 ++++++++++++ .../licenses/cpp-httplib-MIT.txt | 21 ++ .../licenses/nlohmann-json-MIT.txt | 21 ++ .../whisper-vulkan/lunarg-signing-key-pub.asc | 31 +++ packaging/whisper-vulkan/make-sbom.py | 116 +++++++++++ packaging/whisper-vulkan/smoke-runtime.sh | 64 ++++++ packaging/whisper-vulkan/validate-package.sh | 145 ++++++++++++++ tests/test_ggml.py | 112 +++++++++++ tests/test_packaging.py | 180 +++++++++++++++++ 16 files changed, 1117 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/whisper-vulkan.yml create mode 100644 packaging/whisper-vulkan/Dockerfile.build create mode 100644 packaging/whisper-vulkan/Dockerfile.runtime-cpu create mode 100644 packaging/whisper-vulkan/Dockerfile.runtime-vulkan create mode 100755 packaging/whisper-vulkan/build-package.sh create mode 100644 packaging/whisper-vulkan/licenses/cpp-httplib-MIT.txt create mode 100644 packaging/whisper-vulkan/licenses/nlohmann-json-MIT.txt create mode 100644 packaging/whisper-vulkan/lunarg-signing-key-pub.asc create mode 100755 packaging/whisper-vulkan/make-sbom.py create mode 100755 packaging/whisper-vulkan/smoke-runtime.sh create mode 100755 packaging/whisper-vulkan/validate-package.sh create mode 100644 tests/test_packaging.py diff --git a/.github/workflows/whisper-vulkan.yml b/.github/workflows/whisper-vulkan.yml new file mode 100644 index 0000000..e3bdf05 --- /dev/null +++ b/.github/workflows/whisper-vulkan.yml @@ -0,0 +1,189 @@ +name: whisper.cpp Vulkan bundle + +on: + pull_request: + paths: + - packaging/whisper-vulkan/** + - .github/workflows/whisper-vulkan.yml + - dikte/ggml.py + - tests/test_ggml.py + - tests/test_packaging.py + - README.md + - README.tr.md + workflow_dispatch: + inputs: + whisper_version: + description: Upstream whisper.cpp version (without v) + required: true + default: "1.9.3" + type: string + whisper_commit: + description: Peeled commit SHA for that reviewed upstream tag + required: true + default: "371b5a7561823ab2bb32142d2751e35e7534727b" + type: string + publish: + description: Publish a Dikte dependency release + required: true + default: false + type: boolean + +permissions: + contents: read + +concurrency: + group: whisper-vulkan-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + WHISPER_VERSION: ${{ inputs.whisper_version || '1.9.3' }} + WHISPER_COMMIT: ${{ inputs.whisper_commit || '371b5a7561823ab2bb32142d2751e35e7534727b' }} + MANAGED_WHISPER_SHA256: c25ca76504144da488eb74441390a7b9aa7ce547e5f2f391cbd831253c9b54d8 + +jobs: + build: + runs-on: ubuntu-22.04 + timeout-minutes: 45 + steps: + - name: Check out Dikte + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + + - name: Check out pinned whisper.cpp source + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + repository: ggml-org/whisper.cpp + ref: ${{ env.WHISPER_COMMIT }} + path: vendor/whisper.cpp + fetch-depth: 0 + persist-credentials: false + + - name: Validate source coordinates + shell: bash + run: | + set -euo pipefail + [[ "$WHISPER_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] + [[ "$WHISPER_COMMIT" =~ ^[0-9a-f]{40}$ ]] + + - name: Verify source version and commit + shell: bash + run: | + set -euo pipefail + test "$(git -C vendor/whisper.cpp rev-parse HEAD)" = "$WHISPER_COMMIT" + git -C vendor/whisper.cpp fetch --depth=1 origin \ + "refs/tags/v$WHISPER_VERSION:refs/tags/v$WHISPER_VERSION" + test "$(git -C vendor/whisper.cpp rev-list -n1 "v$WHISPER_VERSION")" = "$WHISPER_COMMIT" + echo "SOURCE_DATE_EPOCH=$(git -C vendor/whisper.cpp show -s --format=%ct HEAD)" >> "$GITHUB_ENV" + + - name: Build pinned build environment + run: docker build --pull=false -f packaging/whisper-vulkan/Dockerfile.build -t dikte-whisper-builder packaging/whisper-vulkan + + - name: Build deterministic archive + run: | + docker run --rm \ + -e WHISPER_VERSION -e WHISPER_COMMIT -e SOURCE_DATE_EPOCH \ + -v "$PWD/vendor/whisper.cpp:/src:ro" \ + -v "$PWD/packaging/whisper-vulkan:/packaging:ro" \ + -v "$PWD/work:/work" \ + dikte-whisper-builder \ + bash /packaging/build-package.sh + mkdir -p dist + cp work/out/whisper-bin-ubuntu-vulkan-x64.* dist/ + + - name: Verify reviewed archive digest + shell: bash + run: | + read -r actual _ < dist/whisper-bin-ubuntu-vulkan-x64.tar.gz.sha256 + test "$actual" = "$MANAGED_WHISPER_SHA256" + + - name: Validate archive and ELF contract + run: OUT_DIR=dist packaging/whisper-vulkan/validate-package.sh + + - name: Schema-validate CycloneDX 1.6 SBOM + run: | + docker run --rm \ + -v "$PWD/dist/whisper-bin-ubuntu-vulkan-x64.cdx.json:/sbom.json:ro" \ + cyclonedx/cyclonedx-cli@sha256:252c2e26f468c25fea1e63ecde1bc3198ad6e9dbb57f5ed3236bddcb2281b3a7 \ + validate --input-file /sbom.json --input-format json \ + --input-version v1_6 --fail-on-errors + + - name: CPU fallback smoke test (no Vulkan loader) + run: OUT_DIR=dist packaging/whisper-vulkan/smoke-runtime.sh cpu + + - name: Vulkan plugin-load smoke test (Mesa llvmpipe) + run: OUT_DIR=dist packaging/whisper-vulkan/smoke-runtime.sh vulkan + + - name: Upload reviewed outputs + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: whisper-bin-ubuntu-vulkan-x64 + path: dist/* + if-no-files-found: error + retention-days: 14 + + publish: + if: >- + github.event_name == 'workflow_dispatch' && inputs.publish && + github.ref == 'refs/heads/master' + needs: build + runs-on: ubuntu-22.04 + environment: dependency-release + permissions: + contents: write + id-token: write + attestations: write + artifact-metadata: write + steps: + - name: Download the exact tested outputs + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: whisper-bin-ubuntu-vulkan-x64 + path: dist + + - name: Verify digest sidecar + run: (cd dist && sha256sum --check whisper-bin-ubuntu-vulkan-x64.tar.gz.sha256) + + - name: Attest build provenance + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4 + with: + subject-path: dist/whisper-bin-ubuntu-vulkan-x64.tar.gz + + - name: Attest SBOM to archive + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4 + with: + subject-path: dist/whisper-bin-ubuntu-vulkan-x64.tar.gz + sbom-path: dist/whisper-bin-ubuntu-vulkan-x64.cdx.json + + - name: Publish dependency release + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: whisper.cpp-v${{ inputs.whisper_version }} + RELEASE_TITLE: whisper.cpp v${{ inputs.whisper_version }} Vulkan bundle + RELEASE_NOTES: >- + Pinned source: ggml-org/whisper.cpp@${{ inputs.whisper_commit }}. + Verify with: gh attestation verify + whisper-bin-ubuntu-vulkan-x64.tar.gz + --repo ${{ github.repository }} + run: | + set -euo pipefail + if gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then + echo "refusing to replace existing release $RELEASE_TAG" >&2 + exit 1 + fi + if gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$RELEASE_TAG" >/dev/null 2>&1; then + echo "refusing to replace existing tag $RELEASE_TAG" >&2 + exit 1 + fi + gh api --method POST "repos/$GITHUB_REPOSITORY/git/refs" \ + -f ref="refs/tags/$RELEASE_TAG" \ + -f sha="$GITHUB_SHA" >/dev/null + test "$(gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$RELEASE_TAG" \ + --jq .object.sha)" = "$GITHUB_SHA" + gh release create "$RELEASE_TAG" dist/* \ + --repo "$GITHUB_REPOSITORY" \ + --verify-tag \ + --prerelease \ + --latest=false \ + --title "$RELEASE_TITLE" \ + --notes "$RELEASE_NOTES" diff --git a/README.md b/README.md index 8853b8f..601311e 100644 --- a/README.md +++ b/README.md @@ -163,7 +163,10 @@ running. the program and the model, verifies the sha256 and refuses a download published without one, then keeps a server alive while you dictate. The graphics card is reached through CUDA, ROCm or Vulkan where the build allows. No key, no - account, nothing leaving the machine. + account, nothing leaving the machine. On x86_64 Linux, that same Download + button tries the reviewed Vulkan bundle when a loader is present, and falls + back to upstream's CPU build if it is unavailable. The bundle also carries CPU + backends for systems where a Vulkan device cannot start. - **Silence never reaches the API.** Handed near-silence, a transcription model invents a sentence instead of returning nothing ("Thanks for watching", or in Turkish "Altyazı M.K."). A recording is dropped when nothing rose 10 dB above diff --git a/README.tr.md b/README.tr.md index 2e6a461..0fc592f 100644 --- a/README.tr.md +++ b/README.tr.md @@ -160,6 +160,9 @@ olmasını ister. checksum'suz yayınlanmış bir indirmeyi reddeder, sen dikte ettikçe sunucuyu ayakta tutar. Derleme destekliyorsa ekran kartına CUDA, ROCm ya da Vulkan üzerinden ulaşılır. Anahtar yok, hesap yok, makineden çıkan bir şey yok. + x86_64 Linux'ta aynı İndir düğmesi, Vulkan yükleyicisi varsa incelenmiş Vulkan + paketini dener; paket kullanılamıyorsa upstream'in işlemci derlemesine döner. + Vulkan aygıtı başlatılamadığında kullanılacak işlemci arka uçları da pakettedir. - **Sessizlik API'ye gitmez.** Sessize yakın bir ses verildiğinde model boş dize döndürmez, bir cümle uydurur ("Altyazı M.K.", "Thanks for watching"). *O kaydın kendi* gürültü tabanının 10 dB üstüne en az 0,3 saniye çıkan bir şey diff --git a/dikte/ggml.py b/dikte/ggml.py index c2f8da8..792a72d 100644 --- a/dikte/ggml.py +++ b/dikte/ggml.py @@ -76,6 +76,13 @@ Program = collections.namedtuple("Program", "name repo binary health") WHISPER = Program("whisper", "ggml-org/whisper.cpp", "whisper-server", "") LLAMA = Program("llama", "ggml-org/llama.cpp", "llama-server", "/health") +DIKTE_REPO = "yusufipk/dikte" +MANAGED_WHISPER_RELEASE = "whisper.cpp-v1.9.3" +MANAGED_WHISPER_VERSION = "v1.9.3" +MANAGED_WHISPER_VULKAN = "whisper-bin-ubuntu-vulkan-x64.tar.gz" +MANAGED_WHISPER_SHA256 = ( + "c25ca76504144da488eb74441390a7b9aa7ce547e5f2f391cbd831253c9b54d8" +) # Where the models are listed. Neither list is written into Dikte: a catalogue # in the source means a release of Dikte for every model somebody else @@ -346,8 +353,11 @@ def _extract(archive, into): with tarfile.open(archive, "r:gz") as tar: try: tar.extractall(into, filter="data") - except TypeError: # Python without the extraction filters - tar.extractall(into) + except TypeError as exc: # Python 3.11.0-3 lack extraction filters + raise LocalError(t( + "Could not safely unpack {name} with this Python version", + name=os.path.basename(str(archive)), + )) from exc except (tarfile.TarError, zipfile.BadZipFile, OSError) as exc: raise LocalError(t("Could not unpack {name}: {error}", name=os.path.basename(str(archive)), error=exc)) from exc @@ -370,20 +380,42 @@ def install_program(program, tag="", on_progress=None, should_stop=None, refresh=False): """Fetch and unpack a release. The path to the binary, or "" when stopped. - `tag` is empty for whatever the project released last, which is the point: - a version pinned in Dikte's source would mean a release of Dikte every time - whisper.cpp has one. + Ordinary builds follow the program's newest release. The Linux Vulkan build + comes from Dikte's pinned dependency release instead. """ - try: - tag, assets = hub.release(program.repo, tag or "latest", refresh=refresh) - except hub.HubError as exc: - raise LocalError(str(exc)) from exc - + repo = program.repo + release_tag = tag or "latest" + managed = (not tag and program is WHISPER and sys.platform == "linux" + and platform.machine().lower() in ("x86_64", "amd64") + and _has_vulkan()) item = None - for ending in _wanted_assets(program): - item = next((a for a in assets if a.name.endswith(ending)), None) + if managed: + repo = DIKTE_REPO + release_tag = MANAGED_WHISPER_RELEASE + try: + tag, assets = hub.release(repo, release_tag, refresh=refresh) + except hub.HubError: + # Older Dikte releases have no managed server. The upstream CPU + # build remains the usable answer there and during API failures. + assets = [] + item = next((a for a in assets + if a.name.endswith(MANAGED_WHISPER_VULKAN) + and a.sha256 == MANAGED_WHISPER_SHA256), None) if item: - break + tag = MANAGED_WHISPER_VERSION + + if item is None: + repo = program.repo + wanted = _wanted_assets(program) + try: + tag, assets = hub.release(repo, "latest" if managed else release_tag, + refresh=refresh) + except hub.HubError as exc: + raise LocalError(str(exc)) from exc + for ending in wanted: + item = next((a for a in assets if a.name.endswith(ending)), None) + if item: + break if item is None: # Nothing to download and nothing to install for you: whisper.cpp # publishes no macOS binary, and Homebrew's whisper-cpp is configured @@ -398,7 +430,7 @@ def install_program(program, tag="", on_progress=None, should_stop=None, "or transcribe in the cloud. See the README." )) raise LocalError(t("{repo} {tag} has no build for this machine.", - repo=program.repo, tag=tag)) + repo=repo, tag=tag)) into = BIN_DIR / program.name / tag fresh = into.with_name(tag + ".new") diff --git a/packaging/whisper-vulkan/Dockerfile.build b/packaging/whisper-vulkan/Dockerfile.build new file mode 100644 index 0000000..fae985b --- /dev/null +++ b/packaging/whisper-vulkan/Dockerfile.build @@ -0,0 +1,44 @@ +FROM ubuntu@sha256:2edbbc5dc405e9612ba3584ce95480277e3eb374407b5505fe26f17df77c7dbc + +ARG DEBIAN_FRONTEND=noninteractive +ARG CMAKE_VERSION=3.31.6 +ARG CMAKE_SHA256=5a1133ff103c71eb5120e2cc3de922733e7d8a26a98ae716397e8676adb367bf + +COPY lunarg-signing-key-pub.asc /tmp/lunarg.asc + +RUN set -eux; \ + test "$(sha256sum /tmp/lunarg.asc | cut -d' ' -f1)" = aa1c3c29673140e77f0d6a9aaeed5d9b5621e305ead51c59fae4458bbb4df92b; \ + apt-get update; \ + apt-get install --no-install-recommends -y \ + build-essential=12.9ubuntu3 \ + ca-certificates \ + curl \ + file \ + git \ + gnupg \ + ninja-build=1.10.1-1 \ + patchelf=0.14.3-1 \ + python3 \ + xz-utils; \ + install -d -m 0755 /usr/share/keyrings; \ + gpg --dearmor -o /usr/share/keyrings/lunarg.gpg /tmp/lunarg.asc; \ + printf '%s\n' 'deb [signed-by=/usr/share/keyrings/lunarg.gpg] https://packages.lunarg.com/vulkan jammy main' \ + > /etc/apt/sources.list.d/lunarg-vulkan.list; \ + apt-get update; \ + apt-get install --no-install-recommends -y \ + libvulkan-dev=1.4.313.0~rc1-1lunarg22.04-1 \ + vulkan-headers=1.4.313.0~rc1-1lunarg22.04-1 \ + shaderc=2025.2~rc1-1lunarg22.04-1 \ + spirv-headers=1.6.1+1.4.313.0~rc1-1lunarg22.04-1; \ + curl --fail --location --retry 3 \ + "https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERSION}/cmake-${CMAKE_VERSION}-linux-x86_64.tar.gz" \ + -o /tmp/cmake.tar.gz; \ + test "$(sha256sum /tmp/cmake.tar.gz | cut -d' ' -f1)" = "$CMAKE_SHA256"; \ + tar -xzf /tmp/cmake.tar.gz --strip-components=1 -C /usr/local; \ + rm -rf /var/lib/apt/lists/* /tmp/cmake.tar.gz /tmp/lunarg.asc; \ + cmake --version; \ + glslc --version; \ + test -f /usr/include/vulkan/vulkan.h; \ + test -f /usr/share/cmake/SPIRV-Headers/SPIRV-HeadersConfig.cmake + +WORKDIR /work diff --git a/packaging/whisper-vulkan/Dockerfile.runtime-cpu b/packaging/whisper-vulkan/Dockerfile.runtime-cpu new file mode 100644 index 0000000..89ac704 --- /dev/null +++ b/packaging/whisper-vulkan/Dockerfile.runtime-cpu @@ -0,0 +1,6 @@ +FROM ubuntu@sha256:2edbbc5dc405e9612ba3584ce95480277e3eb374407b5505fe26f17df77c7dbc +ARG DEBIAN_FRONTEND=noninteractive +RUN apt-get update \ + && apt-get install --no-install-recommends -y ca-certificates curl libstdc++6 \ + && rm -rf /var/lib/apt/lists/* +WORKDIR /bundle diff --git a/packaging/whisper-vulkan/Dockerfile.runtime-vulkan b/packaging/whisper-vulkan/Dockerfile.runtime-vulkan new file mode 100644 index 0000000..3a16288 --- /dev/null +++ b/packaging/whisper-vulkan/Dockerfile.runtime-vulkan @@ -0,0 +1,7 @@ +FROM ubuntu@sha256:2edbbc5dc405e9612ba3584ce95480277e3eb374407b5505fe26f17df77c7dbc +ARG DEBIAN_FRONTEND=noninteractive +RUN apt-get update \ + && apt-get install --no-install-recommends -y \ + ca-certificates curl libstdc++6 libvulkan1 mesa-vulkan-drivers vulkan-tools \ + && rm -rf /var/lib/apt/lists/* +WORKDIR /bundle diff --git a/packaging/whisper-vulkan/build-package.sh b/packaging/whisper-vulkan/build-package.sh new file mode 100755 index 0000000..f4e2215 --- /dev/null +++ b/packaging/whisper-vulkan/build-package.sh @@ -0,0 +1,128 @@ +#!/usr/bin/env bash +set -euo pipefail +shopt -s nullglob + +: "${SOURCE_DIR:=/src}" +: "${OUT_DIR:=/work/out}" +: "${WHISPER_VERSION:=1.9.3}" +: "${WHISPER_COMMIT:=371b5a7561823ab2bb32142d2751e35e7534727b}" +: "${SOURCE_DATE_EPOCH:=1787219223}" + +export SOURCE_DATE_EPOCH TZ=UTC LC_ALL=C LANG=C +asset=whisper-bin-ubuntu-vulkan-x64 +build=/work/build +source_copy=/work/source +root="$OUT_DIR/root/$asset" + +rm -rf "$build" "$source_copy" "$OUT_DIR" +mkdir -p "$build" "$root/LICENSES" +# Upstream configures bindings/javascript/package.json in the source directory. +# Build a private copy so the checked-out, verified source remains untouched. +cp -a "$SOURCE_DIR" "$source_copy" +chmod -R u+w "$source_copy" +git config --global --add safe.directory "$source_copy" + +cmake -S "$source_copy" -B "$build" -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_BUILD_RPATH='$ORIGIN' \ + -DCMAKE_INSTALL_RPATH='$ORIGIN' \ + -DCMAKE_BUILD_WITH_INSTALL_RPATH=ON \ + -DCMAKE_C_FLAGS="-ffile-prefix-map=$source_copy=. -fdebug-prefix-map=$source_copy=. -fmacro-prefix-map=$source_copy=." \ + -DCMAKE_CXX_FLAGS="-ffile-prefix-map=$source_copy=. -fdebug-prefix-map=$source_copy=. -fmacro-prefix-map=$source_copy=." \ + -DBUILD_SHARED_LIBS=ON \ + -DGGML_BACKEND_DL=ON \ + -DGGML_CPU_ALL_VARIANTS=ON \ + -DGGML_NATIVE=OFF \ + -DGGML_CCACHE=OFF \ + -DGGML_OPENMP=OFF \ + -DGGML_VULKAN=ON \ + -DWHISPER_BUILD_EXAMPLES=ON \ + -DWHISPER_BUILD_SERVER=ON \ + -DWHISPER_BUILD_TESTS=OFF \ + -DWHISPER_BUILD_IS_DEV=OFF \ + -DWHISPER_CURL=OFF \ + -DWHISPER_SDL2=OFF \ + -DWHISPER_COMMON_FFMPEG=OFF \ + -DWHISPER_BUILD_COMMIT="$WHISPER_COMMIT" \ + -DWHISPER_BUILD_NUMBER=0 +cmake --build "$build" --target whisper-server --parallel "$(nproc)" + +# Package an allowlist, not everything examples/ happens to build in the future. +cp -a "$build/bin/whisper-server" "$root/" +cp -a "$build/bin"/libwhisper.so* "$root/" +cp -a "$build/bin"/libggml.so* "$root/" +cp -a "$build/bin"/libggml-base.so* "$root/" +cp -a "$build/bin"/libggml-cpu*.so* "$root/" +cp -a "$build/bin"/libggml-vulkan.so* "$root/" + +# Strip real ELF files only; preserve the SONAME symlink chains. +while IFS= read -r -d '' file; do + if file "$file" | grep -q ELF; then + strip --strip-unneeded "$file" + patchelf --set-rpath '$ORIGIN' "$file" + fi +done < <(find "$root" -type f -print0) + +cp "$SOURCE_DIR/LICENSE" "$root/LICENSES/whisper.cpp-MIT.txt" +cp /packaging/licenses/cpp-httplib-MIT.txt "$root/LICENSES/" +cp /packaging/licenses/nlohmann-json-MIT.txt "$root/LICENSES/" + +cat > "$root/BUILD-INFO.json" < "$root/$asset.cdx.json" + +( + cd "$root" + find . -type f ! -name SHA256SUMS -print0 \ + | sort -z \ + | xargs -0 sha256sum +) > "$root/SHA256SUMS" + +mkdir -p "$OUT_DIR" +tar --sort=name --owner=0 --group=0 --numeric-owner \ + --mtime="@$SOURCE_DATE_EPOCH" \ + --pax-option=delete=atime,delete=ctime \ + -C "$OUT_DIR/root" -cf - "$asset" \ + | gzip -n -9 > "$OUT_DIR/$asset.tar.gz" +( + cd "$OUT_DIR" + sha256sum "$asset.tar.gz" > "$asset.tar.gz.sha256" +) +cp "$root/$asset.cdx.json" "$OUT_DIR/$asset.cdx.json" diff --git a/packaging/whisper-vulkan/licenses/cpp-httplib-MIT.txt b/packaging/whisper-vulkan/licenses/cpp-httplib-MIT.txt new file mode 100644 index 0000000..47c418e --- /dev/null +++ b/packaging/whisper-vulkan/licenses/cpp-httplib-MIT.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 yhirose + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packaging/whisper-vulkan/licenses/nlohmann-json-MIT.txt b/packaging/whisper-vulkan/licenses/nlohmann-json-MIT.txt new file mode 100644 index 0000000..70c6af6 --- /dev/null +++ b/packaging/whisper-vulkan/licenses/nlohmann-json-MIT.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2013-2022 Niels Lohmann + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packaging/whisper-vulkan/lunarg-signing-key-pub.asc b/packaging/whisper-vulkan/lunarg-signing-key-pub.asc new file mode 100644 index 0000000..59b5d72 --- /dev/null +++ b/packaging/whisper-vulkan/lunarg-signing-key-pub.asc @@ -0,0 +1,31 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mQENBFuOrjYBCADT5MjShtbeSsWHADqVP7PZIp+m/wWkSUA7/FX/qrixhQE9DFyt +XtKSbBdwh+Jg5nsttCUiePtdrrRD1tcyowG256Tus3vOysZzvpfjWA4gcVmTjJXn +gwezKsPZLQi0wvjwQD8ByxnM1i2eiJC4xcMjT21uZkwDfgLTzVO4InWlVyZDB/da +PLJl4r1MqnsI603RKalMQmZzs43YUDssdeOiGOpXvb1Rj0XcsOOqnAEvIwyUWGku +1Hr+b6C9Nj6wksD7TCB10IdOeuwBqFgrVDzicG4fijwnpzA+UUfncIhKYdI/oIvj +mcAPobWzcBkM3uc+Yf/CxlBahzu6jv7AFdT1ABEBAAG0VEx1bmFyRyBTaWduaW5n +IEtleSAoS2V5IHVzZWQgYnkgTHVuYXJHIHRvIHNpZ24gcGFja2FnZXMpIDxsaW51 +eC1wYWNrYWdlc0BsdW5hcmcuY29tPokBTgQTAQoAOBYhBAP11iGjcQ+pWpPYm6qE +UggOOD9+BQJbjq42AhsDBQsJCAcDBRUKCQgLBRYCAwEAAh4BAheAAAoJEKqEUggO +OD9+ECgH/Ro6LVB08FifApBS235v0Af3dsJlZGE0miKu2hR12qAvWackE6//E5GN +5xKSNpgLzV6kyylBntQDhcFzW3hLt/AsMLOXvuxYNFcLes2y10DrqVekNeJiR95V +KiTPI2jP8m4eFpcSnY0riHk2MmstN1icehQhYrWFyUtt3VxSsRWiRDeNUfCHC6YP +MjOXonmTWfH7T+UA2IqLFrt9dAsYGiCtMKVgzaZaZwm727c0aqy0e43nsWqjWxmE +EsEA1RvzjKKyzyixwpnzIyQ8dqL8sH0G3E2OYTlS7A8//yfgykRQVHwg2TsTBKfG +LlTmKj7RCT6GqISo+rbYYo/hZ6l2hH25AQ0EW46uNgEIANZfPWerTPzmvswWqp0P +iQvW+0qTBxZH3gQlwq5s6ahpY1pIebfrL/SAYJUGyjJVcjkG+HBXRGyRxtWFDE+D ++WEuziBfKd3aBUXb5DnvWdCiXeyQnFfwUVYNXhU5PlpAB5M409a30p9gGOrYy3Ah +g4VHhpM9wzGUAOzTwQ4WaC2WkR84sZYyqdKoo6C3m4IR4KHMYXF9nRlPSNEckL9U +MZe6I2uvor9FOPIfIOAI8lN+gbj/anf3lfy0ZYPyUtl3EWveGpWAPvdw3LMKg5QN +B8bR9TkPk0YZyQQcWkmN7gLUg0Vba+PYHH9DRlG8w1rH4TKxXJV3wmHo2aZRF1kc +30kAEQEAAYkBNgQYAQoAIBYhBAP11iGjcQ+pWpPYm6qEUggOOD9+BQJbjq42AhsM +AAoJEKqEUggOOD9+MEUH/2pm2QOttjd7DmEaS4LGvaTlEif0xtymRAh3axGuqQhl +KCZbw0jwsQlo/DwMRZwZHYCj1A/5H8mEg9qNGjF35GEpQTFSQI6Mt7F2DK69J86w +61v8tjxs4eO201ndhy+DRwDwG8vryFldx3f0nEdlE7IusgiUdvkcJPc8rX7p0MJJ +istTREAq8bRnvWYJzd4k3tgwHglEDxyjBRwLtqZyQ19XZb3V/aVKygqvZbwdJyXO +RHAZxK81p9Gp/8VkogJHLx6+3V8UlDepJg9/8MUCBQ9wWkdF0Pfqzgu7xtIHSxvW +62EF4nxqVuC946OIeITgXpd4F+iTFVII8w0P+nyCzac= +=nXAe +-----END PGP PUBLIC KEY BLOCK----- diff --git a/packaging/whisper-vulkan/make-sbom.py b/packaging/whisper-vulkan/make-sbom.py new file mode 100755 index 0000000..1dcf99c --- /dev/null +++ b/packaging/whisper-vulkan/make-sbom.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +import datetime +import hashlib +import json +import os +import uuid +from pathlib import Path + +root = Path(os.environ["ROOT"]) +version = os.environ["VERSION"] +commit = os.environ["COMMIT"] +epoch = int(os.environ["EPOCH"]) +asset = "whisper-bin-ubuntu-vulkan-x64" +sbom_path = root / f"{asset}.cdx.json" + +def digest(path): + h = hashlib.sha256() + with path.open("rb") as stream: + for block in iter(lambda: stream.read(1024 * 1024), b""): + h.update(block) + return h.hexdigest() + +files = [] +for path in sorted(root.rglob("*")): + if path != sbom_path and path.is_file() and not path.is_symlink(): + rel = path.relative_to(root).as_posix() + files.append({ + "type": "file", + "bom-ref": f"file:{rel}", + "name": rel, + "hashes": [{"alg": "SHA-256", "content": digest(path)}], + }) + +ts = datetime.datetime.fromtimestamp( + epoch, datetime.timezone.utc, +).isoformat().replace("+00:00", "Z") +root_ref = f"pkg:github/ggml-org/whisper.cpp@{version}?commit={commit}" +ggml_ref = "pkg:github/ggml-org/ggml@0.20.2" +httplib_ref = "pkg:github/yhirose/cpp-httplib@0.20.0" +json_ref = "pkg:github/nlohmann/json@3.11.2" + +sbom = { + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": f"urn:uuid:{uuid.uuid5(uuid.NAMESPACE_URL, root_ref)}", + "version": 1, + "metadata": { + "timestamp": ts, + "tools": {"components": [ + {"type": "application", "name": "make-sbom.py", "version": "1"}, + {"type": "application", "name": "CMake", "version": "3.31.6"}, + {"type": "application", "name": "glslc", "version": "2025.2"}, + ]}, + "component": { + "type": "application", + "bom-ref": root_ref, + "group": "ggml-org", + "name": "whisper-server", + "version": version, + "purl": root_ref, + "licenses": [{"expression": "MIT"}], + "externalReferences": [{ + "type": "vcs", + "url": f"https://github.com/ggml-org/whisper.cpp/tree/{commit}", + }], + "properties": [ + {"name": "dikte:asset-name", "value": f"{asset}.tar.gz"}, + {"name": "dikte:source-commit", "value": commit}, + {"name": "dikte:runtime:glibc-minimum", "value": "2.34"}, + {"name": "dikte:runtime:glibcxx-minimum", "value": "3.4.30"}, + {"name": "dikte:runtime:vulkan-loader", "value": "optional; libvulkan.so.1"}, + ], + }, + }, + "components": [ + { + "type": "library", + "bom-ref": ggml_ref, + "group": "ggml-org", + "name": "ggml", + "version": "0.20.2", + "purl": ggml_ref, + "licenses": [{"expression": "MIT"}], + "properties": [{ + "name": "dikte:source", + "value": "vendored by the pinned whisper.cpp commit", + }], + }, + { + "type": "library", + "bom-ref": httplib_ref, + "group": "yhirose", + "name": "cpp-httplib", + "version": "0.20.0", + "purl": httplib_ref, + "licenses": [{"expression": "MIT"}], + }, + { + "type": "library", + "bom-ref": json_ref, + "group": "nlohmann", + "name": "json", + "version": "3.11.2", + "purl": json_ref, + "licenses": [{"expression": "MIT"}], + }, + *files, + ], + "dependencies": [{ + "ref": root_ref, + "dependsOn": [ggml_ref, httplib_ref, json_ref] + + [item["bom-ref"] for item in files], + }], +} +json.dump(sbom, fp=os.sys.stdout, indent=2, sort_keys=True) +print() diff --git a/packaging/whisper-vulkan/smoke-runtime.sh b/packaging/whisper-vulkan/smoke-runtime.sh new file mode 100755 index 0000000..22c0626 --- /dev/null +++ b/packaging/whisper-vulkan/smoke-runtime.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -euo pipefail + +mode=${1:?usage: smoke-runtime.sh cpu|vulkan} +: "${OUT_DIR:=work/out}" +: "${FIXTURE_SOURCE:=vendor/whisper.cpp}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +OUT_DIR="$(realpath "$OUT_DIR")" +FIXTURE_SOURCE="$(realpath "$FIXTURE_SOURCE")" +asset=whisper-bin-ubuntu-vulkan-x64 +case "$mode" in + cpu) dockerfile=Dockerfile.runtime-cpu; image=dikte-whisper-runtime-cpu:spike ;; + vulkan) dockerfile=Dockerfile.runtime-vulkan; image=dikte-whisper-runtime-vulkan:spike ;; + *) echo "unknown mode: $mode" >&2; exit 2 ;; +esac + +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT +tar -xzf "$OUT_DIR/$asset.tar.gz" -C "$tmp" +docker build --pull=false -f "$SCRIPT_DIR/$dockerfile" -t "$image" "$SCRIPT_DIR" + +args=("/bundle/$asset/whisper-server" -m /fixtures/model.bin + --host 127.0.0.1 --port 8080 + --inference-path /v1/audio/transcriptions -l auto -sns -nlp) +env_args=() +if [[ "$mode" == cpu ]]; then + args+=(-ng) +else + env_args=(-e LIBGL_ALWAYS_SOFTWARE=1 + -e VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.x86_64.json) +fi + +docker run --rm --name "dikte-whisper-$mode-smoke" \ + -e SMOKE_MODE="$mode" \ + "${env_args[@]}" \ + -v "$tmp/$asset:/bundle/$asset:ro" \ + -v "$FIXTURE_SOURCE/models/for-tests-ggml-base.en.bin:/fixtures/model.bin:ro" \ + -v "$FIXTURE_SOURCE/samples/jfk.wav:/fixtures/jfk.wav:ro" \ + "$image" bash -ec ' + if [ "$SMOKE_MODE" = cpu ] && ldconfig -p | grep -q libvulkan.so.1; then + echo "CPU smoke image unexpectedly has a Vulkan loader" >&2 + exit 1 + fi + "$@" >/tmp/server.log 2>&1 & + pid=$! + trap "kill $pid 2>/dev/null || true" EXIT + for _ in $(seq 1 120); do + kill -0 "$pid" 2>/dev/null || { cat /tmp/server.log; exit 1; } + if curl --silent --show-error --fail --max-time 180 \ + -F file=@/fixtures/jfk.wav -F response_format=json \ + http://127.0.0.1:8080/v1/audio/transcriptions >/tmp/response.json; then + grep -q "\"text\"" /tmp/response.json + if [ "$SMOKE_MODE" = vulkan ]; then + grep -q "loaded Vulkan backend" /tmp/server.log + fi + cat /tmp/response.json + cat /tmp/server.log + exit 0 + fi + sleep 1 + done + cat /tmp/server.log + exit 1 + ' bash "${args[@]}" diff --git a/packaging/whisper-vulkan/validate-package.sh b/packaging/whisper-vulkan/validate-package.sh new file mode 100755 index 0000000..d21a083 --- /dev/null +++ b/packaging/whisper-vulkan/validate-package.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${OUT_DIR:=work/out}" +: "${SOURCE_DIR:=whisper.cpp}" +asset=whisper-bin-ubuntu-vulkan-x64 +archive="$OUT_DIR/$asset.tar.gz" +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT + +test -s "$archive" +(cd "$OUT_DIR" && sha256sum --check "$asset.tar.gz.sha256") +ARCHIVE="$archive" ASSET="$asset" python3 - <<'PY' +import os +import posixpath +import tarfile + +archive = os.environ["ARCHIVE"] +asset = os.environ["ASSET"] + + +def under_root(name): + normalized = posixpath.normpath(name) + return (not posixpath.isabs(normalized) + and normalized != ".." + and not normalized.startswith("../") + and normalized.split("/", 1)[0] == asset) + + +with tarfile.open(archive, "r:gz") as bundle: + for member in bundle: + if not under_root(member.name): + raise SystemExit(f"unsafe archive member: {member.name}") + if member.isdev() or member.isfifo(): + raise SystemExit(f"special archive member: {member.name}") + if not (member.isdir() or member.isfile() + or member.issym() or member.islnk()): + raise SystemExit(f"unsupported archive member: {member.name}") + if member.issym(): + target = posixpath.join(posixpath.dirname(member.name), + member.linkname) + if not under_root(target): + raise SystemExit(f"unsafe symlink: {member.name}") + if member.islnk() and not under_root(member.linkname): + raise SystemExit(f"unsafe hardlink: {member.name}") +PY +tar -xzf "$archive" -C "$tmp" +root="$tmp/$asset" + +test -x "$root/whisper-server" +test -f "$root/libwhisper.so" +test -f "$root/libggml.so" +test -f "$root/libggml-base.so" +test -f "$root/libggml-vulkan.so" +compgen -G "$root/libggml-cpu-*.so" >/dev/null +test -f "$root/LICENSES/whisper.cpp-MIT.txt" +test -f "$root/LICENSES/cpp-httplib-MIT.txt" +test -f "$root/LICENSES/nlohmann-json-MIT.txt" +(cd "$root" && sha256sum --check SHA256SUMS) + +# All shipped ELF objects must be relocatable and must not remember /work. +while IFS= read -r -d '' file; do + file "$file" | grep -q ELF || continue + dynamic=$(readelf -d "$file") + if ! grep -Fq 'Library runpath: [$ORIGIN]' <<<"$dynamic"; then + echo "runpath is not \$ORIGIN in $file" >&2 + exit 1 + fi + if grep -Eq '/(home|tmp|work)/' <<<"$dynamic"; then + echo "build path remains in $file" >&2 + exit 1 + fi +done < <(find "$root" -type f -print0) + +# Vulkan remains a plugin dependency. The executable must start without a loader. +if readelf -d "$root/whisper-server" | grep -q 'libvulkan.so'; then + echo "whisper-server links Vulkan instead of loading it as a plugin" >&2 + exit 1 +fi +readelf -d "$root/libggml-vulkan.so" | grep -q 'libvulkan.so.1' + +# Ubuntu 22.04 establishes the glibc ceiling promised by this artifact. +ROOT="$root" python3 - <<'PY' +import os, pathlib, re, subprocess +root = pathlib.Path(os.environ['ROOT']) +seen = {'GLIBC': set(), 'GLIBCXX': set(), 'CXXABI': set()} +external = { + 'libc.so.6', 'libgcc_s.so.1', 'libm.so.6', 'libstdc++.so.6', + 'libvulkan.so.1', 'ld-linux-x86-64.so.2', +} +for path in root.iterdir(): + if not path.is_file() or path.is_symlink(): + continue + header = subprocess.run(['readelf', '-h', path], text=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL).stdout + if not header: + continue + if 'Machine: Advanced Micro Devices X86-64' not in header: + raise SystemExit(f'wrong ELF architecture: {path.name}') + dynamic = subprocess.run(['readelf', '-d', path], text=True, + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL).stdout + needed = re.findall(r'\(NEEDED\).*\[(.*?)\]', dynamic) + unexpected = [name for name in needed + if name not in external + and not re.fullmatch( + r'lib(?:whisper|ggml(?:-base)?)\.so\.\d+', name)] + if unexpected: + raise SystemExit( + f'unexpected DT_NEEDED in {path.name}: {unexpected}') + if path.name != 'libggml-vulkan.so' and 'libvulkan.so.1' in needed: + raise SystemExit(f'Vulkan is not plugin-only in {path.name}') + contents = path.read_bytes() + for marker in (b'/home/', b'/tmp/', b'/work/'): + if marker in contents: + raise SystemExit( + f'build path {marker!r} remains in {path.name}') + text = subprocess.run(['objdump', '-T', path], text=True, + stdout=subprocess.PIPE, stderr=subprocess.DEVNULL).stdout + for family in seen: + pattern = rf'{family}_([0-9]+(?:\.[0-9]+)+)' + seen[family].update(tuple(map(int, version.split('.'))) + for version in re.findall(pattern, text)) +assert seen['GLIBC'] and max(seen['GLIBC']) <= (2, 34), max(seen['GLIBC']) +assert seen['GLIBCXX'] and max(seen['GLIBCXX']) <= (3, 4, 30), max(seen['GLIBCXX']) +assert seen['CXXABI'] and max(seen['CXXABI']) <= (1, 3, 13), max(seen['CXXABI']) +for family, versions in seen.items(): + print(f'maximum {family} symbol:', '.'.join(map(str, max(versions)))) +PY + +python3 - "$root/$asset.cdx.json" <<'PY' +import json, sys +with open(sys.argv[1], encoding='utf-8') as stream: + doc = json.load(stream) +assert doc['bomFormat'] == 'CycloneDX' +assert doc['specVersion'] == '1.6' +assert doc['metadata']['component']['name'] == 'whisper-server' +assert len(doc['components']) >= 3 +print('SBOM components:', len(doc['components'])) +PY + +LD_LIBRARY_PATH='' "$root/whisper-server" --help >/dev/null 2>&1 + +echo "structure: PASS" diff --git a/tests/test_ggml.py b/tests/test_ggml.py index 44b9305..94a69b5 100644 --- a/tests/test_ggml.py +++ b/tests/test_ggml.py @@ -221,6 +221,7 @@ class InstallProgram(Local): def install(self, *names, archive=None): self.patch_attr(ggml, "_arch", lambda: "x64") + self.patch_attr(ggml, "_has_vulkan", lambda: False) blob = self.archive if archive is None else archive with serving(self.release(*names, archive=blob), blob) as calls: path = ggml.install_program(ggml.WHISPER) @@ -238,6 +239,105 @@ class InstallProgram(Local): "whisper-bin-ubuntu-x64.tar.gz") self.assertTrue(urls[1].endswith("whisper-bin-ubuntu-x64.tar.gz")) + def test_linux_x64_with_vulkan_takes_diktes_accelerated_build(self): + self.patch_attr(ggml, "_arch", lambda: "x64") + self.patch_attr(ggml, "_has_vulkan", lambda: True) + listing = self.release("whisper-bin-ubuntu-vulkan-x64.tar.gz") + listing["tag_name"] = "whisper.cpp-v1.9.3" + managed_sha = hashlib.sha256(self.archive).hexdigest() + with mock.patch.object(ggml, "MANAGED_WHISPER_SHA256", managed_sha, + create=True): + with fake_urlopen(listing, body(self.archive)) as calls: + path = ggml.install_program(ggml.WHISPER) + urls = [call.full_url for call in calls] + self.assertIn( + "/repos/yusufipk/dikte/releases/tags/whisper.cpp-v1.9.3", + urls[0], + ) + self.assertTrue(urls[1].endswith( + "whisper-bin-ubuntu-vulkan-x64.tar.gz")) + self.assertTrue(os.path.isfile(path)) + self.assertEqual("v1.9.3", ggml.installed_version(ggml.WHISPER)) + + def test_an_explicit_whisper_version_still_comes_from_upstream(self): + self.patch_attr(ggml, "_arch", lambda: "x64") + self.patch_attr(ggml, "_has_vulkan", lambda: True) + listing = self.release("whisper-bin-ubuntu-x64.tar.gz") + with fake_urlopen(listing, body(self.archive)) as calls: + ggml.install_program(ggml.WHISPER, tag="v1.9.1") + self.assertIn( + "/repos/ggml-org/whisper.cpp/releases/tags/v1.9.1", + calls[0].full_url, + ) + + def test_linux_arm64_keeps_using_the_upstream_cpu_build(self): + self.patch_attr(ggml, "_arch", lambda: "arm64") + self.patch_attr(ggml.platform, "machine", lambda: "aarch64") + self.patch_attr(ggml, "_has_vulkan", lambda: True) + listing = self.release("whisper-bin-ubuntu-arm64.tar.gz") + with fake_urlopen(listing, body(self.archive)) as calls: + ggml.install_program(ggml.WHISPER) + self.assertIn( + "/repos/ggml-org/whisper.cpp/releases/latest", + calls[0].full_url, + ) + + def test_linux_non_x86_does_not_try_the_managed_x64_build(self): + self.patch_attr(ggml, "_has_vulkan", lambda: True) + listing = self.release("whisper-bin-ubuntu-arm64.tar.gz") + with mock.patch("platform.machine", return_value="ppc64le"): + with fake_urlopen(listing, listing) as calls: + with self.assertRaises(ggml.LocalError): + ggml.install_program(ggml.WHISPER) + self.assertIn( + "/repos/ggml-org/whisper.cpp/releases/latest", + calls[0].full_url, + ) + + def test_a_missing_managed_build_falls_back_to_upstream_cpu(self): + self.patch_attr(ggml, "_arch", lambda: "x64") + self.patch_attr(ggml, "_has_vulkan", lambda: True) + managed = self.release("Dikte-1.1.0-x86_64.AppImage") + managed["tag_name"] = "whisper.cpp-v1.9.3" + upstream = self.release("whisper-bin-ubuntu-x64.tar.gz") + with fake_urlopen(managed, upstream, body(self.archive)) as calls: + path = ggml.install_program(ggml.WHISPER) + urls = [call.full_url for call in calls] + self.assertIn( + "/repos/yusufipk/dikte/releases/tags/whisper.cpp-v1.9.3", + urls[0], + ) + self.assertIn("/repos/ggml-org/whisper.cpp/releases/latest", urls[1]) + self.assertTrue(urls[2].endswith("whisper-bin-ubuntu-x64.tar.gz")) + self.assertTrue(os.path.isfile(path)) + + def test_a_managed_build_with_an_unreviewed_digest_falls_back(self): + self.patch_attr(ggml, "_has_vulkan", lambda: True) + managed = self.release("whisper-bin-ubuntu-vulkan-x64.tar.gz") + managed["assets"][0]["digest"] = "sha256:" + "0" * 64 + upstream = self.release("whisper-bin-ubuntu-x64.tar.gz") + with fake_urlopen(managed, upstream, body(self.archive)) as calls: + try: + path = ggml.install_program(ggml.WHISPER) + except ggml.LocalError as exc: + self.fail(f"unreviewed digest did not fall back: {exc}") + urls = [call.full_url for call in calls] + self.assertEqual(3, len(urls)) + self.assertTrue(urls[2].endswith("whisper-bin-ubuntu-x64.tar.gz")) + self.assertTrue(os.path.isfile(path)) + + def test_an_unavailable_managed_release_falls_back_to_upstream_cpu(self): + self.patch_attr(ggml, "_arch", lambda: "x64") + self.patch_attr(ggml, "_has_vulkan", lambda: True) + upstream = self.release("whisper-bin-ubuntu-x64.tar.gz") + with fake_urlopen(http_error(404), upstream, + body(self.archive)) as calls: + path = ggml.install_program(ggml.WHISPER) + self.assertEqual(3, len(calls)) + self.assertTrue(calls[2].full_url.endswith( + "whisper-bin-ubuntu-x64.tar.gz")) + self.assertTrue(os.path.isfile(path)) + def test_a_release_with_nothing_for_this_machine_says_so(self): self.patch_attr(ggml, "_arch", lambda: "x64") with fake_urlopen(self.release("whisper-bin-Win32.zip")): @@ -399,6 +499,18 @@ class InstallProgram(Local): with self.assertRaises(ggml.LocalError): self.install("whisper-bin-ubuntu-x64.tar.gz", archive=buf.getvalue()) + def test_python_without_safe_tar_filters_refuses_the_archive(self): + archive = self.path("bundle.tar.gz") + archive.write_bytes(self.archive) + destination = self.path("unpacked") + destination.mkdir() + with mock.patch.object(tarfile.TarFile, "extractall", + side_effect=[TypeError("no filter"), None]) as extract: + with self.assertRaises(ggml.LocalError) as caught: + ggml._extract(archive, destination) + self.assertEqual(1, extract.call_count) + self.assertIn("safely", str(caught.exception)) + def test_everything_is_asked_for_over_tls(self): for url in (hub.GITHUB_API, hub.HF_API, hub.HF_FILES): with self.subTest(url=url): diff --git a/tests/test_packaging.py b/tests/test_packaging.py new file mode 100644 index 0000000..2d63e6e --- /dev/null +++ b/tests/test_packaging.py @@ -0,0 +1,180 @@ +"""The release build that makes Linux Vulkan a one-click install.""" + +import hashlib +import io +import json +import os +import pathlib +import shutil +import subprocess +import sys +import tarfile +import tempfile +import unittest + +from dikte import ggml + + +ROOT = pathlib.Path(__file__).parents[1] +PACKAGING = ROOT / "packaging" / "whisper-vulkan" +WORKFLOW = ROOT / ".github" / "workflows" / "whisper-vulkan.yml" + + +class WhisperVulkanPackaging(unittest.TestCase): + @unittest.skipUnless(shutil.which("bash"), "bash is unavailable") + def test_the_release_scripts_parse_as_shell(self): + for name in ("build-package.sh", "validate-package.sh", + "smoke-runtime.sh"): + script = PACKAGING / name + checked = subprocess.run( + ["bash", "-n", script], capture_output=True, text=True, + ) + self.assertEqual("", checked.stderr) + self.assertEqual(0, checked.returncode) + + def test_the_workflow_builds_validates_smokes_and_publishes(self): + workflow = WORKFLOW.read_text(encoding="utf-8") + for step in ("Build deterministic archive", + "Verify reviewed archive digest", + "Validate archive and ELF contract", + "CPU fallback smoke test (no Vulkan loader)", + "Vulkan plugin-load smoke test (Mesa llvmpipe)", + "Publish dependency release"): + self.assertIn(step, workflow) + self.assertNotRegex(workflow, r"uses: [^\n]+@v\d+(?:\s|$)") + + def test_publish_is_safe_for_dikte_and_limited_to_reviewed_master(self): + workflow = WORKFLOW.read_text(encoding="utf-8") + self.assertGreaterEqual(workflow.count("persist-credentials: false"), 2) + self.assertIn("github.ref == 'refs/heads/master'", workflow) + self.assertIn("--prerelease", workflow) + self.assertIn("--latest=false", workflow) + self.assertIn("--verify-tag", workflow) + self.assertIn("refusing to replace existing tag", workflow) + self.assertIn("^[0-9]+\\.[0-9]+\\.[0-9]+$", workflow) + self.assertIn("^[0-9a-f]{40}$", workflow) + publish_script = workflow.split(" - name: Publish dependency release", 1)[1] + publish_script = publish_script.split(" run: |", 1)[1] + self.assertNotIn("${{ inputs.", publish_script) + + def test_bundle_ci_runs_when_its_installer_or_contract_changes(self): + workflow = WORKFLOW.read_text(encoding="utf-8") + for path in ("dikte/ggml.py", "tests/test_packaging.py", + "README.md", "README.tr.md"): + self.assertIn(f"- {path}", workflow) + + def test_the_validator_checks_tar_links_before_extraction(self): + validator = (PACKAGING / "validate-package.sh").read_text( + encoding="utf-8") + for check in ("member.issym()", "member.islnk()", "member.isdev()"): + self.assertIn(check, validator) + + @unittest.skipUnless(sys.platform == "linux" and shutil.which("bash"), + "Linux packaging test is unavailable") + def test_the_validator_rejects_an_escaping_symlink(self): + asset = "whisper-bin-ubuntu-vulkan-x64" + with tempfile.TemporaryDirectory() as temporary: + output = pathlib.Path(temporary) + archive = output / f"{asset}.tar.gz" + with tarfile.open(archive, "w:gz") as bundle: + link = tarfile.TarInfo(f"{asset}/whisper-server") + link.type = tarfile.SYMTYPE + link.linkname = "/etc/passwd" + bundle.addfile(link, io.BytesIO()) + digest = hashlib.sha256(archive.read_bytes()).hexdigest() + (output / f"{asset}.tar.gz.sha256").write_text( + f"{digest} {asset}.tar.gz\n", encoding="utf-8", + ) + checked = subprocess.run( + ["bash", PACKAGING / "validate-package.sh"], + env=os.environ | {"OUT_DIR": str(output)}, + capture_output=True, text=True, + ) + self.assertNotEqual(0, checked.returncode) + self.assertIn("unsafe symlink", checked.stderr) + + def test_the_validator_checks_elf_architecture_dependencies_and_paths(self): + validator = (PACKAGING / "validate-package.sh").read_text( + encoding="utf-8") + for check in ("Advanced Micro Devices X86-64", "unexpected DT_NEEDED", + "path.read_bytes()"): + self.assertIn(check, validator) + + def test_the_builder_and_its_downloads_are_pinned(self): + dockerfile = (PACKAGING / "Dockerfile.build").read_text( + encoding="utf-8") + self.assertRegex(dockerfile, r"FROM ubuntu@sha256:[0-9a-f]{64}") + self.assertIn("CMAKE_SHA256=", dockerfile) + self.assertIn("libvulkan-dev=", dockerfile) + self.assertIn("shaderc=", dockerfile) + key = (PACKAGING / "lunarg-signing-key-pub.asc").read_bytes() + self.assertEqual( + "aa1c3c29673140e77f0d6a9aaeed5d9b5621e305ead51c59fae4458bbb4df92b", + hashlib.sha256(key).hexdigest(), + ) + + def test_the_bundle_has_portable_dynamic_backends(self): + script = (PACKAGING / "build-package.sh").read_text( + encoding="utf-8") + for flag in ("GGML_BACKEND_DL=ON", "GGML_CPU_ALL_VARIANTS=ON", + "GGML_NATIVE=OFF", "GGML_OPENMP=OFF", + "GGML_VULKAN=ON"): + self.assertIn(flag, script) + self.assertIn("libggml-cpu*.so", script) + self.assertIn("libggml-vulkan.so", script) + + def test_the_dependency_release_matches_the_installer(self): + workflow = WORKFLOW.read_text(encoding="utf-8") + script = (PACKAGING / "build-package.sh").read_text( + encoding="utf-8") + self.assertEqual("whisper.cpp-v1.9.3", + ggml.MANAGED_WHISPER_RELEASE) + self.assertEqual("v1.9.3", ggml.MANAGED_WHISPER_VERSION) + self.assertIn("RELEASE_TAG: whisper.cpp-v${{ inputs.whisper_version }}", + workflow) + self.assertIn("WHISPER_VERSION:=1.9.3", script) + commit = "371b5a7561823ab2bb32142d2751e35e7534727b" + self.assertIn(f"WHISPER_COMMIT:={commit}", script) + self.assertIn(commit, workflow) + self.assertIn(ggml.MANAGED_WHISPER_VULKAN, workflow) + self.assertIn(ggml.MANAGED_WHISPER_SHA256, workflow) + + def test_the_bundle_carries_metadata_and_all_required_licenses(self): + script = (PACKAGING / "build-package.sh").read_text( + encoding="utf-8") + for name in ("BUILD-INFO.json", "SHA256SUMS", ".cdx.json"): + self.assertIn(name, script) + for name in ("cpp-httplib-MIT.txt", "nlohmann-json-MIT.txt"): + self.assertTrue((PACKAGING / "licenses" / name).is_file()) + + def _make_test_sbom(self): + with tempfile.TemporaryDirectory() as temporary: + root = pathlib.Path(temporary) + (root / "whisper-server").write_bytes(b"elf") + sbom = root / "whisper-bin-ubuntu-vulkan-x64.cdx.json" + environment = os.environ | { + "ROOT": str(root), + "VERSION": "1.9.3", + "COMMIT": "371b5a7561823ab2bb32142d2751e35e7534727b", + "EPOCH": "1787219223", + } + with sbom.open("w", encoding="utf-8") as output: + subprocess.run( + [sys.executable, PACKAGING / "make-sbom.py"], + env=environment, stdout=output, check=True, + ) + return json.loads(sbom.read_text(encoding="utf-8")), sbom.name + + def test_the_sbom_does_not_record_the_file_being_written(self): + document, sbom_name = self._make_test_sbom() + names = {component["name"] for component in document["components"]} + self.assertNotIn(sbom_name, names) + + def test_the_sbom_lists_ggml(self): + document, _ = self._make_test_sbom() + names = {component["name"] for component in document["components"]} + self.assertIn("ggml", names) + + +if __name__ == "__main__": + unittest.main()