Commit Graph
6 Commits
Author SHA1 Message Date
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
Oleksandr ShyshatskyiandClaude Opus 5 4850be432c Docs: say what is there now, not what the last three commits removed
Deleting test/ and anvil.conf left claims behind in nine files. Every one of
these was present tense about something that no longer exists.

THE CHELPER SYMBOL GATE, in three places that each described it as live:

  notes/20-klipper-fork.md   said pkgs/klipper gates the .so on "both the ELF
                             flags and test-chelper.py's symbol check". It
                             gates on neither now: the ABI is asked once of the
                             installed filesystem in qa/replica/test_abi.py,
                             and the single-source rule -- the .so is compiled
                             from the chelper sources of the tree it ships -- is
                             the whole defence. The page says that instead.
  notes/85-packaging.md      described the KLIPPER_FORK fallback the script no
                             longer had; the script itself is gone too.
  qa-migration.md            "What survives in test/" listed both the 28 unit
                             tests and test-chelper.py as surviving. It is
                             "What survived in test/, and then did not", and it
                             names what went uncovered rather than leaving that
                             to be discovered.

ANVIL.CONF, in the two notes that still read it at runtime:

  notes/80-s6-migration.md   is a dated plan, so it keeps its phases and gains
                             a "Where this ended up" note: anvil.conf and every
                             MOD_* switch were removed, so the phases that
                             source it describe an intermediate state, and the
                             case-*.sh gates they name are qa/replica/ modules.
  notes/85-packaging.md      predicted seed/ emptying via a postinst that seeds
                             anvil.conf.default. It emptied by anvil.conf being
                             deleted instead. Its payload allowlist said six
                             entries including anvil.conf and bin/busybox; the
                             real one in test_ipk.py has five and neither.

THE TWO STANDALONE TREES under tools/, stale before any of this and now fixed:
tools/python/README.md and tools/supervisor/README.md both told the reader to
run their gate with `test/integration/printer-exec.py` over a case-*.sh, and
one offered a `make test-python` target. None of the three has existed for
several commits. Both now point at qa/replica/, which installs the real package
and asks more than hand-feeding a tarball to a case script could. The four
places tools/python/README.md narrates what case-python.sh proves are past
tense, since that is now a record of how the measurements were taken.
versions.env's sqlite3 note pointed at the same dead target.

NAV: every notes/ page was listed except 85-packaging.md, and qa-migration.md
was reachable only by following a link out of Testing, Building or How it
works. Both are in the nav now, which also clears the two "not included in the
nav" lines from every build. .venv/ is gitignored -- docs/requirements.txt
tells you to make one right there in the tree.

Verified: `mkdocs build --strict` is clean and no longer reports pages outside
the nav. qa/static is 123 passed, its 110 errors the same missing
vendor/opkg-utils as before. No .md file is read by any lane, so the check that
matters here is the build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016bLGYpVAeVzAX6NeAxcU42
2026-08-30 01:16:54 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 4ce928f160 The last two artefact ABI gates go; the toolchain check stays
Follow-on to 7dda2fe, which left the gates in tools/ alone pending a call on
whether they were the same rule. Two of the four were.

REMOVED, because qa/replica/test_abi.py now answers exactly this:

  * tools/python/build.sh walked every ELF in the install stage
  * tools/python-packages/build.sh walked every .so in site-packages
  * tools/python-packages/build-libsodium.sh gated the object that ships

Both walkers pinned the e_flags word EXACTLY -- 0x70001405 for an EXEC,
0x70001407 for a DYN -- which made them the strictest of the six and strict
enough to be wrong: the low three bits are NOREORDER/PIC/CPIC and vary
between objects of identical ABI, so libarchive's 0x70001406 is refused by
that rule and loaded happily by the kernel. The replica gate masks them off.
libsodium keeps its readelf output; a build log that says what it produced
is worth having, it just no longer decides anything.

KEPT, because it is a different question about a different object: the
compiler wrapper check in pkgs/lib.sh (and the copies in the two standalone
tools/ trees). It compiles a hello-world and reads THAT header, so it judges
the toolchain rather than any artefact -- something a filesystem sweep
structurally cannot do, since it cannot see a compiler. It also fails in a
second, before a feed is built on a wrapper that quietly lost -mnan=2008,
where the replica gate would report several hundred files minutes later.
test_the_toolchain_wrapper_is_still_checked_before_anything_is_built now
holds it in place from that side.

AND THE GUARD HAD A HOLE, found while doing this. The version in 7dda2fe
grepped the build scripts for nan2008 and mips_abi_gate, so it never saw
pkg_toolchain's check in pkgs/lib.sh -- which is written in raw hex and says
neither word. A gate can be written entirely in hex, so the guard now looks
for 0x7000140 as well, and permits it only in the four files allowed the
wrapper check. It does NOT look for mips32r2: that is also an ISA compiler
flag, and pkgs/3rdparty/openssl/build.sh legitimately passes -mips32r2, so
guarding on it reports the flag -- a false alarm, and then a guard nobody
reads.

Both guards were driven red on purpose: an artefact gate returned to a
recipe, a hex-only gate returned to bin/common.sh (the form the old one
missed), and the wrapper check deleted from pkgs/lib.sh.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NpH87TJJEjDyRhQPu18pgp
2026-08-30 01:02:17 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 214a4e58b9 anvil-core owns firmwareExe and start.sh; kill the 3.8.2 myth
Three things, all downstream of klippy having moved to $MODDIR and our own
CPython 3.13.

SECTIONS 8 AND 9 ARE GONE. Both files install at $MODDIR/prog/ and
anvil-link-prog.sh points the stock paths at them -- from run-append.sh on a
flash and from anvil-core's postinst on `opkg upgrade anvil-core`. Nothing
execs either between run.sh distributing the component and link-prog running,
so the component copy bought no ordering. It cost, though: our firmwareExe
with no payload behind it logs "no anvil-env.sh", finds no s6-svscan and then
`while true; do sleep 3600; done` -- a printer that boots to a blank screen for
ever and never reaches app_startup.sh's own recovery. Absent, that recovery
restores a firmwareExe from a version directory. So patch.sh now REMOVES both
from the component: the stock component carries them, and deleting the two cp
lines alone would have shipped FlashForge's originals.

SECTION 5c IS GONE. It did no work -- two du prints and a comment whose first
line was the falsehood below.

THE FALSEHOOD. "klippy runs on the stock 3.8.2, started by
/usr/prog/klipper/start.sh" had spread well past patch.sh, and it had reached
things people follow:

  - docs/toolchange.md told owners to scp the ff_*.py extras to
    /usr/prog/klipper/klippy/extras/ -- the wrong partition -- and warned that
    an OTA would delete them there. Same wrong path in three ff-*.cfg headers
    and in ff_toolchange.py.
  - versions.env labelled our CPython 3.13 and its eighteen packages
    "SHIPPED BUT NOT USED".
  - docs/how-it-works.md said Moonraker runs on python3.8, contradicting line
    126 of the same file.

All corrected. Historical notes keep their history and get a [Superseded: ...]
marker instead -- including 80-s6-migration.md's "s6-rc-compile is not
shipped", which pkgs/3rdparty/s6-rc/build.sh contradicts deliberately.

Found while reviewing: every docs/ reference to the ff-*.cfg pointed at
pkgs/anvil-core/payload/config/, which does not exist -- they are
anvil-klipper-config's. 28 references across 9 files, several of them broken
markdown links on the published site. All 24 relative links under docs/ now
resolve.

Gates: verify.sh syntax-checks prog/start.sh and prog/firmwareExe out of the
payload (firmwareExe was never syntax-checked before) and fails if the
component carries either. A new replica test asserts both stock paths are
symlinks into $MODDIR/prog and do not dangle. One existing replica test stubbed
firmwareExe by redirecting into $FE, which now writes THROUGH the symlink into
the packaged wrapper; it moves the link aside instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01La5MaDKqTW675k5fsXsP6S
2026-08-30 00:37:28 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 8cc92e8266 A wheel could only be built while three scratch trees happened to coexist
That is why bin/patch.sh section 5c was 825 lines and why a Pillow bump
rebuilt CPython. Cross-building an extension module needs the untrimmed
staging tree (INCLUDEPY points into it), the private sysroot of static C
libraries (pillow links zlib, cffi links libffi) and an x86-64 build-python
of the same version -- and all three existed only mid-section, so the wheels
had to be built there too, on a cache with two stamps that invalidated each
other.

Each of those three is now a durable thing: anvil-python-dev carries the
headers, pkg_deps fills each recipe's own sysroot from the feed, and
pkg_buildpython caches one build-python for all nineteen recipes. So the
coupling goes away with them -- `make packages PKG=python-pillow` builds
Pillow.

  pkg/python            -> anvil-python + anvil-python-dev
  pkg/python-<name> x18 -> anvil-python-distro, anvil-python-pillow, ...

Three verbs in pkg/lib.sh, shared by all nineteen rather than one per awkward
project: pkg_buildpython provides the build-python, pkg_pytarget points its
sysconfig at the target headers in the sysroot, pkg_pywheel builds one wheel
and unpacks it into the staging site-packages. greenlet's gcc 7.2 designated
initializers, lmdb's LMDB_FORCE_CPYTHON and pillow's build_ext options are
recipe lines and a PKG_PY_SETUP_ARGS knob, not three more verbs.

Eighteen versions that were invisible are now in the feed index, anvil-moonraker
declares the thirteen packages it imports, and a printer that does not want
gcode thumbnails can leave Pillow uninstalled.

PY_STAMP, PYPKG_STAMP and PY_BUILD are gone: pkg_stamp derives the key from
PKG_BUILD_DEPENDS, and bin/fetch-assets.sh asks pkg_needs instead of
re-deriving a stamp bin/patch.sh also derived. Two of its four toolchain
conditions were that second spelling.

Measured:
  * 40 packages; 959 site-packages files, none owned by two packages
  * 12 mipsel extension modules -- the count the old gate demanded
  * every Depends resolves inside the feed (24 declare one, 0 unsatisfiable)
  * two cold builds byte-identical, with work/pkg, work/.py-host and the
    unpacked toolchain deleted in between
  * 233 static tests, 4 new: the PYPKG_LIST/recipe agreement both ways, no
    version pinned twice, patch.sh builds every package, and no Depends
    naming a package no recipe produces

Not verified here: the replica suite. It needs the stock FlashForge package,
which the packaging lane deliberately does without.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 18:28:30 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 813973620c The payload carries a CPython 3.13 with a working sqlite3
Nothing runs on it. FF_PYTHON still points at FlashForge's 3.8.2, and the
comment beside it now says at length why changing that line is a one-line way
to break the whole userspace: klippy, Moonraker and ff-startup.py import
third-party C extensions -- tornado, lmdb, cffi, greenlet, pillow, libnacl --
that exist on this printer only as mipsel objects built against 3.8. None has
been cross-built for 3.13 yet. The interpreter ships ahead of the switch for
the same reason s6 did: it puts the artefact on real printers where it can be
measured, and it leaves the switch a one-file change rather than a build
change and a boot change at once.

Why it exists at all: the Moonraker pin is frozen at commit 9d0d09d, December
2023, for exactly one reason -- this firmware's Python has no _sqlite3 and
there is no libsqlite3 anywhere on the box, while Moonraker v0.9.0 moved its
database off lmdb onto sqlite. That is a cost paid at every release. The gate
proves the premise rather than assuming it: the stock 3.8.2, given the library
path it needs to start, imports json and zlib fine and cannot import sqlite3.

Built by bin/patch.sh from eight pinned sources with the Ingenic toolchain
that already builds c_helper.so -- gcc 7.2, glibc 2.29, cross to
mipsel-linux-gnu. Not musl, and this is not a preference: a musl interpreter
cannot dlopen a glibc c_helper.so, which is exactly how klippy loads it.

Three things about the build are load-bearing and all three were found the
hard way:

  * -EL -mnan=2008 must reach the LINK lines, not only the compile lines.
    They are baked into PATH wrapper scripts around the gcc driver rather than
    passed as CFLAGS, because trusting eight separate build systems to forward
    flags to their linkers is how you get legacy-NaN objects that the printer
    refuses to load.
  * _sqlite3 disappears silently without LIBS="-latomic -lm". A static
    libsqlite3.a carries no DT_NEEDED on libm, so configure's
    sqlite3_bind_double probe fails to link and CPython records the module as
    "missing" -- a probe failure, so nothing in the build output says why. The
    build now hard-fails when _sqlite3 is absent rather than shipping an
    interpreter whose whole purpose is gone.
  * OpenSSL's linux-mips32 target hardcodes -mips2, which collides with this
    toolchain's -mfp64. -mips32r2 as a user cflag lands after the target's and
    wins.

Built --disable-shared, unlike FlashForge's own 3.8.2. Static libpython and
static dependencies mean the shipped tree contains no shared object of ours at
all: nothing to version-skew, nothing that can resolve to a /usr/prog copy by
accident, and no new mandatory LD_LIBRARY_PATH entry -- which matters here
specifically, because removing /usr/prog version strings from anvil-env.sh is
the point. The usual objection to static, that C extensions cannot find the
interpreter's symbols, was tested rather than assumed: an out-of-tree
extension cross-built against the staged include/ imports and runs on the
printer with no libpython mapped.

It installs into $MODDIR itself, --prefix=/usr/data/anvil, the same prefix
root s6 uses: bin/python3.13 beside bin/s6-svscan, stdlib in lib/python3.13
beside lib/. One install, not two trees.

That creates a hazard worth naming, because anvil-env.sh PREPENDS $MODDIR/bin
to PATH so s6-svscan can find s6-supervise. `make install` also writes
bin/python3, and a shipped bin/python3 would then sit in front of
FlashForge's for every process that sources anvil-env.sh -- silently handing
klippy and Moonraker an interpreter with none of their extensions. So the
build deletes it and asserts bin/ holds exactly python3.13, verify.sh fails a
package carrying bin/python3, and the gate asks the shell what `python3`
resolves to on anvil-env.sh's own PATH. It answers
/usr/prog/Python-3.8.2/bin/python3, while python3.13 answers ours.

About 28MB and 600-odd files, which takes the install manifest from 70 entries
to 728 -- case-upgrade.sh still passes, which is the check that the manifest
deletion loop scales.

The build image grows a host gcc, which looks redundant beside two
cross-toolchains and is not: cross-building CPython needs a build-python of
exactly the same version to freeze modules and byte-compile the stdlib, so
patch.sh builds an x86-64 3.13 from the same tarball first.

tools/python/ keeps the measurement build and what it cost, as
tools/supervisor/ does for s6. Two gaps recorded there rather than lost: the
printer has NO CA certificates -- /etc/ssl/certs exists and is empty, so TLS
negotiates and verifies nothing, which is true of the stock interpreter too --
and libatomic.so.1 is a new runtime dependency, present on the rootfs,
measured, and removable by linking it statically if that is ever wanted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 23:15:40 +03:00