TRACES: DP-007 | PR-004 Pinned by tag rather than :latest, so a workflow run is reproducible against the image it was written for.
343 lines
17 KiB
Docker
343 lines
17 KiB
Docker
# sae-builder-cpu — the CI build image
|
|
#
|
|
# TRACES: DP-007 | PR-004
|
|
#
|
|
# Build/push: scripts/ci/build_builder_image.sh --push
|
|
# Consumed by: .gitea/workflows/unit-tests.yml (pinned by tag, never :latest)
|
|
# Docs: docs/ci-image.md
|
|
#
|
|
# This is the CPU corner of the DP-008 builder matrix and the DP-007 CI image at
|
|
# the same time — one artifact, two uses. The CUDA and ROCm siblings differ only
|
|
# in the accelerator stack layered on top of this dependency set.
|
|
#
|
|
# CI runs on an Intel N100 with no discrete GPU. Everything here is chosen so
|
|
# that `-DSAE_INFERENCE_BACKEND=ORT -DSAE_GEMM_BACKEND=CPU -DSAE_BUILD_TESTS=ON`
|
|
# configures, builds and runs without a GPU, without a model, and without
|
|
# reaching GitHub.
|
|
|
|
# ─── Base image ──────────────────────────────────────────────────────────────
|
|
#
|
|
# Chosen for the OLDEST glibc to be supported, not for recency. A binary built
|
|
# in a container runs against the *host's* glibc; glibc is backward compatible
|
|
# but not forward, so the build base sets the floor for every machine DP-008's
|
|
# binaries can ever run on. Building on a newer base than the oldest supported
|
|
# host produces the classic `GLIBC_2.xx not found` failure at load time.
|
|
#
|
|
# Debian 12 "bookworm" = glibc 2.36 (Aug 2022). What that floor covers:
|
|
#
|
|
# Distro glibc Covered?
|
|
# Arch / CachyOS (rolling) 2.41+ yes
|
|
# Fedora 37 and later 2.36+ yes ← DP-005's targets are Fedora+Arch
|
|
# Debian 12 / 13 2.36+ yes
|
|
# Ubuntu 24.04 LTS 2.39 yes
|
|
# Ubuntu 22.04 LTS 2.35 NO
|
|
# RHEL / Rocky / Alma 9 2.34 NO
|
|
# Debian 11 2.31 NO
|
|
#
|
|
# The three misses are accepted deliberately: DP-005 puts Debian/Ubuntu out of
|
|
# installer scope and names Fedora + Arch as the supported distros, and every
|
|
# supported Fedora is 2.36 or newer. Going lower costs the toolchain rather than
|
|
# buying reach — Debian 11 ships GCC 10 (incomplete C++20) and Python 3.9, which
|
|
# has no `tomllib` and therefore cannot read the traceability gate's
|
|
# traceability.toml.
|
|
#
|
|
# Escape hatch, recorded now so it is not rediscovered under pressure: if the
|
|
# floor must drop to glibc 2.28 (RHEL 8 / manylinux_2_28 — the same baseline the
|
|
# ONNX Runtime and PyTorch wheels target), the move is a Rocky 8 base plus
|
|
# gcc-toolset-13, and OpenCV/FFmpeg/HDF5 all leave apt for source or
|
|
# EPEL/RPM Fusion. That is a different image, not a flag on this one.
|
|
#
|
|
# Not a glibc problem but worth stating: the binaries this image produces also
|
|
# link OpenCV, FFmpeg and HDF5 shared objects by soname. Making a *portable*
|
|
# release binary (DP-008) is a separate question from the glibc floor, and is
|
|
# answered by static linking or bundling, not by the base image.
|
|
FROM debian:12-slim
|
|
|
|
# Pins. Every version this image installs from source is an ARG so a rebuild is
|
|
# a one-line diff and `docker history` records what a given tag actually holds.
|
|
#
|
|
# ORT 1.28.0 and OpenCV 5.0.0 match the developer machine, so CI and local
|
|
# builds exercise the same libraries rather than merely similar ones.
|
|
# Catch2 / nlohmann_json / nanobind match the FetchContent pins in
|
|
# CMakeLists.txt:248 and tests/CMakeLists.txt:12 exactly — a vendored copy at a
|
|
# different version would be a silent divergence, not a convenience.
|
|
ARG ORT_VERSION=1.28.0
|
|
ARG OPENCV_VERSION=5.0.0
|
|
ARG CATCH2_VERSION=v3.5.3
|
|
ARG NLOHMANN_JSON_VERSION=v3.11.3
|
|
ARG NANOBIND_VERSION=v2.4.0
|
|
|
|
# Stamped so a build can prove which image it ran in, and so a green tick can be
|
|
# traced back to a specific dependency set. See the "Confirm the builder image"
|
|
# step in .gitea/workflows/unit-tests.yml.
|
|
ARG IMAGE_TAG=dev
|
|
ENV SAE_BUILDER=cpu \
|
|
SAE_BUILDER_VERSION=${IMAGE_TAG} \
|
|
SAE_ORT_VERSION=${ORT_VERSION} \
|
|
SAE_OPENCV_VERSION=${OPENCV_VERSION} \
|
|
DEBIAN_FRONTEND=noninteractive
|
|
|
|
# ─── System dependencies ─────────────────────────────────────────────────────
|
|
#
|
|
# One layer, ordered by why it is here rather than alphabetically.
|
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
# Toolchain. bookworm's default gcc is 12.2 — enough for the C++20 the
|
|
# project sets unconditionally (CMakeLists.txt:4). cmake is 3.25, above the
|
|
# 3.21 minimum. Ninja because the N100 has four cores and every second of
|
|
# build scheduling shows.
|
|
build-essential \
|
|
cmake \
|
|
ninja-build \
|
|
pkg-config \
|
|
git \
|
|
ca-certificates \
|
|
curl \
|
|
# Gitea's act_runner executes JS actions (actions/checkout, upload-artifact)
|
|
# with the `node` found *inside* the container. Without this the job cannot
|
|
# even check the repository out. Same reason as the kpnpp-builder image.
|
|
nodejs \
|
|
# HDF5 with the C++ API: galleries are HDF5-native and it is also the VR-001
|
|
# dump format. find_package(HDF5 COMPONENTS CXX) at CMakeLists.txt:273.
|
|
libhdf5-dev \
|
|
# FFmpeg decode. swresample is on this list deliberately: the audio
|
|
# signature (IR-004) downmixes to mono and resamples to 11025 Hz, and
|
|
# tests/test_audio_signature.cpp decodes the golden FLAC fixture, so the
|
|
# test build needs it as much as the main build does.
|
|
libavformat-dev \
|
|
libavcodec-dev \
|
|
libavutil-dev \
|
|
libswscale-dev \
|
|
libswresample-dev \
|
|
# OpenBLAS — required here, not optional. CI has no GPU, so SAE_GEMM_BACKEND
|
|
# =CPU is the only path it ever exercises, and without OpenBLAS the CPU GEMM
|
|
# falls back to a scalar loop that does not scale against a library-sized
|
|
# gallery (AR-027). The build only *warns* when it is missing so a developer
|
|
# without it still gets a working tree; the image must never be that case.
|
|
# Both the main build (CMakeLists.txt:162) and the test target
|
|
# (tests/CMakeLists.txt:42) discover it through pkg-config `openblas`.
|
|
libopenblas-dev \
|
|
# Python: the build itself needs the interpreter and headers
|
|
# (find_package(Python COMPONENTS Interpreter Development.Module) at
|
|
# CMakeLists.txt:254, for the nanobind modules). numpy/h5py/scipy are for
|
|
# the Python-side tooling — fixture generation, replay, validation scripts.
|
|
# From apt rather than pip: bookworm marks the environment externally
|
|
# managed (PEP 668), and apt's h5py is already linked against the same
|
|
# libhdf5 installed above. bookworm's python3 is 3.11, which has tomllib —
|
|
# the traceability gate needs it to read traceability.toml.
|
|
python3 \
|
|
python3-dev \
|
|
python3-numpy \
|
|
python3-h5py \
|
|
python3-scipy \
|
|
# Image codecs for the OpenCV build below. Without these OpenCV silently
|
|
# builds an imgcodecs that cannot read a JPEG, which fails at run time in a
|
|
# gallery build rather than at compile time here.
|
|
libjpeg62-turbo-dev \
|
|
libpng-dev \
|
|
libtiff-dev \
|
|
libwebp-dev \
|
|
libopenjp2-7-dev \
|
|
zlib1g-dev \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# Fail the image build, not the CI run, if OpenBLAS or swresample are not
|
|
# discoverable the way CMakeLists.txt discovers them. An image that ships
|
|
# libopenblas but no openblas.pc would compile the scalar fallback in silence.
|
|
RUN set -eux; \
|
|
pkg-config --exists openblas; \
|
|
echo "openblas $(pkg-config --modversion openblas)"; \
|
|
pkg-config --exists libswresample; \
|
|
echo "swresample $(pkg-config --modversion libswresample)"
|
|
|
|
# ─── ONNX Runtime, CPU provider only ─────────────────────────────────────────
|
|
#
|
|
# The official prebuilt linux-x64 tarball is the CPU build: no CUDA, no
|
|
# TensorRT, no ROCm execution providers. That is the whole requirement here —
|
|
# excluding the GPU providers is not a size optimisation, it is the point.
|
|
#
|
|
# Verified against the 1.28.0 tarball: the shared object's highest versioned
|
|
# symbol requirement is GLIBC_2.27 / GLIBCXX_3.4.21, well under this base's
|
|
# 2.36, so ORT does not raise the floor set above.
|
|
#
|
|
# Installed to /usr/local/{lib,include/onnxruntime} because CMakeLists.txt
|
|
# includes <onnxruntime/onnxruntime_cxx_api.h> and needs the *parent* of that
|
|
# directory on the include path (CMakeLists.txt:108-113).
|
|
#
|
|
# CI never calls a model — the embedder measures ~930 ms/frame on this CPU
|
|
# provider — so ORT is present to satisfy the link, not to run inference.
|
|
RUN set -eux; \
|
|
curl -fsSL -o /tmp/ort.tgz \
|
|
"https://github.com/microsoft/onnxruntime/releases/download/v${ORT_VERSION}/onnxruntime-linux-x64-${ORT_VERSION}.tgz"; \
|
|
mkdir -p /tmp/ort; \
|
|
tar -xzf /tmp/ort.tgz -C /tmp/ort --strip-components=1; \
|
|
cp -a /tmp/ort/lib/libonnxruntime.so* /usr/local/lib/; \
|
|
mkdir -p /usr/local/include/onnxruntime; \
|
|
cp -a /tmp/ort/include/. /usr/local/include/onnxruntime/; \
|
|
ldconfig; \
|
|
rm -rf /tmp/ort /tmp/ort.tgz; \
|
|
test -f /usr/local/include/onnxruntime/onnxruntime_cxx_api.h
|
|
|
|
# ─── OpenCV 5, from source ───────────────────────────────────────────────────
|
|
#
|
|
# This is the reason the image is prebuilt at all. CMakeLists.txt:25 probes for
|
|
# OpenCV 5 first and falls back to 4; the branch targets 5, which no Debian
|
|
# release ships (bookworm has 4.6), and building it inside every CI run would
|
|
# dominate the run on an N100.
|
|
#
|
|
# BUILD_LIST is exactly the seven components find_package asks for
|
|
# (CMakeLists.txt:25-29) — OpenCV resolves their internal dependencies itself.
|
|
# Everything else is off: tests, samples, Java/Python bindings, and the apps.
|
|
#
|
|
# No GUI backend. highgui still builds (find_package REQUIREs the component) but
|
|
# with a stub — CI never calls imshow, and pulling GTK/Qt into a headless build
|
|
# image buys nothing. scene_preview is a developer tool, not a CI target.
|
|
#
|
|
# CUDA/cuDNN explicitly off: DP-007 excludes the GPU stack outright.
|
|
#
|
|
# The source tree and build tree are removed in the same layer, so the ~3 GB of
|
|
# intermediates cost nothing in the published image.
|
|
RUN set -eux; \
|
|
curl -fsSL -o /tmp/opencv.tar.gz \
|
|
"https://github.com/opencv/opencv/archive/refs/tags/${OPENCV_VERSION}.tar.gz"; \
|
|
mkdir -p /tmp/opencv-src; \
|
|
tar -xzf /tmp/opencv.tar.gz -C /tmp/opencv-src --strip-components=1; \
|
|
cmake -S /tmp/opencv-src -B /tmp/opencv-build -G Ninja \
|
|
-DCMAKE_BUILD_TYPE=Release \
|
|
-DCMAKE_INSTALL_PREFIX=/usr/local \
|
|
-DBUILD_LIST=core,imgproc,imgcodecs,videoio,dnn,objdetect,highgui \
|
|
-DBUILD_SHARED_LIBS=ON \
|
|
-DBUILD_TESTS=OFF \
|
|
-DBUILD_PERF_TESTS=OFF \
|
|
-DBUILD_EXAMPLES=OFF \
|
|
-DBUILD_DOCS=OFF \
|
|
-DBUILD_opencv_apps=OFF \
|
|
-DBUILD_JAVA=OFF \
|
|
-DBUILD_opencv_python3=OFF \
|
|
-DWITH_FFMPEG=ON \
|
|
-DWITH_GTK=OFF \
|
|
-DWITH_QT=OFF \
|
|
-DWITH_OPENGL=OFF \
|
|
-DWITH_CUDA=OFF \
|
|
-DWITH_CUDNN=OFF \
|
|
-DOPENCV_GENERATE_PKGCONFIG=ON \
|
|
-DCMAKE_INSTALL_RPATH=/usr/local/lib; \
|
|
cmake --build /tmp/opencv-build --parallel; \
|
|
cmake --install /tmp/opencv-build; \
|
|
ldconfig; \
|
|
rm -rf /tmp/opencv-src /tmp/opencv-build /tmp/opencv.tar.gz
|
|
|
|
# ─── Vendored dependencies: Catch2, nlohmann/json, nanobind ──────────────────
|
|
#
|
|
# All three are FetchContent'ed by the build today, which makes every CI run
|
|
# depend on GitHub being reachable — a network outage would present as a code
|
|
# failure. Baking them in removes that dependency entirely.
|
|
#
|
|
# Catch2 is *installed*, so tests/CMakeLists.txt:6 `find_package(Catch2 3 QUIET)`
|
|
# succeeds and the FetchContent fallback is never reached. Its source is kept as
|
|
# well so the override below can cover the case where find_package somehow does
|
|
# not fire.
|
|
#
|
|
# nanobind must be cloned with submodules: its `ext/robin_map` is a git
|
|
# submodule, and a GitHub source tarball does not contain it. This is the one
|
|
# dependency where "download the tarball" produces a tree that configures and
|
|
# then fails to compile.
|
|
RUN set -eux; \
|
|
mkdir -p /opt/vendor; \
|
|
git clone --depth 1 --branch "${NLOHMANN_JSON_VERSION}" \
|
|
https://github.com/nlohmann/json.git /opt/vendor/nlohmann_json; \
|
|
git clone --depth 1 --branch "${NANOBIND_VERSION}" --recurse-submodules \
|
|
https://github.com/wjakob/nanobind.git /opt/vendor/nanobind; \
|
|
git clone --depth 1 --branch "${CATCH2_VERSION}" \
|
|
https://github.com/catchorg/Catch2.git /opt/vendor/Catch2; \
|
|
cmake -S /opt/vendor/Catch2 -B /tmp/catch2-build -G Ninja \
|
|
-DCMAKE_BUILD_TYPE=Release \
|
|
-DCMAKE_INSTALL_PREFIX=/usr/local \
|
|
-DBUILD_TESTING=OFF; \
|
|
cmake --build /tmp/catch2-build --parallel; \
|
|
cmake --install /tmp/catch2-build; \
|
|
rm -rf /tmp/catch2-build; \
|
|
find /opt/vendor -maxdepth 2 -name .git -exec rm -rf {} +; \
|
|
ldconfig
|
|
|
|
# The initial-cache script the build is configured with. It lives in the image,
|
|
# not in the workflow, so the vendor paths have exactly one owner: move a
|
|
# directory here and no consumer needs editing.
|
|
#
|
|
# FETCHCONTENT_FULLY_DISCONNECTED=ON is the load-bearing line. With it, any
|
|
# FetchContent dependency that is *not* covered by an override above is a hard
|
|
# configure error instead of a silent download — so "this build does not touch
|
|
# GitHub" is enforced by the build system rather than asserted in a comment.
|
|
RUN set -eux; \
|
|
printf '%s\n' \
|
|
'# Baked into sae-builder-cpu. Use with: cmake -C /opt/vendor/vendored-deps.cmake ...' \
|
|
'# TRACES: DP-007' \
|
|
'set(FETCHCONTENT_SOURCE_DIR_NLOHMANN_JSON "/opt/vendor/nlohmann_json" CACHE PATH "vendored in the CI image")' \
|
|
'set(FETCHCONTENT_SOURCE_DIR_NANOBIND "/opt/vendor/nanobind" CACHE PATH "vendored in the CI image")' \
|
|
'set(FETCHCONTENT_SOURCE_DIR_CATCH2 "/opt/vendor/Catch2" CACHE PATH "vendored in the CI image")' \
|
|
'set(FETCHCONTENT_FULLY_DISCONNECTED ON CACHE BOOL "no CI build may fetch from the network")' \
|
|
> /opt/vendor/vendored-deps.cmake; \
|
|
cat /opt/vendor/vendored-deps.cmake
|
|
|
|
# ─── Self-check ──────────────────────────────────────────────────────────────
|
|
#
|
|
# Run the project's own dependency discovery — the same find_package and
|
|
# pkg_check_modules calls CMakeLists.txt makes — against this image, at image
|
|
# build time. An image that cannot satisfy them should fail here, loudly, once,
|
|
# rather than in every CI run that pulls it.
|
|
#
|
|
# Deliberately not a build of the project: the image must be buildable without
|
|
# the repository, and the repository's own configure step is what CI is for.
|
|
RUN set -eux; \
|
|
mkdir -p /tmp/selfcheck; \
|
|
printf '%s\n' \
|
|
'cmake_minimum_required(VERSION 3.21)' \
|
|
'project(sae_image_selfcheck LANGUAGES CXX)' \
|
|
'set(CMAKE_CXX_STANDARD 20)' \
|
|
'set(CMAKE_CXX_STANDARD_REQUIRED ON)' \
|
|
'find_package(OpenCV 5 REQUIRED COMPONENTS core imgproc imgcodecs videoio dnn objdetect highgui)' \
|
|
'message(STATUS "OpenCV ${OpenCV_VERSION}")' \
|
|
'find_package(HDF5 REQUIRED COMPONENTS CXX)' \
|
|
'message(STATUS "HDF5 ${HDF5_VERSION}")' \
|
|
'find_package(Catch2 3 REQUIRED)' \
|
|
'message(STATUS "Catch2 ${Catch2_VERSION}")' \
|
|
'find_package(Python 3.8 REQUIRED COMPONENTS Interpreter Development.Module)' \
|
|
'find_package(PkgConfig REQUIRED)' \
|
|
'pkg_check_modules(AVFORMAT REQUIRED libavformat)' \
|
|
'pkg_check_modules(AVCODEC REQUIRED libavcodec)' \
|
|
'pkg_check_modules(AVUTIL REQUIRED libavutil)' \
|
|
'pkg_check_modules(SWSCALE REQUIRED libswscale)' \
|
|
'pkg_check_modules(SWRESAMPLE REQUIRED libswresample)' \
|
|
'pkg_check_modules(OPENBLAS REQUIRED openblas)' \
|
|
'find_library(ORT_LIB onnxruntime REQUIRED HINTS /usr/lib /usr/local/lib)' \
|
|
'find_path(ORT_INCLUDE onnxruntime_cxx_api.h PATH_SUFFIXES onnxruntime' \
|
|
' HINTS /usr/include/onnxruntime /usr/local/include/onnxruntime /usr/local/include REQUIRED)' \
|
|
'message(STATUS "ORT ${ORT_LIB} / ${ORT_INCLUDE}")' \
|
|
> /tmp/selfcheck/CMakeLists.txt; \
|
|
cmake -S /tmp/selfcheck -B /tmp/selfcheck/build -G Ninja; \
|
|
rm -rf /tmp/selfcheck
|
|
|
|
# Python-side tooling the fixture and validation scripts import. Checked here so
|
|
# a missing wheel is an image failure rather than a mid-run traceback.
|
|
RUN python3 -c "import numpy, h5py, scipy; print('numpy', numpy.__version__, 'h5py', h5py.__version__, 'scipy', scipy.__version__)"
|
|
|
|
# ─── What is deliberately NOT here ───────────────────────────────────────────
|
|
#
|
|
# CUDA, TensorRT, ROCm, and the ORT GPU execution providers
|
|
# No GPU to use them. They belong to the sae-builder-cuda and
|
|
# sae-builder-rocm siblings (DP-008).
|
|
#
|
|
# The ONNX models
|
|
# Seven files, ~725 MB, in Git LFS. T1/T2 tests are model-free by design
|
|
# (tests/CMakeLists.txt:1-4), so the CI image needs none of them, and
|
|
# baking them in would inflate the image roughly tenfold to serve the T3
|
|
# smoke tests alone. Those pull the model they need via LFS in a separate
|
|
# job. The CI workflow checks out with LFS off for the same reason.
|
|
#
|
|
# The repository
|
|
# Nothing from the source tree is COPYed in. The image is a toolchain, and
|
|
# a toolchain that embeds the code it builds has to be rebuilt whenever the
|
|
# code changes — which is exactly the per-run cost this image exists to
|
|
# avoid.
|
|
|
|
WORKDIR /src
|