Commit Graph
17 Commits
Author SHA1 Message Date
Oleksandr ShyshatskyiandClaude Opus 5 e3d5a62860 The ntp service execs ntpclient, the only name that sets the clock
sntpd and ntpclient are one binary that picks its argument parser from
argv[0], and only one of those parsers can step the clock. The step is behind

    if (!dry && ntpc->set_clock)          sntpd.c:485

and set_clock is raised in exactly one place: `case 's'` in the ntpclient
parser, sntpd.c:1459. Under the sntpd name it is initialised to zero with no
option to raise it -- that parser's -s means "log to syslog" -- and the daemon
disciplines the clock FREQUENCY through adjtimex instead. Right for a clock
that is roughly correct and drifting; useless on a printer with no RTC, which
boots in 1970. Parts per million do not close fifty-six years.

So the service has never worked. It resolved, connected, took valid stratum-1
readings, logged a reading line every poll, and left the clock in 1970 for as
long as the machine was up -- healthy by every signal except the one that
matters.

The recipe asks for --with-ntpclient, which is upstream's own install hook and
a symlink rather than a second copy of the binary (asserted, because a cp that
dereferenced it would ship 32KB twice). The run script execs that name with
-s -l, and its option block is rewritten against the ntpclient parser: the two
disagree about -p, and -n does not exist there at all, so the old arguments
would have been a usage error and a service that exited at once.

Measured on a printer: clock forced back two hours, service restarted, correct
again twenty seconds later.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AVUMfbNuKUWWV32PBcydwb
2026-08-31 13:04:28 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 dda4eb4025 s6-rc-update ships, and the compile runs it before current moves
Recompiling on a printer is not enough on its own. s6-rc-init writes
/run/s6-rc/state as one byte per service in the database it booted, and
/run/s6-rc/compiled is a symlink to compiled/current -- so moving `current`
under a running system leaves a state file whose size no longer matches, and
every s6-rc command fails:

    s6-rc: fatal: unable to read valid state in /run/s6-rc/state

Supervision survives it, which is what makes it quiet: s6-svscan and
s6-supervise are a separate mechanism, so every service stays up and
s6-svstat keeps answering while s6-rc itself is unusable until the next boot.
Seen on a printer, 2026-08-31, caused by the postinst added two commits ago.

s6-rc-update is the answer and was not shipped, on the reasoning that an
update is a reboot anyway -- true while updates arrived only as a .tgz, and
false now that a postinst compiles a database. It ships.

ORDER MATTERS, and the first version had it wrong: s6-rc-update reads the
database the live directory is on to know what it migrates FROM, and that is
`current`. Flipping first leaves it comparing the new database against
itself. The compile now migrates while the old database is still current,
and moves the symlink afterwards.

A migration failure is reported, not fatal: the database on disk is right
either way and a reboot applies it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AVUMfbNuKUWWV32PBcydwb
2026-08-31 12:43:22 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 15aa6abdda The boot database is compiled on the printer too, not only by the build
s6-rc's database is generated from anvil-core's etc/s6-rc/source, and the
only thing that ran s6-rc-compile was the payload build. So `opkg upgrade
anvil-core` on a printer wrote new service definitions and left the machine
booting the database compiled from the old ones -- the upgrade landed and
changed nothing.

anvil-core ships bin/anvil-s6-rc-compile.sh and its postinst calls it.
anvil-s6-rc calls it too, because s6-rc-compile writes its own execline
shebang and oneshot runner into every database it produces, so one left by
the previous s6-rc is not one this s6-rc should run.

The payload build calls the same script rather than its own copy of the
compile, passing the database name: given one the script prunes every other
database, which is what keeps two builds of one commit byte-identical.
Without a name it timestamps and keeps the database it replaced, so a printer
that will not boot has one to point `current` back at. Nothing live is
touched either way -- a running supervision tree keeps its database until the
next boot, so an upgrade cannot restart services mid-print.

anvil-s6-rc gains PKG_STAMP_EXTRA: its version comes from the tarball and
cannot see control/postinst, and a stamp that cannot see an input reports
"already current" and ships the previous .ipk.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AVUMfbNuKUWWV32PBcydwb
2026-08-31 12:22:48 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 28eb20f3da Package numpy, because klippy does not start without it
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
2026-08-31 13:42:57 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 58cfd00415 FlashForge's ffmpeg cannot render a rotated webcam, so we build our own
moonraker-timelapse emits -vf 'transpose=1', 'hflip,vflip' and friends
straight from the webcam's rotation and flip settings. The stock ffmpeg is
configured --disable-everything with a narrow enable list, and asking the
binary itself for -filters returns format, null, scale and trim -- so every
one of those renders fails on a printer whose camera is not upright.

So: ffmpeg 4.4.5 and x264, cross-built, static, one 1.8M binary at
$MODDIR/bin/ffmpeg. The enable list is FlashForge's plus the filters they
left out, since flash is scarce and nothing else is wanted.

There is no hardware encoder to use instead, and it is written down in
pkgs/3rdparty/ffmpeg/pkg.conf so nobody re-derives it: the X2600's device
tree declares jpege (a JPEG encoder), jpegd and felix, and felix is an H.264
DECODER -- the die has no H.264 encode block at all. Only felix has a driver
in the stock kernel; jpege and the rotate block have DT nodes and clock gates
and nothing bound to them. Software x264 is the only route to an H.264 mp4
here, which is why moonraker.conf tunes it with -preset ultrafast rather than
leaving the default.

x264 is named anvil-x264-dev outright, as anvil-zlib-dev is: every file in it
is a header or an archive, so a PKG_DEV_FILES split would leave an empty
anvil-x264 beside it that no printer would ever install.

bzip2 joins the build image because x264's only distributed form is a
.tar.bz2 and tar could not unpack it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSmXJv47Wwq2J7FfgCgpCe
2026-08-30 15:47:25 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 d898d105e0 The Moonraker pin was frozen by a missing _sqlite3 we now build ourselves
MOONRAKER_VERSION was 9d0d09d, the last commit before 80c7620 moved the
database from lmdb to sqlite, because FlashForge's python 3.8.2 has no
_sqlite3 and every release after it died loading the database component.
This repo builds and ships its own CPython 3.13 now, and it has one. The pin
is v0.11.0 -- a release tag, not a sha, so PKG_VERSION is just 0.11.0 and no
longer the Debian-style 0.8.0+git<sha> snapshot string. GitHub strips the
leading "v" from the directory inside /archive/<tag>.tar.gz, which is what
${MOONRAKER_VERSION#v} in the recipe is for.

The dependency set is the part that actually moved. Read against 0.11.0's
CORE_COMPONENTS plus the sections moonraker.conf enables:

  dbus-next became dbus-fast. dbus_manager is a core component and imports
  it at module scope, so this is not optional whatever [machine] provider
  says. Its sdist cythonises nine modules; SKIP_CYTHON=1 ships the python
  they were generated from and keeps the package ARCH all. The wheel still
  comes out platform-tagged -- poetry-core marks the distribution impure
  for declaring a build script at all -- but it holds 29 .py and no .so,
  and the content is what ships.

  importlib-metadata is new, imported at module scope by
  utils/source_info.py, and brings zipp. Both build through setuptools_scm,
  so PYPKG_HOST_LIST grew packaging and setuptools-scm. That list is now
  install order: pkg_buildpython pip-installs each with --no-deps against
  no index, flit-core builds packaging, setuptools_scm imports it.

  pyserial-asyncio is gone. Nothing in 0.11.0 imports serial_asyncio;
  utils/async_serial.py drives pyserial over asyncio itself. pyserial stays
  -- klippy needs it and declares it.

LMDB STAYS, for a reason that is not the old one. 0.11.0 converts an
existing data.mdb to sqlite on first start and imports lmdb to read it. With
the module missing it tries to pip install one, and a printer has no index
to install from -- the old database would be dropped in silence. It is the
upgrade path now, not the database, and the comment on WANTED_MODULES says
so.

inotify-simple STAYS AT 1.3.5 though 0.11.0 pins ==2.0.1: 2.0.1 declares its
licence the PEP 639 way and needs setuptools>=77 to build, against the 75.6.0
pinned here for lmdb's sake. It fails at metadata generation, not at import,
which is the kind of thing that reads as a broken recipe later. 1.3.5 carries
every name 0.11.0 asks of it, and moonraker.conf sets file_system_observer:
none, so the import is all that is asked. jinja2 goes to 3.1.6 to match.

Not taken: paho-mqtt, zeroconf, apprise, ldap3 and python-periphery, whose
components this printer does not enable, and msgspec and uvloop, which
Moonraker imports under contextlib.suppress(ImportError).

Verified: every changed sha256 downloads and matches at the URL
fetch-assets.sh builds; all five affected wheels build offline
(--no-index --no-build-isolation) on a real 3.13 with the host backends
installed in the declared order; qa/static 122 passed, including the gates
that cross-check PYPKG_LIST against the recipes and every PKG_DEPENDS
against the feed. The mipsel cross-build and `make qa-replica` have not run
yet, and versions.env still says a bump is gated on the latter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012oRdgKM7scrEsr6waAByaY
2026-08-30 15:07:38 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 2e9f2214a0 Merge master into the comment trim
Master moved twelve commits while this branch ran: patch.sh became
payload.sh, run-pre.sh and run-append.sh became runFirmwareExe.sh, the
install manifest and anvil.conf are gone, and versions.env was trimmed
harder there than here (53 comment lines against 185).

Resolved so the merge contributes comments and nothing else. Verified
mechanically: every one of the 270 files in the tree now carries
exactly master's code, and no file this branch touched is resurrected.

  - master's deletions accepted: bin/patch.sh, bin/verify.sh,
    installer/run-{pre,append}.sh, anvil.conf.in
  - versions.env: master's, which is the better trim
  - 26 files master only renamed through: this branch's trim kept, with
    patch.sh -> payload.sh applied
  - 14 files master rewrote to describe the new installer: master's
    text taken wholesale, to be re-trimmed separately
  - stale references to the manifest and run-append.sh corrected in
    pkgs/3rdparty/busybox/build.sh and pkgs/moonraker/build.sh

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NMMpQTajN7RDJySrhQ2cTE
2026-08-30 16:25:37 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 bf7d0f7e06 The printer has no clock and nothing was setting it
There is no RTC and no battery, so every boot starts at whatever the
kernel was built with and stays there: job timestamps, log ordering and
certificate validity are all wrong on a machine that has been unplugged.
Nothing on the stock rootfs could fix it -- FlashForge's busybox is
v1.31.1 built without the ntpd applet, and its rdate speaks RFC 868.

anvil-sntpd is troglobit's sntpd, a fork of Doolittle's ntpclient: one
small binary, servers and interval on the command line, so there is no
config file, no privsep user and no chroot to place on a read-only
rootfs. It ships $MODDIR/sbin/sntpd and nothing else.

A LONGRUN, NOT A HOOK ON THE WIFI ONESHOT. That oneshot runs once, at
boot, and returns before wlan0 has associated -- so a sync hung off its
end would fire too early, never retry, and never fire at all for the
owner who joins a network from the HelixScreen panel an hour later. And
with no RTC a clock is not set once but maintained. sntpd keeps its
schedule with the network down instead of exiting, which is what lets
this service need no wifi dependency -- just as well, since nothing may
depend on that oneshot.

The poll interval is 300 rather than upstream's 600 because it is also
the ceiling on the down-network retry backoff, and so bounds how long a
printer that joined wifi late keeps the wrong time.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0149y4iyLpEwYKh2PS4fmf4u
2026-08-30 16:03:55 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 5d7754285e Wipe $MODDIR and extract, instead of pruning by a manifest
The installer deletes /usr/data/anvil outright and extracts into the
empty directory. The installed set is the shipped set by construction,
so a renamed file cannot survive beside its replacement and no list has
to be shipped, read or trusted for that to hold.

What that costs is everything else under $MODDIR, and the audit of what
was actually there is docs/notes/86-wipe-and-extract.md. backup/stock
had no restore path in any script and omitted the one file a restore
would need. .prev-root-hash was already dead state. config-installed
was a twelve-file snapshot serving one md5, for a compare whose stated
rationale -- "there is no include-and-override seam" -- stopped being
true when moonraker-custom.conf became the last include, and which
inverted its own purpose by keeping the webcam block and the API
lockdown off any printer that tripped it. So moonraker.conf is
overwritten like every ff-*.cfg.

HelixScreen's /tmp stash is untouched and is now the only preservation
the installer does, so the gate asserts it with a value that could only
have come from the printer.

Gone with it: both delete passes, the forged-path guard, the rmdir
pass, the pre-manifest branch, the unconditional init.d/anvil-service/
anvil.conf removal, the etc/s6 scandir sweep and the $MODDIR __pycache__
sweep -- the last two existed only because a manifest cannot see paths
made at runtime. firmwareExe already makes the scandir itself.

test_upgrade.py rewritten: the negative control is inverted rather than
deleted, and the legacy fixture goes -- there is one path now.

qa/static green (112). The replica lane needs a package in work/out,
which needs config.env with FF_KEY and STOCK_TGZ; not run here.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KXKk3zz8LAUPUDZkVEZ9fR
2026-08-30 15:49:26 +03:00
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 9b5b562a22 Comments: keep the contract and the trap, drop the narration
A sweep over every commented file in the tree. What is kept is what a
reader needs to change the code safely: what a thing is, its signature,
the invariants other files depend on, and the traps that fail silently
(find's non-zero exit under set -e, s6's baked-in libexecdir, the -lm
that decides whether _sqlite3 exists, opkg's compile-time prefix).

What goes is the narration around it: incident histories, arguments
against alternatives already rejected, restatements of the code below,
and paragraphs re-explaining a rule three files already state. One
paragraph in pkgs/3rdparty/opkg/build.sh was duplicated outright.

Comment lines 7971 -> 6158. Every change is comment-only: no
non-comment line differs anywhere in the 86 files, checked
mechanically. Commented-out config directives (#fast_feed: 30000 and
friends) are documentation of defaults and are kept.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NMMpQTajN7RDJySrhQ2cTE
2026-08-30 14:54:50 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 7dda2fee0d ABI gate: one check, on the filesystem the kernel loads from
mips_abi_gate answered "did this recipe stage the right ABI", once per
recipe, over a PKG_ROOT. Four implementations of that one rule had grown up
around it -- the function in bin/common.sh, the call at the packaging
boundary, two recipes that re-ran it on their own output, and a
readelf|grep nan2008 in bin/verify.sh reading a single .so -- and between
them they still could not see the largest binary we ship: bin/patch.sh
unpacks HelixScreen into /usr/prog and no gate ever read that tree.
pkgs/helixscreen/pkg.conf had carried a note saying exactly that.

qa/replica/test_abi.py replaces all of it. It walks the installed
filesystem on the printer's own CPython -- one qemu process, ~24000 files,
8s -- and reads the ELF header of everything it finds. That is the only
place where what the recipes staged, what opkg unpacked, what patch.sh
copied and what FlashForge shipped are all visible at once, and it is the
thing the question is actually about: the kernel does not read a PKG_ROOT.

MEASURED, not assumed: 603 ELF objects on a real install. 569 the loader
handles and 568 of those conform. The exemptions are the other two, both
narrow. ET_REL objects (33 stock .ko files, plus a stray python.o in
FlashForge's CPython 3.8 tree) are not asked about the FP ABI, because
insmod and the linker read them rather than the ELF loader and kernel code
has no FP ABI to agree about -- by e_type, so the python.o needs no case of
its own. And /usr/prog/PROGRAM/ffstartup-arm is an ARM binary FlashForge
ships that nothing in the stock tree names; exempt by path, so an ARM
binary anywhere else still fails.

The architecture is settled before anything is classified, and that
ordering came from planting a counterexample rather than from reasoning: a
big-endian object decodes its own e_type big-endian, so ET_DYN reads back
as 768, lands in neither bucket, and an earlier draft looked straight at it
and passed. Every gate here was driven red on purpose before it was kept --
an x86-64 .so in $MODDIR, a real mipsel object with EF_MIPS_NAN2008
cleared, an ARM binary outside the exempt path, a big-endian object, an
x86-64 object among the .ko files, and a truncated sweep.

test_install.py's two chelper header tests go with it: they read one file
out of the several hundred on the same fixture, a strict subset of what now
runs. Whether the package ships a chelper at all stays test_install.py's
question and is not asked twice. test_ipk.py's assertion that the gate
exists becomes an assertion that a second one has not grown back beside it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NpH87TJJEjDyRhQPu18pgp
2026-08-30 00:47:41 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 39b5cb0d0b ipk-install was a stand-in for an opkg the printer now has
The script says so itself: it existed so phase 1 could ship without
waiting for a cross-built opkg, and it would hand its database over when
the real client arrived. pkgs/3rdparty/opkg builds that client, and
anvil-opkg is in bin/patch.sh's payload roots, so the printer has opkg
and the stand-in has nothing left to do.

Nothing called it. bin/build-payload.py assembles the payload with the
printer's own opkg inside the replica, and the test that existed only to
keep ipk-install off the build path was a guard against a file that is
now gone. Its five round-trip tests went with it.

The shellcheck lane loses one file rather than a rule: _files() refuses a
glob that matches nothing, so the two entries had to go with the script.

Cherry-picked from fc98b04 on worktree-drop-ipk-install, so the two
branches carry the same patch. Resolved against this branch's payload
build: the host opkg that commit still described is deleted here, and the
lane globs are payload/prog/ rather than prog/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VpUmBb4xtmnmtTosonqeNi
2026-08-29 18:51:53 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 5f36cd14e3 Say what the code does, not what it used to
Comment cleanup over the files this branch touched, plus the dead code the
last two commits left:

  * pkgs/lib.sh's pkg_buildopkg, 89 lines building the x86-64 opkg nothing
    calls any more, and the qa test that checked its configure flags
  * patch.sh's "5d. what is no longer here" section, and its mkdir of the
    prefix directories -- build-payload.py replaces $PAYLOAD_DIR wholesale, so
    they were created and then thrown away
  * the two emptied creator5*-config recipe directories

The rest is prose. Comments that narrated a previous design -- what the host
opkg did, which package used to own the chamber config, that prog/ was once a
sibling of payload/ -- are gone rather than shortened; the ones explaining
constraints that still hold are trimmed to the constraint.

docs/building.md and docs/toolchange.md describe the packages as they are.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VpUmBb4xtmnmtTosonqeNi
2026-08-29 18:42:57 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 09e1e6521f patch.sh was asking the payload questions its recipes had already answered
Four sections went. Three of them were checks: _sqlite3 by name, which
pkgs/3rdparty/python/build.sh already made; libsodium's symlink, which
pkgs/3rdparty/libsodium/build.sh already made; and an ABI walk of the whole
payload, which bin/build-packages.sh makes over every PKG_ROOT on the way into
every .ipk -- from the packaging loop rather than a build.sh, so cached trees
are re-read too.

The fourth was not a check. config.env's BUSYBOX_BIN was copied into the
payload here, which made it the one file no package owned: an allowance in the
"every file is owned" test, an ABI gate of its own because the payload-wide one
ran before the copy, and a file no `opkg remove` could take away. It is
pkgs/3rdparty/busybox now, PKG_WHEN-gated on BUSYBOX_BIN, and it needed a third
source verb -- pkg_prebuilt, beside pkg_unpack and pkg_intree -- because a
recipe names its source exactly once and this one's source is a path someone
typed. Once it was a package, nothing in the payload came from outside the
feed, which is what made the payload-wide ABI gate redundant rather than merely
duplicated.

The "at least twelve extension modules in site-packages" count was the one
check with nowhere to go and no duplicate. It is pkg_pynative in the seven
python-* recipes that ship a .so: an aggregate could say that a native package
had fallen back to a pure-python wheel but never which one, and each of those
recipes can say it about itself.

What that costs is written into the section it left: a gate inside build.sh
does not run on a cache hit, and no python recipe sets PKG_STAMP_EXTRA. That is
pkg_stamp's problem, not an argument for a second copy of every check here.

Measured: `make packages` builds anvil-busybox_1.31.1-1_mipsel_xburst2.ipk,
`make build` assembles a 32-package payload whose bin/busybox is listed in
anvil-busybox.list, and qa-static is 265 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011gKRUCqj18TVF1Vz1oMpmC
2026-08-29 15:24:20 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 51f524b5cf Merge worktree-comment-cleanup
The branch tightens comments across the tree -- removing the ones that
explained the code's past rather than the code. It branched before both the
pkg/ -> pkgs/ rename and the migration of bin/patch.sh onto the feed, so most
of its conflicts were with text describing code that has since moved or gone.

Resolved by taking its brevity and correcting what it could not have known:

  bin/patch.sh   ours, entirely. Its only non-comment change there deletes a
                 trailing comment from a line this branch rewrote, and every
                 comment it edits annotates one of the eight staging sections
                 that no longer exist.
  bin/fetch-assets.sh  ours. Theirs reinstates a KLIPPER_FORK condition; that
                 knob was deleted with the move to pkgs/ -- a recipe names its
                 source exactly once.
  pkgs/lib.sh    theirs, except the two hunks where our side carries code it
                 never saw (pkg_payload_hash and pkg_dir; the PKG_CC_SHARED
                 branch of pkg_build). Its prose, our functions.
  bin/common.sh  theirs throughout, with pkg/ -> pkgs/ and $MOD_PAYLOAD ->
                 $PAYLOAD_DIR, and minus its claim that the toolchain is used
                 "by bin/patch.sh for Klipper's c_helper.so" -- patch.sh
                 compiles nothing now.
  pkg/anvil-core/build.sh  deleted. The file is pkgs/anvil-core/build.sh and
                 was rewritten as a loop; its comments are superseded.

Two of its deletions were right about things this branch had wrong:

  bzip2 named a pkg_make verb that was retired into pkg_build.

  zlib claimed a ./configure line of its own as "the one sanctioned
  exception". It has not had one since PKG_CONFIGURE_AUTO=0 and CHOST; the
  rule in qa/static/test_ipk.py is absolute, and its docstring says so. I had
  kept that paragraph in an earlier pass of this merge and was wrong to.

Three stale hints in the Makefile went with them: work/pkg is filled by
`make packages`, not by bin/patch.sh, in the two gate targets and one error
message.

Verified: the payload rebuilds to the same 31 packages and 154 ELF objects,
and the six files whose bytes changed are five payload scripts this branch
comment-edited (zero non-comment lines between them) plus the opkg status
file, which records their sizes. qa-static 259; make test 9 gates, 0 failed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 11:55:49 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 f67e7a269f ls pkg/ was thirty-eight entries and four of them mattered
pkg/ -> pkgs/, and the thirty-four recipes that build a pinned tarball and
carry no files of this repo move down a level into pkgs/3rdparty/. What is
left at the top is the four components a person actually edits: anvil-core,
klipper, moonraker, helixscreen.

THE LINE IS MECHANICAL, NOT EDITORIAL, and that is why it can be a directory
rather than a field in pkg.conf. "Do we actively modify it" drifts -- the day
somebody patches zlib nobody would agree which side it belongs on, and the
answer would be a rename touching CI and docs decided by taste. "Does it carry
files of ours" is a fact about the tree: it is exactly whether the recipe has
a payload/, prog/ or seed/, checkable in one assertion, and
test_recipe_layout.py now demands the move in both directions -- a 3rdparty
recipe that grows one of the three must come up, and a top-level recipe with
none of them must go down, or the top level slowly refills and the split stops
meaning anything.

ONE FUNCTION KNOWS THE TWO DEPTHS. pkgs/lib.sh's pkg_dir resolves a recipe id
to its directory and nothing else spells either path -- not a recipe, not
bin/build-packages.sh, not a test. Names stay unique across both levels
because pkg_out, pkg_stamp and the .ipk filename are all keyed by the bare
name; a duplicate would resolve to whichever level is searched first and
build the wrong thing silently, so that is a test too.

Recipes now source `. ./bin/common.sh` instead of counting `..` from $0.
Depth-counting would have needed two spellings for two levels, and it was
never buying anything: `. pkg/lib.sh` on the next line already required the
working directory to be the repo root.

A SECOND SILENT-SHRINK BUG, and this one was live. qa/static's _files()
expanded a list of globs and returned whatever matched, so "pkgs/*/build.sh"
quietly stopped covering thirty-four recipes the moment they moved -- the lane
stayed green, and the only visible sign was a test count dropping by 34 in a
run nobody was counting. _files() now refuses a pattern that matches nothing.
That is the same failure the ash-compatibility check had a guard for and the
syntax check did not, which is why the guard reads the way it does.

AND ONE PACKAGE WAS SHIPPING PYTHON BYTECODE. anvil-core stages a directory of
.py helpers wholesale, so any test that imported one left a __pycache__ beside
it -- gitignored, invisible in a diff, copied by cp -a like anything else. The
package's contents depended on whether pytest had been run on the machine that
built it: two builds of one commit, different sha256, cause nowhere in the
tree. What it shipped was worthless as well as unstable, compiled by the build
image's python 3.11 for a printer running the 3.13 this repo cross-builds. The
sweep is in pkg_ship now, once, and the two hand-written copies in
pkgs/klipper and pkgs/moonraker are deleted -- the trap is set for any recipe
that stages a tree of .py files, and the two recipes that had hit it had each
solved it locally while the third, which had not, was the one shipping.

Measured:
  * the rename changes no package bytes. Same cached build trees, feed built
    with the diff stashed and unstashed: all 41 .ipk match.
  * with the bytecode sweep, anvil-core returns to exactly the bytes the
    previous commit produced.
  * no package in the feed contains a .pyc or a __pycache__.
  * make qa-static: 247 passed (236 before this work + 11 layout tests).
  * make test: 8 passed, 0 failed, including unpack/patch/pack/verify on the
    synthetic stock package. make test-py: 181 passed.

Not explained: anvil-python-dev produced two different sha256 from an
UNCHANGED work/pkg/python at two points today, under code that was identical
both times -- so it is not reproducible from a cached tree, and it is not this
commit's doing (the stashed and unstashed builds above agree on it). Worth a
look; the dev split in bin/build-packages.sh is where to start.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 20:04:20 +03:00