Commit Graph
19 Commits
Author SHA1 Message Date
Oleksandr ShyshatskyiandClaude Sonnet 5 3462bc5f01 Warn against power-off on the start.img splash
A first-boot migration can run a couple of minutes with nothing else
on screen; the DO NOT POWER OFF note is the same warning the later
ffscreen.py frames already carry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FSRJrZwsmud2y346iyCyaB
2026-08-30 15:33:23 +03:00
Oleksandr ShyshatskyiandClaude Sonnet 5 1ceaa09b46 Ship our own start.img instead of FlashForge's
Rendered once with ffscreen.py (the same tool that draws every later
boot frame) at the panel's native 480x800@32 raw framebuffer geometry,
then committed like runFirmwareExe.sh -- ours now, not stock.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FSRJrZwsmud2y346iyCyaB
2026-08-30 15:29:53 +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 83a4600df8 Stop naming things that are not there any more
A sweep for comments and messages that describe machinery this tree no longer
has. Each one read as current and was not.

  * bin/verify.sh, retired two commits before this branch, was still named as
    the thing `make release` and docs/building.md were explaining the absence
    of. The absence needs no explaining -- `make qa-replica` is the gate.
  * `make test-recovery` does not exist. docs/hardware-testing.md told owners
    it "used to" prove the stock rollback; the useful half of that -- nothing
    tests the rollback today, treat it as unverified -- is kept and the dead
    target name is not. runFirmwareExe.sh pointed at it too.
  * run-append.sh in bin/build-payload.py, case-install.sh in
    docs/printer-replica.md: both files are gone.
  * `make test`/`make test-py` over test/, and the note that both suites run
    in CI "until a case-*.sh has a green replacement" -- test/ was deleted and
    there are no case scripts left to replace.
  * docs/building.md still said a package carries the software component.

CI's shellcheck globs are the one real defect this turned up. Two of the four
-- pkgs/*/payload/init.d/S* and pkgs/*/prog/*.sh -- match nothing: the recipes
stopped using both paths, and payload/prog/ is where those scripts live now.
shellcheck given a pattern that expands to nothing writes usage to stderr and
exits 1, so that step has been failing for its globs rather than for the code.
Fixed to the paths that exist, with a pointer to
qa/static/test_shell_syntax.py, which addresses the same files and refuses a
glob that matches nothing -- the guard this step did not have.

Verified: the corrected invocation runs clean over the 21 files it now
matches, and 112 static tests pass.

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 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 2a6331a831 We own runFirmwareExe.sh; the install is no longer spliced into run.sh
app_startup.sh runs whatever it finds under that name in the package it just
decrypted. That is the entire contract -- plus the exit code, where 0 means
"flashed, power-cycle me" and non-zero means "carry on booting" -- so owning
the name is all it takes to own the install. We were not using it. We shipped
FlashForge's copy verbatim and reached the printer through the one exec hook
their file offers: the software component's run.sh, into which bin/patch.sh
spliced two marked blocks with 40 lines of regex.

installer/runFirmwareExe.sh is that file now. run-pre.sh and run-append.sh are
folded into it unchanged in substance -- the manifest delete, the HelixScreen
settings rescue, the s6 scandir sweep, the config merge, the password dance are
all the same code -- and the two orderings that mattered are kept explicitly
rather than by where a regex put them: the backups run BEFORE any component is
installed, because the software component ships /usr/prog's shadow and passwd
and its run.sh copies them over the live ones.

The four component installers become one function. Their semantics are kept
exactly, including that a component failing md5sum.list is discarded rather
than half-installed, and that every previous version directory is wiped before
the new one lands. What is NOT kept is the third of that file which never ran:
create_key(), defined and never called, and built its paths without a slash so
it would have written /usr/progprivate.pem; SSID_NAME/SSID_PASS, assigned and
never read; the commented-out kernel version gate; the screwflag factory bypass
of the model check, which we ship no screwflag for.

bin/pack.sh stages it, rewriting MACHINE=, PID= and MOD_PW_AUTO= and then
GREPPING for all three -- a package whose gate still said Creator5Pro because a
sed missed would install on the wrong machine. MOD_PW_AUTO is 0 in the
checkout, which is the safe default and not an arbitrary one: test_upgrade.py
runs the unsubstituted file, and 1 would have it reroll the replica's root
password and drop a password file on /mnt every run.

The software component still ships, unchanged, and its run.sh is left exactly
as FlashForge wrote it. Dropping it is the next commit; this one moves the
vehicle without changing what reaches /usr/prog.

Gates: qa/static/test_shell_syntax.py already covered installer/*.sh under both
bash -n and the printer's ash plus shellcheck, so the new file was checked the
day it landed -- 91 pass. test_upgrade.py runs the real installer over two
synthetic payloads and is repointed at it.

Two doc claims corrected while their sections were open, both false before this
change rather than because of it: how-it-works.md said app_startup.sh's
firmwareExe recovery "still works" -- it cannot, because it copies out of the
version directory and ours has had firmwareExe deleted from it -- and it
described a Moonraker preflight-and-rollback that went with the /usr/prog copy;
moonraker-preflight.py exists nowhere in the tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CH7Yb4wuPhnppHAB25UX7e
2026-08-30 15:34:19 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 c92b3c9e86 anvil.conf is gone; a component runs because it is installed
The file held three unrelated things behind one name: gates deciding which
services boot, switches for the splash / the startup wait / the first-boot
import, and tunables for the camera and the nice values. Every one of them
defaulted to on, and what each offered was a half-installed printer -- a state
nobody chose on purpose and everything downstream carried a second answer for.

firmwareExe no longer expands the ok-all bundle and filters it, it asks for the
bundle: BOOT_SET=ok-all. A printer that should not run a component does not
have it -- `opkg remove`, or a stock FlashForge package.

The settings worth keeping are each stated once now, in the service that uses
them: the camera's resolution in its run script, nice 10 there and nice 5 in
moonraker's (the values every install was getting from the template, not the
0 the inline fallback said). ff-startup's `up` stops passing --timeout and
--klipper-tries at all, because 240 and 6 are already ff-startup.py's own
defaults and two statements of a default can disagree.

Nothing sources the file, so nothing reads it, so it must not survive an
upgrade looking editable. run-append.sh stops preserving it and removes it
unconditionally beside init.d and anvil-service.sh -- unconditionally because
the pre-manifest sweep removes seven DIRECTORIES and would leave this file
sitting at the top of $MODDIR. The two upgrade tests that asserted the edit
survived now assert the file does not.

seed/ leaves anvil-core entirely, which takes with it the recipe's one
exception to "every sibling directory is shipped", the seed mount in the
replica plumbing, and its exemption in the .ipk allowlist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015E7armHFDcnZFZwdAbqkbt
2026-08-30 01:09:02 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 a534b4a744 klipperDaemon is a link too; its nice value moves to anvil.conf
The third file in $MODDIR/prog was the only one anvil-link-prog.sh did not
own. It could not be a link because its CONTENT was machine-specific: the
installer seded FlashForge's KLIPPER_NICENESS into it on every flash, off the
file it was replacing. That is the whole reason it was hand-copied, and the
hand-copy is what silently broke -- its guard named a path the file had
stopped shipping at, so no printer got the shim at all.

We do not carry FlashForge's number forward. NICE_KLIPPER is ours now, in
anvil.conf beside NICE_MOONRAKER and NICE_CAM, templated from config.env by
patch.sh section 10 like the other two, and read by etc/s6-rc/source/klipper/run
the way camera/run and moonraker/run read theirs. Default 0 -- normal priority,
which is what stock shipped and almost certainly what you want: klipper_pri.sh
already puts klippy's main thread on SCHED_FIFO 50 and its serial reader thread
calls os.nice(-20) itself, so this knob only matters when the chrt did not take.

With nothing machine-specific left in it, klipperDaemon is one more link_one.
Whether the stock software carries a klipperDaemon does not matter: link_one
replaces whatever it finds, and it runs after the payload has landed. The 14
lines of sed/mv/chmod in run-append.sh are gone, and an `opkg upgrade
anvil-core` now changes what the printer runs for all three files instead of
two.

Gates: the replica symlink test covers all three paths, and a new one RUNS
`klipperDaemon start` -- it must exit 0, say klipper is supervised, and leave no
second klippy behind. The file being in place said nothing about what it does,
which is how the old breakage hid.

Verified: the nice branch over an explicit 0, a real value, and no anvil.conf at
all; and the anvil.conf substitution with NICE_KLIPPER set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01La5MaDKqTW675k5fsXsP6S
2026-08-30 00:58:16 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 9aece5b890 klipperDaemon was never replaced on any printer
installer/run-append.sh guards the klipperDaemon replacement with

    if [ -f $MODDIR/bin/klipperDaemon ] && [ -d /usr/prog/klipper ]; then

and the file ships at $MODDIR/prog/klipperDaemon. It moved there in 057a3a1
("The boot was seven scripts in filename order. It is one compiled graph now")
and this guard did not, so the test has been false on every printer since: the
whole block was skipped in silence, and every release kept FlashForge's own
klipperDaemon.

What that means on a machine: stock `klipperDaemon start` forks a second,
UNSUPERVISED klippy beside the s6-supervised one -- two processes fighting for
/dev/ttyS4 and /tmp/uds. Our shim exists to make that `start` a no-op, and it
has not been installed. The UI's restart-firmware path and a habit over ssh both
call it.

Nothing could have caught it. sh -n parses it, shellcheck likes it, and both
lanes stay green because the branch never runs. The niceness grep in
etc/s6-rc/source/klipper/run kept working by accident: it reads
/usr/prog/klipper/klipperDaemon, which is still FlashForge's, and FlashForge's
number is what it wanted.

So the fix comes with the gate it needed. qa/static/test_moddir_paths.py asserts
that every literal `[ -f $MODDIR/... ]` existence guard in installer/ and in the
payload scripts names a path some recipe actually ships -- the shell half of
test_no_undefined_names, and the same class of failure: a name that resolves
nowhere, on a path that only runs on a printer. Three exemptions, each with a
reason: anvil.conf (templated, user state), .prev-root-hash (written at
runtime), and helix-launcher.sh (inside anvil-helixscreen's upstream tarball,
shipped but not committed).

Verified by reintroducing the bug: the test fails with
"installer/run-append.sh:218  $MODDIR/bin/klipperDaemon".

Found while confirming that firmwareExe and start.sh are anvil-core's and get
symlinked by anvil-link-prog.sh -- they are, since 214a4e5. klipperDaemon is the
third file in payload/prog/ and the only one still hand-copied by the installer
rather than linked, which is why it could drift.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01La5MaDKqTW675k5fsXsP6S
2026-08-30 00:50:43 +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 53292b4d78 klippy runs from $MODDIR, so patch.sh stops staging a second copy
The klipper s6-rc service already execs klippy under $FF_PYTHON -- our own
CPython 3.13 -- but it read the tree from /usr/prog/klipper/klippy: a copy
bin/patch.sh staged into the software component out of the very anvil-klipper
package that also installed it under $MODDIR. Two copies of one tree, and the
one that ran was the one a stock flash could overwrite.

One line moves it: KLIPPY=$MODDIR/klipper/klippy/klippy.py.

That deletes sections 1 and 2 of bin/patch.sh -- the cp -a of the klippy tree,
the chelper.tar wrapped around a .so the package already contained, the rm -rf
of the stock extras/ and kinematics/, and the ff_*.py copy. The software
component now carries firmwareExe, start.sh and run.sh and nothing else of
ours, which is the phase 7 end state in docs/notes/80-s6-migration.md.

It also lets pkgs/klipper declare the dependencies it has had since the
interpreter moved. cffi and greenlet reached the printer only through a
hardcoded list in patch.sh, so every build had them and no `opkg install
anvil-klipper` did; pyserial and jinja2 came only as anvil-moonraker's
Depends, so a BUILD_MOONRAKER=0 build shipped a klippy that dies the first
time it opens an MCU. Both are real Depends now and MOD_ROOTS drops to ten.

Also repointed: run-append.sh's __pycache__ sweep, which was clearing the tree
nothing imports while klippy writes bytecode into the one it does -- and those
runtime-created directories are in no .install-manifest, so the manifest
delete walks past them.

verify.sh reads the klippy tree and c_helper.so out of the payload (extracting
./klipper alone), and gains a check that the component does NOT carry a klippy
tree. New replica tests assert the service execs the payload tree under
$FF_PYTHON and that cffi/greenlet/serial/jinja2 import on it.

Comments in anvil-env.sh, verify.sh and pkgs/klipper/pkg.conf that still said
klippy runs on FlashForge's 3.8.2 are corrected; that stopped being true when
FF_PYTHON moved.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01La5MaDKqTW675k5fsXsP6S
2026-08-29 23:59:58 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 abd4a01d73 Adopt the s6-rc boot graph -- DOES NOT BOOT YET, see below
Merge of qa-suite-bridgehead into the packaging line. The seven init.d/S*
scripts and the hand-written etc/s6 servicedirs are gone; anvil-core ships
etc/s6-rc/source/ and bin/patch.sh compiles it into etc/s6-rc/compiled/ after
the payload is installed. klipperDaemon joins firmwareExe and start.sh in
payload/prog/, all three linked into place by anvil-link-prog.sh.

WHAT IS KNOWN BROKEN. qa/replica/test_s6rc.py fails: the tree never comes up,
because s6-rc-init dies with

    unable to supervise service directories in /run/s6-rc/servicedirs:
    Value too large for defined data type

That is EOVERFLOW out of the readdir() loop in s6rc_servicedir_manage.c, which
checks errno after every entry. Measured, and these are ruled out:

  * not the live directory's filesystem -- fails identically with -l on
    /usr/data as with the default /run, and the inodes there are 3 and
    1358496, nowhere near 32 bits
  * not the database -- s6-rc-db lists all eleven services out of it, and a
    database recompiled inside the replica by the TARGET s6-rc-compile fails
    exactly like the one compiled natively on x86
  * not a missing -D_FILE_OFFSET_BITS=64 -- both s6-svscan and s6-rc-init
    reference readdir64 and never plain readdir

What is left is the toolchain. qa-suite-bridgehead built s6, s6-rc, execline
and skalibs with the Bootlin musl toolchain, where readdir has no EOVERFLOW
path at all; the packaging line builds them as recipes against the Ingenic
glibc, where it does. s6-svscan looked healthy only because it was started on
an EMPTY scandir, which never reaches the loop.

Everything else is green: 262 static tests, and bin/verify.sh passes on the
73MB package, including the graph's own "compiled s6-rc database" check.

Resolution notes: the five MOD_* switches stay deleted -- firmwareExe defaults
every gate to 1, so nothing changes -- but MOD_CAM is now worth having by hand
on a cameraless printer, because the camera is a longrun that waits out
timeout-up. anvil-service.sh went with init.d; nothing sourced it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VpUmBb4xtmnmtTosonqeNi
2026-08-29 19:27:07 +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 26e3388895 The printer picks its own chamber config
anvil-klipper-config carries printer.base.cfg and BOTH chamber configs, under
config/chamber/<Machine>.cfg, and anvil-link-prog.sh symlinks the right one
into /usr/data/config. The model comes from FlashForge's own app_startup.sh,
which carries MACHINE= at its top -- so the machine answers the question about
itself instead of a build being told the answer and having to be right.

THAT REMOVES A PACKAGE PAIR THAT COULD NOT COEXIST. anvil-klipper-creator5-config
and -creator5pro-config each owned config/printer.chamber.cfg, so they
Conflicted, opkg refused the pair with exit 255, and bin/patch.sh had to pick
one from TARGET_MACHINE. The payload was model-specific for the sake of one
file; it is not any more. TARGET_MACHINE still names the output, because
runFirmwareExe.sh gates on it.

THE SOFTWARE COMPONENT'S klipper/config IS NOW BYTE-IDENTICAL TO STOCK --
diffed, not assumed. printer.base.cfg was the last thing patch.sh wrote there.

The seven configs we do NOT modify stay stock and unpackaged, and that works
because Klipper resolves [include] against the directory of the file doing the
including, by the path as opened rather than the resolved target
(configfile.py: dirname = os.path.dirname(source_filename)). So a symlinked
printer.base.cfg still finds printer.filament.cfg and friends beside it in
/usr/data/config.

run-append.sh no longer copies the files the linker links -- copying would only
put a real file in the way of the symlink about to replace it. moonraker.conf
and moonraker-custom.conf keep their branches: those are user-editable, and a
symlink would lose the .mod-new preservation a tuned trusted_clients block
depends on.

anvil-klipper now Depends on anvil-klipper-config, which is where that
dependency always belonged -- it used to arrive only because the per-model
packages pulled it in, and naming it in MOD_ROOTS would answer a missing
dependency in the one place a printer's own `opkg install` never reads.

prog/ is gone as a recipe subtree. Its own test said an empty one was the good
outcome; it emptied, so the rule went with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VpUmBb4xtmnmtTosonqeNi
2026-08-29 18:24:33 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 722804364a The boot path follows the package, not the last .tgz
firmwareExe and start.sh have to sit on /usr/prog because FlashForge's own
scripts look for them by absolute path. They are package files now
(anvil-core, under payload/prog/) and anvil-link-prog.sh replaces the copies
on /usr/prog with symlinks into $MODDIR, so what the printer executes is what
the package owns.

WHAT THAT BUYS: `opkg upgrade anvil-core` on a running printer used to update
$MODDIR and change nothing the boot path reads -- the new firmwareExe sat in
the payload while the old copy kept being run, and only a full .tgz could land
it. The postinst calls the same script, so an upgrade is now enough.

THE LINKS ARE MADE AFTER EXTRACTION, and that is the whole ordering. Stock
run.sh distributes the component at its lines 107-179 and run-append.sh does
not extract anvil.tar.xz until line 180, so the component cannot carry the
links itself: on a first install they would dangle, and on an upgrade they
would resolve to the payload being replaced and silently install the previous
release's files.

The component still carries real copies. That is what keeps a printer
bootable when the payload is not there, and patch.sh now takes them from the
installed payload rather than from a directory beside the recipe -- the same
move it already makes for the klippy fork.

A postinst needed the recipe system to have maintainer scripts at all, so
build-packages.sh copies pkgs/<name>/control/* into CONTROL/. Note what that
exposes: patch.sh installs with a host opkg under --offline-root
--force-postinstall, and that combination DOES run maintainer scripts, with
IPKG_INSTROOT empty, so their absolute paths are taken as written. Inside the
build image that is harmless -- there is no /usr/prog there -- and the
script's guard is what makes it a quiet no-op rather than an error.

pkg_payload_hash covers control/ too, or an edited postinst would read as
"already current". It takes two find calls because `find a b` with b missing
exits non-zero, which under the recipes' set -euo pipefail killed every build
whose recipe has no control/ -- silently, the existing 2>/dev/null swallowing
the one line that said so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VpUmBb4xtmnmtTosonqeNi
2026-08-29 17:45:20 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 5b463b889a Five switches that were only ever set to 1
MOD_WEB, MOD_CAM, MOD_UI, MOD_SSH and MOD_WIFI all defaulted to on, and what
each one really offered was a half-installed printer: Mainsail with no
moonraker behind it, a camera that would not stream, a screen dark on purpose,
a root password deliberately not set on a machine whose entire recovery story
is "ssh in". Nobody wants those states. What someone who sets one actually
wants is a stock printer, and that is one FlashForge package away.

What they cost was a second answer to every question. Each init script carried
a gate, each `down` file argued its existence from a flag rather than from the
ordering that was the real reason, and anvil.conf had five lines that told a
user they had a choice.

MOD_SSH was the clearest case: it never turned ssh off. dropbear is the
rootfs's own and listens either way, so all the flag did was decline to set a
password.

Each gate is replaced by the fact it was standing in front of, not by a
constant:

  S60nginx, S62moonraker  nothing -- the web stack is what this mod is
  S65camera               there is no video device, which the script already
                          finds out and reports
  S50wifi                 there is no $WIFI_CONF, likewise
  S80ui                   HelixScreen is not installed, which was already the
                          other half of the condition

To leave a piece out, leave it out: BUILD_MAINSAIL, BUILD_MOONRAKER and
BUILD_HELIX decide what goes into a build, and `opkg remove` decides what stays
on a printer. What survives in anvil.conf is what has a range rather than a
state -- MOD_CAM_RES/FPS/PORT and the NICE_* priorities -- plus MOD_SPLASH,
MOD_STARTUP, MOD_IMPORT and MOD_S6, which are untouched here.

The `down` files stay. They were argued from MOD_WEB and MOD_CAM, but the
argument never depended on them: s6-svscan starts a service the instant the
directory appears, which is before any init script has checked a prerequisite.
Their text now says so.

Measured: `make build` produces a payload with no live read of any of the five,
and qa-static is 265 passed with shellcheck over the edited init scripts. The
test suite still exercises the removed flags and has not been updated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011gKRUCqj18TVF1Vz1oMpmC
2026-08-29 15:24:36 +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 dff6fe3198 anvil-core was holding four other components' configuration
It held all of it for one reason: it was the first recipe written, so
everything of ours started there. With the files visible per recipe the
misfilings read straight off the tree.

    moonraker.conf              -> pkgs/moonraker, with the server it configures
    ff-*.cfg, printer.base.cfg  -> pkgs/klipper-config (new)
    printer.chamber.cfg         -> pkgs/klipper-creator5-config and
                                   pkgs/klipper-creator5pro-config (new)

PRINTER.BASE.CFG WAS IN pkgs/klipper AND THAT WAS WRONG TWICE. It is Klipper
CONFIG rather than any part of the Klipper program -- it is the file carrying
[include ff-toolchange.cfg] and [include printer.chamber.cfg], so it is the
entry point to everything in the config package -- and pkgs/klipper is gated
on BUILD_KLIPPER while this file is installed under BUILD_TOOLCHANGE. A
BUILD_KLIPPER=stock build has no pkgs/klipper recipe at all and was still
reaching into its prog/ directory for a file.

THE ff-*.cfg ARE NOT IN pkgs/klipper EITHER, for the same gate: ask for
BUILD_KLIPPER=stock and that recipe does not exist, and the toolchanger would
have become a silent casualty of a flag about which klippy is shipped.
FlashForge's own klippy loads ff_*.py and reads these .cfg perfectly well.
They travel with the feature they configure and the feature's flag is
BUILD_TOOLCHANGE.

THE MODEL IS A PACKAGE NAME NOW, not a build flag. printer.chamber.cfg.creator5
and .creator5pro used to be picked by patch.sh from TARGET_MACHINE and copied
under their real name, which made the model a property of the BUILD: two
builds of one commit produced different bytes under one version number, and
nothing in the feed or on the printer recorded which machine a package was
for. Both are built on every build; patch.sh stages the one this tarball is
for. This is the feed's first use of Provides/Conflicts, both on a virtual
anvil-klipper-chamber-config -- Provides so anvil-klipper-config can depend on
"a chamber config" and either satisfies it, Conflicts so a printer cannot
install both and have two packages own one path with opkg letting whichever
unpacked last win. Neither field alone expresses "exactly one", and
printer.base.cfg's [include printer.chamber.cfg] is unconditional, so "none"
is a printer whose klippy does not start.

printer.chamber.cfg also CHANGED WHERE IT LANDS: $MODDIR/config, like the
ff-*.cfg, instead of riding the software component to /usr/prog and reaching
/usr/data/config only because the stock run.sh copies that whole directory.
run-append.sh overwrites it on the same mod-owned terms, and by this repo's
own rule -- files the update overwrites must say so -- it now carries the
DO NOT EDIT banner the ff-*.cfg have always had. test_config_ownership.py is
what demanded that, which is what it is for.

TWO FLAGS BECAME HONEST, and both are behaviour changes:

  * BUILD_TOOLCHANGE=0 shipped the toolchanger's .cfg anyway, because they
    rode in anvil-core while everything else the flag controls was gated.
    It no longer does.
  * BUILD_MOONRAKER=0 overwrote the printer's moonraker.conf with one written
    for a server it was not going to install -- [webcam anvil] and all. It no
    longer does, which is what docs/building.md already said the flag meant.

WHAT DELIBERATELY DID NOT MOVE. init.d/ and etc/s6/ look like they belong to
the services they start, and moving them would brick a printer: those services
are gated at RUNTIME by flags in anvil.conf, while the packages that would own
the scripts are gated at BUILD time. A BUILD_KLIPPER=stock tarball has no
anvil-klipper package, so it would carry no S70klipper and nothing would start
Klipper at all. The script that reads a flag must not sit behind a second,
different flag. nginx.conf stays for a plainer reason: nginx is the rootfs's
binary and there is no package here to give it to.

Measured:
  * 44 packages (41 + three config packages), feed builds clean.
  * anvil-moonraker carries config/moonraker.conf; anvil-klipper-config the
    seven ff-*.cfg; each model package one printer.chamber.cfg. Checked by
    listing the built .ipk, which is how the first attempt was caught staging
    moonraker.conf and never shipping it -- pkg_ship takes an explicit list.
  * make test: 8 passed, 0 failed, including unpack/patch/pack/verify.
  * bin/verify.sh gained two gates over the payload -- a chamber config and
    the ff-*.cfg -- and BOTH WERE PROVEN BY BREAKING THE BUILD: deleting the
    staging line fails patch.sh's own check, and bypassing that too fails
    verify with "mod payload has no config/printer.chamber.cfg". Which is
    also the proof that a normal build carries them.
  * make qa-static: 256 passed. make test-py: 181 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 20:24:54 +03:00
Oleksandr ShyshatskyiandClaude Opus 5 54a1e72cc4 A file that ships belongs to a package, and the path should say which
payload/ was organised by DESTINATION -- init.d/, bin/, etc/s6/,
klipper/config/ -- plus an assets/ directory of three .conf files. That was
right when there was one owner. With 38 recipes it held three different kinds
of file behind identical-looking paths:

  * files a recipe stages into its .ipk,
  * files that go to /usr/prog and CANNOT be in a package of ours, because
    every path in one lands under $MODDIR,
  * files that never ship as files at all -- run-pre.sh and run-append.sh are
    spliced into FlashForge's own run.sh at build time.

The only thing recording which package owned any of it was a thirty-line
comment at the top of pkg/anvil-core/build.sh. A comment cannot be checked.

Files now live with the recipe that owns them, and the subtree says how they
ship: payload/ is staged into the .ipk laid out exactly as it lands under
$MODDIR, prog/ is placed on /usr/prog by patch.sh, seed/ is templated or
seeded user state. installer/ sits outside pkg/ and holds the two files that
are never files on a printer. qa/static/test_recipe_layout.py fails if a
fourth kind of directory appears, which is the failure this is actually for:
a misfiled file is caught by a build, an unruled directory is caught by
nothing.

anvil-core's recipe is a loop over a directory now. Six pkg_stage lines with
hand-written destinations plus a glob became one loop, because payload/ IS the
overlay -- a file added to that tree ships without an edit to build.sh.

A REAL BUG FELL OUT. anvil-core's PKG_STAMP_EXTRA was an eight-path list
naming payload/ and assets/ subtrees by hand, and one of those paths was
payload/helixscreen/ -- HelixScreen's printer-database entry, hashed into the
cache key of the package that does NOT contain it. Editing that json rebuilt
anvil-core and left pkg/helixscreen sitting in the cache. pkg_payload_hash is
now the one way a recipe keys its own files and it can only hash its own.

WHAT ELSE MOVED, and why each is not where it looks like it should be:
  * ff-*.cfg stay with anvil-core rather than pkg/klipper. They are the
    package that has always shipped them; ownership moves in the next commit,
    with the two model-config packages. This one is a move, not a repackaging.
  * printer.base.cfg is pkg/klipper's prog/, not payload/: it is FlashForge's
    file with one block replaced and it lands beside their Klipper tree.
  * moonraker.conf is anvil-core's for now; moonraker-custom.conf is
    pkg/moonraker's seed/, because it is the user's and a package member is
    overwritten on every upgrade by definition.

The cfgdir fixture is a merged copy of both config directories rather than a
path into the checkout. The split is a fact about which package ships what --
on the printer there is no split, the stock run.sh force-copies
printer.base.cfg into /usr/data/config/ beside the ff-*.cfg, which is why its
includes are bare and relative. A test about include resolution has to model
where files END UP or it is testing the repository's filing system.

qa/lib/replica.py's one payload mount is three, and entrypoint.sh reassembles
/tmp/payload out of them byte-for-byte as it was. Not re-pointed: every
case-*.sh copies out of /tmp/payload with 2>/dev/null, so a path that stops
resolving does not fail -- it copies nothing and the case runs green against
an empty $MODDIR.

Measured:
  * all 41 .ipk byte-identical. anvil-core and helixscreen were rebuilt from
    HEAD and from this tree and compared directly; the other 39 match the
    feed built before the move.
  * that comparison caught a mistake in this commit: the HelixScreen json was
    mirrored one directory too high (helixscreen/printer_database.d/ instead
    of helixscreen/config/printer_database.d/). A move that changes no bytes
    is only a claim until it is diffed.
  * anvil-core: two cold builds byte-identical.
  * make test: 8 passed, 0 failed -- including unpack/patch/pack/verify on the
    synthetic stock package, which is the gate that covers patch.sh end to end.
  * make qa-static: 244 passed (236 + 8 new). make test-py: 181 passed.

Known gap, pre-existing and left alone: pkg/*/payload/bin/*.sh is in no ash
glob, so wifi-action.sh's dialect is unchecked. The old glob was payload/*.sh,
one level above it, so this is not a regression -- it belongs with the fix.

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