The previous commit proved every component IMPORTS. Importing is not serving,
so this runs it. case-moonraker313.sh starts Moonraker on the exact command
line payload/etc/s6/moonraker/run execs -- only with our interpreter --
against the mod's own assets/moonraker.conf, and asks the printer what
happened:
* bound :7125 in 3s, with /proc/PID/cmdline confirming our interpreter and
our entry point rather than a coincidence
* GET /server/info answers 200 with 23 components loaded and none failed.
/machine/system_info, a different component reading this box's own /proc,
reports cpu: mips and python 3.13.7 [GCC 7.2.0]
* the database really works, which is the entire reason for the exercise: a
POST wrote a namespace, a GET read it back, the namespace appears beside
Moonraker's own, and data.mdb is 147456 bytes on disk -- through lmdb's
CPython extension, not the compile-at-import cffi fallback that would try
to invoke a compiler this printer does not have
* still alive and still answering 120 seconds later, with no tracebacks in
its log, and SIGTERM released the port in 2s
* the RUNNING process maps zero libraries under /usr/prog, with
LD_LIBRARY_PATH completely unset -- not merely purged of /usr/prog
* negative controls: nothing on :7125 beforehand, and with site-packages
moved aside it never binds at all
libsodium was the last /usr/prog string in the picture and is now ours.
libsodium 1.0.20, cross-built with the Ingenic toolchain in 24 seconds, gated
at 0x70001407 nan2008/o32/mips32r2, needing only libpthread, libc and the
loader, 406KB stripped into $MODDIR/lib. It computes rather than merely
loading: sodium_version_string is 1.0.20, its BLAKE2b-256 matches CPython's
own _blake2 on the same kernel, and libnacl.sign.Signer/Verifier -- the exact
ed25519 pair authorization.py imports -- signs, verifies, and rejects a
tampered message.
It costs anvil-env.sh nothing, which was not obvious and had to be measured.
libnacl's third fallback builds a path out of its own __file__, so from
$MODDIR/lib/python3.13/site-packages/libnacl it resolves $MODDIR/lib/libsodium.so
by absolute path -- working with LD_LIBRARY_PATH unset entirely. Recorded in
the README, because it works only because the prefix happens to contain the
string "lib": a prefix without it would break this silently.
klippy was scanned rather than guessed at. Its module-scope third-party
imports are cffi, greenlet, jinja2, pyserial and numpy; four of the five are
built and proven, and cffi already dlopens the printer's real c_helper.so.
numpy is at module scope only in extras/stepper_resonance_tester.py, which
Klipper loads on demand, and it is a multi-day problem that costs input-shaper
calibration rather than printing. There is no removed-stdlib usage anywhere in
klippy -- no imp, distutils, asyncore, cgi or getargspec.
What is left is packaging, not capability, and the README says so in order.
The sharp one: section 5c's trim deletes include/ and config-3.13-*, which the
package build needs, so the package build has to run inside 5c against the
untrimmed stage. And the ABI gate walks the build cache rather than the
payload, so anything staged straight into $MOD_PAYLOAD would bypass it --
coverage, not logic.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
10 KiB
The Python packages Moonraker and klippy need, cross-built for mipsel
The second half of phase 6. tools/python/ builds the interpreter;
this builds what runs on it. Not yet wired into bin/patch.sh -- see
"What is left" at the bottom. build.sh is the measurement harness, preserved
here because it is where the findings live, exactly as tools/supervisor/
is for s6.
Everything below was run in the printer replica on the real
rootfs.squashfs, not read off a project page.
test/integration/printer/case-pyext.sh is that run, kept.
What builds
19 packages, 12 mipsel extension modules, from a 56-second clean build in one
throwaway debian:bookworm. Every .so is gated at e_flags=0x70001407,
nan2008/o32/mips32r2 -- shared objects carry EF_MIPS_PIC, so they read
…07 where an executable reads …05.
Native: cffi, greenlet, lmdb, markupsafe, streaming-form-data, tornado speedups, pillow. Pure: jinja2, distro, inotify-simple, libnacl, dbus-next, preprocess-cancellation, pyserial, pyserial-asyncio, pycparser, smart_open, setuptools.
The list is deliberately not moonraker-requirements.txt, which installs
every optional component's dependencies. It is the closure of what the
enabled component set imports -- moonraker.conf's sections plus
CORE_COMPONENTS -- plus klippy's. So no apprise, ldap3, paho-mqtt, zeroconf
or python-periphery. msgspec and uvloop are skipped on purpose: Moonraker
guards both with contextlib.suppress(ImportError) and falls back to the
stdlib, measured.
The method
No crossenv. The staged interpreter already carries
_sysconfigdata__linux_mipsel-linux-gnu, so exporting
_PYTHON_SYSCONFIGDATA_NAME at an x86-64 build-python of the same 3.13.7
makes setuptools answer every question for the target. The stage is
bind-mounted at /usr/data/anvil so INCLUDEPY resolves unmodified. Plus the
gcc-wrapper trick from tools/python/build.sh, which is what gets
-EL -mnan=2008 onto the link lines.
What fought back, and what it cost
-
greenlet was the expected blocker, for an unexpected reason. The MIPS stack-switching assembly was never a problem. greenlet 3.x is C++ and writes its
PyTypeObjects as designated initializers, which gcc 7.2 accepts only in declaration order with no gaps:sorry, unimplemented: non-trivial designated initializers not supportedThe exact rule was probed rather than assumed: in-order contiguous is fine, a trailing gap is fine, an interior or leading gap is refused.
fill-designators.pywrites the skipped fields back as explicit zeros, taking field order from the target's own headers and refusing to guess when a designator is not in them. 55 fields across two files, semantically a no-op -- static storage duration, those fields were already zero -- and the result switches out and back, in order, on the printer. -
pip silently downloaded x86-64 manylinux wheels for cffi, greenlet and tornado on the first run. Three wrong-architecture
.sofiles sailed straight through the build and only the ABI gate caught them.--no-binary :all:is mandatory here, not caution. -
lmdb chose its cffi backend and produced a
py3-none-anywheel withmdb.cinside -- which is phase 6's recorded trap rebuilt from scratch: on the printer that path tries to invoke a compiler that does not exist.LMDB_FORCE_CPYTHON=1fixes it. NoteLMDB_FORCE_CFFI=0makes it worse: the variable is tested for presence, so"0"selects cffi. -
pillow 10.3.0 cannot be built by any 3.13, cross or not: its
setup.pydoesexec(...)then readslocals()["__version__"], which PEP 667 broke. 11.0.0 is the first version that works. -
streaming-form-data 1.13.0 (Moonraker's pin) ships pregenerated Cython C calling the 4-argument
_PyLong_AsByteArray, which grew a fifth parameter in 3.13. 1.19.1 instead, which then wantssmart_openat module scope.
What it proves
Beyond "it imports", which proves very little on its own:
- cffi does
cdef+dlopen+ call through libm -- klippy's exact ABI-mode usage. - cffi dlopens the printer's own
/usr/prog/klipper/klippy/chelper/c_helper.so, resolvesstepcompress_alloc,steppersync_allocanditersolve_generate_steps, and calls one. That is phase 7's central question -- whether a Python we built can drive FlashForge's C helper -- answered yes, and it is the reason the interpreter had to be glibc and not musl. - lmdb writes and a second process reads back.
- pillow decodes, resizes and re-encodes a PNG: the real thumbnail path.
- all 16 Moonraker components import on 3.13, the list taken from
CORE_COMPONENTS+moonraker.confrather than written down here. - zero
/usr/proglibraries mapped, with every extension imported. - Negative control: move the tree aside and every third-party import fails.
Size
13.5MB built, 2.9MB gzipped, 1073 files; 18.2MB on the printer once
__pycache__ exists. With the interpreter, about 50MB.
setuptools/pkg_resources are 4.9MB of that -- 36% -- and are not needed at
runtime: measured, with them removed lmdb still loads its CPython extension
and all 16 components still import. The phase-6 trap was the lmdb egg's cffi
fallback, and installing lmdb as a wheel removes it. Dropping them is a size
decision, not a correctness one.
Moonraker SERVES on 3.13 -- settled, not assumed
Importing is not serving, so it was run. test/integration/printer/case-moonraker313.sh
starts Moonraker on the exact command line payload/etc/s6/moonraker/run
execs, only with our interpreter, against the mod's own assets/moonraker.conf:
- bound :7125 in 3s,
/proc/PID/cmdlineconfirming our interpreter and our entry point GET /server/info200 with 23 components loaded and 0 failed;/machine/system_info(a different component, reading this box's /proc) reportscpu: mips,python 3.13.7 [GCC 7.2.0]- the database really works: POST wrote a namespace, GET read it back, the
namespace appears beside Moonraker's own, and
data.mdbis 147456 bytes on disk -- throughlmdb/cpython.cpython-313-mipsel-linux-gnu.so, not the compile-at-import cffi fallback - alive and still answering 120 seconds later, zero tracebacks in its log, and SIGTERM released the port in 2s
- the running process maps 0 libraries under
/usr/prog, withLD_LIBRARY_PATHcompletely unset -- not merely purged of/usr/prog - negative controls: nothing on :7125 beforehand, and with
site-packagesmoved aside it never binds
What is still unproven is endurance and integration, not capability: no real
hardware, no live klippy attached (klippy_connected=False throughout, so the
klippy_apis paths loaded but were never exercised), no upload through
streaming_form_data, no websocket client, and it has not been run through
S62moonraker under s6 on 3.13 -- the entry point was driven directly, on
purpose, so that a failure would be the interpreter and not an init script.
libsodium: built into the prefix, and it costs anvil-env.sh nothing
build-libsodium.sh cross-builds libsodium 1.0.20 with the Ingenic toolchain
(24s), gated at 0x70001407 nan2008/o32/mips32r2, SONAME libsodium.so.26,
needing only libpthread, libc and the loader. 406KB stripped, into
$MODDIR/lib.
It does not need a new LD_LIBRARY_PATH entry, which is the pleasant part.
libnacl's third fallback is __file__[0:__file__.find("lib")+3] + "/libsodium.so", and __file__ is
$MODDIR/lib/python3.13/site-packages/libnacl/__init__.py, so it resolves
$MODDIR/lib/libsodium.so by absolute path -- measured working with
LD_LIBRARY_PATH unset entirely. Worth writing down that this works because
the prefix happens to contain the string lib; a prefix without it would
break silently. Adding $MODDIR/lib to the path also works and is the
belt-and-braces option, but it would put our libraries in front of everything
for every mod process, which is the kind of entry this file's own history
argues against.
It computes as well as loads: sodium_version_string() is 1.0.20, its
BLAKE2b-256 matches CPython's own _blake2 on the same kernel, and
libnacl.sign.Signer/Verifier -- the exact ed25519 pair authorization.py
imports -- signs, verifies and rejects a tampered message. With it,
/usr/prog/libsodium/lib can come off ANVIL_LIBS, though only as part of the
same one-line FF_PYTHON switch, since 3.8's libnacl still needs it.
What is left before FF_PYTHON can switch
Capability is settled -- Moonraker serves, the database works, libsodium is ours. What is left is packaging and integration:
- Wire into
bin/patch.sh. The blocker is that section 5c's trim deletesinclude/andlib/python3.13/config-3.13-*and then removes the staging tree, and the package build needs both. Cleanest fix is to run the package build inside 5c against the untrimmed stage BEFORE the trim, rather than caching a second tree. Add a step for libsodium, a step to stage the 19 packages intolib/python3.13/site-packages/, and move every pin intoversions.envwith sha256s. - Extend the ABI gate's reach. It walks
$PY_BUILD/binand$PY_BUILD/lib-- the build cache, not the payload -- so libsodium and the twelve extension.sofiles staged straight into$MOD_PAYLOADwould bypass it. No logic change needed, the two-wordDYN -> 0x70001407rule already covers them; only coverage. - Run it through
S62moonrakerunder s6 on 3.13. The entry point was driven directly on purpose, so the s6 readiness and restart machinery is still gated only against 3.8. - Then the switch itself:
FF_PYTHONinanvil-env.sh, and/usr/prog/libsodium/liboffANVIL_LIBSin the same commit. - klippy is not blocked by packages: cffi, greenlet, jinja2, markupsafe
and pyserial all build, and
c_helper.sobinds.numpyis the only gap, wanted solely byextras/stepper_resonance_tester.py, which Klipper loads on demand. numpy on 3.13 means numpy >= 2.1 and a Meson cross-file, and that genuinely is a multi-day rabbit hole. It costs input-shaper calibration, not printing.
Unverified: no real hardware, replica only; nothing has run under sustained load; and the greenlet designator patch is a no-op by argument plus a working switch test, not by upstream blessing.