Files
LocalAI/backend/cpp/audio-cpp/package.sh
Ettore Di Giacinto c5ad013def backend(audio-cpp): package the backend image
Bundles the dependency closure for the from-scratch image, the dlopened ggml
CPU-variant shared objects that ldd cannot see, and upstream's bundled
silero_vad and marblenet_vad assets so VAD works with no download.

The bundled loader sits in the package ROOT rather than at lib/ld.so. run.sh
execs it, which makes /proc/self/exe name the loader, and this backend has two
consumers of that path: ggml discovers the libggml-cpu-*.so by listing
dirname(/proc/self/exe), and resolve_model_path expands bundled:<name> under the
same directory. Rooting the loader makes the binary, the ggml objects and
assets/ share the one directory all three resolution mechanisms agree on.
llama-cpp's lib/ld.so layout would need assets/ moved into lib/ as well.

The image builds against apt gRPC and protobuf, like Dockerfile.ds4 and unlike
Dockerfile.privacy-filter. The from-source gRPC that install-base-deps.sh and
the base-grpc-* images supply vendors protobuf 26, which pulls abseil into
message_lite.h; with SPM_PROTOBUF_PROVIDER=package that collides with
sentencepiece's vendored mini-abseil and every absl::internal reference becomes
ambiguous. Noble's protobuf 3.21.12 predates the abseil dependency and is the
pair every earlier verification of this backend ran against.

Assisted-by: Claude:claude-opus-5 [Claude Code]
Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
2026-07-29 19:03:33 +00:00

147 lines
5.8 KiB
Bash
Executable File

#!/bin/bash
# Assemble backend/cpp/audio-cpp/package, which becomes the whole content of the
# FROM scratch backend image. Nothing outside this directory exists at run time.
set -euo pipefail
CURDIR=$(dirname "$(realpath "$0")")
REPO_ROOT="${CURDIR}/../../.."
PACKAGE_DIR="$CURDIR/package"
BUILD_DIR="$CURDIR/build"
rm -rf "$PACKAGE_DIR"
mkdir -p "$PACKAGE_DIR/lib" "$PACKAGE_DIR/assets"
cp -avf "$CURDIR/grpc-server" "$PACKAGE_DIR/"
cp -fv "$CURDIR/run.sh" "$PACKAGE_DIR/"
# ENGINE_ENABLE_CPU_ALL_VARIANTS builds the ggml backends as shared objects that
# are dlopened at run time, so ldd cannot see them and the dependency walk below
# would leave the image with no CPU backend at all. They also cannot go in lib/:
# ggml DISCOVERS them by listing dirname(/proc/self/exe) and the current
# directory, so being on a library path is not enough, they have to be in a
# directory ggml scans. run.sh execs the bundled loader from the package root
# exactly so that directory is this one. cmake writes them to build/bin, not
# next to build/grpc-server, which is why this reads from bin/.
#
# -a keeps the libggml.so -> libggml.so.0 -> libggml.so.0.12.0 symlink chain,
# so the SONAME the binary asks for still names a file here.
for pattern in '*.so*' '*.dylib*'; do
if compgen -G "$BUILD_DIR/bin/$pattern" > /dev/null; then
# shellcheck disable=SC2086
cp -avf "$BUILD_DIR/bin/"$pattern "$PACKAGE_DIR/"
fi
done
# Upstream ships silero_vad and marblenet_vad as small runtime assets.
# resolve_model_path() expands "bundled:<name>" to
# dirname(/proc/self/exe)/assets/<name>, so copying them here is what makes VAD
# work with nothing downloaded.
for asset in silero_vad marblenet_vad; do
src="$CURDIR/audio.cpp/assets/framework/models/$asset"
if [ -d "$src" ]; then
cp -rfv "$src" "$PACKAGE_DIR/assets/"
else
echo "package.sh: bundled asset missing: $src" >&2
echo "package.sh: run 'make audio.cpp' before packaging" >&2
exit 1
fi
done
UNAME_S=$(uname -s)
if [ "$UNAME_S" = "Darwin" ]; then
echo "package.sh: Darwin dylib bundling is handled by the darwin build script"
ls -lah "$PACKAGE_DIR/" "$PACKAGE_DIR/assets/"
exit 0
fi
# The loader goes in the package ROOT, not in lib/. run.sh explains why at
# length; the short version is that exec'ing it makes dirname(/proc/self/exe)
# the directory it sits in, and both the ggml backend scan and the bundled:
# asset lookup need that to be the package root.
if [ -f "/lib64/ld-linux-x86-64.so.2" ]; then
cp -arfLv /lib64/ld-linux-x86-64.so.2 "$PACKAGE_DIR/ld.so"
elif [ -f "/lib/ld-linux-aarch64.so.1" ]; then
cp -arfLv /lib/ld-linux-aarch64.so.1 "$PACKAGE_DIR/ld.so"
else
echo "package.sh: unknown architecture" >&2
exit 1
fi
# Libraries the host GPU driver stack owns. package_gpu_libs deliberately ships
# the CUDA/Vulkan runtime but not the driver, because the driver has to match
# the kernel module on whatever host runs the image. Copying the build host's
# copy in would pin it to the build host instead.
is_driver_lib() {
case "$(basename "$1")" in
libcuda.so*|libnvidia-*) return 0 ;;
*) return 1 ;;
esac
}
# Bundle the full dependency closure. grpc-server links the distro gRPC,
# protobuf and absl stack; copying only the C/C++ runtime leaves the scratch
# image unable to start. The walk runs over the PACKAGED binary, not the one in
# $CURDIR, because its RUNPATH is $ORIGIN: only from inside the package does
# libggml.so.0 resolve to the copy shipped above rather than to nothing.
# The dlopened ggml objects are walked too, since a dependency of theirs that
# grpc-server does not itself link would otherwise be missed.
{
ldd "$PACKAGE_DIR/grpc-server"
for so in "$PACKAGE_DIR"/*.so*; do
[ -f "$so" ] || continue
ldd "$so"
done
} | awk '$2 == "=>" && $3 ~ /^\// { print $3 }' | sort -u | \
while read -r so; do
# Skip what is already inside the package: the ggml objects resolve through
# $ORIGIN and re-copying them into lib/ would ship two copies of each.
case "$so" in "$PACKAGE_DIR"/*) continue ;; esac
if is_driver_lib "$so"; then
echo "package.sh: leaving driver-owned library to the host: $so"
continue
fi
cp -arfLv "$so" "$PACKAGE_DIR/lib/"
done
GPU_LIB_SCRIPT="${REPO_ROOT}/scripts/build/package-gpu-libs.sh"
if [ -f "$GPU_LIB_SCRIPT" ]; then
echo "Packaging GPU libraries for BUILD_TYPE=${BUILD_TYPE:-cpu}..."
# shellcheck source=/dev/null
source "$GPU_LIB_SCRIPT" "$PACKAGE_DIR/lib"
package_gpu_libs
fi
# Resolve every dependency through the same loader and library path the
# from-scratch image uses. Two distinct failures are rejected, because the
# loader can still fall back to the host's default directories: a dependency it
# could not resolve at all, and one it resolved to a file OUTSIDE the package,
# which would validate here and be absent in the image.
validation_failed=0
validate_object() {
local object="$1"
"$PACKAGE_DIR/ld.so" --library-path "$PACKAGE_DIR/lib:$PACKAGE_DIR" \
--list "$object" | awk -v pkg="$PACKAGE_DIR/" -v obj="$object" '
$2 == "=>" && $3 == "not" {
print "package.sh: unresolved dependency of " obj ": " $1 > "/dev/stderr"
bad = 1
}
$2 == "=>" && $3 ~ /^\// && index($3, pkg) != 1 {
print "package.sh: dependency of " obj " resolved outside the package: " $0 > "/dev/stderr"
bad = 1
}
END { exit bad }
'
}
validate_object "$PACKAGE_DIR/grpc-server" || validation_failed=1
for so in "$PACKAGE_DIR"/*.so*; do
[ -f "$so" ] || continue
validate_object "$so" || validation_failed=1
done
if [ "$validation_failed" -ne 0 ]; then
exit 1
fi
echo "audio-cpp package contents:"
ls -lah "$PACKAGE_DIR/" "$PACKAGE_DIR/lib/" "$PACKAGE_DIR/assets/"