Sanad_Package_1/Dockerfile

102 lines
5.2 KiB
Docker

# syntax=docker/dockerfile:1
# ─────────────────────────────────────────────────────────────────────────────
# Sanad Package 1 — Basic Communication.
#
# SELF-CONTAINED: builds from a public base image with NO dependency on a
# `sanad-base` image or a sibling `Sanad/` / `sanad_pkg/` checkout. The Sanad
# engine is vendored at ./vendor/Sanad and the license/bus lib at
# ./vendor/sanad_pkg, so the package repo builds and runs entirely on its own.
#
# Build context MUST be THIS package directory:
# docker build -t sanad-p1:latest .
# (docker-compose.yml uses `context: .`. On a Jetson Docker without buildx:
# DOCKER_BUILDKIT=0 docker build -t sanad-p1:latest .)
# ─────────────────────────────────────────────────────────────────────────────
ARG BASE_OS_IMAGE=python:3.10-slim-bookworm
FROM ${BASE_OS_IMAGE}
ENV DEBIAN_FRONTEND=noninteractive \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONPATH=/app
WORKDIR /app
# System deps: shared (audio) + P1 (PortAudio + a C toolchain so pyaudio's
# extension compiles on the slim base) + iproute2 (`ip`).
# `ip` is REQUIRED for chest ('builtin') audio: voice/audio_io.py _find_g1_local_ip()
# runs `ip -4 -o addr` to find the host's 192.168.123.x address for the G1 chest-mic
# UDP multicast — without it the live voice subprocess crashes on FileNotFoundError.
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates libsndfile1 alsa-utils pulseaudio-utils iproute2 \
portaudio19-dev libportaudio2 build-essential python3-dev \
&& rm -rf /var/lib/apt/lists/*
# Python deps (base + P1 merged).
COPY requirements.txt /tmp/requirements.txt
RUN python3 -m pip install --no-cache-dir --upgrade pip \
&& python3 -m pip install --no-cache-dir -r /tmp/requirements.txt
# ── Optional: Unitree SDK — G1 chest (builtin) audio over DDS ─────────────────
# WITH_UNITREE_SDK=1 (default) builds CycloneDDS + installs unitree_sdk2_python so
# the chest mic/speaker work out of the box. Wrapped so a failure NEVER breaks the
# image — chest audio is then unavailable (use SANAD_AUDIO_PROFILE=plugged); USB
# (plugged) audio always works without the SDK. Set =0 for a leaner image.
# NOTE: build the FULL CycloneDDS (do NOT pass -DBUILD_IDLC=NO) — the `cyclonedds`
# Python binding's find_package(CycloneDDS) needs idlc, else it fails with
# "Could not locate cyclonedds". Pin the binding to match the 0.10.x C library.
ARG WITH_UNITREE_SDK=1
ENV CYCLONEDDS_HOME=/usr/local \
LD_LIBRARY_PATH=/usr/local/lib
RUN if [ "$WITH_UNITREE_SDK" = "1" ]; then \
( set -eux; \
apt-get update; \
apt-get install -y --no-install-recommends git cmake build-essential; \
git clone --depth 1 -b releases/0.10.x https://github.com/eclipse-cyclonedds/cyclonedds /tmp/cyclonedds; \
cmake -S /tmp/cyclonedds -B /tmp/cyclonedds/build -DCMAKE_INSTALL_PREFIX=/usr/local; \
cmake --build /tmp/cyclonedds/build --target install -j"$(nproc)"; \
CYCLONEDDS_HOME=/usr/local CMAKE_PREFIX_PATH=/usr/local python3 -m pip install --no-cache-dir "cyclonedds==0.10.2"; \
git clone --depth 1 https://github.com/unitreerobotics/unitree_sdk2_python /opt/unitree_sdk2_python; \
python3 -m pip install --no-cache-dir -e /opt/unitree_sdk2_python; \
python3 -c "import unitree_sdk2py; print('unitree_sdk2py OK')"; \
rm -rf /tmp/cyclonedds /var/lib/apt/lists/*; \
) || echo "WARN[P1]: Unitree SDK build failed — chest (builtin) audio unavailable; use SANAD_AUDIO_PROFILE=plugged"; \
else echo "WITH_UNITREE_SDK=0 — skipping Unitree SDK (USB/plugged audio only)"; fi
# License/bus shim + PUBLIC verification key (vendored — no sanad_pkg sibling).
COPY vendor/sanad_pkg /app/sanad_pkg
RUN mkdir -p /etc/sanad && cp /app/sanad_pkg/pubkey.ed25519 /etc/sanad/pubkey.ed25519
# Canonical Sanad engine (vendored — no Sanad/ sibling, no sanad-base).
COPY vendor/Sanad /app/Sanad
# P1 launcher + routes + entrypoint + config + static.
COPY app_p1.py /app/app_p1.py
COPY routes_p1.py /app/routes_p1.py
COPY entrypoint.sh /app/entrypoint.sh
COPY config /app/pkg1_config
COPY static /app/pkg1_static
RUN chmod +x /app/entrypoint.sh
# Ship KEYLESS — blank any Gemini key baked into the vendored Sanad config so the
# vendor key never ships; the customer adds their own via the dashboard.
COPY strip_key.py /tmp/strip_key.py
RUN python3 /tmp/strip_key.py && rm -f /tmp/strip_key.py
# Sanity: the vendored namespace imports cleanly.
RUN python3 - <<'PY'
import importlib.util as u, sys
mods = ("sanad_pkg.license", "sanad_pkg.bus", "Sanad")
ok = all(u.find_spec(m) for m in mods)
print("P1 self-contained: vendored modules importable:", ok)
sys.exit(0 if ok else 1)
PY
ENV SANAD_PACKAGE=P1 \
SANAD_DASHBOARD_PORT=8011 \
SANAD_DASHBOARD_HOST=0.0.0.0 \
SANAD_P1_STATIC=/app/pkg1_static \
SANAD_LICENSE=/etc/sanad/sanad.lic \
SANAD_PUBKEY=/etc/sanad/pubkey.ed25519
EXPOSE 8011
ENTRYPOINT ["/app/entrypoint.sh"]