Skip to content

Commit 163e578

Browse files
committed
release : publish macOS CLI binaries (arm64, x64, universal)
Adds two jobs to release.yml. macos-cpu builds arm64 on macos-26 and x86_64 on macos-15-intel; macos-universal lipo-fuses the two trees into a universal archive. Three tarballs, each smoke-tested from the extracted archive on its own runner before upload, so the test covers @loader_path resolving the dylibs once the tree has left the build machine. Uses GGML_BACKEND_DL with GGML_CPU_ALL_VARIANTS, one CPU backend per micro-architecture selected at load time. GGML_NATIVE has to stay off for the per-variant flags to apply at all; with it off the Apple variants (apple_m1, apple_m2_m3, apple_m4) cover dotprod, i8mm and SME in a single archive instead of pinning the build to whichever CPU the runner happened to have. The CPU and Metal modules are therefore one-sided, Metal being arm64 only. The fuse walks the union of both trees: matched files are lipo'd, one-sided backends are copied through thin, and anything else that is one-sided fails the job rather than vanishing from the archive. A verify step then requires every non-backend Mach-O to carry both slices, checked with lipo -archs. The release job writes a single SHA256SUMS manifest covering every platform. The asset upload filter only accepted .zip and .tar.gz, so SHA256SUMS is added to it explicitly. README gains a section on the prebuilt macOS archives, including the Gatekeeper quarantine flag, since the archives are unsigned.
1 parent eacbd82 commit 163e578

2 files changed

Lines changed: 251 additions & 1 deletion

File tree

.github/workflows/release.yml

Lines changed: 227 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,219 @@ jobs:
136136
path: whisper-bin-ubuntu-${{ matrix.build }}.tar.gz
137137
name: whisper-bin-ubuntu-${{ matrix.build }}.tar.gz
138138

139+
macos-cpu:
140+
runs-on: ${{ matrix.os }}
141+
needs: determine-tag
142+
if: ${{ needs.determine-tag.outputs.should_release == 'true' }}
143+
144+
strategy:
145+
matrix:
146+
include:
147+
- build: arm64
148+
os: macos-26
149+
defines: "-DGGML_METAL_EMBED_LIBRARY=ON"
150+
# macos-15-intel is the last x86_64 image available on Actions, and it is
151+
# scheduled to go away in August 2027 (actions/runner-images#13046).
152+
# Metal is off here because the hosted Intel runners have no GPU, which is
153+
# the same reason ggml-org/llama.cpp disables it for its x64 macOS build.
154+
- build: x64
155+
os: macos-15-intel
156+
defines: "-DGGML_METAL=OFF"
157+
158+
steps:
159+
- name: Clone
160+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
161+
162+
- name: ccache
163+
uses: ggml-org/ccache-action@v1.2.21
164+
with:
165+
key: release-${{ matrix.os }}-cpu
166+
evict-old-files: 1d
167+
168+
# Same shape as ubuntu-cpu: backends built as loadable dylibs, one CPU backend per
169+
# micro-architecture, selected at load time. GGML_NATIVE has to be off for
170+
# GGML_CPU_ALL_VARIANTS to drive the per-variant flags at all; left on, it would
171+
# instead compile a single backend for whichever CPU the runner happened to have,
172+
# which on a refreshed arm64 image means -mcpu=native+i8mm+sme and a binary that
173+
# will not start on an M1. The Apple variants are apple_m1 (dotprod),
174+
# apple_m2_m3 (+i8mm) and apple_m4 (+sme), so one archive covers every Mac
175+
# without giving up the newer instructions on the machines that have them.
176+
- name: Build
177+
run: |
178+
cmake -B build \
179+
-DCMAKE_BUILD_TYPE=Release \
180+
-DCMAKE_INSTALL_RPATH='@loader_path' \
181+
-DCMAKE_BUILD_WITH_INSTALL_RPATH=ON \
182+
-DCMAKE_OSX_DEPLOYMENT_TARGET=13.3 \
183+
-DGGML_BACKEND_DL=ON \
184+
-DGGML_CPU_ALL_VARIANTS=ON \
185+
-DGGML_NATIVE=OFF \
186+
-DWHISPER_BUILD_IS_DEV=${{ env.WHISPER_BUILD_IS_DEV }} \
187+
${{ matrix.defines }}
188+
cmake --build build --config Release -j $(sysctl -n hw.logicalcpu)
189+
190+
- name: Pack artifacts
191+
run: |
192+
cp LICENSE ./build/bin/
193+
tar -czvf whisper-bin-macos-${{ matrix.build }}.tar.gz \
194+
-s ",^\.,whisper-bin-macos-${{ matrix.build }}," \
195+
-C ./build/bin .
196+
197+
# Runs from an extracted copy of the archive rather than from build/bin, so the
198+
# test covers what actually ships: @loader_path resolving the dylibs next to the
199+
# binary once the tree has moved off the machine that built it.
200+
- name: Smoke test
201+
run: |
202+
mkdir -p "${RUNNER_TEMP}/unpacked"
203+
tar -xzf whisper-bin-macos-${{ matrix.build }}.tar.gz \
204+
-C "${RUNNER_TEMP}/unpacked" --strip-components 1
205+
"${RUNNER_TEMP}/unpacked/whisper-cli" \
206+
-m models/for-tests-ggml-base.en.bin \
207+
-f samples/jfk.wav
208+
209+
- name: Upload artifacts
210+
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
211+
with:
212+
path: whisper-bin-macos-${{ matrix.build }}.tar.gz
213+
name: whisper-bin-macos-${{ matrix.build }}.tar.gz
214+
215+
# Fuses the two thin builds above into one universal tree. Everything that exists on
216+
# both sides is lipo'd. The CPU and Metal backends are built per micro-architecture and
217+
# so only ever exist on one side; those are copied through thin, which is what
218+
# GGML_BACKEND_DL expects - the loader walks every libggml-*.so next to the binary
219+
# and skips the ones it cannot load, so the backends belonging to the other
220+
# architecture are simply never used. Anything else that is one-sided is a packaging
221+
# bug and fails the job rather than disappearing from the archive silently.
222+
macos-universal:
223+
runs-on: macos-26
224+
needs:
225+
- determine-tag
226+
- macos-cpu
227+
if: ${{ needs.determine-tag.outputs.should_release == 'true' }}
228+
229+
steps:
230+
- name: Clone
231+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
232+
233+
- name: Download arm64 artifact
234+
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
235+
with:
236+
name: whisper-bin-macos-arm64.tar.gz
237+
path: ./thin
238+
239+
- name: Download x64 artifact
240+
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
241+
with:
242+
name: whisper-bin-macos-x64.tar.gz
243+
path: ./thin
244+
245+
- name: Unpack
246+
run: |
247+
mkdir -p arm64 x64
248+
tar -xzf ./thin/whisper-bin-macos-arm64.tar.gz -C arm64 --strip-components 1
249+
tar -xzf ./thin/whisper-bin-macos-x64.tar.gz -C x64 --strip-components 1
250+
251+
(cd arm64 && find . \( -type f -o -type l \) | sed 's,^\./,,') | sort > "${RUNNER_TEMP}/arm64.list"
252+
(cd x64 && find . \( -type f -o -type l \) | sed 's,^\./,,') | sort > "${RUNNER_TEMP}/x64.list"
253+
sort -u "${RUNNER_TEMP}/arm64.list" "${RUNNER_TEMP}/x64.list" > "${RUNNER_TEMP}/all.list"
254+
255+
- name: Fuse
256+
run: |
257+
# GGML_BACKEND_DL emits loadable backends as libggml-*.so, one per
258+
# micro-architecture, so they have no counterpart in the other tree.
259+
# Everything else has to be present on both sides.
260+
is_backend() {
261+
case "$1" in
262+
libggml-*.so) return 0 ;;
263+
*) return 1 ;;
264+
esac
265+
}
266+
267+
exists() { [ -e "$1" ] || [ -L "$1" ]; }
268+
269+
rc=0
270+
while IFS= read -r f; do
271+
mkdir -p "universal/$(dirname "$f")"
272+
273+
if exists "arm64/$f" && exists "x64/$f"; then
274+
if [ -L "arm64/$f" ]; then
275+
cp -a "arm64/$f" "universal/$f"
276+
elif file -b "arm64/$f" | grep -q "Mach-O"; then
277+
lipo -create "arm64/$f" "x64/$f" -output "universal/$f"
278+
else
279+
cp -a "arm64/$f" "universal/$f"
280+
fi
281+
elif is_backend "$(basename "$f")"; then
282+
if exists "arm64/$f"; then
283+
cp -a "arm64/$f" "universal/$f"
284+
else
285+
cp -a "x64/$f" "universal/$f"
286+
fi
287+
else
288+
echo "::error::${f} exists for only one architecture and is not a backend"
289+
rc=1
290+
fi
291+
done < "${RUNNER_TEMP}/all.list"
292+
293+
exit $rc
294+
295+
# lipo -archs rather than parsing lipo -info, so the check does not depend on the
296+
# order the architectures happen to be listed in.
297+
- name: Verify
298+
run: |
299+
rc=0
300+
while IFS= read -r f; do
301+
if ! { [ -e "universal/$f" ] || [ -L "universal/$f" ]; }; then
302+
echo "::error::${f} is missing from the fused tree"
303+
rc=1
304+
continue
305+
fi
306+
307+
[ -L "universal/$f" ] && continue
308+
file -b "universal/$f" | grep -q "Mach-O" || continue
309+
310+
archs="$(lipo -archs "universal/$f")"
311+
case "$(basename "$f")" in
312+
libggml-*.so)
313+
printf ' backend %-44s %s\n' "$f" "$archs"
314+
;;
315+
*)
316+
if [[ " ${archs} " == *" arm64 "* && " ${archs} " == *" x86_64 "* ]]; then
317+
printf ' universal %-44s %s\n' "$f" "$archs"
318+
else
319+
echo "::error::${f} is not universal (${archs})"
320+
rc=1
321+
fi
322+
;;
323+
esac
324+
done < "${RUNNER_TEMP}/all.list"
325+
326+
exit $rc
327+
328+
- name: Pack artifacts
329+
run: |
330+
tar -czvf whisper-bin-macos-universal.tar.gz \
331+
-s ",^\.,whisper-bin-macos-universal," \
332+
-C ./universal .
333+
334+
# Only the arm64 slice can be exercised here, since the hosted arm64 runners do not
335+
# have Rosetta. The x86_64 slice is the same build the x64 job above already ran
336+
# natively, so it is covered there.
337+
- name: Smoke test
338+
run: |
339+
mkdir -p "${RUNNER_TEMP}/unpacked"
340+
tar -xzf whisper-bin-macos-universal.tar.gz \
341+
-C "${RUNNER_TEMP}/unpacked" --strip-components 1
342+
"${RUNNER_TEMP}/unpacked/whisper-cli" \
343+
-m models/for-tests-ggml-base.en.bin \
344+
-f samples/jfk.wav
345+
346+
- name: Upload artifacts
347+
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
348+
with:
349+
path: whisper-bin-macos-universal.tar.gz
350+
name: whisper-bin-macos-universal.tar.gz
351+
139352
windows:
140353
runs-on: windows-latest
141354
needs: determine-tag
@@ -596,6 +809,8 @@ jobs:
596809
needs:
597810
- determine-tag
598811
- ubuntu-cpu
812+
- macos-cpu
813+
- macos-universal
599814
- ios-xcode-build
600815
- windows
601816
- windows-blas
@@ -629,6 +844,17 @@ jobs:
629844
with:
630845
subject-path: 'release/*'
631846

847+
# One manifest for the whole release rather than a .sha256 next to each archive:
848+
# it adds a single asset instead of one per artifact, and it can be checked in one
849+
# go with `shasum -a 256 -c SHA256SUMS`. Written after the attestation so the
850+
# attested subjects stay exactly the artifacts themselves.
851+
- name: Checksum release artifacts
852+
run: |
853+
cd ./release
854+
sha256sum * > "${RUNNER_TEMP}/SHA256SUMS"
855+
mv "${RUNNER_TEMP}/SHA256SUMS" .
856+
cat SHA256SUMS
857+
632858
- name: Create and push git tag
633859
run: |
634860
TAG="${{ needs.determine-tag.outputs.tag_name }}"
@@ -658,7 +884,7 @@ jobs:
658884
const fs = require('fs');
659885
const release_id = '${{ steps.create_release.outputs.id }}';
660886
for (let file of await fs.readdirSync('./release')) {
661-
if (path.extname(file) === '.zip' || file.endsWith('.tar.gz')) {
887+
if (path.extname(file) === '.zip' || file.endsWith('.tar.gz') || file === 'SHA256SUMS') {
662888
console.log('uploadReleaseAsset', file);
663889
await github.repos.uploadReleaseAsset({
664890
owner: context.repo.owner,

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,30 @@ cmake --build build -j --config Release
9191
./build/bin/whisper-cli -f samples/jfk.wav
9292
```
9393

94+
### Prebuilt macOS binaries
95+
96+
Releases also ship `whisper-cli` for macOS on the
97+
[releases page](https://github.com/ggml-org/whisper.cpp/releases), as `arm64`, `x64` and
98+
`universal` archives, so no build step is needed:
99+
100+
```bash
101+
# verify the download against the SHA256SUMS asset published with the release
102+
grep whisper-bin-macos-arm64.tar.gz SHA256SUMS | shasum -a 256 -c
103+
104+
tar -xzf whisper-bin-macos-arm64.tar.gz
105+
cd whisper-bin-macos-arm64
106+
./whisper-cli -m /path/to/ggml-base.en.bin -f /path/to/audio.wav
107+
```
108+
109+
The archives are neither signed nor notarized. macOS marks anything downloaded through a
110+
browser as quarantined, and Finder carries that mark onto the files it extracts, so
111+
Gatekeeper refuses to run them. Extracting with `tar`, as above, avoids this; otherwise
112+
clear the flag:
113+
114+
```bash
115+
xattr -dr com.apple.quarantine whisper-bin-macos-arm64
116+
```
117+
94118
---
95119

96120
For a quick demo, simply run `make base.en`.

0 commit comments

Comments
 (0)