mirror of
https://github.com/Klipper4FlashForge/firmware.git
synced 2026-10-22 05:12:16 -05:00
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
129 lines
5.5 KiB
Python
129 lines
5.5 KiB
Python
"""The two facts about recipe layout that have a consequence.
|
|
|
|
A recipe directory has a fixed shape -- build.sh and pkg.conf beside
|
|
payload/, seed/ and control/ -- and recipes live at two depths, pkgs/<name>/
|
|
for the ones carrying files of this repo and pkgs/3rdparty/<name>/ for the ones
|
|
building a pinned tarball. That shape is documented in docs/building.md and in
|
|
pkgs/lib.sh, which is the only code that depends on it.
|
|
|
|
MOST OF IT IS NOT ASSERTED HERE, and that is deliberate. This file used to hold
|
|
seven more tests: that a recipe contains no fourth kind of directory, that each
|
|
kind is still used by someone, that a 3rdparty recipe carries no files of ours
|
|
and a first-party one carries some, that installer/ holds exactly two named
|
|
files. Every one of them compared the tree against a description of the tree.
|
|
None could go red for a reason a printer would notice -- a stray file in a
|
|
recipe directory is not read by anything, and a recipe filed at the wrong depth
|
|
still builds the same .ipk -- while all seven went red the moment the layout
|
|
legitimately changed, which is the opposite of what a test is for.
|
|
|
|
What is left is the two that bite:
|
|
"""
|
|
import os
|
|
|
|
import pytest
|
|
|
|
from lib.paths import ROOT
|
|
|
|
pytestmark = pytest.mark.static
|
|
|
|
MODDIR_PREFIX = ("usr", "opt", "var", "proc", "sys", "dev")
|
|
|
|
THIRD = os.path.join(ROOT, "pkgs", "3rdparty")
|
|
|
|
|
|
def recipes():
|
|
"""(name, directory) for every recipe, at either level."""
|
|
out = []
|
|
for base in (os.path.join(ROOT, "pkgs"), THIRD):
|
|
for n in sorted(os.listdir(base)):
|
|
d = os.path.join(base, n)
|
|
if os.path.isfile(os.path.join(d, "pkg.conf")):
|
|
out.append((n, d))
|
|
return out
|
|
|
|
|
|
def test_there_are_recipes():
|
|
"""A glob that silently matches nothing is a test that passes hollow.
|
|
|
|
Both tests below iterate recipes(); this is what stops them reporting
|
|
green over an empty list.
|
|
"""
|
|
assert len(recipes()) > 10
|
|
|
|
|
|
def test_nothing_under_a_payload_escapes_the_prefix():
|
|
"""payload/ is a $MODDIR overlay, so it may not name an absolute path.
|
|
|
|
A file at payload/usr/bin/x stages to $MODDIR/usr/bin/x -- correct-looking
|
|
and wrong. The whole reason the mod lives on /usr/data is that /usr is
|
|
FlashForge's and an OTA rewrites it, so a package that owned a path up
|
|
there would survive exactly until the next factory update and take
|
|
whatever it overwrote with it.
|
|
|
|
qa/static/test_ipk.py asks the same question of every BUILT .ipk, which is
|
|
the stronger form. This one is kept beside it because it runs on a bare
|
|
checkout with no feed, and names the recipe rather than the artefact.
|
|
"""
|
|
bad = []
|
|
for name, rd in recipes():
|
|
d = os.path.join(rd, "payload")
|
|
for top in sorted(os.listdir(d)) if os.path.isdir(d) else []:
|
|
if top in MODDIR_PREFIX:
|
|
bad.append("%s payload/%s" % (name, top))
|
|
assert not bad, (
|
|
"payload/ is rooted at $MODDIR, not at /: %s" % ", ".join(bad))
|
|
|
|
|
|
def test_recipe_names_are_unique_across_the_two_levels():
|
|
"""pkg_out, pkg_stamp and the .ipk filename are all keyed by the bare name.
|
|
|
|
A name at both levels would resolve to whichever pkgs/lib.sh's pkg_dir
|
|
searches first and quietly build the other one's package. Nothing
|
|
downstream can catch that: the wrong package builds perfectly.
|
|
"""
|
|
names = [n for n, _ in recipes()]
|
|
dupes = sorted({n for n in names if names.count(n) > 1})
|
|
assert not dupes, (
|
|
"recipe name at both levels: %s -- pkg_dir would silently pick one"
|
|
% ", ".join(dupes))
|
|
|
|
|
|
def test_klipper_still_depends_on_numpy():
|
|
"""The one Depends in this repo whose absence is a printer that will not boot.
|
|
|
|
Every other name in anvil-klipper's Depends means "klippy imports this".
|
|
numpy means "klippy does not start": extras/stepper_resonance_tester.py
|
|
opens with a bare `import numpy as np`, klippy.py:122 walks EVERY config
|
|
section through a load_object, and klippy.py:103 does not catch
|
|
ImportError -- so with printer.base.cfg including FlashForge's
|
|
printer.vibration.cfg ([stepper_resonance_tester]), dropping this line does
|
|
not lose a feature, it bricks the boot.
|
|
|
|
It is asserted HERE, statically, because the replica gates that would catch
|
|
it (qa/replica/test_klippy_extras_import.py, test_numpy.py) need an image
|
|
baked and a container up, and this one is a grep. It is the cheapest gate
|
|
on the most expensive mistake, and the mistake has already been made once:
|
|
two comments in this repo asserted the module guarded its own import, and
|
|
the dependency did not exist at all until the note that disproved them.
|
|
|
|
See docs/notes/44-vfa-calibration.md.
|
|
"""
|
|
conf = os.path.join(ROOT, "pkgs", "klipper", "pkg.conf")
|
|
assert os.path.isfile(conf), "pkgs/klipper/pkg.conf is gone"
|
|
text = open(conf, encoding="utf-8").read()
|
|
|
|
# The recipe it must name has to be a recipe, or the Depends resolves to
|
|
# nothing at install time and opkg refuses the package.
|
|
assert any(n == "python-numpy" for n, _ in recipes()), \
|
|
"no python-numpy recipe, so anvil-klipper's Depends names a package " \
|
|
"no feed of ours builds"
|
|
|
|
depends = "".join(
|
|
line for line in text.splitlines(keepends=True)
|
|
if not line.lstrip().startswith("#"))
|
|
assert "anvil-python-numpy" in depends, (
|
|
"anvil-klipper no longer depends on anvil-python-numpy. That is not a "
|
|
"lost feature -- klippy loads [stepper_resonance_tester] from "
|
|
"FlashForge's printer.vibration.cfg and dies on the ImportError, so "
|
|
"the printer never reaches ready.")
|