mirror of
https://github.com/mudler/LocalAI.git
synced 2026-09-13 06:45:26 -04:00
* test: make coverage failures observable Keep per-root logs, reject concurrent coverage runs, and avoid relying on /bin/sleep in the worker timeout test. Assisted-by: Codex:gpt-5 Signed-off-by: Richard Palethorpe <io@richiejp.com> * test: parallelize coverage without remote fixtures Assisted-by: Codex:gpt-5 [apply_patch] [exec_command] Signed-off-by: Richard Palethorpe <io@richiejp.com> * test: add offline resource infrastructure Introduce versioned resource manifests, a checksum-verified CAS preparer, offline test wrappers, and a guarded network transport. Replace live Hugging Face, GitHub, and OCI cases with deterministic fixtures and inject fixture metadata into importer discovery. Assisted-by: Codex:gpt-5 Signed-off-by: Richard Palethorpe <io@richiejp.com> * test: enforce offline resource replay Assisted-by: Codex:gpt-5 Signed-off-by: Richard Palethorpe <io@richiejp.com> * test: harden offline resource refresh Assisted-by: Codex:gpt-5 Signed-off-by: Richard Palethorpe <io@richiejp.com> * test: expose slow coverage waits Assisted-by: Codex:gpt-5 Signed-off-by: Richard Palethorpe <io@richiejp.com> * test: eliminate avoidable wall-clock waits Inject a clock into Hugging Face retry handling, reuse a process-scoped PostgreSQL container with per-spec schemas in the nodes suite, and poll local import jobs promptly. Assisted-by: Codex:gpt-5 Signed-off-by: Richard Palethorpe <io@richiejp.com> * test: remove repeated fixture startup waits Share PostgreSQL fixtures across parallel endpoint and agent suite workers, and make the worker Free deadline injectable so the wedged-backend test does not spend five seconds on wall-clock time. Assisted-by: Codex:gpt-5 Signed-off-by: Richard Palethorpe <io@richiejp.com> * test: fix offline resource CI portability Normalize Docker archive metadata before content addressing, derive archive checksums during explicit refreshes, make network lint portable to macOS, and prepare distributed images before running their offline suite. Assisted-by: Codex:gpt-5 Signed-off-by: Richard Palethorpe <io@richiejp.com> * ci: cache Go modules before offline tests Warm the complete module graph before the Linux and macOS test jobs enter offline replay mode, so tool dependencies such as Ginkgo are not fetched through the guarded proxy. Assisted-by: Codex:gpt-5 Signed-off-by: Richard Palethorpe <io@richiejp.com> * test: drop the static network lint in favour of real isolation The offline test suite already prevents tests from reaching the network twice over: run-test-linux-offline.sh puts the test process in a cgroup and REJECTs egress outside the private ranges, and HardenedTransport installs testnetwork.LocalGuard to refuse dials that resolve to a public address. Both fail the test with a precise error at the moment of the dial. test-network-lint.sh added neither. Its diff stage defaulted to a HEAD base, so on a clean checkout it compared the tree against itself and inspected nothing; the branch's own commits were never examined. It only produced output when an earlier job step dirtied the tree, and then it matched a bare https?:// against whatever changed. make react-ui runs npm install rather than npm ci, so CI rewrote core/http/react-ui/package-lock.json and the lint reported an npm registry URL as forbidden test network access: + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz", Its fingerprint stage was self-defeating in a quieter way: hashing the whole tree's network-mechanism inventory meant every rebase onto a master that touched any _test.go needed a manual baseline bump, so the check mostly caught its own staleness. Remove the script, its make target and the two prerequisite edges, along with the test-network: fixture markers that existed only to suppress it. The isolation itself is untouched. Assisted-by: Claude:claude-opus-5 [go vet] Signed-off-by: Richard Palethorpe <io@richiejp.com> * ci: keep hidden files in the offline test bundle artifact Cherry-picked from15a37b0acon the remote branch. The offline bundle lives under .cache/, which actions/upload-artifact skips by default, so the Linux job packed an artifact missing the very file the next step restores. The other half of15a37b0acmoved test-network-lint out of the `test` and `test-coverage` prerequisite lists into a recipe line, so parallel make could not fingerprint the tree while generated fixtures were still changing. That is dropped: the preceding commit removes the lint entirely, and the race it worked around is one more reason a whole-tree fingerprint was the wrong mechanism. Assisted-by: Codex:gpt-5 Signed-off-by: Richard Palethorpe <io@richiejp.com> * refactor: share bounded exponential backoff Use overflow-safe saturating arithmetic for retry delays across model import polling, downloads, registration, node operations, and model loading. Keep model import status checks responsive initially while capping their interval at 500ms. Assisted-by: Codex:gpt-5 Signed-off-by: Richard Palethorpe <io@richiejp.com> * ci: mirror Jetson Python wheels Keep the CUDA aarch64 wheel subset in GHCR and serve it as a local PEP 503 index during L4T backend builds, preserving last-known-good packages through upstream outages. Assisted-by: Codex:gpt-5 Signed-off-by: Richard Palethorpe <io@richiejp.com> * docs(agents): index the Jetson wheels mirror Mention the GHCR-hosted L4T wheel mirror in the CI caching guide summary so maintainers can find its outage and cache documentation. Assisted-by: Codex:gpt-5 Signed-off-by: Richard Palethorpe <io@richiejp.com> * ci: add defensive build network proxy Record build destinations and byte counts, retry observable idempotent HTTP downloads, and isolate explorer database tests that race under coverage. Assisted-by: Codex:gpt-5 Signed-off-by: Richard Palethorpe <io@richiejp.com> * fix(kokoros): implement updated backend trait Return unimplemented for image upscaling, matching the backend's other unsupported modalities after the protobuf API update. Assisted-by: Codex:gpt-5 Signed-off-by: Richard Palethorpe <io@richiejp.com> * fix(ci): clear recovered proxy errors Do not mark a request failed when a later safe retry succeeds. Assisted-by: Codex:gpt-5 Signed-off-by: Richard Palethorpe <io@richiejp.com> * ci: require HTTPS build interception Inject a short-lived proxy CA into BuildKit and Dockerfile RUN steps, reject plain HTTP and opaque tunnels, and retain method/status/byte telemetry for verified HTTPS traffic. Assisted-by: Codex:gpt-5 Signed-off-by: Richard Palethorpe <io@richiejp.com> * fix(ci): preserve system trust in unproxied builds Mount the generated interception CA at a dedicated secret path and add it to the trust bundle only in proxy-aware dependency stages. This prevents optional secret mounts from masking the system CA bundle in ordinary backend test builds. Install the requested Go toolchain before starting the proxy and satisfy cleanup error checks found by CI lint. Assisted-by: Codex:gpt-5 Signed-off-by: Richard Palethorpe <io@richiejp.com> * fix(ci): persist build proxy trust Install the generated proxy CA through the system-managed local certificate directory so ca-certificates upgrades retain it. Avoid turning canceled matrix jobs into proxy cleanup failures. Assisted-by: Codex:gpt-5 Signed-off-by: Richard Palethorpe <io@richiejp.com> * fix(ci): trust proxy in nested build scripts Install the build proxy CA before nested source fetches, route the DS4 package setup through the HTTPS mirror helper, and avoid repeated OCI setup in gallery behavior tests. Assisted-by: Codex:gpt-5 Signed-off-by: Richard Palethorpe <io@richiejp.com> * fix(ci): use HTTPS apt sources for Bonsai Rewrite ARM64 package sources before installing GCC and check gallery fixture cleanup errors so the optimized tests satisfy errcheck. Assisted-by: Codex:gpt-5 Signed-off-by: Richard Palethorpe <io@richiejp.com> * fix(privacy-filter): trust build proxy CA Install the mounted build proxy certificate before privacy-filter's make target fetches its HTTPS sources, for both source and prebuilt builder paths.\n\nAssisted-by: Codex:gpt-5 Signed-off-by: Richard Palethorpe <io@richiejp.com> * test: fail on hidden offline egress Count cgroup-scoped firewall rejects and fail the offline test harness with bounded aggregate diagnostics. Inject the gen-audio GGUF probe so fixture-backed importer tests do not attempt real network access. Assisted-by: Codex:gpt-5 Signed-off-by: Richard Palethorpe <io@richiejp.com> * fix(ci): preserve system CA trust Build a combined runner certificate bundle instead of replacing public roots with the generated proxy CA. Centralize additive container installation in the shared proxy CA helper. Assisted-by: Codex:gpt-5 Signed-off-by: Richard Palethorpe <io@richiejp.com> --------- Signed-off-by: Richard Palethorpe <io@richiejp.com> Co-authored-by: localai-org-maint-bot <306269227+localai-org-maint-bot@users.noreply.github.com>
695 lines
26 KiB
Bash
695 lines
26 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
#
|
|
# use the library by adding the following line to a script:
|
|
# source $(dirname $0)/../common/libbackend.sh
|
|
#
|
|
# If you want to limit what targets a backend can be used on, set the variable LIMIT_TARGETS to a
|
|
# space separated list of valid targets BEFORE sourcing the library, for example to only allow a backend
|
|
# to be used on CUDA and CPU backends:
|
|
#
|
|
# LIMIT_TARGETS="cublas cpu"
|
|
# source $(dirname $0)/../common/libbackend.sh
|
|
#
|
|
# You can use any valid BUILD_TYPE or BUILD_PROFILE, if you need to limit a backend to CUDA 12 only:
|
|
#
|
|
# LIMIT_TARGETS="cublas12"
|
|
# source $(dirname $0)/../common/libbackend.sh
|
|
#
|
|
# You can switch between uv (conda-like) and pip installation methods by setting USE_PIP:
|
|
# USE_PIP=true source $(dirname $0)/../common/libbackend.sh
|
|
#
|
|
# ===================== user-configurable defaults =====================
|
|
PYTHON_VERSION="${PYTHON_VERSION:-3.10}" # e.g. 3.10 / 3.11 / 3.12 / 3.13
|
|
PYTHON_PATCH="${PYTHON_PATCH:-18}" # e.g. 18 -> 3.10.18 ; 13 -> 3.11.13
|
|
PY_STANDALONE_TAG="${PY_STANDALONE_TAG:-20250818}" # release tag date
|
|
# Enable/disable bundling of a portable Python build
|
|
PORTABLE_PYTHON="${PORTABLE_PYTHON:-false}"
|
|
|
|
# If you want to fully pin the filename (including tuned CPU targets), set:
|
|
# PORTABLE_PY_FILENAME="cpython-3.10.18+20250818-x86_64_v3-unknown-linux-gnu-install_only.tar.gz"
|
|
: "${PORTABLE_PY_FILENAME:=}"
|
|
: "${PORTABLE_PY_SHA256:=}" # optional; if set we verify the download
|
|
# =====================================================================
|
|
|
|
# Default to uv if USE_PIP is not set
|
|
if [ "x${USE_PIP:-}" == "x" ]; then
|
|
USE_PIP=false
|
|
fi
|
|
|
|
# ----------------------- helpers -----------------------
|
|
function _is_musl() {
|
|
# detect musl (Alpine, etc)
|
|
if command -v ldd >/dev/null 2>&1; then
|
|
ldd --version 2>&1 | grep -qi musl && return 0
|
|
fi
|
|
# busybox-ish fallback
|
|
if command -v getconf >/dev/null 2>&1; then
|
|
getconf GNU_LIBC_VERSION >/dev/null 2>&1 || return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
function _triple() {
|
|
local os="" arch="" libc="gnu"
|
|
case "$(uname -s)" in
|
|
Linux*) os="unknown-linux" ;;
|
|
Darwin*) os="apple-darwin" ;;
|
|
MINGW*|MSYS*|CYGWIN*) os="pc-windows-msvc" ;; # best-effort for Git Bash
|
|
*) echo "Unsupported OS $(uname -s)"; exit 1;;
|
|
esac
|
|
|
|
case "$(uname -m)" in
|
|
x86_64) arch="x86_64" ;;
|
|
aarch64|arm64) arch="aarch64" ;;
|
|
armv7l) arch="armv7" ;;
|
|
i686|i386) arch="i686" ;;
|
|
ppc64le) arch="ppc64le" ;;
|
|
s390x) arch="s390x" ;;
|
|
riscv64) arch="riscv64" ;;
|
|
*) echo "Unsupported arch $(uname -m)"; exit 1;;
|
|
esac
|
|
|
|
if [[ "$os" == "unknown-linux" ]]; then
|
|
if _is_musl; then
|
|
libc="musl"
|
|
else
|
|
libc="gnu"
|
|
fi
|
|
echo "${arch}-${os}-${libc}"
|
|
else
|
|
echo "${arch}-${os}"
|
|
fi
|
|
}
|
|
|
|
function _portable_dir() {
|
|
echo "${EDIR}/python"
|
|
}
|
|
|
|
function _portable_bin() {
|
|
# python-build-standalone puts python in ./bin
|
|
echo "$(_portable_dir)/bin"
|
|
}
|
|
|
|
function _portable_python() {
|
|
if [ -x "$(_portable_bin)/python3" ]; then
|
|
echo "$(_portable_bin)/python3"
|
|
else
|
|
echo "$(_portable_bin)/python"
|
|
fi
|
|
}
|
|
|
|
|
|
# macOS loader env for the portable CPython
|
|
_macosPortableEnv() {
|
|
if [ "$(uname -s)" = "Darwin" ]; then
|
|
export DYLD_LIBRARY_PATH="$(_portable_dir)/lib${DYLD_LIBRARY_PATH:+:${DYLD_LIBRARY_PATH}}"
|
|
export DYLD_FALLBACK_LIBRARY_PATH="$(_portable_dir)/lib${DYLD_FALLBACK_LIBRARY_PATH:+:${DYLD_FALLBACK_LIBRARY_PATH}}"
|
|
fi
|
|
}
|
|
|
|
# Good hygiene on macOS for downloaded/extracted trees
|
|
_unquarantinePortablePython() {
|
|
if [ "$(uname -s)" = "Darwin" ]; then
|
|
command -v xattr >/dev/null 2>&1 && xattr -dr com.apple.quarantine "$(_portable_dir)" || true
|
|
fi
|
|
}
|
|
|
|
# ------------------ ### PORTABLE PYTHON ------------------
|
|
function ensurePortablePython() {
|
|
local pdir="$(_portable_dir)"
|
|
local pbin="$(_portable_bin)"
|
|
local pyexe
|
|
|
|
if [ -x "${pbin}/python3" ] || [ -x "${pbin}/python" ]; then
|
|
_macosPortableEnv
|
|
return 0
|
|
fi
|
|
|
|
mkdir -p "${pdir}"
|
|
local triple="$(_triple)"
|
|
|
|
local full_ver="${PYTHON_VERSION}.${PYTHON_PATCH}"
|
|
local fn=""
|
|
if [ -n "${PORTABLE_PY_FILENAME}" ]; then
|
|
fn="${PORTABLE_PY_FILENAME}"
|
|
else
|
|
# generic asset name: cpython-<full_ver>+<tag>-<triple>-install_only.tar.gz
|
|
fn="cpython-${full_ver}+${PY_STANDALONE_TAG}-${triple}-install_only.tar.gz"
|
|
fi
|
|
|
|
local url="https://github.com/astral-sh/python-build-standalone/releases/download/${PY_STANDALONE_TAG}/${fn}"
|
|
local tmp="${pdir}/${fn}"
|
|
echo "Downloading portable Python: ${fn}"
|
|
# curl with retries; fall back to wget if needed
|
|
if command -v curl >/dev/null 2>&1; then
|
|
curl -L --fail --retry 3 --retry-delay 1 -o "${tmp}" "${url}"
|
|
else
|
|
wget -O "${tmp}" "${url}"
|
|
fi
|
|
|
|
if [ -n "${PORTABLE_PY_SHA256}" ]; then
|
|
echo "${PORTABLE_PY_SHA256} ${tmp}" | sha256sum -c -
|
|
fi
|
|
|
|
echo "Extracting ${fn} -> ${pdir}"
|
|
# always a .tar.gz (we purposely choose install_only)
|
|
tar -xzf "${tmp}" -C "${pdir}"
|
|
rm -f "${tmp}"
|
|
|
|
# Some archives nest a directory; if so, flatten to ${pdir}
|
|
# Find the first dir with a 'bin/python*'
|
|
local inner
|
|
inner="$(find "${pdir}" -type f -path "*/bin/python*" -maxdepth 3 2>/dev/null | head -n1 || true)"
|
|
if [ -n "${inner}" ]; then
|
|
local inner_root
|
|
inner_root="$(dirname "$(dirname "${inner}")")" # .../bin -> root
|
|
if [ "${inner_root}" != "${pdir}" ]; then
|
|
# move contents up one level
|
|
shopt -s dotglob
|
|
mv "${inner_root}/"* "${pdir}/"
|
|
rm -rf "${inner_root}"
|
|
shopt -u dotglob
|
|
fi
|
|
fi
|
|
|
|
_unquarantinePortablePython
|
|
_macosPortableEnv
|
|
# Make sure it's runnable
|
|
pyexe="$(_portable_python)"
|
|
"${pyexe}" -V
|
|
}
|
|
|
|
# init handles the setup of the library
|
|
function init() {
|
|
BACKEND_NAME=${PWD##*/}
|
|
MY_DIR=$(realpath "$(dirname "$0")")
|
|
BUILD_PROFILE=$(getBuildProfile)
|
|
|
|
EDIR=${MY_DIR}
|
|
if [ "x${ENV_DIR:-}" != "x" ]; then
|
|
EDIR=${ENV_DIR}
|
|
fi
|
|
|
|
if [ ! -z "${LIMIT_TARGETS:-}" ]; then
|
|
isValidTarget=$(checkTargets ${LIMIT_TARGETS})
|
|
if [ ${isValidTarget} != true ]; then
|
|
echo "${BACKEND_NAME} can only be used on the following targets: ${LIMIT_TARGETS}"
|
|
exit 0
|
|
fi
|
|
fi
|
|
|
|
echo "Initializing libbackend for ${BACKEND_NAME}"
|
|
}
|
|
|
|
|
|
# getBuildProfile will inspect the system to determine which build profile is appropriate:
|
|
# returns one of the following:
|
|
# - cublas12
|
|
# - cublas13
|
|
# - hipblas
|
|
# - intel
|
|
function getBuildProfile() {
|
|
if [ x"${BUILD_TYPE:-}" == "xcublas" ] || [ x"${BUILD_TYPE:-}" == "xl4t" ]; then
|
|
if [ ! -z "${CUDA_MAJOR_VERSION:-}" ]; then
|
|
echo ${BUILD_TYPE}${CUDA_MAJOR_VERSION}
|
|
else
|
|
echo ${BUILD_TYPE}
|
|
fi
|
|
return 0
|
|
fi
|
|
|
|
if [ -d "/opt/intel" ]; then
|
|
echo "intel"
|
|
return 0
|
|
fi
|
|
|
|
if [ -n "${BUILD_TYPE:-}" ]; then
|
|
echo ${BUILD_TYPE}
|
|
return 0
|
|
fi
|
|
|
|
echo "cpu"
|
|
}
|
|
|
|
|
|
# Make the venv relocatable:
|
|
# - rewrite venv/bin/python{,3} to relative symlinks into $(_portable_dir)
|
|
# - normalize entrypoint shebangs to /usr/bin/env python3
|
|
# - optionally update pyvenv.cfg to point to the portable Python directory (only at runtime)
|
|
# Usage: _makeVenvPortable [--update-pyvenv-cfg]
|
|
_makeVenvPortable() {
|
|
local update_pyvenv_cfg=false
|
|
if [ "${1:-}" = "--update-pyvenv-cfg" ]; then
|
|
update_pyvenv_cfg=true
|
|
fi
|
|
|
|
local venv_dir="${EDIR}/venv"
|
|
local vbin="${venv_dir}/bin"
|
|
|
|
[ -d "${vbin}" ] || return 0
|
|
|
|
# 1) Replace python symlinks with relative ones to ../../python/bin/python3
|
|
# (venv/bin -> venv -> EDIR -> python/bin)
|
|
local rel_py='../../python/bin/python3'
|
|
|
|
for name in python3 python; do
|
|
if [ -e "${vbin}/${name}" ] || [ -L "${vbin}/${name}" ]; then
|
|
rm -f "${vbin}/${name}"
|
|
fi
|
|
done
|
|
ln -s "${rel_py}" "${vbin}/python3"
|
|
ln -s "python3" "${vbin}/python"
|
|
|
|
# 2) Update pyvenv.cfg to point to the portable Python directory (only at runtime)
|
|
# Use absolute path resolved at runtime so it works when the venv is copied
|
|
if [ "$update_pyvenv_cfg" = "true" ]; then
|
|
local pyvenv_cfg="${venv_dir}/pyvenv.cfg"
|
|
if [ -f "${pyvenv_cfg}" ]; then
|
|
local portable_dir="$(_portable_dir)"
|
|
# Resolve to absolute path - this ensures it works when the backend is copied
|
|
# Only resolve if the directory exists (it should if ensurePortablePython was called)
|
|
if [ -d "${portable_dir}" ]; then
|
|
portable_dir="$(cd "${portable_dir}" && pwd)"
|
|
else
|
|
# Fallback to relative path if directory doesn't exist yet
|
|
portable_dir="../python"
|
|
fi
|
|
local sed_i=(sed -i)
|
|
# macOS/BSD sed needs a backup suffix; GNU sed doesn't. Make it portable:
|
|
if sed --version >/dev/null 2>&1; then
|
|
sed_i=(sed -i)
|
|
else
|
|
sed_i=(sed -i '')
|
|
fi
|
|
# Update the home field in pyvenv.cfg
|
|
# Handle both absolute paths (starting with /) and relative paths
|
|
if grep -q "^home = " "${pyvenv_cfg}"; then
|
|
"${sed_i[@]}" "s|^home = .*|home = ${portable_dir}|" "${pyvenv_cfg}"
|
|
else
|
|
# If home field doesn't exist, add it
|
|
echo "home = ${portable_dir}" >> "${pyvenv_cfg}"
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# 3) Rewrite shebangs of entry points to use env, so the venv is relocatable
|
|
# Only touch text files that start with #! and reference the current venv.
|
|
local ve_abs="${vbin}/python"
|
|
local sed_i=(sed -i)
|
|
# macOS/BSD sed needs a backup suffix; GNU sed doesn't. Make it portable:
|
|
if sed --version >/dev/null 2>&1; then
|
|
sed_i=(sed -i)
|
|
else
|
|
sed_i=(sed -i '')
|
|
fi
|
|
|
|
for f in "${vbin}"/*; do
|
|
[ -f "$f" ] || continue
|
|
# Fast path: check first two bytes (#!)
|
|
head -c2 "$f" 2>/dev/null | grep -q '^#!' || continue
|
|
# Only rewrite if the shebang mentions the (absolute) venv python
|
|
if head -n1 "$f" | grep -Fq "${ve_abs}"; then
|
|
"${sed_i[@]}" '1s|^#!.*$|#!/usr/bin/env python3|' "$f"
|
|
chmod +x "$f" 2>/dev/null || true
|
|
fi
|
|
done
|
|
}
|
|
|
|
|
|
# Apply the venv to the current process: VIRTUAL_ENV, PATH, PYTHONHOME hygiene.
|
|
# Equivalent to the runtime portion of `source bin/activate`, but computed from
|
|
# $EDIR (resolved at runtime via realpath) instead of the path baked into
|
|
# bin/activate at venv-create time. `uv venv` (and `python -m venv`) both bake
|
|
# the create-time absolute path in, so sourcing activate on a relocated venv —
|
|
# e.g. one built at /vllm/venv inside a Docker stage and unpacked under
|
|
# /backends/cuda13-vllm-development/venv at runtime — silently prepends a
|
|
# stale, non-existent path to $PATH. Doing the setup ourselves sidesteps that;
|
|
# this is the same approach `uv run` takes internally.
|
|
_activateVenv() {
|
|
export VIRTUAL_ENV="${EDIR}/venv"
|
|
export PATH="${EDIR}/venv/bin:${PATH}"
|
|
unset PYTHONHOME
|
|
}
|
|
|
|
# ensureVenv makes sure that the venv for the backend both exists, and is activated.
|
|
#
|
|
# This function is idempotent, so you can call it as many times as you want and it will
|
|
# always result in an activated virtual environment
|
|
function ensureVenv() {
|
|
local interpreter=""
|
|
|
|
if [ "x${PORTABLE_PYTHON}" == "xtrue" ] || [ -e "$(_portable_python)" ]; then
|
|
echo "Using portable Python"
|
|
ensurePortablePython
|
|
interpreter="$(_portable_python)"
|
|
else
|
|
# Prefer system python${PYTHON_VERSION}, else python3, else fall back to bundled
|
|
if command -v python${PYTHON_VERSION} >/dev/null 2>&1; then
|
|
interpreter="python${PYTHON_VERSION}"
|
|
elif command -v python3 >/dev/null 2>&1; then
|
|
interpreter="python3"
|
|
else
|
|
echo "No suitable system Python found, bootstrapping portable build..."
|
|
ensurePortablePython
|
|
interpreter="$(_portable_python)"
|
|
fi
|
|
fi
|
|
|
|
if [ ! -d "${EDIR}/venv" ]; then
|
|
if [ "x${USE_PIP}" == "xtrue" ]; then
|
|
# --copies is only needed when we will later relocate the venv via
|
|
# _makeVenvPortable (PORTABLE_PYTHON=true). Some Python builds —
|
|
# notably macOS system Python — refuse to create a venv with
|
|
# --copies because the build doesn't support it. Fall back to
|
|
# symlinks in that case.
|
|
local venv_args=""
|
|
if [ "x${PORTABLE_PYTHON}" == "xtrue" ]; then
|
|
venv_args="--copies"
|
|
fi
|
|
"${interpreter}" -m venv ${venv_args} "${EDIR}/venv"
|
|
_activateVenv
|
|
"${interpreter}" -m pip install --upgrade pip
|
|
else
|
|
if [ "x${PORTABLE_PYTHON}" == "xtrue" ]; then
|
|
uv venv --python "${interpreter}" "${EDIR}/venv"
|
|
else
|
|
uv venv --python "${PYTHON_VERSION}" "${EDIR}/venv"
|
|
fi
|
|
fi
|
|
if [ "x${PORTABLE_PYTHON}" == "xtrue" ]; then
|
|
# During install, only update symlinks and shebangs, not pyvenv.cfg
|
|
_makeVenvPortable
|
|
fi
|
|
fi
|
|
|
|
# We call it here to make sure that when we source a venv we can still use python as expected
|
|
if [ -x "$(_portable_python)" ]; then
|
|
_macosPortableEnv
|
|
fi
|
|
|
|
if [ "x${VIRTUAL_ENV:-}" != "x${EDIR}/venv" ]; then
|
|
_activateVenv
|
|
fi
|
|
}
|
|
|
|
|
|
function runProtogen() {
|
|
ensureVenv
|
|
|
|
# The protoc that grpcio-tools bundles stamps a Protobuf "gencode" version
|
|
# into backend_pb2.py, and Protobuf refuses to import a stub whose gencode is
|
|
# newer than the installed runtime. Left unpinned, grpcio-tools resolves to
|
|
# the newest release (1.82.1, gencode 7.35.0) which a backend holding the
|
|
# runtime lower (vLLM resolves protobuf to 6.33.6) then cannot import,
|
|
# crashing with "grpc service not ready" before it ever loads a model.
|
|
#
|
|
# Resolve the generator in a THROWAWAY environment so the backend's own venv
|
|
# keeps exactly the dependency set its requirements files declared. The
|
|
# generator is a build tool and has no business editing the runtime deps: the
|
|
# spec below is chosen to fit the venv as it stands, never to change it.
|
|
# Falls back to unpinned when neither version can be detected.
|
|
# See mudler/LocalAI#10718, #10940.
|
|
local protobuf_version grpcio_version protogen_env protogen_python
|
|
protobuf_version="$(python -c 'import importlib.metadata as m; print(m.version("protobuf"))' 2>/dev/null || true)"
|
|
grpcio_version="$(python -c 'import importlib.metadata as m; print(m.version("grpcio"))' 2>/dev/null || true)"
|
|
|
|
# The stubs impose TWO independent constraints on the generator, and both
|
|
# have to hold or the backend dies on import:
|
|
#
|
|
# backend_pb2.py needs protobuf runtime >= gencode
|
|
# backend_pb2_grpc.py needs installed grpcio >= grpcio-tools
|
|
#
|
|
# Bound grpcio-tools from both sides and let the resolver find the newest
|
|
# version that satisfies them. The protobuf ceiling makes it back off to an
|
|
# older grpcio-tools when the runtime is behind, which is what bounds the
|
|
# gencode; the grpcio ceiling keeps the _grpc stub loadable. Pinning only one
|
|
# side is what made the earlier attempts fail, in both directions.
|
|
local -a protogen_spec=("grpcio-tools")
|
|
if [ -n "${grpcio_version}" ]; then
|
|
protogen_spec=("grpcio-tools<=${grpcio_version}")
|
|
fi
|
|
if [ -n "${protobuf_version}" ]; then
|
|
protogen_spec+=("protobuf<=${protobuf_version}")
|
|
fi
|
|
|
|
protogen_env="$(mktemp -d)"
|
|
|
|
if [ "x${USE_PIP}" == "xtrue" ]; then
|
|
python -m venv "${protogen_env}"
|
|
"${protogen_env}/bin/pip" install "${protogen_spec[@]}"
|
|
else
|
|
uv venv "${protogen_env}"
|
|
VIRTUAL_ENV="${protogen_env}" uv pip install "${protogen_spec[@]}"
|
|
fi
|
|
protogen_python="${protogen_env}/bin/python"
|
|
|
|
pushd "${EDIR}" >/dev/null
|
|
# Drop the cached bytecode along with the sources. CPython validates a
|
|
# .pyc against the source's mtime *and size*, both of which can be
|
|
# unchanged across a regeneration (the gencode triple is the same width
|
|
# whether it reads 7.35.0 or 6.33.5), so a stale backend_pb2.pyc can be
|
|
# reused in place of the stub we just generated.
|
|
rm -f backend_pb2.py backend_pb2.pyi backend_pb2_grpc.py
|
|
rm -rf __pycache__
|
|
# Generate with the throwaway toolchain; the output is plain Python and
|
|
# carries no dependency on the interpreter that produced it.
|
|
"${protogen_python}" -m grpc_tools.protoc -I../../ -I./ --python_out=. --grpc_python_out=. backend.proto
|
|
|
|
# Verify with the BACKEND's python, which is the interpreter that has to
|
|
# import these at model load. Fail the build rather than ship a backend
|
|
# that cannot import its own stubs: otherwise the gencode/runtime
|
|
# mismatch only surfaces in a released image, as an opaque
|
|
# "grpc service not ready".
|
|
# Check BOTH stubs: backend_pb2 catches a gencode ahead of the protobuf
|
|
# runtime, backend_pb2_grpc catches generated code ahead of the installed
|
|
# grpcio. Checking only the former lets the latter reach CI, or users.
|
|
if ! python -c 'import backend_pb2, backend_pb2_grpc' >/dev/null; then
|
|
echo "runProtogen: generated stubs are not importable by the backend venv (protobuf ${protobuf_version:-unknown}, grpcio ${grpcio_version:-unknown})" >&2
|
|
exit 1
|
|
fi
|
|
popd >/dev/null
|
|
|
|
rm -rf "${protogen_env}"
|
|
}
|
|
|
|
|
|
# When JETSON_WHEELS_DIR points at a directory of wheels mirrored from
|
|
# pypi.jetson-ai-lab.io (CI bind-mounts the jetson-wheels OCI image there —
|
|
# see backend/Dockerfile.python and .github/workflows/jetson-wheels.yml),
|
|
# installRequirements serves it on localhost as a PEP 503 index and swaps the
|
|
# jetson index host in the requirements files for the local one.
|
|
#
|
|
# The upstream index has a history of multi-hour 502 outages, and a 502 on
|
|
# any project page aborts the whole uv resolution — uv consults every
|
|
# configured index for every requirement, so even PyPI-hosted packages die
|
|
# with it. The local index instead 404s for anything it doesn't carry, which
|
|
# resolvers cleanly follow up on PyPI; only the jetson-built wheels (torch
|
|
# and friends) resolve locally. When JETSON_WHEELS_DIR is unset — the
|
|
# default, e.g. building on a real Jetson — nothing changes and the upstream
|
|
# index is used as written in the requirements files.
|
|
JETSON_PYPI_HOST="pypi.jetson-ai-lab.io"
|
|
_JETSON_MIRROR_PID=""
|
|
_JETSON_MIRROR_URL=""
|
|
|
|
function _stopJetsonMirror() {
|
|
if [ -n "${_JETSON_MIRROR_PID}" ]; then
|
|
kill "${_JETSON_MIRROR_PID}" 2>/dev/null || true
|
|
_JETSON_MIRROR_PID=""
|
|
_JETSON_MIRROR_URL=""
|
|
fi
|
|
}
|
|
|
|
function _startJetsonMirror() {
|
|
local script_dir port_file port tries
|
|
# An empty dir is the JETSON_WHEELS_IMAGE=scratch default in
|
|
# Dockerfile.python: no mirror was provided, use upstream as-is.
|
|
if [ -z "$(find "${JETSON_WHEELS_DIR}" -name '*.whl' -print -quit 2>/dev/null)" ]; then
|
|
echo "jetson wheels dir ${JETSON_WHEELS_DIR} has no wheels, using upstream ${JETSON_PYPI_HOST}"
|
|
return 0
|
|
fi
|
|
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
port_file="$(mktemp)"
|
|
rm -f "${port_file}"
|
|
python3 "${script_dir}/pypi_mirror_server.py" --root "${JETSON_WHEELS_DIR}" --port-file "${port_file}" &
|
|
_JETSON_MIRROR_PID=$!
|
|
trap _stopJetsonMirror EXIT
|
|
tries=0
|
|
until [ -s "${port_file}" ]; do
|
|
tries=$((tries + 1))
|
|
if [ ${tries} -gt 50 ] || ! kill -0 "${_JETSON_MIRROR_PID}" 2>/dev/null; then
|
|
echo "WARNING: local jetson wheel mirror failed to start, using upstream ${JETSON_PYPI_HOST}"
|
|
_stopJetsonMirror
|
|
return 0
|
|
fi
|
|
sleep 0.2
|
|
done
|
|
port="$(cat "${port_file}")"
|
|
rm -f "${port_file}"
|
|
_JETSON_MIRROR_URL="http://127.0.0.1:${port}"
|
|
echo "serving jetson wheels from ${JETSON_WHEELS_DIR} at ${_JETSON_MIRROR_URL}"
|
|
}
|
|
|
|
# installRequirements looks for several requirements files and if they exist runs the install for them in order
|
|
#
|
|
# - requirements-install.txt
|
|
# - requirements.txt
|
|
# - requirements-${BUILD_TYPE}.txt
|
|
# - requirements-${BUILD_PROFILE}.txt
|
|
#
|
|
# BUILD_PROFILE is a more specific version of BUILD_TYPE, ex: cuda-12 or cuda-13
|
|
# it can also include some options that we do not have BUILD_TYPES for, ex: intel
|
|
#
|
|
# NOTE: for BUILD_PROFILE==intel, this function does NOT automatically use the Intel python package index.
|
|
# you may want to add the following line to a requirements-intel.txt if you use one:
|
|
#
|
|
# --index-url https://download.pytorch.org/whl/xpu
|
|
#
|
|
# If you need to add extra flags into the pip install command you can do so by setting the variable EXTRA_PIP_INSTALL_FLAGS
|
|
# before calling installRequirements. For example:
|
|
#
|
|
# source $(dirname $0)/../common/libbackend.sh
|
|
# EXTRA_PIP_INSTALL_FLAGS="--no-build-isolation"
|
|
# installRequirements
|
|
function installRequirements() {
|
|
ensureVenv
|
|
declare -a requirementFiles=(
|
|
"${EDIR}/requirements-install.txt"
|
|
"${EDIR}/requirements.txt"
|
|
"${EDIR}/requirements-${BUILD_TYPE:-}.txt"
|
|
)
|
|
|
|
if [ "x${BUILD_TYPE:-}" != "x${BUILD_PROFILE}" ]; then
|
|
requirementFiles+=("${EDIR}/requirements-${BUILD_PROFILE}.txt")
|
|
fi
|
|
if [ "x${BUILD_TYPE:-}" == "x" ]; then
|
|
requirementFiles+=("${EDIR}/requirements-cpu.txt")
|
|
fi
|
|
requirementFiles+=("${EDIR}/requirements-after.txt")
|
|
if [ "x${BUILD_TYPE:-}" != "x${BUILD_PROFILE}" ]; then
|
|
requirementFiles+=("${EDIR}/requirements-${BUILD_PROFILE}-after.txt")
|
|
fi
|
|
|
|
# This is needed to build wheels that e.g. depends on Python.h
|
|
if [ "x${PORTABLE_PYTHON}" == "xtrue" ]; then
|
|
export C_INCLUDE_PATH="${C_INCLUDE_PATH:-}:$(_portable_dir)/include/python${PYTHON_VERSION}"
|
|
fi
|
|
|
|
if [ -n "${JETSON_WHEELS_DIR:-}" ] && [ -d "${JETSON_WHEELS_DIR}" ]; then
|
|
_startJetsonMirror
|
|
fi
|
|
|
|
local installFile
|
|
for reqFile in ${requirementFiles[@]}; do
|
|
if [ -f "${reqFile}" ]; then
|
|
installFile="${reqFile}"
|
|
if [ -n "${_JETSON_MIRROR_URL}" ] && grep -q "${JETSON_PYPI_HOST}" "${reqFile}"; then
|
|
installFile="$(mktemp)"
|
|
sed "s,https://${JETSON_PYPI_HOST},${_JETSON_MIRROR_URL},g" "${reqFile}" > "${installFile}"
|
|
echo "rewrote ${JETSON_PYPI_HOST} in ${reqFile} to the local wheel mirror (${installFile})"
|
|
fi
|
|
echo "starting requirements install for ${reqFile}"
|
|
if [ "x${USE_PIP}" == "xtrue" ]; then
|
|
pip install ${EXTRA_PIP_INSTALL_FLAGS:-} --requirement "${installFile}"
|
|
else
|
|
uv pip install ${EXTRA_PIP_INSTALL_FLAGS:-} --requirement "${installFile}"
|
|
fi
|
|
echo "finished requirements install for ${reqFile}"
|
|
fi
|
|
done
|
|
|
|
_stopJetsonMirror
|
|
|
|
runProtogen
|
|
}
|
|
|
|
# startBackend discovers and runs the backend GRPC server
|
|
#
|
|
# You can specify a specific backend file to execute by setting BACKEND_FILE before calling startBackend.
|
|
# example:
|
|
#
|
|
# source ../common/libbackend.sh
|
|
# BACKEND_FILE="${MY_DIR}/source/backend.py"
|
|
# startBackend $@
|
|
#
|
|
# valid filenames for autodiscovered backend servers are:
|
|
# - server.py
|
|
# - backend.py
|
|
# - ${BACKEND_NAME}.py
|
|
function startBackend() {
|
|
ensureVenv
|
|
# Update pyvenv.cfg before running to ensure paths are correct for current location
|
|
# This is critical when the backend position is dynamic (e.g., copied from container)
|
|
if [ "x${PORTABLE_PYTHON}" == "xtrue" ] || [ -x "$(_portable_python)" ]; then
|
|
_makeVenvPortable --update-pyvenv-cfg
|
|
fi
|
|
|
|
# Set up GPU library paths if a lib directory exists
|
|
# This allows backends to include their own GPU libraries (CUDA, ROCm, etc.)
|
|
if [ -d "${EDIR}/lib" ]; then
|
|
export LD_LIBRARY_PATH="${EDIR}/lib:${LD_LIBRARY_PATH:-}"
|
|
echo "Added ${EDIR}/lib to LD_LIBRARY_PATH for GPU libraries"
|
|
fi
|
|
|
|
if [ ! -z "${BACKEND_FILE:-}" ]; then
|
|
exec "${EDIR}/venv/bin/python" "${BACKEND_FILE}" "$@"
|
|
elif [ -e "${MY_DIR}/server.py" ]; then
|
|
exec "${EDIR}/venv/bin/python" "${MY_DIR}/server.py" "$@"
|
|
elif [ -e "${MY_DIR}/backend.py" ]; then
|
|
exec "${EDIR}/venv/bin/python" "${MY_DIR}/backend.py" "$@"
|
|
elif [ -e "${MY_DIR}/${BACKEND_NAME}.py" ]; then
|
|
exec "${EDIR}/venv/bin/python" "${MY_DIR}/${BACKEND_NAME}.py" "$@"
|
|
fi
|
|
}
|
|
|
|
|
|
# runUnittests discovers and runs python unittests
|
|
#
|
|
# You can specify a specific test file to use by setting TEST_FILE before calling runUnittests.
|
|
# example:
|
|
#
|
|
# source ../common/libbackend.sh
|
|
# TEST_FILE="${MY_DIR}/source/test.py"
|
|
# runUnittests $@
|
|
#
|
|
# be default a file named test.py in the backends directory will be used
|
|
function runUnittests() {
|
|
ensureVenv
|
|
if [ ! -z "${TEST_FILE:-}" ]; then
|
|
testDir=$(dirname "$(realpath "${TEST_FILE}")")
|
|
testFile=$(basename "${TEST_FILE}")
|
|
pushd "${testDir}" >/dev/null
|
|
python -m unittest "${testFile}"
|
|
popd >/dev/null
|
|
elif [ -f "${MY_DIR}/test.py" ]; then
|
|
pushd "${MY_DIR}" >/dev/null
|
|
python -m unittest test.py
|
|
popd >/dev/null
|
|
else
|
|
echo "no tests defined for ${BACKEND_NAME}"
|
|
fi
|
|
}
|
|
|
|
|
|
##################################################################################
|
|
# Below here are helper functions not intended to be used outside of the library #
|
|
##################################################################################
|
|
|
|
# checkTargets determines if the current BUILD_TYPE or BUILD_PROFILE is in a list of valid targets
|
|
function checkTargets() {
|
|
targets=$@
|
|
declare -a targets=($targets)
|
|
for target in ${targets[@]}; do
|
|
if [ "x${BUILD_TYPE:-}" == "x${target}" ]; then
|
|
echo true; return 0
|
|
fi
|
|
if [ "x${BUILD_PROFILE}" == "x${target}" ]; then
|
|
echo true; return 0
|
|
fi
|
|
done
|
|
echo false
|
|
}
|
|
|
|
init
|