# Title: Dockerfile — baked vast.ai GPU analysis image
# Description:
#     Builds the private image vast.ai instances pull for bulk lc0 +
#     Stockfish analysis. Everything the worker needs is baked in rather
#     than fetched at boot: the lc0 TRT binary, the NVIDIA TensorRT
#     runtime it links against, BT4 weights, Syzygy 3-4-5 tablebases and a
#     pinned wood-league-worker wheel. vast volumes are host-scoped, so
#     the image is the only thing that travels with an instance.
#
#     NOT for a public registry: the TensorRT operator tarball is baked.
#
# Changelog:
#     2026-09-30 (#278): lc0 now arrives through the build context, already
#         unpacked and checksum-verified by the workflow using GITHUB_TOKEN.
#         It was previously fetched by unauthenticated curl from this repo's
#         own release assets, which began returning 404 when the repo went
#         private. The workflow extracts it rather than passing the tarball,
#         because carrying the tarball into the build cost three copies on
#         disk and the runner was lost mid-build. TensorRT is now mandatory:
#         the bake and its verification no longer sit behind
#         `if [ -n "${WLW_TRT_URL}" ]`, which let a blank secret produce a
#         green image containing no TensorRT at all.
#     2026-09-30 (#280, #281): Syzygy and BT4 arrive through the build
#         context from this repo's `baked-assets-v1` release, rather than
#         being scraped from tablebase.sesse.net as 290 requests and pulled
#         from storage.lczero.org on every build. Both are fixed
#         computations; neither was checksummed before.
#     (No changelog was kept before that date; see git history.)

FROM nvidia/cuda:12.4.1-cudnn-runtime-ubuntu22.04

# No default: a stale hardcoded version silently baked the wrong worker
# into freshly tagged images (#133). CI resolves this from the tag; a
# manual build must pass --build-arg WORKER_VERSION=<ver> explicitly.
ARG WORKER_VERSION
# Asserted against the baked tablebase set: 145 tables x .rtbw + .rtbz.
ARG SYZYGY_EXPECTED_FILES=290
ARG WLW_TRT_URL
ENV DEBIAN_FRONTEND=noninteractive

RUN apt-get update && apt-get install -y --no-install-recommends \
      python3.11 python3-pip stockfish curl ca-certificates \
    && rm -rf /var/lib/apt/lists/* \
    && update-alternatives --install /usr/bin/python python /usr/bin/python3.11 1

# lc0 (TRT build) — baked, not volume-fetched. Arrives already unpacked
# and checksum-verified from the workflow, which owns the download because
# the release it lives in is private; see
# .github/workflows/build-vast-worker.yml.
COPY vast/artifacts/lc0 /opt/lc0
RUN test -x /opt/lc0/lc0 \
      || { echo "FATAL: /opt/lc0/lc0 missing or not executable"; exit 1; }

# TensorRT libraries (operator tarball; baked because registry is private).
# The NVIDIA tarball extracts to /opt/trt/TensorRT-<ver>/targets/<arch>/lib,
# but LD_LIBRARY_PATH expects a stable /opt/trt/lib — symlink the real
# lib dir there (version-agnostic via the libnvinfer.so.10 location).
#
# Mandatory, not optional (#278). This image pins WLW_LC0_BACKEND=onnx-trt
# and the cuda base ships no TensorRT, so without these libraries lc0
# cannot load the ONNX-TRT provider. Both this step and its verification
# below used to sit behind `if [ -n "${WLW_TRT_URL}" ]`, so a blank secret
# produced a green image with no TensorRT in it — discovered only after a
# paid GPU launch.
RUN set -eu; \
    test -n "${WLW_TRT_URL:-}" \
      || { echo "FATAL: WLW_TRT_URL build-arg is empty — refusing to bake an image with no TensorRT (#278)"; exit 1; }; \
    curl -fsSL "${WLW_TRT_URL}" -o /tmp/trt.tar.gz; \
    mkdir -p /opt/trt; \
    tar -xzf /tmp/trt.tar.gz -C /opt/trt; \
    rm /tmp/trt.tar.gz; \
    trt_lib_dir="$(find /opt/trt -name 'libnvinfer.so.10' -printf '%h\n' | head -1)"; \
    test -n "${trt_lib_dir}" \
      || { echo "FATAL: no libnvinfer.so.10 inside the TensorRT tarball"; exit 1; }; \
    ln -sfn "${trt_lib_dir}" /opt/trt/lib

# Build-time guard: the TRT lib path has silently broken before. Fail the
# BUILD (no GPU needed) instead of discovering it after a paid launch.
RUN test -e /opt/trt/lib/libnvinfer.so.10 \
      || { echo "FATAL: /opt/trt/lib/libnvinfer.so.10 missing — TRT lib path wrong"; \
           ls -lL /opt/trt/lib 2>&1 | head; exit 1; }
RUN echo "OK: /opt/trt/lib/libnvinfer.so.10 resolves"

# BT4 weights + Syzygy 3-4-5 — baked, and now mirrored on this repo's
# `baked-assets-v1` release rather than fetched from tablebase.sesse.net
# (290 separate requests, 5m11s) and storage.lczero.org on every build
# (#280, #281). Both are fixed computations that never change. The workflow
# downloads and checksum-verifies them; see build-vast-worker.yml.
COPY vast/artifacts/syzygy /opt/syzygy
COPY vast/artifacts/BT4.pb.gz /opt/weights/BT4.pb.gz

# Keep the completeness assertion that the download loop used to carry: a
# short tablebase set degrades endgame analysis silently rather than
# failing, so it must fail the BUILD.
RUN set -eu; \
    got="$(ls -1 /opt/syzygy | grep -cE '\.(rtbw|rtbz)$' || true)"; \
    echo "Syzygy: baked=${got} expected=${SYZYGY_EXPECTED_FILES}"; \
    test "${got}" -eq "${SYZYGY_EXPECTED_FILES}" \
      || { echo "FATAL: Syzygy incomplete (${got}/${SYZYGY_EXPECTED_FILES})"; exit 1; }
RUN test -s /opt/weights/BT4.pb.gz \
      || { echo "FATAL: /opt/weights/BT4.pb.gz missing or empty"; exit 1; }

RUN test -n "${WORKER_VERSION}" \
      || { echo "FATAL: WORKER_VERSION build-arg is empty — refusing to bake an unpinned worker (#133)"; exit 1; }
RUN python3.11 -m pip install --no-cache-dir "wood-league-worker==${WORKER_VERSION}"

# Bake a known-good L40S lc0 calibration so a fresh instance is a cache
# hit and skips the ~7.5-min MinibatchSize sweep (#150, Phase 1). The
# fingerprint uses the weights *basename* + backend (gpu/lc0_version
# empty, exactly as lc0.py calls get_tuned_opts), so this hits on any
# GPU running this image's BT4 / onnx-trt config; a mismatch silently
# falls back to a correct full sweep. Build-time guard (same
# philosophy as the TRT/Syzygy/WORKER_VERSION guards): fail the BUILD
# if the bake is missing or malformed rather than discover a silent
# full-sweep regression after a paid launch.
COPY vast/lc0_tuning.l40s.json /data/wlw/lc0_tuning.json
RUN python3.11 - <<'PY'
import json, sys
d = json.load(open("/data/wlw/lc0_tuning.json"))
fp = d["fingerprint"]
ok = (
    bool(fp.get("backend"))
    and fp.get("weights") == "BT4.pb.gz"
    and isinstance(d.get("minibatch_size"), int)
    and d["minibatch_size"] >= 1
)
sys.exit(0 if ok else "FATAL: baked lc0_tuning.json malformed (#150)")
PY

COPY vast/onstart.sh /usr/local/bin/wlw-vast-onstart
RUN chmod +x /usr/local/bin/wlw-vast-onstart

# Worker asset paths point at the in-image bake locations.
ENV WLW_LC0_PATH=/opt/lc0/lc0 \
    WLW_LC0_WEIGHTS_PATH=/opt/weights/BT4.pb.gz \
    WLW_SYZYGY_PATH=/opt/syzygy \
    WLW_STOCKFISH_PATH=/usr/games/stockfish \
    WLW_LC0_BACKEND=onnx-trt \
    LD_LIBRARY_PATH=/opt/trt/lib:${LD_LIBRARY_PATH:-} \
    WLW_DATA_DIR=/data/wlw

ENTRYPOINT ["/usr/local/bin/wlw-vast-onstart"]
