From 3a8692ff71b31ebcde0e2a57aa7f1fd046506bec Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Mon, 27 Jul 2026 04:18:16 +0000 Subject: [PATCH] backend(audio-cpp): build for Darwin Metal Bespoke C++ Darwin path like ds4 and privacy-filter: an includeDarwin matrix entry, a backends/audio-cpp-darwin make target, a gated workflow step, and the metal image entries plus metal/metal-darwin-arm64 capability keys in the backend gallery. The build script deliberately does NOT reassemble the package the way privacy-filter-darwin.sh does. It runs the backend's own `make package` and copies the result, so the Darwin package keeps the root-level layout the Linux one has: grpc-server, run.sh, the ggml objects and assets/ in one directory, with lib/ for the dylib closure. Hand-assembling would drop assets/, and assets/ is what makes the bundled: model paths resolve with nothing downloaded. The dylib walk is a full transitive closure rather than the single level ds4 and llama-cpp do, because Homebrew's grpc++ pulls libgrpc, abseil, upb, cares and OpenSSL that grpc-server does not link itself, and a level-1 walk ships a package that only works on a machine that already has Homebrew grpc. Two fixes folded in, both in the backend Makefile: - an EMPTY CUDA_MAJOR_VERSION fell through to the CUDA 12 architecture list, which contains 120a-real and so needs nvcc >= 12.8. A local BUILD_TYPE=cublas build on a 12.0-12.7 host failed to compile where upstream's documented default (native) worked. EMPTY now maps to native, 12 and 13 keep their lists, and any other non-empty value is an error on cublas builds. CI always passes a major, so CI is unaffected. - the Darwin branch now points CMake at Homebrew's keg-only libomp. AppleClang ships no OpenMP runtime and nothing is symlinked into /opt/homebrew, so FindOpenMP finds neither the library nor the header, and audio.cpp calls find_package(OpenMP REQUIRED) whenever ENGINE_ENABLE_OPENMP is on. Without the hint the macOS build would have died at configure time. If the keg is absent the build disables OpenMP instead of failing. Assisted-by: Claude:claude-opus-5 [Claude Code] Signed-off-by: Ettore Di Giacinto --- .github/backend-matrix.yml | 16 +- .github/workflows/backend_build_darwin.yml | 13 +- Makefile | 6 +- backend/cpp/audio-cpp/Makefile | 43 ++++- backend/cpp/audio-cpp/package.sh | 14 +- backend/index.yaml | 25 ++- scripts/build/audio-cpp-darwin.sh | 178 +++++++++++++++++++++ scripts/lib/backend-filter.mjs | 23 +-- 8 files changed, 286 insertions(+), 32 deletions(-) create mode 100755 scripts/build/audio-cpp-darwin.sh diff --git a/.github/backend-matrix.yml b/.github/backend-matrix.yml index 2ce1b8c27..f95311be8 100644 --- a/.github/backend-matrix.yml +++ b/.github/backend-matrix.yml @@ -3165,8 +3165,8 @@ include: # builder image: this is the ds4 shape, not the llama-cpp one. # # No ROCm entry: upstream has no HIP configuration. No CUDA arm64 or L4T - # entry: upstream documents and validates CUDA on x86 only. Darwin/Metal - # lands with scripts/build/audio-cpp-darwin.sh, which does not exist yet. + # entry: upstream documents and validates CUDA on x86 only. Darwin/Metal is in + # the includeDarwin matrix below, built by scripts/build/audio-cpp-darwin.sh. # # No vulkan entry either, though Dockerfile.audio-cpp and the backend Makefile # both handle BUILD_TYPE=vulkan for local builds. Every other vulkan backend @@ -6291,6 +6291,18 @@ includeDarwin: - backend: "privacy-filter" tag-suffix: "-metal-darwin-arm64-privacy-filter" lang: "go" + # audio-cpp is the same shape: a C++/ggml backend built by a bespoke darwin + # script (make backends/audio-cpp-darwin), which reuses the backend's own + # package.sh so the Darwin package keeps the root-level layout the Linux image + # has (grpc-server, run.sh and assets/ in one directory, dylibs in lib/). + # No build-type: the backend Makefile turns ENGINE_ENABLE_METAL on from + # uname -s. lang=go drives runner/toolchain selection only - there is no + # backend/go/audio-cpp, which is why backend_build_darwin.yml and + # DARWIN_BESPOKE_BUILDERS in scripts/lib/backend-filter.mjs both route this + # backend away from the generic Go path. + - backend: "audio-cpp" + tag-suffix: "-metal-darwin-arm64-audio-cpp" + lang: "go" # LocalVQE has no Metal path; on Apple Silicon it builds CPU-only (GGML_METAL # OFF) but is still a native arm64 image. Uses the darwin/metal build profile. - backend: "localvqe" diff --git a/.github/workflows/backend_build_darwin.yml b/.github/workflows/backend_build_darwin.yml index e4eef7124..6b8b2a89b 100644 --- a/.github/workflows/backend_build_darwin.yml +++ b/.github/workflows/backend_build_darwin.yml @@ -244,8 +244,19 @@ jobs: make protogen-go make backends/privacy-filter-darwin + # audio-cpp is a C++/ggml backend like ds4 and privacy-filter - a single + # grpc-server with otool dylib bundling, plus bundled VAD assets - so it + # gets its own bespoke darwin script rather than the generic + # build-darwin-go-backend path, which would look for a backend/go/audio-cpp + # that does not exist. Keep this set in sync with DARWIN_BESPOKE_BUILDERS + # in scripts/lib/backend-filter.mjs. + - name: Build audio-cpp backend (Darwin Metal) + if: inputs.backend == 'audio-cpp' + run: | + make backends/audio-cpp-darwin + - name: Build ${{ inputs.backend }}-darwin - if: inputs.backend != 'llama-cpp' && inputs.backend != 'ds4' && inputs.backend != 'privacy-filter' + if: inputs.backend != 'llama-cpp' && inputs.backend != 'ds4' && inputs.backend != 'privacy-filter' && inputs.backend != 'audio-cpp' run: | make protogen-go BACKEND=${{ inputs.backend }} BUILD_TYPE=${{ inputs.build-type }} USE_PIP=${{ inputs.use-pip }} make build-darwin-${{ inputs.lang }}-backend diff --git a/Makefile b/Makefile index 03185f539..828dac878 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ # Disable parallel execution for backend builds -.NOTPARALLEL: backends/diffusers backends/llama-cpp backends/turboquant backends/bonsai backends/outetts backends/piper backends/stablediffusion-ggml backends/trellis2cpp backends/trellis2cpp-darwin backends/whisper backends/crispasr backends/parakeet-cpp backends/moss-transcribe-cpp backends/faster-whisper backends/silero-vad backends/local-store backends/valkey-store backends/cloud-proxy backends/huggingface backends/rfdetr backends/rfdetr-cpp backends/insightface backends/speaker-recognition backends/kitten-tts backends/kokoro backends/chatterbox backends/llama-cpp-darwin backends/neutts build-darwin-python-backend build-darwin-go-backend backends/mlx backends/diffuser-darwin backends/mlx-vlm backends/mlx-audio backends/mlx-distributed backends/stablediffusion-ggml-darwin backends/vllm backends/vllm-omni backends/longcat-video backends/sglang backends/moonshine backends/pocket-tts backends/qwen-tts backends/faster-qwen3-tts backends/qwen-asr backends/nemo backends/voxcpm backends/whisperx backends/ace-step backends/acestep-cpp backends/fish-speech backends/voxtral backends/opus backends/trl backends/llama-cpp-quantization backends/kokoros backends/sam3-cpp backends/qwen3-tts-cpp backends/moss-tts-cpp backends/magpie-tts-cpp backends/vllm-cpp backends/omnivoice-cpp backends/vibevoice-cpp backends/localvqe backends/tinygrad backends/sherpa-onnx backends/ds4 backends/ds4-darwin backends/liquid-audio backends/supertonic backends/depth-anything-cpp backends/privacy-filter backends/privacy-filter-darwin backends/audio-cpp +.NOTPARALLEL: backends/diffusers backends/llama-cpp backends/turboquant backends/bonsai backends/outetts backends/piper backends/stablediffusion-ggml backends/trellis2cpp backends/trellis2cpp-darwin backends/whisper backends/crispasr backends/parakeet-cpp backends/moss-transcribe-cpp backends/faster-whisper backends/silero-vad backends/local-store backends/valkey-store backends/cloud-proxy backends/huggingface backends/rfdetr backends/rfdetr-cpp backends/insightface backends/speaker-recognition backends/kitten-tts backends/kokoro backends/chatterbox backends/llama-cpp-darwin backends/neutts build-darwin-python-backend build-darwin-go-backend backends/mlx backends/diffuser-darwin backends/mlx-vlm backends/mlx-audio backends/mlx-distributed backends/stablediffusion-ggml-darwin backends/vllm backends/vllm-omni backends/longcat-video backends/sglang backends/moonshine backends/pocket-tts backends/qwen-tts backends/faster-qwen3-tts backends/qwen-asr backends/nemo backends/voxcpm backends/whisperx backends/ace-step backends/acestep-cpp backends/fish-speech backends/voxtral backends/opus backends/trl backends/llama-cpp-quantization backends/kokoros backends/sam3-cpp backends/qwen3-tts-cpp backends/moss-tts-cpp backends/magpie-tts-cpp backends/vllm-cpp backends/omnivoice-cpp backends/vibevoice-cpp backends/localvqe backends/tinygrad backends/sherpa-onnx backends/ds4 backends/ds4-darwin backends/liquid-audio backends/supertonic backends/depth-anything-cpp backends/privacy-filter backends/privacy-filter-darwin backends/audio-cpp backends/audio-cpp-darwin GOCMD=go GOTEST=$(GOCMD) test @@ -1201,6 +1201,10 @@ backends/privacy-filter-darwin: build bash ./scripts/build/privacy-filter-darwin.sh ./local-ai backends install "ocifile://$(abspath ./backend-images/privacy-filter.tar)" +backends/audio-cpp-darwin: build + bash ./scripts/build/audio-cpp-darwin.sh + ./local-ai backends install "ocifile://$(abspath ./backend-images/audio-cpp.tar)" + build-darwin-python-backend: build bash ./scripts/build/python-darwin.sh diff --git a/backend/cpp/audio-cpp/Makefile b/backend/cpp/audio-cpp/Makefile index d6e7e28aa..9b29cdc7d 100644 --- a/backend/cpp/audio-cpp/Makefile +++ b/backend/cpp/audio-cpp/Makefile @@ -50,11 +50,27 @@ CMAKE_ARGS ?= -DCMAKE_BUILD_TYPE=Release -DAUDIOCPP_DEPLOYMENT_BUILD=ON # Setting it here also pins ggml's copy, since its default is guarded by # `if (NOT DEFINED CMAKE_CUDA_ARCHITECTURES)`. CUDA_MAJOR_VERSION is the CI # build-arg, forwarded by Dockerfile.audio-cpp. +# +# An EMPTY major maps to `native`, NOT to the CUDA 12 list. Only CI declares a +# major; a developer running `BUILD_TYPE=cublas make` locally declares none, and +# the CUDA 12 list contains 120a-real, which needs nvcc >= 12.8. Falling through +# to it turned every local build on a CUDA 12.0-12.7 host into a compile error, +# where upstream's documented behaviour ("Leave CMAKE_CUDA_ARCHITECTURES unset +# to build for the GPUs present at build time") worked. `native` restores that. +# It does require a GPU to enumerate, so the escape hatch for a GPU-less local +# cross-build is to set CUDA_ARCHITECTURES on the command line, which the ?= +# assignments below leave untouched. CUDA_MAJOR_VERSION ?= ifeq ($(CUDA_MAJOR_VERSION),13) CUDA_ARCHITECTURES ?= 75-virtual;80-virtual;86-real;89-real;120a-real;121a-real -else +else ifeq ($(CUDA_MAJOR_VERSION),12) CUDA_ARCHITECTURES ?= 50-virtual;61-virtual;70-virtual;75-virtual;80-virtual;86-real;89-real;120a-real +else ifeq ($(CUDA_MAJOR_VERSION),) + CUDA_ARCHITECTURES ?= native +else ifeq ($(BUILD_TYPE),cublas) + # Gated on cublas so that `make clean` with a stray CUDA_MAJOR_VERSION in the + # environment still works; the variable means nothing to any other build. + $(error CUDA_MAJOR_VERSION=$(CUDA_MAJOR_VERSION) has no architecture list here (12 and 13 do). Leave it empty for a native build, or pass CUDA_ARCHITECTURES explicitly.) endif ifeq ($(BUILD_TYPE),cublas) @@ -62,7 +78,32 @@ ifeq ($(BUILD_TYPE),cublas) else ifeq ($(BUILD_TYPE),vulkan) CMAKE_ARGS += -DENGINE_ENABLE_VULKAN=ON else ifeq ($(UNAME_S),Darwin) + # Metal. ggml embeds the shader library by default (GGML_METAL_EMBED_LIBRARY + # defaults to GGML_METAL), so the package needs no .metallib beside the + # binary. Darwin builds go through scripts/build/audio-cpp-darwin.sh. CMAKE_ARGS += -DENGINE_ENABLE_METAL=ON + # AppleClang ships no OpenMP runtime and Homebrew's libomp is keg-only, so + # neither libomp.dylib nor omp.h is symlinked into /opt/homebrew and CMake's + # FindOpenMP cannot find them on its own (the workflow's `brew link libomp` + # is a no-op for a keg-only formula, and its failure is swallowed). + # audio.cpp calls find_package(OpenMP REQUIRED COMPONENTS CXX) whenever + # ENGINE_ENABLE_OPENMP is ON, so with no hint the macOS build dies at + # configure time before compiling anything. OpenMP_ROOT is honoured by the + # find_library/find_path calls inside FindOpenMP under CMP0074, which is NEW + # here because audio.cpp requires CMake 3.20. + # + # If the keg is absent, turn OpenMP off rather than fail: the tree's only + # include is guarded by #ifdef _OPENMP and a #pragma omp without + # -fopenmp is simply ignored, so an OpenMP-less build is correct, just + # single-threaded in the host-side DSP loops. + ifeq ($(origin LIBOMP_PREFIX),undefined) + LIBOMP_PREFIX := $(shell brew --prefix libomp 2>/dev/null) + endif + ifneq ($(and $(wildcard $(LIBOMP_PREFIX)/lib/libomp.dylib),$(wildcard $(LIBOMP_PREFIX)/include/omp.h)),) + CMAKE_ARGS += -DOpenMP_ROOT=$(LIBOMP_PREFIX) + else + CMAKE_ARGS += -DENGINE_ENABLE_OPENMP=OFF + endif else # Portable Linux CPU. Upstream wires this to GGML_BACKEND_DL + # GGML_CPU_ALL_VARIANTS + $ORIGIN rpath, so one build serves every CPU diff --git a/backend/cpp/audio-cpp/package.sh b/backend/cpp/audio-cpp/package.sh index f6ead13f9..ba91d3746 100755 --- a/backend/cpp/audio-cpp/package.sh +++ b/backend/cpp/audio-cpp/package.sh @@ -48,18 +48,18 @@ for asset in silero_vad marblenet_vad; do done # Everything below this point is Linux-only: a bundled ELF loader, an ldd walk -# and an ld.so --list validation. The macOS equivalents (otool -L, install_name -# rewriting, a codesign pass) belong to a scripts/build/audio-cpp-darwin.sh that -# DOES NOT EXIST YET; writing it is Task 16's job. +# and an ld.so --list validation. The macOS equivalent is the otool -L closure in +# scripts/build/audio-cpp-darwin.sh, which picks up from the exit 0 below. # -# WARNING FOR WHOEVER WRITES IT. The obvious move is to copy +# WARNING FOR ANYONE REWORKING THAT SCRIPT. The obvious move is to copy # scripts/build/privacy-filter-darwin.sh, and that script assembles its own # package under build/darwin and never calls package.sh at all. Adapted as-is it # will silently omit assets/, and the bundled: model path form then resolves to # nothing, which takes the only zero-download verification path in this backend -# with it. The Darwin package needs the same root-level layout as the Linux one: -# grpc-server, run.sh, the ggml dylibs and assets/ in ONE directory, with -# lib/ for the rest. run.sh's Darwin branch execs grpc-server directly, so +# with it. That is why audio-cpp-darwin.sh copies THIS directory instead of +# rebuilding one. The Darwin package needs the same root-level layout as the +# Linux one: grpc-server, run.sh, the ggml dylibs and assets/ in ONE directory, +# with lib/ for the rest. run.sh's Darwin branch execs grpc-server directly, so # _NSGetExecutablePath already names the package root; nothing else is needed # beyond putting the files there. UNAME_S=$(uname -s) diff --git a/backend/index.yaml b/backend/index.yaml index 429b55dec..a8f426f1b 100644 --- a/backend/index.yaml +++ b/backend/index.yaml @@ -148,16 +148,19 @@ - audio-transcription - CPU - CUDA - # No metal/metal-darwin-arm64 key (no Darwin matrix entry yet) and no vulkan - # key (the vulkan image would carry a Vulkan loader with no Mesa ICD; see the - # audio-cpp block in .github/backend-matrix.yml). Pointing a capability at a - # tag CI never builds hands the user a pull failure; leaving it out lets - # SystemState.Capability() fall through to `default` and the CPU image. + - Metal + # No vulkan key: the vulkan image would carry a Vulkan loader with no Mesa ICD + # (see the audio-cpp block in .github/backend-matrix.yml). Pointing a + # capability at a tag CI never builds hands the user a pull failure; leaving it + # out lets SystemState.Capability() fall through to `default` and the CPU + # image. capabilities: default: "cpu-audio-cpp" nvidia: "cuda12-audio-cpp" nvidia-cuda-12: "cuda12-audio-cpp" nvidia-cuda-13: "cuda13-audio-cpp" + metal: "metal-audio-cpp" + metal-darwin-arm64: "metal-audio-cpp" - &whispercpp name: "whisper" alias: "whisper" @@ -2037,6 +2040,8 @@ nvidia: "cuda12-audio-cpp-development" nvidia-cuda-12: "cuda12-audio-cpp-development" nvidia-cuda-13: "cuda13-audio-cpp-development" + metal: "metal-audio-cpp-development" + metal-darwin-arm64: "metal-audio-cpp-development" - !!merge <<: *stablediffusionggml name: "stablediffusion-ggml-development" capabilities: @@ -6895,3 +6900,13 @@ uri: "quay.io/go-skynet/local-ai-backends:master-gpu-nvidia-cuda-13-audio-cpp" mirrors: - localai/localai-backends:master-gpu-nvidia-cuda-13-audio-cpp +- !!merge <<: *audiocpp + name: "metal-audio-cpp" + uri: "quay.io/go-skynet/local-ai-backends:latest-metal-darwin-arm64-audio-cpp" + mirrors: + - localai/localai-backends:latest-metal-darwin-arm64-audio-cpp +- !!merge <<: *audiocpp + name: "metal-audio-cpp-development" + uri: "quay.io/go-skynet/local-ai-backends:master-metal-darwin-arm64-audio-cpp" + mirrors: + - localai/localai-backends:master-metal-darwin-arm64-audio-cpp diff --git a/scripts/build/audio-cpp-darwin.sh b/scripts/build/audio-cpp-darwin.sh new file mode 100755 index 000000000..b5badccf2 --- /dev/null +++ b/scripts/build/audio-cpp-darwin.sh @@ -0,0 +1,178 @@ +#!/bin/bash +# Darwin/Metal build for the audio-cpp backend. +# +# Modelled on scripts/build/ds4-darwin.sh rather than llama-cpp-darwin.sh: +# audio-cpp is a single grpc-server, like ds4 and privacy-filter, whereas +# llama-cpp ships three binaries and splits its ggml objects between the package +# root and lib/. One deliberate departure from BOTH of those scripts, and from +# privacy-filter-darwin.sh in particular: +# +# THIS SCRIPT DOES NOT REASSEMBLE THE PACKAGE. It runs the backend's own +# `make package` and copies the result. +# +# privacy-filter-darwin.sh hand-assembles build/darwin and never calls its +# package.sh. Copied here that would silently drop assets/, and assets/ is what +# makes the "bundled:silero_vad" / "bundled:marblenet_vad" model paths resolve +# with nothing downloaded - the only zero-download path this backend has. +# backend/cpp/audio-cpp/package.sh already produces the exact layout the image +# needs, and its Darwin branch stops right before the Linux-only loader/ldd work +# and hands over to this script. Reusing it means the Linux and Darwin packages +# cannot drift. +# +# What this script adds on top of package.sh is the part that is genuinely +# macOS-only: walking otool -L to bundle the non-system dylibs the binary needs. +set -euo pipefail +set -x + +IMAGE_NAME="${IMAGE_NAME:-localai/audio-cpp-darwin}" +PLATFORMARCH="${PLATFORMARCH:-darwin/arm64}" + +# NATIVE=false matches ds4-darwin.sh and the backend Makefile's own default: the +# image is published for every Apple Silicon Mac, not just the CI runner's, so +# the CPU kernels must not be compiled for the build host's exact ISA. +pushd backend/cpp/audio-cpp +make NATIVE=false grpc-server package +popd + +rm -rf build/darwin +mkdir -p build/darwin backend-images +# -a, not -r: package.sh may have left symlink chains among the ggml objects +# (libggml.dylib -> libggml.0.dylib -> ...), and the SONAME the binary asks for +# has to keep naming a file here. +cp -a backend/cpp/audio-cpp/package/. build/darwin/ + +# THE LAYOUT ASSERTION - the Darwin half of the one in +# backend/cpp/audio-cpp/package.sh, and the reason that one is written at such +# length. run.sh's Darwin branch execs grpc-server directly, so +# _NSGetExecutablePath names the package ROOT. Two independent consumers read +# that directory: ggml's backend registry, which discovers dlopened +# libggml-cpu-* objects by scanning it, and resolve_model_path(), which expands +# "bundled:" to /assets/. Tidying assets/ or the ggml +# objects into lib/, which is where the llama-cpp layout would put them, +# produces a package that builds, publishes, and then fails at run time with +# "model path does not exist: /lib/assets/...". Fail here instead. +for required in grpc-server run.sh assets/silero_vad assets/marblenet_vad; do + if [ ! -e "build/darwin/$required" ]; then + echo "audio-cpp-darwin.sh: missing from the package root: $required" >&2 + echo "audio-cpp-darwin.sh: grpc-server, run.sh, the ggml dylibs and" >&2 + echo "audio-cpp-darwin.sh: assets/ all have to sit in ONE directory," >&2 + echo "audio-cpp-darwin.sh: because run.sh execs the binary from there" >&2 + echo "audio-cpp-darwin.sh: and both the ggml backend scan and the" >&2 + echo "audio-cpp-darwin.sh: bundled: asset lookup are relative to it." >&2 + exit 1 + fi +done + +# Apple Silicon: pick up Homebrew-installed protobuf utf8_validity if present +# (same as ds4-darwin.sh - it is a transitive dep otool may not surface). +if [[ "$(uname -s)" == "Darwin" && "$(uname -m)" == "arm64" ]]; then + ADDITIONAL_LIBS=${ADDITIONAL_LIBS:-$(ls /opt/homebrew/Cellar/protobuf/**/lib/libutf8_validity*.dylib 2>/dev/null || true)} +else + ADDITIONAL_LIBS=${ADDITIONAL_LIBS:-""} +fi +for file in $ADDITIONAL_LIBS; do + cp -fLv "$file" build/darwin/lib +done + +# Bundle the FULL dylib closure, not just grpc-server's direct dependencies. +# +# ds4-darwin.sh and llama-cpp-darwin.sh walk one level. That is enough only +# while every indirect dependency happens to be linked directly by the binary +# too. It is not enough here: Homebrew's grpc++ pulls libgrpc, ~40 libabsl_*, +# libupb, libcares, libre2 and OpenSSL, and grpc-server links few of those +# itself. A missing one resolves on the build host (the install names are +# absolute Homebrew paths) and fails on a user's Mac that has no Homebrew grpc, +# which is exactly the failure a bundled package exists to prevent. The Linux +# side already does a full ldd closure in package.sh; this is its equivalent. +# +# Leaf-name deduplication is what makes DYLD_LIBRARY_PATH work: run.sh exports +# lib/ and the package root, and dyld searches those by leaf name BEFORE +# falling back to the absolute install name recorded in the binary. +# +# /bin/bash on macOS is 3.2, so no associative arrays: the seen-set is a +# space-delimited string matched with `case`. +BUNDLED_LEAVES=" " +UNRESOLVED=0 + +bundle_dylib_closure() { + local object="$1" + local dep leaf src + while read -r dep; do + case "$dep" in + *.dylib) ;; + *) continue ;; + esac + leaf=$(basename "$dep") + case "$BUNDLED_LEAVES" in + *" $leaf "*) continue ;; + esac + + src="$dep" + case "$dep" in + @*) + # @rpath / @loader_path / @executable_path are resolved against + # the package itself, so such a dependency is satisfiable only + # if the file is already inside it. Nothing produces one today + # (a Metal build sets neither BUILD_SHARED_LIBS nor + # GGML_BACKEND_DL, so ggml is static and package.sh copies no + # dylibs at all), but if that changes, a missing one has to be + # loud: dyld fails at load with nothing else to go on. + if [ -e "build/darwin/$leaf" ]; then + src="build/darwin/$leaf" + elif [ -e "build/darwin/lib/$leaf" ]; then + src="build/darwin/lib/$leaf" + else + echo "audio-cpp-darwin.sh: unresolved @rpath dependency of ${object}: ${dep}" >&2 + UNRESOLVED=1 + continue + fi + BUNDLED_LEAVES="${BUNDLED_LEAVES}${leaf} " + bundle_dylib_closure "$src" + continue + ;; + esac + + # System dylibs live in the dyld shared cache and have no file on disk. + # That absence is the filter that keeps libSystem, libc++ and the + # frameworks out of the package; there is no allow-list to maintain. + [ -e "$dep" ] || continue + + BUNDLED_LEAVES="${BUNDLED_LEAVES}${leaf} " + # -L dereferences: Homebrew's unversioned names are symlinks into the + # Cellar, and copying the link would land a dangling relative symlink. + # The binary asks for whatever install name is recorded, so the file has + # to exist under that leaf. + cp -fLv "$dep" build/darwin/lib/ + bundle_dylib_closure "$dep" + done < <(otool -L "$object" | awk 'NR > 1 { print $1 }') + # Explicit, because `set -e` would abort on whatever status the loop + # happened to end on. + return 0 +} + +bundle_dylib_closure build/darwin/grpc-server +if compgen -G "build/darwin/*.dylib" > /dev/null; then + for object in build/darwin/*.dylib; do + bundle_dylib_closure "$object" + done +fi + +echo "Bundled libraries:" +ls -la build/darwin/lib + +if [ "$UNRESOLVED" -ne 0 ]; then + echo "audio-cpp-darwin.sh: refusing to package: see the unresolved dependencies above." >&2 + exit 1 +fi + +# Nothing above rewrites an install name or otherwise edits grpc-server, which +# matters on Apple Silicon: the linker ad-hoc signs the binary, and any +# in-place edit would invalidate that signature and make the package +# unlaunchable. cp preserves it. +./local-ai util create-oci-image \ + build/darwin/. \ + --output ./backend-images/audio-cpp.tar \ + --image-name "$IMAGE_NAME" \ + --platform "$PLATFORMARCH" + +rm -rf build/darwin diff --git a/scripts/lib/backend-filter.mjs b/scripts/lib/backend-filter.mjs index c14a7c243..c5f7aab52 100644 --- a/scripts/lib/backend-filter.mjs +++ b/scripts/lib/backend-filter.mjs @@ -107,13 +107,10 @@ export function inferBackendPathDarwin(item) { if (item.backend === "privacy-filter") { return `backend/cpp/privacy-filter/`; } - // audio-cpp is C++ too. There is no Darwin matrix entry for it yet: the - // Metal build needs scripts/build/audio-cpp-darwin.sh and a - // `backends/audio-cpp-darwin` make target, neither of which exists, so this - // case (and the DARWIN_BESPOKE_BUILDERS membership below) is inert today. It - // is here so that adding the entry is a one-line change that cannot - // accidentally route a C++ backend through the generic - // build-darwin-go-backend path, which would look for backend/go/audio-cpp/. + // audio-cpp is C++ too (built via `make backends/audio-cpp-darwin`); same + // lang=go-for-runner convention, source under backend/cpp. Without this the + // generic path below would look for backend/go/audio-cpp/, which does not + // exist, and no change to the real sources would ever trigger a Darwin build. if (item.backend === "audio-cpp") { return `backend/cpp/audio-cpp/`; } @@ -155,14 +152,10 @@ export function backendChanged(backend, pathPrefix, changedFiles) { // without it is a Python backend (see .github/backend-matrix.yml). const isDarwinPython = item => !item.lang; -// backend_build_darwin.yml routes llama-cpp, ds4 and privacy-filter to their -// own bespoke make targets; every other lang=go entry goes through -// `make build-darwin-go-backend` -> scripts/build/golang-darwin.sh. -// -// audio-cpp is listed ahead of its Darwin matrix entry (see -// inferBackendPathDarwin): it is a C++/ggml backend that will need the same -// bespoke treatment, and listing it here now means the entry, when it lands, -// cannot be silently claimed by the generic Go path. +// backend_build_darwin.yml routes llama-cpp, ds4, privacy-filter and audio-cpp +// to their own bespoke make targets; every other lang=go entry goes through +// `make build-darwin-go-backend` -> scripts/build/golang-darwin.sh. Keep this +// set in sync with the `if:` conditions in that workflow. const DARWIN_BESPOKE_BUILDERS = new Set([ "llama-cpp", "ds4",