# Frametop runtime image (see pack/README.md).
#
# One image that holds everything Frametop needs: the frozen toolchain and C
# libraries, its native binaries, and its locked Python environment. A given
# image tag always behaves the same, on a PC and on the Frame.
#
# setup/dev-container.sh stays the source of truth for the package list on the
# Frame's dev container; the list below freezes the runtime-relevant subset of
# it plus the toolchain (the binaries are built in this image). Keep the two in
# sync on purpose: dev-container.sh documents *why* each package is there.
FROM registry.fedoraproject.org/fedora-toolbox:44@sha256:034cb7c472038e2d879ddc19568106a1342aa24403f0d641a0f73dda638707ad AS runtime-common

# Toolchain (the binaries are built in here) and the libraries they link.
RUN dnf -y install --setopt=install_weak_deps=False \
      gcc gcc-c++ make pkgconf-pkg-config curl unzip just git cmake ninja-build \
      pipewire-devel libxkbcommon-devel libinput-devel systemd-devel dbus-devel \
      libdrm-devel mesa-libgbm-devel wayland-devel vulkan-loader-devel \
      vulkan-headers plasma-wayland-protocols wlroots-devel pixman-devel \
      libstdc++-static glibc-static jsoncpp-devel zstd \
      # remote displays (stream/): moonlight-embedded's libgamestream and
      # moonlight-common-c, and the host's sound (Opus, PulseAudio)
      openssl-devel libcurl-devel expat-devel libuuid-devel json-devel \
      opus-devel pulseaudio-libs-devel \
      # Frametop Voice (voice/): glslc and the SPIR-V headers for whisper.cpp's
      # Vulkan backend; ft-voiced records with pw-record and plays its sounds with pw-play
      glslc spirv-headers-devel pipewire-utils \
      # Frametop's keyboard (keyboard/, TaiKeid's Full Keyboard): Cairo and Pango
      # draw it, SDL2 for its preview, and its input methods: Hangul, Pinyin (pyzy),
      # Japanese (Anthy, loaded at run time)
      cairo-devel pango-devel json-c-devel sdl2-compat-devel libhangul-devel pyzy-devel \
      anthy-unicode-devel \
      # Frametop Input Settings (Kirigami, PySide6) and remote desktop (krdp):
      # keep the native KDE/Qt stack in matching Fedora packages
      python3-shiboken6 kf6-kirigami kf6-qqc2-desktop-style \
      qt6-qtwayland qt6-qtsvg qt6-qtimageformats qt6-qtpdf \
      breeze-icon-theme plasma-breeze krdp freerdp tigervnc-x11-server xrandr \
    && dnf -y remove 'glibc-langpack-*' \
    && dnf -y install glibc-langpack-en \
    && dnf clean all \
    && rm -rf /usr/share/doc/* /usr/share/man/* /usr/share/info/* \
              /usr/share/wallpapers

# OpenVR's API library for linking, from the SDK tag the build scripts pin
# (scripts/openvr.sh fetches the headers). The image has no SteamVR, so the
# binaries link this copy; their rpath names SteamVR's folder first, so on the
# Frame they load SteamVR's own libopenvr_api, as the on-device builds do.
RUN install -d /opt/frametop/bin /opt/frametop/lib \
 && curl -fsSL https://github.com/ValveSoftware/openvr/raw/v2.15.6/bin/linuxarm64/libopenvr_api.so \
      -o /opt/frametop/lib/libopenvr_api.so \
 && echo "cc3671d24dd23fb61494e8a8327cf4fe7f638a60508ff7225984a03f2f5947e9  /opt/frametop/lib/libopenvr_api.so" | sha256sum -c

# Python: Fedora's own python3 (the digest-pinned base freezes it), with the
# locked packages in a venv on top. The venv sees the system site-packages,
# because PySide6 and Kirigami come from Fedora for this python3: one interpreter
# imports both, as in the dev container on the Frame. The check names the
# recorders' huggingface_hub (uploads) and cryptography (encrypted exports) too:
# the tests that need them skip without them. PySide6 itself is checked in the
# runtime stage at the end, where the builder's export lands. Rebuilt only when
# the lockfile changes, so source edits below reuse this layer.
# uv sync --frozen --group tracker --group voice: the default dev group (pytest/ruff
# for CI's in-image tests) plus the tracker stack and Frametop Voice's (sherpa-onnx);
# the types group (mypy) stays out.
# --no-cache keeps uv's download cache out of the image.
# uv is a pinned, checked build tool, not a runtime dependency. Extract it,
# create the environment, and remove it in one layer so neither uv nor uvx
# takes space in an installed image.
ENV UV_PROJECT_ENVIRONMENT=/opt/frametop/venv UV_PYTHON=/usr/bin/python3 UV_PYTHON_DOWNLOADS=never
COPY pyproject.toml uv.lock .python-version /src/frametop/
RUN uvdir=$(mktemp -d) \
 && curl -fsSL https://github.com/astral-sh/uv/releases/download/0.12.23/uv-aarch64-unknown-linux-gnu.tar.gz \
      -o "$uvdir/uv.tar.gz" \
 && echo "6524bd338177ed50d035d39354e12545e993bbeba2ecbddf0480c5b3a81d313f  $uvdir/uv.tar.gz" | sha256sum -c \
 && tar -xzf "$uvdir/uv.tar.gz" --strip-components=1 -C "$uvdir" uv-aarch64-unknown-linux-gnu/uv \
 && "$uvdir/uv" venv --system-site-packages /opt/frametop/venv \
 && cd /src/frametop && "$uvdir/uv" sync --frozen --group tracker --group voice --no-cache \
 && /opt/frametop/venv/bin/python -c 'import numpy, cv2, sherpa_onnx, sentencepiece, huggingface_hub, cryptography' \
 && rm -rf "$uvdir"

# Preserve separate base/environment layers for cached builds and rollback.
# Keep the toolchain and headers: ft dev shell/test still work in this image.
# PySide's full Fedora RPM also requires WebEngine/3D/etc. The builder exports
# the unchanged bindings Frametop uses, against these same system Qt libraries.
WORKDIR /src/frametop
ENV FRAME_IN_BOX=1 OPENVR_LIB=/opt/frametop/lib
ENV PATH=/opt/frametop/venv/bin:/opt/frametop/bin:/usr/local/bin:/usr/bin:/usr/sbin
RUN ln -s /run/host/opt/steamvr /opt/steamvr
CMD ["sleep", "infinity"]

FROM runtime-common AS builder
RUN rpm -qa --qf '%{NEVRA}\n' | sort > /tmp/runtime-rpms \
 && dnf -y install --setopt=install_weak_deps=False python3-pyside6 \
 && rpm -qa --qf '%{NEVRA}\n' | sort > /tmp/builder-rpms \
 && test -z "$(comm -23 /tmp/runtime-rpms /tmp/builder-rpms)" \
 && dnf clean all \
 && install -d /runtime/opt/frametop/bin /runtime/opt/frametop/lib

# The sources, then the binaries, built by the same build scripts as on the
# Frame (FRAME_IN_BOX=1: they run here instead of entering the dev container).
# Each program stays in its build/ folder in /src/frametop and is copied to
# /opt/frametop.
COPY . /src/frametop
RUN screens/build.sh \
 && install -t /runtime/opt/frametop/bin screens/build/ft-screens screens/build/ft-handtest screens/build/ft-spotpanel \
 && rm -f screens/build/*.o
RUN pointer/helper/build.sh && pointer/driver/build.sh \
 && install -t /runtime/opt/frametop/bin pointer/helper/build/ft-pointer \
 && install -t /runtime/opt/frametop/lib pointer/driver/build/driver_ft_pointer.so
RUN gaze/build.sh \
 && install -t /runtime/opt/frametop/bin gaze/build/ft-gaze gaze/build/ft-gazepanel
# Frametop's keyboard (TaiKeid's Full Keyboard): it stays in keyboard/build/frametop,
# with the input method engines, Steam bridge and SteamVR manifests it finds beside
# itself, where frametop-keyboard.service runs it from. The rest of the build goes.
RUN keyboard/build.sh \
 && cd keyboard/build/frametop \
 && find . -mindepth 1 -maxdepth 1 ! -name framekeyboard ! -name framekeyboard-native-guard \
      ! -name engines ! -name bridge ! -name vr -exec rm -rf {} + \
 && strip --strip-unneeded framekeyboard framekeyboard-native-guard engines/*.so
RUN power/build.sh \
 && install -t /runtime/opt/frametop/bin power/build/ft-powerd
RUN stream/build.sh \
 && install -t /runtime/opt/frametop/bin stream/build/ft-stream \
 && rm -rf stream/build/mlc stream/build/gamestream
# Our own eye tracker: ft-eyegrab (it runs on the host, as root) and ft-eyes'
# build/venv, which here only points at the venv above.
RUN gaze/tracker/build.sh \
 && install -t /runtime/opt/frametop/bin gaze/tracker/build/ft-eyegrab
# Frametop Voice: whisper.cpp's server (Vulkan), and build/venv pointing at the venv
# above. Its speech models aren't in the image: voice/models.sh fetches them on install.
RUN voice/build.sh \
 && install -t /runtime/opt/frametop/bin voice/build/bin/whisper-server
# Hand tracking: ft-camd (static: it runs on the host, with the file
# capabilities hands/run.sh install sets) and ft-hands, which builds ncnn at the
# tag hands/Makefile pins. Both stay in hands/build/, where their units and
# file capabilities point, so they aren't copied to /opt/frametop.
RUN hands/build.sh \
 && test -x hands/build/ft-camd && test -x hands/build/ft-hands
# The Hand Recorder's headset panel: it stays in hands/rec/build/, where the recorder
# (hands/rec/session.py) runs it from.
RUN hands/rec/build.sh \
 && test -x hands/rec/build/ft-handpanel

# A release installs from this image (pack/install-release.sh): it copies
# /src/frametop, built, to the Frame for its host side (services, the driver,
# the desktop's scripts), and its install.sh installs this distrobox (1.8.2.5,
# as a source install clones it, here by commit) to run the image as the
# container those programs start in (scripts/in-box).
RUN git init -q pack/build/distrobox \
 && cd pack/build/distrobox \
 && git fetch -q --depth 1 https://github.com/89luca89/distrobox.git 40c3cd724faa434aeb0a23e28776665b92de68bd \
 && git checkout -q FETCH_HEAD \
 && rm -rf .git
# Export Fedora's selected Python bindings without changing their native code.
RUN python3 pack/pyside-runtime.py --repo /src/frametop --destination /runtime

# The default environment: Frametop's binaries and the venv's python3 (Fedora's
# interpreter, with the locked packages and the Fedora Qt stack) first.
# sleep infinity as the default command keeps the image usable with distrobox
# create, like the toolbox base.
FROM runtime-common AS runtime
COPY --from=builder /runtime/usr/ /usr/
COPY --from=builder /runtime/opt/ /opt/
COPY --from=builder /src/frametop/ /src/frametop/
RUN python3 pack/pyside-runtime.py --verify /usr/share/frametop/pyside-runtime.json \
 && /usr/bin/python3 -c 'from PySide6 import QtCore, QtGui, QtQml, QtQuick, QtQuickControls2' \
 && python3 -c 'import numpy, cv2'
