Files
firmware/docker/Dockerfile.build
Oleksandr ShyshatskyiandClaude Opus 5 28eb20f3da Package numpy, because klippy does not start without it
extras/stepper_resonance_tester.py opens with a bare `import numpy as np`;
klippy.py:122 walks EVERY config section through a load_object whose
importlib.import_module is not wrapped in except ImportError; printer.base.cfg
includes FlashForge's printer.vibration.cfg, which declares
[stepper_resonance_tester]. Since klippy moved to our own CPython 3.13 that has
not been a printer missing resonance testing. It has been a klippy that never
reaches ready.

So this is not a scientific extra. anvil-klipper depends on anvil-python-numpy
the way it depends on cffi, except that the others mean "klippy imports this"
and this one means "klippy starts". It also gives back SHAPER_CALIBRATE and
TEST_RESONANCES, dead since the same switch, and the VFA calibration behind
STEPPER_RESONANCE_FACTORY_CALIBRATE that docs/notes/44-vfa-calibration.md
traces end to end.

WHAT WAS NOT OBVIOUS, in the order it bit.

numpy 2.x builds with meson-python, and meson has never heard of
_PYTHON_SYSCONFIGDATA_NAME -- the trick every other python-* recipe here leans
on. pkg_mesoncross writes a real cross file instead, naming the $PKG_XW
wrappers rather than the raw toolchain: the wrappers carry -EL -mnan=2008, and
a cross file that reaches past them builds a tree that passes every gate which
only reads symbols and is refused at exec() on the printer. pkg_pywheel grew a
PKG_PY_PIP_ARGS branch to pass the file as --config-settings, which is the one
thing PEP 517 does standardise.

The cross file's `python =` is the BUILD python, and that is not a mistake:
meson introspects an interpreter by running it and asking sysconfig, and
pkg_pytarget has already made that sysconfig answer for mipsel. The old trick
does reach meson, just not by its own mechanism.

numpy's meson.build hard-errors below gcc 8.4 and ours is 7.2. The gate is the
SciPy Toolchain Roadmap's support statement, not a requirement of the code:
this toolchain compiles if constexpr, structured bindings, std::optional,
std::string_view, and pocketfft_hdronly.h -- 3635 lines of the heaviest
template C++ in the tree, and the half np.fft.rfft needs. Probed before the
floor was lowered, and lowered rather than removed, behind a guard that fails
loudly if upstream moves the check. 19 extension modules, every one at e_flags
0x70001405/0x70001407, 14MB shipped after pruning tests/, f2py/, stubs and
static libs.

meson-python is self-hosting, so pkg_buildpython runs meson and ninja just to
install the backend -- hence ninja-build in the image and PYPKG_HOST_LIST in
dependency order rather than alphabetical. And pip puts meson and cython in
work/.py-host/bin, which nothing had ever looked in: without that on PATH the
backend fails to install with `meson executable "meson" not found` while pip
list shows it right there.

THE GATE THAT WOULD HAVE CAUGHT ALL OF IT is qa/replica/test_klippy_extras_import.py:
walk the real /usr/data/config include graph, map every section to its extra,
import each on the printer's own interpreter. It needs no MCU, because config
load happens before klippy:mcu_identify. Nothing asked that question before,
which is how two comments in this repo came to assert the module guarded its
own import. test_numpy.py asks for numeric answers rather than a module object
-- linalg.lstsq specifically, because -Dallow-noblas selects the bundled
lapack_lite and _r2_sinusoidal_fit calls it every calibration round. And
test_install.py's include check read only ff-*.cfg, so FlashForge's four --
printer.vibration.cfg among them -- were never asked about at all.

ff-toolchange.cfg wraps the two search commands in _FF_SHAPER_PREP, as
SHAPER_CALIBRATE already was. STEPPER_RESONANCE_FACTORY_CALIBRATE is
deliberately NOT wrapped: it is a gcode_macro, not a registered command, and
same-named sections merge under RawConfigParser(strict=False) -- a wrapper
would not wrap it, it would BE it, with rename_existing pointing at itself. It
needs none, since its body is two calls to a command that is wrapped. No
SAVE_CONFIG in any macro either; it restarts klippy and stays the operator's.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EJr2x7aDaY889qwSxbdLt8
2026-08-31 13:42:57 +03:00

59 lines
3.1 KiB
INI

# The only thing that runs on your machine is Docker itself.
#
# Every make target executes inside this image: the build scripts, the tests
# and the install simulations. The docker CLI is included so the simulation
# targets can start sibling containers through the mounted socket, and
# qemu-user-static so the printer's real MIPS userland can be executed.
#
# A HOST C compiler is here as well, which looks redundant beside two
# cross-toolchains: cross-building CPython needs a build-python of exactly the
# same version to freeze modules and byte-compile the stdlib, and that one
# needs the host's own gcc. pkg-config and perl are for the same section --
# CPython's configure probes libffi through pkg-config, and OpenSSL's
# Configure is a perl program.
#
# zlib1g-dev is for THE HOST HALF of that build: the build-python needs a
# working zlib module, or ensurepip cannot unpack pip's bundled .whl. CPython
# records the module as missing and carries on, so the symptom lands three
# steps later as "No module named pip". The TARGET's zlib is unrelated.
#
# `patch` is for one package: lmdb's setup.py shells out to /usr/bin/patch BY
# ABSOLUTE PATH to patch the bundled liblmdb, and raises "Applying patch
# failed" when it is absent -- with nothing to say that a small utility is
# what is missing. lmdb is Moonraker's database at the pinned commit.
#
# libarchive-dev IS DEBIAN'S, DELIBERATELY, AND IS THE ONE EXCEPTION TO
# versions.env. pkg_buildopkg builds an x86-64 opkg to assemble the payload,
# and opkg requires libarchive. That opkg is a BUILD TOOL that never leaves
# this container; the pin-and-hash rule governs what reaches the printer, and
# the printer's opkg is cross-compiled against the libarchive OUR feed built.
# Same call already made for gcc.
#
# ninja-build IS DEBIAN'S ON THE SAME TERMS, and MESON IS NOT -- the split is
# the point. numpy builds with meson-python, so both are needed; but meson is
# a Python module our build-python imports, and bookworm's 1.0.1 is below the
# >=1.2.99 numpy's meson.build demands, so it is pinned in versions.env with
# the rest of PYPKG_HOST_LIST. ninja is a program meson merely execs, no pin of
# ours would change what it emits, and nothing it produces reaches a printer
# except through the cross compiler that is pinned.
#
# IT IS NEEDED EARLIER THAN NUMPY, which is the part that surprises: meson-python
# is self-hosting (backend-path = ['.']), so pkg_buildpython runs meson and
# ninja just to install the backend itself.
FROM debian:bookworm-slim
ENV DEBIAN_FRONTEND=noninteractive
RUN echo 'Acquire::Retries "3";' > /etc/apt/apt.conf.d/80-retries \
&& apt-get update && apt-get install -y --no-install-recommends \
bash coreutils tar xz-utils gzip bzip2 openssl unzip curl ca-certificates \
python3 python3-jinja2 python3-pytest python3-pyflakes \
python3-pytest-xdist \
binutils file findutils grep sed gawk make git diffutils patch procps \
squashfs-tools qemu-user-static docker.io shellcheck \
gcc g++ libc6-dev zlib1g-dev pkg-config perl \
libarchive-dev ninja-build \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /src
CMD ["bash"]