diff --git a/.github/workflows/unsloth-sd-prebuilt.yml b/.github/workflows/unsloth-sd-prebuilt.yml
index 291ee0f7a..9452a95a9 100644
--- a/.github/workflows/unsloth-sd-prebuilt.yml
+++ b/.github/workflows/unsloth-sd-prebuilt.yml
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: MIT
# Copyright 2026-present the Unsloth AI Inc. team.
-name: Unsloth SD prebuilt (CPU/Apple/CUDA)
+name: Unsloth SD prebuilt (CPU/Apple/CUDA/Vulkan/ROCm)
# Build and publish OUR OWN stable-diffusion.cpp (sd-cli + sd-server) prebuilts for
# the platforms where the native engine is the FASTER choice: CPU (Linux/WSL/Windows),
@@ -19,6 +19,11 @@ name: Unsloth SD prebuilt (CPU/Apple/CUDA)
# build-unix -- macOS arm64 (Metal) + x64, Linux x64 + arm64 (matrix).
# build-windows-- Windows x64 (MSVC + Ninja).
# build-linux-cuda -- Linux x64, CUDA 12.8, best effort (continue-on-error).
+# build-linux-vulkan, build-linux-rocm, build-windows-vulkan
+# -- the AMD legs, same best-effort rule as CUDA. Without them an AMD host gets
+# no accelerated sd-cli from this mirror at all: the Studio installer falls
+# back to an upstream build that lacks the MiniMax-H3 fixes, and when that
+# one does not come up, to the CPU build (unsloth#8814).
# assemble -- fingerprint gate, sha256 + manifest, coverage gate, atomic
# draft->publish. If any of the five CPU/Apple legs fails, assemble
# is skipped and nothing is published (the Studio needs the full
@@ -615,9 +620,340 @@ jobs:
if-no-files-found: error
retention-days: 7
+ # Linux Vulkan. Works on every AMD card the Mesa RADV driver covers, RDNA 1 and 2 included,
+ # which ROCm does not, and needs nothing on the host beyond the Vulkan loader every AMD
+ # desktop already has.
+ build-linux-vulkan:
+ name: Linux-Ubuntu-22.04-x86_64-vulkan
+ needs: resolve
+ continue-on-error: true
+ if: ${{ needs.resolve.outputs.exists != 'true' || github.event_name == 'workflow_dispatch' }}
+ runs-on: ubuntu-22.04
+ steps:
+ - name: Checkout mirror (tooling)
+ uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
+ with:
+ path: tooling
+ fetch-depth: 1
+
+ - name: Download source @ ${{ needs.resolve.outputs.tag }}
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ name: ${{ needs.resolve.outputs.source_artifact }}
+ path: srcpkg
+ - name: Extract source
+ run: |
+ set -eux
+ mkdir -p src
+ tar -xzf "srcpkg/sd-source-${{ needs.resolve.outputs.tag }}.tar.gz" -C src
+
+ - name: Vulkan SDK pieces
+ run: |
+ sudo apt-get update -qq
+ sudo apt-get install -y -qq libvulkan-dev glslc spirv-headers
+
+ - name: Build sd-cli + sd-server (Vulkan)
+ working-directory: src
+ run: |
+ set -euo pipefail
+ cmake -B build \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DSD_BUILD_EXAMPLES=ON \
+ -DSD_SERVER_BUILD_FRONTEND=OFF \
+ -DSD_WEBP=OFF -DSD_WEBM=OFF \
+ -DGGML_NATIVE=OFF \
+ -DSD_VULKAN=ON
+ cmake --build build --config Release -j "$(nproc)" --target sd-cli sd-server
+
+ - name: Package bundle
+ env:
+ BIN_DIR: ${{ github.workspace }}/src/build/bin
+ OUT_DIR: ${{ github.workspace }}/dist
+ TAG: ${{ needs.resolve.outputs.tag }}
+ LABEL: Linux-Ubuntu-22.04-x86_64-vulkan
+ COMMIT: ${{ needs.resolve.outputs.commit }}
+ SOURCE_REPO: ${{ github.repository }}
+ LICENSE_FILE: ${{ github.workspace }}/src/LICENSE
+ run: python3 tooling/scripts/unsloth/package_bundle.py
+
+ - name: Upload bundle
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: sd-${{ needs.resolve.outputs.tag }}-bin-Linux-Ubuntu-22.04-x86_64-vulkan
+ path: dist/sd-${{ needs.resolve.outputs.tag }}-bin-Linux-Ubuntu-22.04-x86_64-vulkan.zip
+ if-no-files-found: error
+ retention-days: 7
+
+ # Linux ROCm, built against AMD's TheRock wheels like upstream's build.yml, with one
+ # difference that decides whether it runs at all: the ROCm userspace the binary was linked
+ # against ships in the bundle. Upstream's ROCm zip does not, so it dies on a host whose
+ # ROCm is a different version or absent, which is how the unsloth#8814 reporter's install
+ # went from "installed" to "load_failed" in one second. Same shape as unslothai/llama.cpp's
+ # ROCm leg: libamdhip64, hipblas, rocblas and rocblas/library beside the binaries, rpath
+ # $ORIGIN, so only the kernel driver has to come from the host.
+ build-linux-rocm:
+ name: Linux-Ubuntu-24.04-x86_64-rocm-7.14.0
+ needs: resolve
+ continue-on-error: true
+ if: ${{ needs.resolve.outputs.exists != 'true' || github.event_name == 'workflow_dispatch' }}
+ runs-on: ubuntu-24.04
+ env:
+ ROCM_VERSION: "7.14.0"
+ # Consumer parts, upstream's Windows list. CDNA hosts run diffusers and never reach this.
+ GPU_TARGETS: "gfx1010;gfx1011;gfx1012;gfx1030;gfx1031;gfx1032;gfx1033;gfx1034;gfx1035;gfx1036;gfx1100;gfx1101;gfx1102;gfx1103;gfx1150;gfx1151;gfx1152;gfx1153;gfx1200;gfx1201"
+ # rocm-sdk re-expands clang from the wheel every run, so its mtime always differs and
+ # the default compiler_check=mtime misses every object.
+ CCACHE_COMPILERCHECK: content
+ steps:
+ - name: Checkout mirror (tooling)
+ uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
+ with:
+ path: tooling
+ fetch-depth: 1
+
+ - name: Free disk for the ROCm wheels
+ run: |
+ set -eux
+ sudo rm -rf /usr/local/lib/android /usr/share/dotnet /opt/ghc /usr/local/.ghcup \
+ /usr/local/share/boost /usr/share/swift /opt/hostedtoolcache
+ docker system prune -af || true
+ df -h /
+
+ - name: Download source @ ${{ needs.resolve.outputs.tag }}
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ name: ${{ needs.resolve.outputs.source_artifact }}
+ path: srcpkg
+ - name: Extract source
+ run: |
+ set -eux
+ mkdir -p src
+ tar -xzf "srcpkg/sd-source-${{ needs.resolve.outputs.tag }}.tar.gz" -C src
+
+ - name: Install ROCm (TheRock wheels)
+ run: |
+ set -euo pipefail
+ sudo apt-get update -qq && sudo apt-get install -y -qq ninja-build patchelf
+ python3 -m venv .rocm
+ . .rocm/bin/activate
+ python -m pip install -q --upgrade pip
+ python -m pip install -q --index-url https://repo.amd.com/rocm/whl-multi-arch/ "rocm[libraries,devel]==${ROCM_VERSION}"
+ ROCM_PATH="$(rocm-sdk path --root)"
+ {
+ echo "ROCM_PATH=$ROCM_PATH"
+ echo "HIP_PATH=$ROCM_PATH"
+ echo "CMAKE_PREFIX_PATH=$(rocm-sdk path --cmake)"
+ echo "LD_LIBRARY_PATH=$ROCM_PATH/lib:${LD_LIBRARY_PATH:-}"
+ } >> "$GITHUB_ENV"
+ echo "$(rocm-sdk path --bin)" >> "$GITHUB_PATH"
+ echo "$(pwd)/.rocm/bin" >> "$GITHUB_PATH"
+
+ - name: ccache key
+ id: cckey
+ run: echo "archs=$(echo "$GPU_TARGETS" | sha256sum | cut -c1-8)" >> "$GITHUB_OUTPUT"
+
+ - name: ccache
+ uses: hendrikmuhs/ccache-action@d62db5f07c26379fc4b4e0916f098a92573c3b03 # v1.2.23
+ with:
+ key: sd-rocm-${{ env.ROCM_VERSION }}-${{ steps.cckey.outputs.archs }}-${{ needs.resolve.outputs.tag }}
+ restore-keys: |
+ sd-rocm-${{ env.ROCM_VERSION }}-${{ steps.cckey.outputs.archs }}
+ append-timestamp: false
+ variant: ccache
+ max-size: 2G
+ save: false
+
+ - name: Build sd-cli + sd-server (ROCm)
+ working-directory: src
+ run: |
+ set -euo pipefail
+ cmake -B build -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DCMAKE_HIP_COMPILER="$(hipconfig -l)/clang" \
+ -DCMAKE_HIP_FLAGS="-mllvm --amdgpu-unroll-threshold-local=600" \
+ -DSD_BUILD_EXAMPLES=ON \
+ -DSD_SERVER_BUILD_FRONTEND=OFF \
+ -DSD_WEBP=OFF -DSD_WEBM=OFF \
+ -DGGML_NATIVE=OFF \
+ -DSD_HIPBLAS=ON \
+ -DHIP_PLATFORM=amd \
+ -DGPU_TARGETS="$GPU_TARGETS" \
+ -DCMAKE_C_COMPILER_LAUNCHER=ccache \
+ -DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
+ -DCMAKE_HIP_COMPILER_LAUNCHER=ccache
+ cmake --build build --config Release -j "$(nproc)" --target sd-cli sd-server
+ ccache --show-stats || true
+
+ - name: Bundle the ROCm runtime beside the binaries
+ run: |
+ set -euo pipefail
+ BIN="${GITHUB_WORKSPACE}/src/build/bin"
+ # Every library the binaries resolve from inside the wheel tree. ldd already walks
+ # the closure, so one pass over the binaries and the shared libs beside them is the
+ # whole list.
+ ldd "$BIN"/sd-cli "$BIN"/sd-server "$BIN"/*.so 2>/dev/null \
+ | awk -v r="$ROCM_PATH/" 'index($3, r) == 1 { print $3 }' | sort -u > "$RUNNER_TEMP/rocm-libs.txt"
+ if [ ! -s "$RUNNER_TEMP/rocm-libs.txt" ]; then
+ echo "ERROR: nothing resolved from $ROCM_PATH; is this a HIP build?" >&2
+ exit 1
+ fi
+ while read -r so; do cp -L "$so" "$BIN/$(basename "$so")"; done < "$RUNNER_TEMP/rocm-libs.txt"
+ # rocBLAS loads its Tensile kernels from
/rocblas/library at
+ # runtime; without that directory every GEMM fails after the load succeeded.
+ mkdir -p "$BIN/rocblas"
+ cp -r "$ROCM_PATH/lib/rocblas/library" "$BIN/rocblas/"
+ if ls "$BIN"/libhipblaslt.so* >/dev/null 2>&1 && [ -d "$ROCM_PATH/lib/hipblaslt/library" ]; then
+ mkdir -p "$BIN/hipblaslt"
+ cp -r "$ROCM_PATH/lib/hipblaslt/library" "$BIN/hipblaslt/"
+ fi
+ for f in "$BIN"/sd-cli "$BIN"/sd-server "$BIN"/*.so*; do
+ [ -f "$f" ] && patchelf --set-rpath '$ORIGIN' "$f"
+ done
+ echo "bundled $(wc -l < "$RUNNER_TEMP/rocm-libs.txt") ROCm libraries"
+ du -sh "$BIN"
+ # The proof the bundle stands alone: resolve with the wheel tree hidden.
+ if LD_LIBRARY_PATH= ldd "$BIN/sd-cli" | grep -i 'not found'; then
+ echo "ERROR: unresolved libraries after bundling" >&2
+ exit 1
+ fi
+
+ - name: Package bundle
+ env:
+ BIN_DIR: ${{ github.workspace }}/src/build/bin
+ OUT_DIR: ${{ github.workspace }}/dist
+ TAG: ${{ needs.resolve.outputs.tag }}
+ LABEL: Linux-Ubuntu-24.04-x86_64-rocm-7.14.0
+ COMMIT: ${{ needs.resolve.outputs.commit }}
+ SOURCE_REPO: ${{ github.repository }}
+ LICENSE_FILE: ${{ github.workspace }}/src/LICENSE
+ run: python3 tooling/scripts/unsloth/package_bundle.py
+
+ - name: Upload bundle
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: sd-${{ needs.resolve.outputs.tag }}-bin-Linux-Ubuntu-24.04-x86_64-rocm-7.14.0
+ path: dist/sd-${{ needs.resolve.outputs.tag }}-bin-Linux-Ubuntu-24.04-x86_64-rocm-7.14.0.zip
+ if-no-files-found: error
+ retention-days: 7
+
+ - name: Evict stale ccache files
+ if: ${{ !cancelled() }}
+ continue-on-error: true
+ run: ccache --evict-older-than 14d
+
+ - name: Save ccache
+ if: ${{ always() }}
+ uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
+ with:
+ path: ${{ github.workspace }}/.ccache
+ key: ccache-sd-rocm-${{ env.ROCM_VERSION }}-${{ steps.cckey.outputs.archs }}-${{ needs.resolve.outputs.tag }}-
+
+ # Windows Vulkan. The CPU leg with the LunarG SDK and -DSD_VULKAN=ON; signed and verified
+ # the same way, since Smart App Control judges these PEs too.
+ build-windows-vulkan:
+ name: win-vulkan-x64
+ needs: resolve
+ continue-on-error: true
+ if: ${{ needs.resolve.outputs.exists != 'true' || github.event_name == 'workflow_dispatch' }}
+ runs-on: windows-2022
+ environment: release-signing
+ env:
+ VULKAN_VERSION: 1.4.328.1
+ steps:
+ - name: Checkout mirror (tooling)
+ uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
+ with:
+ path: tooling
+ fetch-depth: 1
+
+ - name: Download source @ ${{ needs.resolve.outputs.tag }}
+ uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
+ with:
+ name: ${{ needs.resolve.outputs.source_artifact }}
+ path: srcpkg
+
+ - name: Extract source
+ shell: bash
+ run: |
+ set -eux
+ mkdir -p src
+ tar -xzf "srcpkg/sd-source-${{ needs.resolve.outputs.tag }}.tar.gz" -C src
+
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
+ with:
+ python-version: "3.11"
+ - name: Install Ninja
+ run: choco install ninja --no-progress
+ - name: Install Vulkan SDK
+ shell: pwsh
+ run: |
+ curl.exe -o $env:RUNNER_TEMP/VulkanSDK-Installer.exe -L "https://sdk.lunarg.com/sdk/download/${env:VULKAN_VERSION}/windows/vulkansdk-windows-X64-${env:VULKAN_VERSION}.exe"
+ & "$env:RUNNER_TEMP\VulkanSDK-Installer.exe" --accept-licenses --default-answer --confirm-command install
+ Add-Content $env:GITHUB_ENV "VULKAN_SDK=C:\VulkanSDK\${env:VULKAN_VERSION}"
+ Add-Content $env:GITHUB_PATH "C:\VulkanSDK\${env:VULKAN_VERSION}\bin"
+ - name: Setup MSVC
+ uses: ilammy/msvc-dev-cmd@0b201ec74fa43914dc39ae48a89fd1d8cb592756 # v1.13.0
+
+ - name: Build sd-cli + sd-server (Vulkan)
+ shell: pwsh
+ working-directory: src
+ run: |
+ cmake -S . -B build -G Ninja `
+ -DCMAKE_BUILD_TYPE=Release `
+ -DCMAKE_CXX_FLAGS='/bigobj' `
+ -DSD_BUILD_EXAMPLES=ON `
+ -DSD_SERVER_BUILD_FRONTEND=OFF `
+ -DSD_WEBP=OFF -DSD_WEBM=OFF `
+ -DGGML_NATIVE=OFF `
+ -DSD_VULKAN=ON
+ cmake --build build --config Release -j 3 --target sd-cli sd-server
+
+ - name: Sign Windows binaries
+ uses: ./tooling/.github/actions/sign-windows
+ with:
+ path: src/build/bin
+ azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
+ azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
+ azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
+ azure-account: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
+ azure-certificate-profile: ${{ secrets.AZURE_CERTIFICATE_PROFILE_NAME }}
+
+ - name: Package bundle
+ shell: pwsh
+ env:
+ BIN_DIR: ${{ github.workspace }}/src/build/bin
+ OUT_DIR: ${{ github.workspace }}/dist
+ TAG: ${{ needs.resolve.outputs.tag }}
+ LABEL: win-vulkan-x64
+ COMMIT: ${{ needs.resolve.outputs.commit }}
+ SOURCE_REPO: ${{ github.repository }}
+ LICENSE_FILE: ${{ github.workspace }}/src/LICENSE
+ run: python tooling/scripts/unsloth/package_bundle.py
+
+ - name: Verify every PE in the bundle is signed
+ shell: pwsh
+ run: |
+ $ErrorActionPreference = 'Continue'
+ $zips = @(Get-ChildItem dist -Filter *.zip -ErrorAction SilentlyContinue |
+ ForEach-Object { $_.FullName })
+ if ($zips.Count -eq 0) {
+ Write-Host '::error::no bundle in dist/; packaging produced nothing to verify'
+ exit 1
+ }
+ & tooling/.github/scripts/assert-windows-bundle-signed.ps1 -Path $zips
+ exit $LASTEXITCODE
+
+ - name: Upload bundle
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: sd-${{ needs.resolve.outputs.tag }}-bin-win-vulkan-x64
+ path: dist/sd-${{ needs.resolve.outputs.tag }}-bin-win-vulkan-x64.zip
+ if-no-files-found: error
+ retention-days: 7
+
assemble:
name: Assemble + publish
- needs: [resolve, build-unix, build-windows, build-linux-cuda]
+ needs: [resolve, build-unix, build-windows, build-linux-cuda, build-linux-vulkan, build-linux-rocm, build-windows-vulkan]
# Consumed by `reclaim` to tell "these bundles are now release assets" from
# "nothing will ever read these". Set only after draft=false lands.
outputs:
@@ -671,7 +1007,7 @@ jobs:
--publish-repo "$GITHUB_REPOSITORY"
ls -la dist
- - name: Coverage gate (all 5 CPU/Apple assets present)
+ - name: Coverage gate (all 5 CPU/Apple assets present; CUDA, Vulkan and ROCm are best effort)
run: |
set -eu
TAG='${{ needs.resolve.outputs.tag }}'
@@ -718,7 +1054,7 @@ jobs:
AHEAD='${{ needs.resolve.outputs.ahead }}'
COMMIT='${{ needs.resolve.outputs.commit }}'
REPO="$GITHUB_REPOSITORY"
- NOTES="Automated Unsloth stable-diffusion.cpp CPU + Apple prebuild (sd-cli + sd-server), built from [\`${COMMIT}\`](https://github.com/${REPO}/commit/${COMMIT}) in this repository. GPU hosts use diffusers/torch; this native engine targets CPU (Linux/WSL/Windows) and Apple (Metal)."
+ NOTES="Automated Unsloth stable-diffusion.cpp prebuild (sd-cli + sd-server), built from [\`${COMMIT}\`](https://github.com/${REPO}/commit/${COMMIT}) in this repository. CPU (Linux/WSL/Windows) and Apple (Metal) always; the CUDA, Vulkan and ROCm assets ship when their leg built."
PINS='${{ needs.resolve.outputs.pins }}'
if [ "${AHEAD:-0}" -gt 0 ]; then
NOTES="${NOTES}"$'\n\n'"Not a stock build: the tree is ${AHEAD} commits past the ${UPSTREAM_TAG} release it descends from, which is what the \`-u\` suffix on the tag marks. \`sd-prebuilt-manifest.json\` records the commit."
diff --git a/scripts/unsloth/package_bundle.py b/scripts/unsloth/package_bundle.py
index 6c87db466..9684b880a 100755
--- a/scripts/unsloth/package_bundle.py
+++ b/scripts/unsloth/package_bundle.py
@@ -35,6 +35,10 @@
# (static builds usually have none; Metal / a shared ggml can add a few).
_BINARIES = ("sd-cli", "sd-server", "sd-cli.exe", "sd-server.exe")
_LIB_SUFFIXES = (".dylib", ".so", ".dll", ".metal", ".metallib")
+# Kernel-library trees a ROCm build loads at runtime, relative to the library that owns them
+# (/rocblas/library). Shipped with their layout intact; everything else
+# is flattened.
+_KERNEL_TREES = ("rocblas", "hipblaslt")
_FINGERPRINT = "Compiled by the Unsloth team"
@@ -57,15 +61,28 @@ def _is_runtime_lib(name: str) -> bool:
return ".so." in lowered
-def _collect(bin_dir: Path) -> list[Path]:
- """The binaries + sibling runtime libs to ship. Recurse so a nested bin/ layout
- (some generators emit build/bin/, some build/bin/Release/) is still captured."""
- found: list[Path] = []
+def _kernel_tree_path(p: Path, bin_dir: Path) -> Path | None:
+ """``rocblas/library/`` for a file inside a kernel tree, else None."""
+ rel = p.relative_to(bin_dir)
+ for i, part in enumerate(rel.parts[:-1]):
+ if part in _KERNEL_TREES and rel.parts[i + 1 : i + 2] == ("library",):
+ return Path(*rel.parts[i:])
+ return None
+
+
+def _collect(bin_dir: Path) -> list[tuple[Path, str]]:
+ """(file, archive-relative name) for the binaries + sibling runtime libs to ship. Recurse
+ so a nested bin/ layout (some generators emit build/bin/, some build/bin/Release/) is still
+ captured. Kernel trees keep their layout; everything else lands flat."""
+ found: list[tuple[Path, str]] = []
for p in sorted(bin_dir.rglob("*")):
if not p.is_file():
continue
- if p.name in _BINARIES or _is_runtime_lib(p.name):
- found.append(p)
+ tree = _kernel_tree_path(p, bin_dir)
+ if tree is not None:
+ found.append((p, tree.as_posix()))
+ elif p.name in _BINARIES or _is_runtime_lib(p.name):
+ found.append((p, p.name))
return found
@@ -83,11 +100,11 @@ def main() -> int:
return 2
files = _collect(bin_dir)
- have_cli = any(f.name in ("sd-cli", "sd-cli.exe") for f in files)
+ have_cli = any(f.name in ("sd-cli", "sd-cli.exe") for f, _ in files)
if not have_cli:
print(f"package_bundle: no sd-cli under {bin_dir}; refusing to package", file = sys.stderr)
return 1
- have_server = any(f.name in ("sd-server", "sd-server.exe") for f in files)
+ have_server = any(f.name in ("sd-server", "sd-server.exe") for f, _ in files)
out_dir.mkdir(parents = True, exist_ok = True)
stem = f"sd-{tag}-bin-{label}"
@@ -108,10 +125,11 @@ def main() -> int:
# Deterministic-ish: sort members; drop the archive if it already exists.
zip_path.unlink(missing_ok = True)
with zipfile.ZipFile(zip_path, "w", compression = zipfile.ZIP_DEFLATED) as zf:
- for f in files:
- # Flatten under the named top-level dir; keep just the basename so the
- # binaries sit at sd--bin-