Files
firmware/tools/replica/ffsim/gates.py
Oleksandr ShyshatskyiandClaude Opus 5 b98552386b No FlashForge component ships; patch.sh is payload.sh
The software component existed to give the mod one exec hook. It is not needed
for that any more, so a release now carries our installer and our payload and
nothing else: 26MB smaller, 21MB of which was FlashForge's own firmwareExe
travelling back to the partition it was already on, along with their klippy
tree, their wifi module and their configs.

WHAT IT GIVES BACK. /usr/prog/PROGRAM/software is no longer rewritten, so the
version directory there stays FlashForge's with their genuine firmwareExe
inside it -- and app_startup.sh's five-second watchdog, which copies one out of
that directory when nothing by that name is running, has something to copy
again. A printer whose payload fails to install now comes up on the stock UI
instead of a blank screen. That recovery has been broken since we started
deleting their binary out of a component we shipped; docs/how-it-works.md
claimed it worked throughout.

THE ROOT PASSWORD moves with it, because it used to ride in the component's
shadow file. bin/pack.sh bakes ROOT_PW_HASH into the installer, single-quoted
-- an unquoted $6$... would have the printer's shell expand $6 as a positional
parameter and ship a truncated hash -- and the installer writes it to
/usr/prog/etc/shadow. The random-password path asks one question instead of
four: FlashForge ship the same root hash on every machine, so a live shadow
still carrying it means nobody has set a password and it is ours to set. The
.prev-root-hash recording, the shipped-vs-live comparison and the restore-it-
afterwards step were all machinery for surviving a file that nothing overwrites
any more, and are gone. pack.sh checks the hardcoded constant against the stock
package it built from, because getting it wrong is silent and total: every
printer would read as "already set" and none would ever get a password.

bin/patch.sh is bin/payload.sh. It patched nothing any more -- no splice, no
shadow edit, no firmwareExe deletion -- and what is left is the payload: the
feed installed by the printer's own opkg, the seeded config, the manifest. Its
sections are renumbered 1-5 with no gaps, and the five tombstone comments
marking where removed sections used to be are gone with them.

test_recipe_layout.py loses six of its nine tests. Each compared the tree
against a description of the tree -- no fourth directory kind, each kind still
used, 3rdparty carries no files of ours, installer/ holds exactly two named
files -- and none could go red for a reason a printer would notice, while all
six went red the moment the layout legitimately changed. The two that bite are
kept: a payload/ path escaping $MODDIR, and a recipe name at both levels, which
makes pkg_dir silently build the wrong package.

SW_VER is deleted from config.env.example and both workflows: it stamped a
component that is not built.

Gates: 112 static tests pass. The generated installer is verified after
substitution -- all four lines present, and `sh -n` over the result, because a
hash with a stray character in it would otherwise reach a printer as a syntax
error whose only symptom is a package that installs nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CH7Yb4wuPhnppHAB25UX7e
2026-08-30 15:38:25 +03:00

77 lines
3.5 KiB
Python

"""What is left of the gates: the cross-built python tree, as a tarball.
THERE ARE NO GATES IN HERE. There were fifteen, driving thirteen case
scripts, and every one of them is a module under qa/ now. The name is kept
because the two survivors have no better home yet and a rename would be the
only thing in the diff that could break an import.
What survives, and why:
_python_trees / _python_tarball `make boot-screen-sim` renders its frames
with the interpreter we cross-build, so
something has to hand it that tree.
`extract_rootfs` was the other survivor -- it unsquashed the printer's real
userland out of the stock package for a locally built replica. There is no
such replica any more: PRINTER_IMAGE is the one source of a machine, and
tools/replica/build-printer-image.sh does its own extraction from the public
firmware without needing a stock package at all.
"""
import shutil
import tarfile
def _python_trees(config):
"""Every recipe output that makes up the printer's python prefix.
NINETEEN TREES, NOT ONE. This used to be work/.py313, the single directory
bin/payload.sh cross-built the interpreter and its site-packages into
together. CPython is pkgs/3rdparty/python now and each of the eighteen
third-party packages is a pkgs/3rdparty/python-* of its own, so what a
printer sees is the union of their bin/ and lib/ -- which is exactly what
the payload's python packages provide, in this order.
The interpreter comes first so that a half-built checkout fails on the
thing the caller actually needs rather than on a package that depends on it.
"""
pkg = config.root / "work" / "pkg"
return [pkg / "python"] + sorted(d for d in pkg.glob("python-*") if d.is_dir())
def _python_tarball(config):
"""The cross-built CPython 3.13 tree, packed the way a printer sees it.
The interpreter is configured --prefix=/usr/data/anvil and its stdlib lives
in lib/python3.13/, so a tarball of the merged bin/ + lib/ unpacks straight
into $MODDIR and every file lands where it was compiled to expect itself.
Returns None when nothing has built it yet: whether that is a Skip or a
fallback is the caller's question.
THE DEV HALF RIDES ALONG and is deliberately not filtered out. work/pkg/python
holds the whole build -- headers, lib/pkgconfig and config-3.13-* included --
because the split into anvil-python and anvil-python-dev happens where the
.ipk files are made. Which paths those are is pkgs/3rdparty/python/pkg.conf's business,
and repeating the list here would be a second spelling that goes stale
silently. This is a test fixture unpacked into a simulator, not something a
printer installs: 3MB of headers it will never open costs nothing, and the
package boundary is gated where it is made.
"""
trees = _python_trees(config)
if not (trees[0] / "bin" / "python3.13").is_file():
return None
staged = config.root / "work" / ".py-gate"
if staged.is_dir():
shutil.rmtree(str(staged))
for tree in trees:
for sub in ("bin", "lib"):
src = tree / sub
if src.is_dir():
shutil.copytree(str(src), str(staged / sub), dirs_exist_ok=True,
symlinks=True)
out = config.root / "work" / ".py-gate.tgz"
with tarfile.open(str(out), "w:gz") as tar:
for sub in ("bin", "lib"):
if (staged / sub).is_dir():
tar.add(str(staged / sub), arcname=sub)
return str(out)