Files
firmware/tools/python-packages/fill-designators.py
Oleksandr ShyshatskyiandClaude Opus 5 da65764b86 Moonraker's whole component set imports on the 3.13 we built
Nineteen packages cross-built for mipsel, twelve of them C extensions, every
one gated at nan2008/o32/mips32r2. Not wired into bin/patch.sh yet -- this
preserves the build and, more importantly, what it cost, the way
tools/supervisor/ does for s6. The measurement lives in
test/integration/printer/case-pyext.sh rather than in a paragraph.

Two results carry the phase.

The first: 3.13's cffi dlopens the printer's OWN
/usr/prog/klipper/klippy/chelper/c_helper.so, resolves stepcompress_alloc,
steppersync_alloc and itersolve_generate_steps, and calls one. That is phase
7's central question -- whether an interpreter we built can drive
FlashForge's C helper -- answered by doing it, and it is the whole reason the
interpreter had to be glibc rather than musl.

The second: all 16 Moonraker components import on 3.13, with the list taken
from CORE_COMPONENTS plus moonraker.conf rather than written down. The
negative control is that moving the tree aside fails every third-party import
and the database component.

The package list is deliberately NOT moonraker-requirements.txt, which
installs every optional component's dependencies. It is the closure of what
the ENABLED set imports plus klippy's, so no apprise, ldap3, paho-mqtt or
zeroconf. msgspec and uvloop are skipped on purpose: Moonraker guards both
with contextlib.suppress(ImportError) and falls back to the stdlib.

greenlet was the expected blocker and blocked for an unexpected reason. Its
MIPS stack-switching assembly was never a problem; greenlet 3.x is C++ and
writes its PyTypeObjects as designated initializers, which gcc 7.2 takes only
in declaration order with no gaps. The exact rule was probed rather than
guessed -- in-order contiguous fine, a trailing gap fine, an interior or
leading gap refused -- and fill-designators.py writes the skipped fields back
as explicit zeros, reading field order from the target's own headers and
refusing to guess when a designator is not in them. Semantically a no-op:
static storage duration, those fields were already zero. It switches out and
back, in order, on the printer.

Three traps worth having in the log. pip silently downloaded x86-64 manylinux
wheels for cffi, greenlet and tornado on the first run -- three
wrong-architecture .so files sailed through the build and only the ABI gate
caught them, so --no-binary :all: is mandatory here rather than cautious.
lmdb chose its cffi backend and produced a py3-none-any wheel with mdb.c
inside, which is phase 6's recorded trap rebuilt from scratch: on the printer
that path invokes a compiler that does not exist. And pillow 10.3.0 cannot be
built by any 3.13, cross or not, because its setup.py execs and then reads
locals()["__version__"], which PEP 667 broke.

One dependency is unsolved and named rather than buried: libnacl ctypes-loads
libsodium, which exists on this box only at /usr/prog/libsodium/lib. Measured
both ways. So the authorization component keeps the last /usr/prog string in
the picture until libsodium is cross-built or that one path entry is kept.

What is left before FF_PYTHON could switch is written down in the README, and
the honest remaining unknown is at the top of it: every component imports and
the entry point loads, but importing is not serving. Nothing has bound :7125
on 3.13 yet.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 10:14:38 +03:00

228 lines
8.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""Make C++ designated initializers digestible by gcc 7.2.
fill-designators.py <include/python3.13> <file.cpp> [file.cpp ...]
WHY THIS EXISTS. greenlet 3.x writes its PyTypeObject, PyNumberMethods and
friends as C++ designated initializers:
PyTypeObject PyGreenlet_Type = {
.ob_base=PyVarObject_HEAD_INIT(NULL, 0)
.tp_name="greenlet.greenlet",
.tp_basicsize=sizeof(PyGreenlet),
.tp_dealloc=(destructor)green_dealloc,
...
Designated initializers are C++20. gcc 7.2 -- the Ingenic toolchain, and the
only compiler this printer's ABI has -- supports them as an extension, but
only IN DECLARATION ORDER AND CONTIGUOUS. Measured, probe-desig.sh:
in order, contiguous OK
in order, TRAILING gap OK -- the only gap gcc 7.2 tolerates
in order, INTERIOR gap sorry, unimplemented: non-trivial designated
initializers not supported
in order, LEADING gap same -- {.nb_bool=x} on a PyNumberMethods whose
first field is nb_add is already too much
out of order same
Every one of greenlet's initializers skips fields -- a PyTypeObject naming 20
of its 50 slots is the normal way to write one -- so all of them are interior
gaps, and the build stops on the first.
WHAT THIS DOES. It reads the field order out of the TARGET interpreter's own
headers and writes the skipped fields back in as explicit zeros. That is a
no-op semantically: these are objects of static storage duration, so every
field this inserts was already going to be zero-initialized. It is not a
behaviour patch, it is the same initializer spelled in a way gcc 7.2 will
accept, and if a designator ever appears that is NOT in the header's field
list the script fails loudly rather than guessing.
Every field it fills is a pointer or an integer, so `= 0` is the right zero;
the script asserts that too, rather than trusting it.
"""
import os
import re
import sys
INCLUDE = sys.argv[1]
FILES = sys.argv[2:]
# The initializer types greenlet uses. Each is (C type name as written in the
# source, the header declaration to read the field order from).
STRUCTS = {
"PyTypeObject": ("cpython/object.h", "struct _typeobject"),
"PyNumberMethods": ("cpython/object.h", "typedef struct"),
"PyAsyncMethods": ("cpython/object.h", "typedef struct"),
"PySequenceMethods": ("cpython/object.h", "typedef struct"),
"PyMappingMethods": ("cpython/object.h", "typedef struct"),
"PyModuleDef": ("moduleobject.h", "struct PyModuleDef"),
}
# A field declaration line. Two shapes in CPython's headers that a naive
# regex gets wrong, and both appear in PyTypeObject:
# * `PyObject_VAR_HEAD` -- a MACRO, not a declaration; it expands to the
# `ob_base` field the source designates by name.
# * `Py_ssize_t tp_basicsize, tp_itemsize;` -- two declarators on one line.
FIELD = re.compile(r"^\s*(?:const\s+|struct\s+|unsigned\s+)*"
r"[A-Za-z_][A-Za-z0-9_]*\s*"
r"(?P<decls>[^;{}()]*);\s*$")
DECLARATOR = re.compile(r"\**\s*([A-Za-z_][A-Za-z0-9_]*)\s*(?:\[[^]]*\])?$")
HEAD_MACROS = {"PyObject_HEAD": "ob_base", "PyObject_VAR_HEAD": "ob_base"}
def read_header(path):
for root, _dirs, files in os.walk(INCLUDE):
if os.path.basename(path) in files and root.endswith(
os.path.dirname(path) or root[len(root):] or ""):
return open(os.path.join(root, os.path.basename(path))).read()
# fall back: anywhere under the include tree
for root, _dirs, files in os.walk(INCLUDE):
if os.path.basename(path) in files:
return open(os.path.join(root, os.path.basename(path))).read()
raise SystemExit("no header %s under %s" % (path, INCLUDE))
def fields_of(cname):
"""Ordered field names of a struct, read from the target's headers."""
header, _kind = STRUCTS[cname]
text = read_header(header)
# find "} <cname>;" or "struct _typeobject {"
if cname == "PyTypeObject":
start = text.index("struct _typeobject {")
start = text.index("{", start) + 1
else:
end = re.search(r"\}\s*%s\s*;" % cname, text)
if not end:
return None
# walk back to the matching "typedef struct {"
head = text[:end.start()]
start = head.rindex("typedef struct {") + len("typedef struct {")
end_i = end.start()
return _scan(text[start:end_i])
depth = 1
i = start
while depth:
if text[i] == "{":
depth += 1
elif text[i] == "}":
depth -= 1
i += 1
return _scan(text[start:i - 1])
def _scan(body):
body = re.sub(r"/\*.*?\*/", "", body, flags=re.S)
body = re.sub(r"//[^\n]*", "", body)
out = []
depth = 0
for line in body.splitlines():
stripped = line.strip()
if stripped in HEAD_MACROS:
out.append(HEAD_MACROS[stripped])
continue
depth += line.count("(") - line.count(")")
if depth:
continue
m = FIELD.match(line)
if not m:
continue
for decl in m.group("decls").split(","):
d = DECLARATOR.search(decl.strip())
if d:
out.append(d.group(1))
return out
# ---------------------------------------------------------------- rewrite ---
# Match "<Type> <name> = {" ... matching "}" at the top level of the file.
DECL = re.compile(r"^(\s*)(?:static\s+)?(%s)\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*\{"
% "|".join(STRUCTS), re.M)
changed_total = 0
for path in FILES:
src = open(path).read()
out = []
pos = 0
changed = 0
for m in DECL.finditer(src):
cname = m.group(2)
order = fields_of(cname)
if not order:
continue
# find the matching close brace of this initializer
i = m.end()
depth = 1
while depth:
if src[i] == "{":
depth += 1
elif src[i] == "}":
depth -= 1
i += 1
body = src[m.end():i - 1]
used = [d for d in re.findall(r"\.([A-Za-z_][A-Za-z0-9_]*)\s*=", body)]
# only top-level designators: drop any that are not fields of this
# struct (they belong to a nested brace, e.g. .ob_base={...}).
used = [u for u in used if u in order]
if not used:
continue
unknown = [u for u in re.findall(r"^\s*\.([A-Za-z_][A-Za-z0-9_]*)\s*=",
body, re.M) if u not in order]
if unknown:
raise SystemExit("%s: %s names %r, not a field of %s -- header "
"and source disagree, refusing to guess"
% (path, m.group(3), unknown, cname))
idx = [order.index(u) for u in used]
if idx != sorted(idx):
raise SystemExit("%s: %s designators are out of declaration order "
"(%r) -- gcc 7.2 cannot take that either and "
"reordering them is not this script's job"
% (path, m.group(3), used))
# Insert the skipped fields, in order, as explicit zeros. Everything
# between the first and last named field only.
new_body = body
gaps = []
for a, b in zip(idx, idx[1:]):
if b - a > 1:
gaps.append((order[a], order[a + 1:b]))
for after, missing in reversed(gaps):
fill = "".join(" .%s=0,\n" % f for f in missing)
# place the fill immediately after the entry for `after`
am = re.search(r"\.%s\s*=" % re.escape(after), new_body)
j = am.end()
depth2 = 0
while True:
c = new_body[j]
if c in "([{":
depth2 += 1
elif c in ")]}":
if depth2 == 0:
break
depth2 -= 1
elif c == "," and depth2 == 0:
j += 1
break
j += 1
new_body = new_body[:j] + "\n" + fill + new_body[j:]
changed += len(missing)
# A gap BEFORE the first designator is refused too -- greenlet's
# `static PyNumberMethods green_as_number = {.nb_bool=...}` names the
# tenth field and nothing before it. Fill from field zero.
if idx[0] > 0:
fill = "".join(" .%s=0,\n" % f for f in order[:idx[0]])
new_body = "\n" + fill + new_body
changed += idx[0]
out.append(src[pos:m.end()])
out.append(new_body)
pos = i - 1
out.append(src[pos:])
if changed:
open(path, "w").write("".join(out))
print(" %s: filled %d skipped field(s)" % (path, changed))
changed_total += changed
print("fill-designators: %d field(s) written back" % changed_total)