102 lines
5.2 KiB
Docker
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"]
|