mirror of
https://github.com/Klipper4FlashForge/firmware.git
synced 2026-10-21 21:02:16 -05:00
master
73
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
80a4d64ffd |
Prepare next firmware release
Add Fluidd and custom boot scripts, migrate DC24V control, update the web UIs and filament macros, improve startup status reporting, and expand upgrade and replica coverage. |
||
|
|
5718603f1c |
Document how a spool's material reaches OrcaSlicer (#25)
OrcaSlicer 2.4.0's Moonraker agent offers to map a project's filaments onto the printer's slots, and it learns those slots from the `lane_data` namespace in Moonraker's database. Nothing in the tree said where that data comes from, so the question "what does this need to work?" had to be answered by reading three codebases: ff_toolchange's status surface, HelixScreen's AMS backend and override store, and MoonrakerPrinterAgent.cpp. The note is that answer as one diagram plus the reasoning around it. Klipper supplies the slot count and nothing else -- it cannot sense what filament is in a tool; the operator supplies the identity through HelixScreen; Moonraker's database is the only durable record; OrcaSlicer reads it once at connect and never writes back. It also records the two things that surprised us. The mapping is not applied at print time -- start_local_print() ignores ams_mapping, so it selects presets and temperatures at slice time and does not rewrite tool numbers. And the reason this does not work on our build yet is not a missing feature but a missing merge: AmsBackendToolChanger was the last AMS backend without a FilamentSlotOverrideStore, upstream gave it one on 2026-08-24, and our last catch-up from upstream is four days older than that. Claude-Session: https://claude.ai/code/session_01PU5nCo85xuFQuub4kJzWt9 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
5f237eff62 |
Version the pinned recipes on the release, not on memory (#21)
apk decides whether to upgrade from the version string alone. Three recipes
pin an upstream version -- anvil-moonraker on ${MOONRAKER_VERSION#v},
anvil-klipper on the pinned commit, anvil-helixscreen on its tag -- and each
ships a payload/ of ours beside that upstream tree. Our half changes without
the string moving, so the feed serves new bytes under the name every printer
already holds and `apk upgrade` finds nothing to do. #16 did exactly that to
moonraker.conf, the file that moves Moonraker's config to
/usr/data/anvil-data/config.
PKG_RELEASE is that string's other half, and it is the release date now:
pkg_release_stamp, monotone, with the same-day letter as a trailing digit so
20260827b sorts between 20260827 and 20260828. Nobody has to notice that a
payload changed, which is the only version of this that holds.
PKG_STAMP_EXTRA="$(pkg_payload_hash)" is not this and reads as though it were.
It hashes payload/ so the BUILD rebuilds; it says nothing to a printer. All
three recipes had it, and all three would have shipped stale.
test_recipe_layout.py makes it the rule for recipes added later: a pinned
recipe with a payload/ must take pkg_release_stamp, and the failure names the
recipe and the line to write.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
1afd5dd2d2 |
Leave FlashForge's klipperDaemon alone (#19)
It is the one file a stock flash cannot undo, and replacing it is what left
owners with a printer that would not print. A stock package restores the other
two paths the mod points at itself -- its run.sh copies its own firmwareExe and
start.sh over them, and busybox `cp -f` unlinks a symlink rather than writing
through it. It carries no klipperDaemon at all: not in the software component,
not in its md5sum.list, no line in run.sh. So the link outlived the mod, and
stock's start.sh, whose last line is
/usr/prog/klipper/klipperDaemon start
ran our shim, whose `start` does nothing on purpose. The UI came up, klippy did
not, and flashing stock again changed nothing because there was nothing in it
to change.
The shim guarded that same line: klippy is an s6-rc longrun here, and stock's
klipperDaemon forks a second, unsupervised one that fights it for /dev/ttyS4
and /tmp/uds. But no caller is left on a modded machine. Our own start.sh
replaces stock's and asks s6-rc; FlashForge's firmwareExe execs
/usr/prog/klipper/start.sh and names no other script (`strings` on their
binary); Moonraker runs with `provider: none`, so a restart from Mainsail goes
over the API rather than through a service script; ff-startup and S70klipper
drove it once and neither exists now. What remains is a person typing
`klipperDaemon start` over ssh, who gets FlashForge's behaviour and a second
klippy -- s6-rc and s6-svc are the supported way, and that trade buys back a
printer that can be returned to stock.
test_install.py asks the machine the inverted question now: klipperDaemon is
not a link into $MODDIR, it exists, and it is FlashForge's own script.
Printers that already took an earlier release still carry the link and cannot
be repaired from here -- nothing of ours runs during a stock flash. The
package at Klipper4FlashForge/stock-recovery puts their copy back.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
7c74f025ae |
Document pressure-advance calibration (#15)
* Document automatic pressure-advance calibration FF_PA_CALIBRATE/FF_PA_PROBE and HelixScreen's calibration-screen integration shipped without owner/experienced-user documentation -- only the reverse-engineering notes covered it. Adds a table row and full section to how-calibration-works.md, and links back to it from the notes page. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0147AQiPYgtafuy1x5NjqJuy * Add a simple pressure-advance calibration page Owner-facing procedure alongside XYZ tool calibration and bed mesh -- FF_PA_CALIBRATE with a temp and tool number, what the output means, and how to save it. how-calibration-works.md's mechanism section now links here instead of duplicating the command walkthrough. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0147AQiPYgtafuy1x5NjqJuy * Point pressure-advance results at the slicer, not printer.cfg PA is a filament property, not a printer one -- a value baked into printer.cfg is wrong the moment you switch material. Recommend the slicer's per-filament Pressure Advance field instead. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0147AQiPYgtafuy1x5NjqJuy * Correct pressure-advance page: there is no factory value Unlike tool offsets and bed mesh, PA calibration doesn't import a factory number on first boot -- FF_PA_CALIBRATE only measures and reports one. Say that instead of implying it's pre-populated. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0147AQiPYgtafuy1x5NjqJuy --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com> |
||
|
|
b3710c37c4 |
Merge pull request #13 from Klipper4FlashForge/worktree-pa-sweep-chute
The PA sweep never moves the carriage, so park it over the purge chute |
||
|
|
9973019ec8 |
A printer ships knowing where its updates come from
Phase 3 of docs/notes/85-packaging.md was three things: signing, a URL the
printer asks, and a tree at that URL. The migration landed the first. This is
the second, and the tooling for the third.
THE POINTER HAS TO SHIP BEFORE THE HOST EXISTS, which is why this lands with
nothing serving yet. A feed cannot tell a printer where the feed is: a machine
installed without etc/apk/repositories needs a USB stick before it can ever
fetch anything, which is the thing packages were adopted to stop. So the file
travels in the .tgz, naming a host that does not answer yet, and every printer
installed from today starts fetching the day one does.
The cost of that is bounded, and was read out of apk's source rather than
assumed: an unreachable repository fails `apk upgrade` and nothing else --
apk_db_repository_check is called from app_upgrade.c and not from app_add.c,
so `apk add ./file.apk` does not consult a repository at all.
http://reforge.8941973.xyz/apk/mipsel_xburst2/anvil.adb
bin/common.sh's FEED_URL is the base, config.env can override it for a fork,
and pkgs/anvil-core generates the file from it beside the public key it
already shipped. Three things about that URL:
* IT NAMES THE INDEX FILE, not a directory. src/repoparser.c reads a line
ending .adb as a v3 index in place, with the packages as siblings named by
the pkgname-spec mkndx wrote into it; anything else sends apk looking for
<url>/<arch>/APKINDEX.tar.gz. That is the layout this repo already builds
and the stick already uses, so the network feed is the same shape --
exercised by every payload build rather than only in production. The v3
directory form would work too, but only with
`mkndx --pkgname-spec '${arch}/${name}-${version}.apk'`.
* THE ARCH IS A DIRECTORY. Package file names carry no architecture, so two
architectures served from one directory would be two files with one name.
* PLAIN HTTP. libfetch defaults to SSL_VERIFY_PEER and this printer has no
certificate bundle -- the same gap tools/python/README.md records for
CPython. Every package is signed, and the signature rather than the
transport is what decides whether a printer installs one.
bin/publish-feed.sh writes the matching tree and optionally rsyncs it.
Packages go first and the index last, because the index names every package by
sha256 and a mirror carrying a new index with old packages hands printers a
checksum for a file that 404s. Nothing is deleted at the far end. It refuses
an unsigned feed unless ALLOW_UNSIGNED=1: a printer verifies against the key
that shipped in its own anvil-core, so an unsigned feed is one no printer
installs from. The release workflow attaches the same tree as
reforge-apk-feed.tar.gz, so a host can be filled from any release -- including
ones tagged before the host existed -- without rebuilding.
THE REPLICA STOPPED WRITING THE FILE IT USED TO DELETE.
case-build-payload.sh wrote its own etc/apk/repositories naming /mnt and
removed it before tarring; it now passes --repositories-file with a path on
the stick, which apk reads INSTEAD of the default (database.c consults the
default file and repositories.d only when no file is named). So what ships is
whatever anvil-core installed and the build never reads it. The `apk info`
line takes the same flag: without it that one command reads the shipped file
and reaches for the network, which a build's output must not depend on.
Measured: 47 packages built from a cold cache, the payload assembled in the
replica twice, qa/static at 319 passed. Two of those are new -- the default
URL in common.sh (read from the file, not resolved, so a fork setting
FEED_URL in config.env still passes) and the built payload carrying a
package-owned entry that ends /mipsel_xburst2/anvil.adb.
What is left is operational: a host answering at $FEED_URL, and who runs
publish-feed.sh -- a release step with a deploy secret, or a person with the
tarball.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017rgrDeu9YAUuMM2G7gKTpL
|
||
|
|
e5d6e5025a |
Merge pull request #12 from Klipper4FlashForge/relax-max-residual
Relax max_residual to 0.5 mm |
||
|
|
8cd284d3ee |
A driver latch found already on is debris, not state to preserve
_guarded snapshots the pins on entry and _restore puts back what it found, which is right for every value except one. If a run ever ends with the latch still on, the next run snapshots 1.00, faithfully restores 1.00, and the drivers stay dead -- one aborted run turning into a printer that relatches itself on every subsequent run. Release the latch instead of reinstating it; only a non-latched value is worth restoring truthfully. Also correct an overstatement in the previous commit. A klippy or MCU shutdown does NOT leave the drivers latched: [output_pin] hands shutdown_value, which defaults to 0., to setup_start_value, so the MCU drives PH2/PH3 low by itself. The guard earns its keep on the ordinary failures -- a command_error, a cancel, an exception out of the sweep -- which unwind through _restore with the machine still running. The latched and released values are named rather than spelled 1.00 and 0.00 at four call sites, since the comparison in the snapshot now depends on them agreeing. |
||
|
|
2b92a76f2f |
The PA sweep never moves the carriage, so park it over the chute first
SET_PIN PIN=enable_pin_tmc_x VALUE=1.00 disables the X driver outright. It is not Klipper's stepper enable -- that is !PJ6 and !PJ2 -- but a second latch on PH2/PH3 that Klipper knows nothing about. So for the whole sweep every G1 X in the ladder is planned, step-generated and clocked out against dead drivers, and the gram of filament falls straight down from wherever the toolhead was standing. clearNozzleEddy settles it. Just before its jal to paTestMgr it builds the string resetTmcPa replays: X = 275 + tool_dx, Y = 254 + tool_dy, Z = probeZ + 2.8, from the constants at 0xdb6b2c/0xdb6b28/0xdb6b24 -- the purge chute, the same pair ff-filament.cfg carries as purge_x and purge_y, with a -8.0 that is literally our tool_dy[3]. The app parks over the chute, kills X and Y, dumps into the chute, and then tells Klipper the carriage is still at the chute, which is only sane because it never left. We inherited the latch and the height without the park, so the ladder piled onto the plate wherever the nozzle happened to be. Park in _FF_FILAMENT_PREP's own three moves before the latch goes on, replay SET_KINEMATIC_POSITION after, and shout when the pins will not unlatch rather than leaving a printer that cannot move. The replay lands after RESTORE_GCODE_STATE, not before: set_position fires toolhead:set_position and gcode_move recomputes last_position from base_position/homing_position, so it has to see the restored offsets. |
||
|
|
7b4ce414b5 |
max_residual guards against a mis-trigger, not against probe scatter
The 0.05 mm default was derived from what the guard can resolve rather than from what the machine actually measures: with four symmetric points one bad point of error e shows up as only ~e/4 in the residual while moving the fitted centre by e/2, so 0.05 trips at centre errors of about 0.1 mm. That is inside the ESTOP's own repeatability. A healthy run on a Creator 5 Pro fits with a worst residual of 0.23, which the guard rejected -- and rejecting means aborting without saving, so a good calibration was thrown away. Default is now 0.5, which trips at centre errors of about 1 mm. A probe that missed the bore edge or fired on the way in still lands far outside that; ordinary scatter of a couple of tenths passes. 0 still disables, and the option is still settable per printer. The other guards are unchanged: min_radius/max_radius are off by default, gap_min/gap_max is a 1.5..5 mm window around a nominal 3.2 mm rise, and plate_z_tolerance is one-sided at 0.8 mm. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013DYQe3sx9XDhz59PDHCDNU |
||
|
|
b55461cd6b |
Cancelling an object leaves the SD reader with nothing to pace it
EXCLUDE_OBJECT installs the ExcludeObject move transform, and inside a cancelled object _ignore_move updates Python state without ever calling next_transform.move(). Nothing reaches the toolhead, so the lookahead buffer that normally blocks gcode.run_script never fills, and virtual_sdcard.work_handler runs on its own yields alone -- one reactor.pause per 8192-byte read, about 330 lines apart. The eboard's soft-PWM refresh misses its deadline and the MCU shuts down with "Timer too close". Measured on a Creator 5 Pro: 37 s from "Excluding object" to the shutdown, with 1 Hz status queries answered in 1.30-1.78 s throughout while the main MCU sat at mcu_awake=0.000 and gcodein=0. EXCLUDE_OBJECT_DEFINE is not the cost. The line costs 115 ms through the live klippy and nothing thereafter; three prints of the same file without a cancel complete with bytes_retransmit=0. Removing it removes the object there is to cancel, not the work. tools/gcode-bench holds the harnesses, including the shlex fast path that takes that DEFINE from 91.6 ms to 5.9 ms on the printer -- worth having for large plates, but not this bug. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AVUMfbNuKUWWV32PBcydwb |
||
|
|
734a04db45 |
Make the apk feed buildable by CI, and signable by a release
PR #10 was green on this machine and red on both CI jobs. Three separate faults, none of which a local signed build could show. apk-tools is cloned from gitlab.alpinelinux.org, which answers a GitHub runner with HTTP 418 or with nothing until the connection times out. It clones from the GitHub mirror now. Which host serves it cannot change what is fetched: the pin is a commit sha, and git names a commit by the hash of its content, so a mirror answering with this sha is serving this tree or is not answering. get_git also sets the remote url on every run, so a checkout made before this commit does not keep fetching from the host that refuses. anvil-core's cache key hashed the feed's public key as sha256sum "${APK_SIGN_KEY:-/dev/null}.pub" 2>/dev/null | cut ... With no key that file is absent, sha256sum is non-zero, pipefail makes the pipeline non-zero, and build-packages.sh -- which sources every pkg.conf under `set -euo pipefail` -- died before emitting a single package. The 2>/dev/null ate the reason, so `make packages` exited 1 having said only that it had sealed a cache entry. Every signed build passed. Every CI build is unsigned. It is now pkg_signkey_hash, which answers `unsigned` and succeeds. The build container mounts the checkout and nothing else, so a key at the documented ~/.anvil/anvil.rsa was visible in the developer's shell and invisible to apk. The Makefile reads APK_SIGN_KEY out of $(CONFIG_ENV) and bind-mounts its directory read-only. This survived review because the key used while writing the migration sat under work/, inside the one directory already mounted. Gates for all three, each verified against its own bug by reverting the fix under it. The recipe-reads-unsigned test runs pkg_conf under the same flags build-packages.sh uses -- the existing _conf helper does not, and would have stayed green throughout. Its companion asserts the recipe SET does not change with the key, because pkg_recipes evaluates PKG_WHEN in a subshell and a recipe that fails to source is dropped rather than reported: broken only when unsigned, a package would leave the feed with nothing turning red. Both are scoped to what pkg_recipes lists, so busybox -- gated off whenever there is no stock firmware to take one from -- is not asked a question it cannot answer. release.yml takes the private key PEM from secrets.APK_SIGN_KEY, writes it to $RUNNER_TEMP outside the checkout where no later step can archive it into a release, and derives the public half rather than storing a second secret that could drift out of step with the first. Unset, the release builds unsigned with a warning: a fork has no such secret, and firmware nobody else can build is worse than a feed untrusted on a stick the printer was handed by hand. Verified from a cold work/host and work/packages, three ways: no key (the CI case), a key inside the tree, and a key outside it at the documented location. Each gives 47 packages plus the index, a payload, 286 static and 135 replica tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013rbKEyLXrzSE6wm3iNApVp |
||
|
|
f8b925e48f |
Replace opkg with apk, and sign the feed
docs/notes/85-packaging.md chose opkg in 2026-08 on one argument: every package
is cross-built here anyway, so the winner is whichever format is cheapest to
PRODUCE -- and .ipk had an upstream tool where .apk meant abuild, a keypair and
an Alpine host. Two lines of that comparison were wrong.
apk mkpkg is an applet of apk itself: -I key:value metadata, -F directory, no
keypair, no abuild, no Alpine host. The "making a package" row described
apk-tools 2. And the on-device client was counted a wash when it is better than
one -- openssl and zlib were already cross-built for CPython, and libarchive
existed in this tree only for opkg, so adopting apk REMOVES a cross-build.
What made it possible at all is that apk resolves its database relative to
--root and uses the same root as the file destination, while / here is a
read-only squashfs -- measured: neither /lib/apk nor /etc/apk can be created.
pkgs/3rdparty/apk-tools/prefix.patch splits the two. Upstream already carries
root_fd and dest_fd separately and merely aliases them in context.c, so the
patch defaults the database root to $MODDIR and leaves files extracting at /.
That is opkg's compile-time VARDIR plus `dest root /`, in apk's spelling.
THE FEED IS SIGNED NOW, which it never was: opkg signed the index and
bin/build-packages.sh signed nothing, so nothing was verified on a printer.
apk verifies with raw PEM public keys, not X.509 -- measured on the machine,
where every certificate directory is empty. ./bin/apk-keygen.sh makes an RSA
pair, APK_SIGN_KEY in config.env points at the private half, and the public
half both seeds the USB stick (so the first install can verify) and ships in
anvil-core (so the printer keeps trusting). An empty APK_SIGN_KEY still builds
an unsigned feed, deliberately: `make packages` has to run on a bare checkout
or the packaging lane stops being a CI gate.
WHAT IT COST is a carried patch, against an internal with one consumer, on a
branch mid v2->v3 transition, on an architecture with no upstream apk port to
share the rebasing. That is the standing risk and it does not go away.
Four bugs this shook out, all silent, all found by a gate rather than by
reading:
* The prune deleted seventeen packages it had just built. The all -> noarch
mapping went into the emitter and not into pkg_pkgfile, so the prune looked
for a name nothing wrote. `make packages` exited 0.
* Whitespace-separated Depends dropped nineteen packages. opkg's pkg_parse.c
splits that field on ',' and nothing else, so the whole list became one
dependency whose name contained spaces and resolved to nothing -- and opkg
neither warned nor failed. `make build` shipped a payload with no Tornado
in it. test_the_payload_is_dependency_closed is what catches that now.
* Ownership was recorded as `nobody`. apk resolves an owner to a NAME against
<--root>/etc/passwd and the build lane runs --user with no passwd entry, so
every file in every package would have installed as uid 65534, with no
error at build or install time. build-packages.sh writes a two-line
synthetic passwd naming the build user root and points --root at it.
* Four recipes carried versions apk cannot parse -- two spelled a commit
+git<sha> where the grammar's hash slot is ~, x264 had a snapshot name
whose -2245 reads as a malformed release, and helixscreen had
0.99.115.creator5.3 where apk allows one trailing letter and then only a
suffix from a closed set of nine words. All four were valid opkg versions,
which is why nothing had noticed. pkg_version_ok gates every recipe now.
A v3 .apk begins with ADBd magic and is not a tar stream, so there is no apk
equivalent of the `ar -p | gunzip | tar` that hand-unpacked anvil-opkg: you
cannot open a package without a package manager. bin/build-payload.py puts the
cross-built binary on the stick at /mnt/apk instead. It is the same build
anvil-apk-tools contains, that package installs normally, and the bootstrap
copy never lands.
Two gates retired rather than moved. apk's installed database is plain text
(F: sets a directory, R: names a file in it), so the payload-ownership check
reads one file where it used to glob info/*.list. And it carries no clock at
all -- apk records no install time and mkpkg sets no build-time -- so the
Installed-Time normalisation case-build-payload.sh did on every build is gone,
and the test asserts the ABSENCE of a t: line instead. Clock-free by
construction rather than by repair.
Also removed: PKG_SECTION, which 45 recipes declared and nothing read once
apk's schema had no field for it, and the `make apk-host` target -- the
packager is built on demand by bin/build-packages.sh and stamped on the pin,
the way pkg_toolchain and pkg_buildpython already work.
Verified from a cold work/host and work/packages: 47 packages built and signed,
`make build` installs 35 and verifies them against the shipped public key,
`make qa-static` 195 passed, `make qa-replica` 135 passed -- the printer
installing off a simulated USB stick and booting. The installed set was checked
against the declared closure of the eleven roots: 35 expected, 35 installed,
nothing extra and nothing missing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rbKEyLXrzSE6wm3iNApVp
|
||
|
|
5ccfc85558 |
Make the import gate assert the module it exists for, and prove it bites
test_every_named_extra_imports skips a section whose module is absent -- deliberately, because that is what klippy's load_object does. But it means the test could go green on a machine where the klippy tree had lost stepper_resonance_tester, having never asked the question it was written for. So it now names that module and asserts it actually imported. Checked in both directions before being trusted. On the baked replica, with the real shipped tree under qemu: the module imports with numpy present, and with site-packages/numpy moved aside it raises ModuleNotFoundError at line 1 -- an ImportError, which is the one load_object does not catch. That is the original diagnosis reproduced on a machine rather than argued from source. All three lanes green: qa-static 148, qa-replica 135, mkdocs --strict. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJr2x7aDaY889qwSxbdLt8 |
||
|
|
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 |
||
|
|
a10930aa26 |
Name the thing that actually applies the VFA numbers: GigaDevice's mclib
The note said where the calibration is stored and left the consumer implied. It is two layers and only the first is ours. klippy/extras/mclib.py is fork-only and is not FlashForge's either -- its header is Copyright (C) 2024 Dongzhi Yu <dongzhi.yu@gigadevice.com>, so mclib is the GD32 vendor's motor-control library. On the host it is pure transport: _build_config emits the motor description through add_config_cmd, the block Klipper replays on every MCU connect, which is what makes SAVE_CONFIG's td*_amp/phase survive a reboot. The application is in the GD32H757ZG firmware at 600 MHz, and its command set says what it is: phase resistance, inductance, torque constant, bus voltage, current-loop kp/ki, motor identification. That is a closed-loop current- controlled driver synthesising phase currents, not a step/dir chip -- the 1x/ 2x/4x injection is added to the commutation waveform inside that loop. The fork ships upstream's src/ but no GD32 port and no mclib.c, so we cannot rebuild mainBoardGD.bin or read the injection maths. It does not matter: ff_mcu_bringup.py flashes nothing, the printer keeps FlashForge's firmware, and its dictionary already carries mclib_set_resonance_damp. Follows from that, and worth having written down: the compensation is applied by the driver and is live whether or not stepper_resonance_tester.py can import numpy. mclib.py imports only logging and stepper. A printer with saved td* values keeps them; only the calibration is blocked. Also checked while in there -- both phase results are normalised % 2pi before return, so nothing negative reaches the %u field on the wire. Documentation only. mkdocs build --strict passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJr2x7aDaY889qwSxbdLt8 |
||
|
|
6a4067e863 |
VFA calibration is one import away, and that import stops klippy booting
The stock app's "magic" VFA calibration is not app-side math at all. Traced
from CommMgr::calibration_vfa (0x0075e694), it sends five G-code strings and
the last one is STEPPER_RESONANCE_FACTORY_CALIBRATE -- a macro in FlashForge's
printer.vibration.cfg over klippy/extras/stepper_resonance_tester.py, which
searches amplitude and phase for the motor torque-ripple compensation the
closed-loop driver applies at 1x/2x/4x the electrical frequency. Every piece is
already here: the module is byte-identical in our vendored fork, the mclib
sections are in our printer.base.cfg, and mclib_set_resonance_damp is in the
stock mainBoardGD dictionary we never rebuild.
WHAT THE TRACE TURNED UP ON THE WAY. Two comments in this repo said the numpy
gap was a small item because the module guards its own import. It does not:
line 1 of stepper_resonance_tester.py is a bare `import numpy as np`, and
klippy.py:122 loads EVERY config section through a load_object that does not
catch ImportError -- the default=None argument only covers a missing file.
printer.base.cfg:19 includes printer.vibration.cfg, which declares
[stepper_resonance_tester]. So on the CPython 3.13 klippy has run on since
|
||
|
|
bd0df7c28d |
FF_PA_CALIBRATE runs FlashForge's PA sweep and reports the number
The host half of the touchscreen's automatic pressure advance, ported from CommMgr::paTestMgr. ff_pa.py drives the recovered sequence verbatim -- the same seven candidates in the same scrambled order, the same Y ladder, the same 5-sweep / 3-winner / mean-of-3 selection, the same "only verdict 9 passes" -- and prints the result. It saves nothing and applies nothing; the measurement itself stays where it already works, in the closed eBoard firmware we ship unchanged, reached through [pa_adjust]'s existing command wrappers. It borrows those wrappers rather than declaring its own. Declaring our own would be safe (lookup_query_command builds a fresh wrapper per call and SerialRetryCommand registers per send), but it would duplicate a closed MCU's protocol strings and would need a config-time callback on [mcu eboard], letting a report-only tool block startup on a machine whose eBoard is unplugged. Deliberate divergences, all commented at the point they matter: the extruder's own pressure advance is restored on exit, which the app never does because SET_PA_ADVANCE overrides it and we do not; a run where every line came back with the same verdict is refused, because "smallest passing wins" then yields the smallest candidate wearing a measurement's clothes; cold extruder, empty tool and out-of-range geometry are refused before anything moves rather than failing mid-line with the eBoard armed; SET_KINEMATIC_POSITION is not replayed, since our sweep is plain G1 under Klipper's own kinematics. What is NOT changed is the timing. pa_action is an immediate MCU command and there is no M400 between the last G1 and ACTION=0; the scorer was tuned against that, and Klipper's own input throttle is what bounds the window. FF_PA_PROBE draws one line and prints one verdict. It exists because the first real question on hardware is whether the eBoard discriminates between candidates at all, and answering that should not cost a full run. Checked: pyflakes clean; qa/static 143 passed; both packages build and both files land in the .ipk; the include set and the shipped ff-*.cfg set match the way the replica gate checks them. The selection logic was exercised against stubs -- it picks 0.0150/0.0200/0.0150 -> 0.016667 from a canned verdict table, refuses an all-9 run, and refuses fewer than three winners. Nothing in the repo can load klippy off-hardware, so behaviour on a machine is still unproven. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kw2rehvQJ7LSc28hPikfqk |
||
|
|
5b291d0b46 |
The PA verdict comes off a serial sensor on the eBoard's second UART
eBoard.bin is a stock Klipper MCU build, so the handlers are reachable without symbols: eBoard.hex loads at 0x08010000, command_index sits at 0x0801a288 with 16-byte entries indexed by the dictionary's command id, and all 77 ids line up. pa_action is id 5 (0x080115ec), get_emcu_pa_value id 6 (0x080115d0). ACTION=11 arms a capture: it zeroes an 8000-byte buffer at 0x200001fc and the result word at 0x200000a0, then enables TIM1/TIM4/TIM8 and DMA1 ch5. The buffer is filled by the USART3 idle-line handler at 0x08016734, which validates a fixed 11-byte frame and stores the big-endian uint16 payload, up to 1990 samples -- and only while the capture flag is set, which is what makes it the PA trace. On stop, a background task at 0x08016c60 runs analyse(buffer, 2000) and parks the verdict where get_emcu_pa_value reads it. So the signal is a serial sample stream from a separate module, not the accelerometer, the eddy coil or an ADC channel -- the earlier guess in this file was wrong and is removed. Which transducer is on that UART is still open. The point that matters for the port: nothing ever looks at the printed lines. They exist only because you cannot make a real pressure transient without really extruding. The scoring is live, off a signal the toolhead already streams, so keeping FlashForge's eBoard firmware gets us the same verdicts for free. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kw2rehvQJ7LSc28hPikfqk |
||
|
|
42d876d5eb |
The auto-PA sweep is host code; only the per-line verdict is in the eBoard
firmwareExe runs a real PA calibration at print start, inside the nozzle clean. CommMgr::paTestMgr @0x791640 prints one slow-fast-slow line per candidate at Y = 50 + 5*i for seven fixed PA values, asks the eBoard to score each line, keeps the smallest that scores 9, repeats until three sweeps have produced a winner and averages those three. All of that is host code and is recovered here in full. Only the verdict crosses into the closed toolhead MCU, over two commands that pa_adjust.py already forwards -- and pa_adjust.py is in our tree byte-identical to FlashForge's, with [pa_adjust] already in printer.base.cfg. So PA_ACTION and PA_GET should answer on a Reforge machine today; that is step one of the port. This supersedes "sensor unknown" in 60-background.md and "callable, not reproducible" in 25-app-vs-klipper-ownership.md. What stays unknown is narrower: which sensor the eBoard samples, what ACTION=11 and PC=666 select, and what the non-9 verdicts mean. Two findings that shape the port. PA_GET only respond_info()s its value, so the sweep cannot be a G-code macro -- it needs an extra that queries the eBoard directly. And PA_ACTION is an immediate MCU command, so FlashForge never synchronises the measurement window with motion; the port must copy that timing rather than improve it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Kw2rehvQJ7LSc28hPikfqk |
||
|
|
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 |
||
|
|
7abed5da91 |
Rebase the note onto the installer that exists
Master moved under this branch: patch.sh is payload.sh, run-pre.sh and run-append.sh are one runFirmwareExe.sh we own, and the install is no longer spliced into FlashForge's run.sh. Every path and line number in the note pointed at files that are not there. The argument survives intact -- the manifest, backup/stock and config-installed are all still on master, just moved -- so this is a re-derivation of the citations, not a rethink. One section did change: .prev-root-hash is resolved. Nothing records or restores the hash any more; runFirmwareExe.sh:169-171 just deletes it as a secret left by older installs. So the ordering risk against the password branch is gone, and the wipe subsumes those three lines too. backup/stock also reads better now: the script itself explains at :157-161 why firmwareExe is not in the backup, which is the point the note was making the long way round. mkdocs build --strict green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KXKk3zz8LAUPUDZkVEZ9fR |
||
|
|
52afaad8e1 |
Say what the installer does now, not what it used to
Four claims in the new note described code or logic that is not there. The backup/stock section said make test-recovery "is still referenced" by hardware-testing.md; that page already says the gate is retired, so the sentence invented a staleness that does not exist. Property (a) was illustrated with a renamed init script starting nginx twice -- the failure that prompted the manifest, but init.d has not shipped since s6-rc took over, which is why test_upgrade.py deliberately avoids those names. The cancels section told a future reader to go and mark the two phase notes, which this branch had already done. And the allowlist paragraph still called it phase 2's to-do list after cancelling phase 2. The pointers in 80- and 85- also read as settled when nothing has landed; both now say proposal. The 80- one leaned on that note's gate item (c), which names anvil.conf -- removed outright some releases ago. mkdocs build --strict green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KXKk3zz8LAUPUDZkVEZ9fR |
||
|
|
f2e0e5fcb2 |
The manifest protected three files, and none of them needed it
Proposal: on update, rm -rf $MODDIR and extract into the empty directory. No manifest, no selective delete, no compatibility branch. The audit is the note. Against a full wipe .install-manifest was protecting backup/stock, .prev-root-hash and config-installed, and each one falls: nothing restores from backup/stock and the documented recovery is a stock reflash; .prev-root-hash is removed by the password branch; config-installed is 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 got included last, and which inverts its own purpose by keeping the webcam block and API lockdown off any printer that trips it. So moonraker.conf joins ff-*.cfg and is simply overwritten, owner- dropped files under $MODDIR become an accepted loss, and phase 2's on-printer half in 85-packaging.md is cancelled rather than owed: a printer that never deletes selectively needs no file database for it. Phases 1 and 2 in 80-s6-migration.md and 85-packaging.md point here. No code has moved. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KXKk3zz8LAUPUDZkVEZ9fR |
||
|
|
56b5b6c334 |
The printer image is the only replica; make rootfs was a second way nothing took
`make rootfs` unsquashed rootfs.squashfs out of the stock package into
work/rootfs, and a thin container bind-mounted it with PROG_DUMP supplying
/usr/prog from the factory image. Every caller set PRINTER_IMAGE anyway --
both workflows wrote it into test.env BEFORE running `make rootfs`, so CI
unpacked a 93MB package, unsquashed a rootfs and downloaded a 182MB factory
image on every push and read none of it. build-printer-image.sh never used
work/rootfs either: Dockerfile.full does its own extraction from the same
public firmware and needs no stock package at all.
So the fallback went, and PROG_DUMP with it -- it was only ever read when the
replica was built locally, and both implementations already passed it empty
otherwise. Gone: the target, extract-rootfs.py, gates.extract_rootfs and its
two tar helpers, the thin printer/Dockerfile, the `prebuilt` flag threaded
through both Replica implementations, and Replica.start's want_output, whose
only message was that the local build was about to be slow.
PRINTER_IMAGE is pinned to 1.9.7-1.2.9-20260810 rather than :latest. It is
now the entire substrate the brick-safety gates run on and the only input
nothing checksums, so a floating tag would let it change under a green suite
-- the drift the sha256 pins in both workflows exist to prevent.
THREE THINGS THAT COULD NOT HAVE PASSED, found while doing it:
* Neither workflow built the feed. bin/patch.sh has refused to start
without one since
|
||
|
|
f289d6cc8d |
Two notes still said verify.sh checks things it no longer can
Follow-on to |
||
|
|
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
|
||
|
|
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
|
||
|
|
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 |
||
|
|
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
|
||
|
|
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 |
||
|
|
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
|
||
|
|
21cf19f2e2 |
BUILD_KLIPPER=stock kept a Klipper nothing else was written for
The flag chose between the Klipper4FlashForge fork and FlashForge's own v0.12 tree. Every other thing the mod ships had already stopped being neutral about that choice: the ff_*.py extras are the fork's package's own files, the ff-*.cfg declare sections only those extras implement, and c_helper.so is compiled from the tree it ships beside. A stock build was a printer with the mod's configuration and none of the code behind it -- and the previous commit turned that into a hard error, which is what showed the flag had nothing left to select. So it is gone rather than guarded. pkgs/klipper loses its PKG_WHEN, patch.sh section 1 loses the case and is straight-line, verify.sh section 7 stops being conditional -- a package with no klippy tree is now unrepresentable rather than merely gated -- and fetch-assets.sh pulls the fork tarball on every build, the way it already does s6 and CPython. Two fixture-lane repairs come with it. test/run-tests.py said BUILD_KLIPPER=stock to keep the lane off the network. That reason had already expired: since the payload became the feed installed, the lane runs build-packages first, and that needs the ~203MB Ingenic toolchain from vendor/ for CPython alone. The Klipper tarball is 27MB beside it, cached on versions.env like everything else. The lane now builds the fork, so verify.sh's ABI and symbol checks on c_helper.so run there for the first time. And the synthetic Moonraker tarball unpacked to moonraker-fixture/, while pkgs/moonraker/build.sh looks for moonraker-$MOONRAKER_VERSION/ -- the shape GitHub generates. That lane could therefore never build anvil-moonraker at all. It passed only while work/pkg/moonraker held a tree from a REAL build, because pkg_stamp keys on the version and not on the tarball; the first run against a cold cache died with "no moonraker/moonraker.py", a true statement about a tarball nobody could have made work. CI caches work/pkg on versions.env, so the first run after any pin bump would have hit it. Confirmed pre-existing by running the parent commit against the same cold cache: it fails identically. make qa-static 261 passed; make test 9 gates, 0 failed, with the fixture lane building Klipper and Moonraker from its own assets for the first time; CI's shellcheck gate clean. |
||
|
|
53a2bc010b |
Five extras were on the firmware partition because of a flag
pkgs/klipper/prog/klippy/extras/ff_*.py sat on /usr/prog for one reason, and it was not a technical one: anvil-klipper answers to BUILD_KLIPPER and the extras answered to BUILD_TOOLCHANGE, and a package cannot hold files gated differently from itself. So they stayed outside every package, copied to the firmware partition by bin/patch.sh. They are anvil-klipper's now, at payload/klipper/klippy/extras/, and BUILD_TOOLCHANGE no longer gates them. An extra is inert until a config section instantiates it, and those sections are anvil-klipper-config's, where the flag still applies -- so the gate bought nothing and cost the mismatch. Inside the tree is the only place they can go: klippy resolves an extra as dirname(klippy.py)/extras/<name>.py (klippy.py:90-103), with no search path and no environment variable. bin/patch.sh still copies them into the software component -- klippy runs from /usr/prog until the s6 service lands -- but reads them out of the payload now, the way section 1 already reads the klippy tree, so the recipe directory is not a second source for a file a package holds. It names the five rather than globbing ff_*.py: the fork ships an ff_eddy.py of its own and the glob could not tell them apart. Verified the component still gets exactly the same five files. The flags are coupled by this, and patch.sh says so rather than shipping a broken printer: BUILD_TOOLCHANGE=1 with BUILD_KLIPPER=stock now fails with the fix in the message. It has to -- the extras are written against the fork's v0.13 klippy, and dropping them onto FlashForge's v0.12 is the half-overwritten mixture that shipped as v20260824. The fixture lane in test/run-tests.py used exactly that pair (BUILD_KLIPPER=stock is a no-network convenience there) and now sets BUILD_TOOLCHANGE=0. pkgs/klipper/pkg.conf gains PKG_STAMP_EXTRA=$(pkg_payload_hash): KLIPPER_VERSION says nothing about payload/, so without it an edit to ff_toolchange.py would read as "already current". make qa-static 261 passed; make test 9 gates, 0 failed. The .ipk carries all five under klipper/klippy/extras/. |
||
|
|
dce1f62146 |
Naming the release and letting Depends work out the rest
bin/patch.sh enumerated the payload by hand: every recipe from pkg_recipes, minus the -dev packages, minus the chamber config for the other model. Forty lines to compute a closure opkg already computes, and computing it here meant the Depends fields were never read by anything. A printer running `opkg install anvil-moonraker` would have been the first to find out whether they were right. The feed is indexed, so point opkg at it -- `src anvil file:$PKG_FEED` and an `opkg update` -- and name the twelve packages the release IS. opkg pulls the other nineteen and marks them Auto-Installed. `file:` is answered before curl is reached at all (libopkg/opkg_download.c:134), which is why this works with the --disable-curl host opkg. The PKG_WHEN gates still reach here, by absence: a gated-off recipe leaves no .ipk in the feed, so BUILD_KLIPPER=stock has nothing to install and no BUILD_* flag is restated outside the pkg.conf that owns it. The cost is that a misspelled root drops out the same silent way, which test_the_payload_roots_name_real_packages now catches, and test_the_payload_roots_stay_a_short_list keeps a missing file from being answered by naming the package here instead of in the depending recipe's PKG_DEPENDS. Four of the twelve are Recommends, a field opkg does not have: pillow and preprocess-cancellation are Moonraker's thumbnail path, which pkgs/moonraker argues out of Depends; greenlet and cffi are klippy's, which pkgs/klipper cannot declare while klippy still runs under FlashForge's interpreter. anvil-core is the one version that cannot be left to opkg -- its PKG_VERSION is MOD_VER, today's date -- so its expected filename is still checked before installing. Payload verified against a build from the parent commit: 3298 paths, modes and symlinks identical, every file's sha256 identical except var/lib/opkg/status, which improves. Dependencies now say "Auto-Installed: yes" and the flag field is "ok" rather than "user" on packages nobody asked for by name. 31 packages, 154 ELF objects, as before. make qa-static 261 passed; make test 9 gates, 0 failed. |
||
|
|
a588644c14 |
Phase 2's assembly half, written down as done rather than as a sketch
docs/notes/85-packaging.md sketched this phase as a five-line shell loop using pkgs/ipk-install. What shipped differs in the tool and in three findings that only turned up while doing it, so the section is rewritten as a description. What it now records: Why a real opkg and not ipk-install, and that ipk-install keeps its job on the printer -- with the two configure flags that fail SILENTLY if dropped, --prefix=$MODDIR and --disable-shared, both of which the build checks against the binary it produced. That a chroot was tried and rejected, with the measurement: it needs root, unshare -Ur is blocked by seccomp, and the resulting payload cannot be deleted by the build-lane user -- which breaks the next make build. That -dev is filtered by package NAME rather than by "has a runtime half", and what asking the wrong question cost: 40 packages and 2165 ELF objects instead of 31 and 154. That the database ships and is in the manifest, why the two alternatives are worse, and the gap that remains -- a printer that opkg-installs something extra keeps its files and loses its stanza across a .tgz flash. THAT MAINTAINER SCRIPTS DO NOT RUN UNDER --offline-root and that opkg 0.7.0 sets no IPKG_INSTROOT at all. This contradicts an assumption written earlier in the same document: the plan to package firmwareExe and start.sh and let a postinst place them on /usr/prog does not work as written. Said plainly, where the assumption was made. The before/after gate the migration cleared, and the PKG_EXCLUDE bug that fell out of it. docs/building.md: the pipeline description, and that make packages comes before a cold make build. The packages skill gains pkg_buildopkg and the prefix trap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e3e0c7d141 |
The model is the one fact opkg cannot work out for itself
anvil-klipper-config declared Depends: anvil-klipper-chamber-config, which reads correctly -- ff-chamber.cfg drives a heater it does not declare -- and points the arrow the wrong way. `opkg install anvil-klipper-config` resolves a virtual name by picking whichever provider it finds first, and the two providers here describe DIFFERENT HARDWARE: a plain Creator 5 handed the Pro's config gets a [heater_generic] for an element it does not have. A coin flip decided by feed order. And the command a person would actually type, `opkg install anvil-klipper-creator5-config`, pulled in nothing at all. So the dependency runs from the model package to the shared one. You install the package named after your printer and the toolchanger's config follows; nothing sensible installs anvil-klipper-config by name. Provides and Conflicts stay exactly as they were -- they are what makes the two models mutually exclusive, which is a separate job from getting them installed. The invariant this drops at the opkg layer -- that a chamber config is present at all, since printer.base.cfg's include is unconditional -- is held two other places that already existed: bin/patch.sh refuses to finish a payload without one, and bin/verify.sh fails a package that ships without one. Restoring it here would need a mutual dependency, and betting on opkg resolving a cycle is worse than a check that has already been proven to fire. Measured: control files carry the flipped Depends, make qa-static 256 passed, make test 8 passed 0 failed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
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>
|
||
|
|
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>
|
||
|
|
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>
|
||
|
|
09a9f868e3 |
klippy compiles c_helper.so at first run, and this printer has no compiler
So the "build system" for that .so genuinely is one gcc line -- the argument
list in klippy/chelper/__init__.py -- and pkg_build could not express it,
because it always ran make. That is why Klipper was the last cross-build
still living in bin/patch.sh, which unpacked the Ingenic toolchain into
work/.mips-toolchain, wrote its own gcc invocation, cached on an mtime
comparison and gated the result itself: every one of the things pkg/lib.sh
exists to do once.
PKG_CC_SHARED is that line, appended to `$CC -shared -fPIC`. A knob and not
a verb, for the reason pkg_build's own header gives about the three
mechanisms with one user each: the recipe says WHAT to link, pkg_build owns
HOW, and -EL -mnan=2008 stay spelled once in pkg_toolchain where the wrapper
is gated before anything is built on it.
pkg/klipper -> anvil-klipper_0.13.0+gitb722a4c8-1_mipsel_xburst2.ipk
Both gates moved into the recipe, next to the ship list they check -- the
same move the s6 presence checks made. They now run on `make packages` too,
so a c_helper.so that is not nan2008, or one missing a symbol the klippy
beside it cdefs, fails the build that produced the .ipk rather than only a
full firmware build. The symbol gate is the one that matters: cffi resolves
lazily, so a stale .so imports cleanly and dies at connect on the printer.
KLIPPER_FORK is gone. It pointed the build at a local checkout instead of
the pin, which is a second source for a recipe allowed exactly one -- and it
is the same variable that let v20260824-nova-kakhovka ship a klippy tree
nobody had built, set to "" in CI. To build your own fork, put its tarball
in vendor/ and re-pin KLIPPER_VERSION and KLIPPER_SHA256; the sha is then
the guarantee every other source in this repo carries. bin/fetch-assets.sh
loses two of its conditions with it, and test/test-chelper.py loses the
config.env fallback that printed "SKIP: no KLIPPER_FORK configured" -- a
green line for a check that had not run.
bin/patch.sh section 1 is now staging and nothing else: a cp -a into the
SOFTWARE component, where the stock klipperDaemon looks, plus the
chelper.tar its run.sh extracts. That tar stays out of the .ipk, where it
would be a second copy of a file the package already contains. The package
installs the tree under $MODDIR/klipper, inert until phase 7 of
docs/notes/80-s6-migration.md makes the installed copy the live one -- at
which point these lines are deleted rather than moved.
Measured:
* two cold builds of pkg/klipper byte-identical, work/pkg deleted between
* c_helper.so is 0x70001407 -- nan2008, o32, mips32r2
* all 66 functions klippy declares are exported, checked after pkg_ship
strips rather than before
* the whole feed still builds: 38 recipes, 41 packages
* 236 static tests, including the staged-recipe list that now names klipper
* BUILD_KLIPPER=stock removes the recipe from pkg_recipes entirely
Not verified here: bin/patch.sh end to end. It needs the stock FlashForge
package, which the packaging lane deliberately does without; bin/verify.sh's
klippy-tree and chelper.tar assertions are what cover the staging rewrite.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
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>
|
||
|
|
46d2973165 |
A printer with no compiler was carrying 248K of headers
PKG_DEV_FILES splits one build into two archives: <name> with what a printer runs, <name>-dev with the headers and the static library only a build machine opens. Installing anvil-execline + anvil-s6 + anvil-s6-rc now leaves bin/ and libexec/ and nothing else -- include/ and lib/ are gone. The three libraries that are ONLY ever linked against -- zlib, libarchive, skalibs -- have no runtime half at all, so they are renamed rather than split: anvil-zlib-dev, anvil-libarchive-dev, anvil-skalibs-dev. The name now says what the section already did. THE RULE CHANGED, deliberately. "One recipe builds one package" becomes "one recipe builds one SOURCE, and may emit several packages from that one build". The rule was written to stop one script compiling several different upstream projects -- zlib inside the opkg recipe -- and a dev split is not that: one source, one configure, one version, sorted afterwards. The invariant that actually catches the thing it was for is countable and unchanged: exactly one source verb per recipe. The split MOVES files, never copies. A path in both archives is a path two packages own, opkg lets whichever installed last win, and removing either deletes a file the other still lists -- damage that surfaces long after the install that caused it. Two tests hold the line, both negative-tested by making a runtime package carry a dev file. Renaming also exposed a feed bug worth its own fix: the old anvil-zlib, anvil-libarchive and anvil-skalibs archives stayed in work/packages and opkg-make-index went on advertising three packages no recipe produced any more. A full build now prunes what no recipe claims. A single-recipe build does not, because it was not asked about the rest. Fourteen packages, byte-identical across two cold builds. 154 static tests. docs/notes/85-packaging.md phase 2 gains the target this is all for: bin/pack.sh should be handed a tree produced by installing the feed, not one patch.sh assembled, at which point every staging copy in patch.sh -- each of them a second description of what a package already contains -- goes away. CPython is the blocker and is next. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6e97f946b0 |
execline was 2.1MB unstripped against musl; it is 1008KB and mandatory
Eight recipes: skalibs, execline, s6, s6-rc, mainsail, moonraker,
helixscreen, anvil-core. bin/patch.sh loses 183 lines and stops being a
build system for any of them.
The supervision stack moves to the Ingenic glibc toolchain and links libc
dynamically, so the musl toolchain -- the only second libc in this tree --
is deleted along with MUSL_TOOLCHAIN_*, S6_STAMP and S6_BUILD. The
measurement that chose musl compared two STATIC builds; dynamic glibc was
never in it. Measured now: s6's shipped tree is 696K against ~930K for the
static musl one, and the whole stack including execline and s6-rc is 2.0M.
Three things that were not guesses:
* s6 and s6-rc do not link against glibc 2.29 without -lpthread. skalibs'
pthread_mutex_tailock reaches pthread_mutex_timedlock, which glibc kept
in libpthread until 2.34. Upstream never sees it and musl never had the
split. PKG_MAKE_ARGS is the lever, because s6's PTHREAD_LIB is wired
only to --enable-nsss and its Makefile appends LDLIBS last.
* s6 ships 21 binaries, not 13. s6-rc's generated scripts exec s6-sudo,
s6-sudoc, the s6-ipcserver chain, s6-sudod and s6-fdholder-daemon --
found by running a real up/down cycle with a restricted PATH, not by
reading the documentation.
* --enable-static-libc has never been a skalibs option. Its configure
warns on unknown flags and patch.sh sent that to /dev/null, so it was a
no-op for as long as section 5b existed.
execline ships whole. A subset picked from today's services fails the day
somebody adds a pipeline, at boot, naming a program nobody chose to leave
out. tools/supervisor/README.md and 80-s6-migration.md said it was not
needed; both now say why that stopped being true.
PY_STAMP was spelled in three places and happened to agree -- the same
shape as the s6 stamp bug, caught before it inverted. It is in common.sh
now, and the test that guarded S6_STAMP by name is rewritten around the
rule so it covers both and dies with neither.
HelixScreen's binaries reach mips_abi_gate for the first time: patch.sh
never gated them. They pass, 3 objects, nan2008/o32/mips32r2.
anvil-core carries a content hash in PKG_STAMP_EXTRA because its version is
a release date: pinned, an edit to payload/ would read as current; unpinned,
it rebuilds daily. The version says which release, the stamp says what is
in it. anvil.conf stays out -- it is user state that run-append.sh
preserves, and a package member is overwritten on every upgrade.
Twelve packages, byte-identical across two cold builds. 152 static tests.
The replica suite has NOT been run: docker is unavailable here, and the s6
change ships without its behavioural gate.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
c8e33a5604 |
zlib was compiled twice because it belonged to whoever needed it first
pkg/opkg/build.sh unpacked zlib, built it into a private sysroot, unpacked
libarchive, built that against it, and then built the one binary it shipped.
Section 5c did the same with its own zlib, from the same pinned tarball with
the same flags. Neither copy could see the other, because a library built
inside somebody else's recipe has no version, no package, and no way to be
reused.
One build.sh now produces exactly one package. A recipe names what it builds
against in PKG_BUILD_DEPENDS and pkg_deps unpacks those out of the feed with
opkg-unbuild -- upstream's inverse of opkg-build, from the checkout that
already provides both. That dissolves the bootstrap: nothing needs a working
opkg in order to build packages, so opkg is an ordinary recipe that waits on
nothing except its own dependencies, and no qemu enters the build path.
Building against the package rather than the build tree is the part worth
paying for. A package that forgets a header fails the next recipe's configure,
on the build that produced it.
One libc. skalibs, s6 and opkg were musl because a STATIC glibc s6 tree
measured 73MB -- a static-to-static comparison that never considered dynamic.
Two libcs also meant two of every library both worlds wanted, which a feed
cannot express without lying about one of them. opkg moves to the Ingenic
toolchain and links libc.so.6 dynamically with zlib and libarchive inside it;
it got smaller (212K against 332K). Section 5b stays musl until it becomes a
recipe too, and is now the only place left where one script builds two
libraries.
Found on the way, each of which had been true for a while:
* ar archives are not reproducible. Every member header carries an mtime and
the builder's uid/gid, and the symbol index carries another timestamp --
none of it reachable by SOURCE_DATE_EPOCH or opkg-build's -o 0 -g 0,
because it is inside a file the tarball merely contains. objcopy -D fixes
the members; the cross binutils is 2.27 and its ranlib -D writes a fresh
current timestamp into the index instead of zeroing it, so the index is
rewritten with the build image's ranlib.
* The ABI gate accepted exactly 0x70001405 and 0x70001407, both measured from
linked binaries where crt objects set NOREORDER. Object files need not:
libarchive's xxhash.o is 0x70001406, identical ABI, and the gate called it
wrong. It also read one Flags: line, while readelf -h on an archive prints
one per member -- 122 for libarchive.a. It now masks the three non-ABI bits
and checks every header.
* bin/fetch-assets.sh compared work/.s6/.version against two fields where
patch.sh wrote three, so the test could never be false and 71MB was
re-hashed every run. The comment above it described a fast path that had
never once been taken.
Gate: 127 static tests (29 packaging), and `make packages` from a cold cache
producing four byte-reproducible .ipk files plus the index. The end-to-end
install under qemu has NOT been re-run since opkg went dynamic -- a dynamic
binary needs qemu -L <sysroot>, and the command is written down in
docs/notes/85-packaging.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
5025aac171 |
opkg-utils already builds ipks, and opkg is the second package that proves lib.sh
Two corrections to the packaging PoC, both from review.
FIRST: THERE WAS ALREADY A TOOL. bin/mkipk.sh is deleted. opkg-utils ships
opkg-build and opkg-make-index -- 423 lines of POSIX sh and a python indexer,
upstream, maintained -- and this repo now drives them instead of re-deriving
the format. What is left of build-packages.sh is the part that is genuinely
ours: laying out the tree opkg-build packages. The hand-rolled version worked,
and it was still us reimplementing member ORDER, control validation and the tar
flags that make a build reproducible, all of which fail as "a package that
inspects fine and installs nowhere".
opkg-utils is PINNED BY COMMIT, uniquely in this repo, and not by choice: it
publishes no release tarball anywhere. Upstream's cgit has snapshots disabled,
downloads.yoctoproject.org carries opkg but not opkg-utils, and the GitHub
mirror last saw a commit in 2012 with no tags. A git sha is a hash of the whole
tree -- the same guarantee every sha256 here gives -- and fetch-assets.sh
verifies HEAD against it and refuses a dirty checkout. Both were tested.
SECOND: A SECOND PACKAGE, AND IT IS opkg ITSELF. pkg/lib.sh now holds the part
of a cross-build that is the same every time -- the two toolchains, the gcc
wrappers that bake -EL -mnan=2008 into the DRIVER because autotools link lines
do not all forward CFLAGS, the ABI self-test of those wrappers, autotools,
the build cache, and pkg_ship. Both recipes fit on a screen and share exactly
that file.
opkg is the honest test of it: different toolchain (Bootlin musl, not Ingenic
glibc), different libc, different link mode (static, not shared), and a
dependency chain three builds deep (zlib -> libarchive -> opkg) against
libsodium's none. It is static musl for the same reason s6 is -- a standalone
binary that must match nothing -- so no libarchive.so or libz.so ships beside
it. qa/static/test_ipk.py fails any recipe that sources its own toolchain,
spells its own -mnan=2008, or calls ./configure directly; the gate was checked
by breaking a recipe on purpose. zlib is the one named exception, because its
configure has never accepted --host.
THE LOOP IS CLOSED ON THE TARGET. The opkg this repo builds -- 775KB, static,
nan2008/o32/mips32r2 -- runs under qemu-mipsel and installs both our packages,
including itself, then lists their files and removes them leaving nothing
behind. The opkg it installed then runs and reports its version. Its status
database landed at /usr/data/anvil/var/lib/opkg, which is where pkg/ipk-install
writes, because both agree rather than by coincidence.
Three findings paid for by building it, all in the note:
* opkg BAKES ITS STATE DIRECTORY IN AT COMPILE TIME -- --offline-root does
not move it. Built with any other prefix it believes nothing is installed
and reinstalls the world. Third instance of this trap here after s6's
libexecdir and execline's shebangdir.
* opkg 0.6.3 does not build against musl: two files call basename() without
<libgen.h> and get away with it only under glibc. 0.7.0 fixed both, which
is the difference between a pin bump and carrying a patch.
* libtool SWALLOWS -static. A program configured LDFLAGS=-static comes out
dynamic and the only symptom is a NEEDED entry nobody reads. -all-static,
at make time, is the flag that means it -- hence the explicit check.
Both packages are byte-identical across a full cold rebuild. The musl toolchain
fetch now also triggers on a stale work/.opkg, and its cache key lives in
common.sh so the fetcher and the recipe cannot spell it differently -- the
hazard already written above pypkg_stamp.
Also fixed: two `# shellcheck disable=... -- prose` directives were malformed,
which made shellcheck abandon those files rather than check them. A gate that
examined nothing, in the file arguing against those.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
057a3a11d7 |
The boot was seven scripts in filename order. It is one compiled graph now.
firmwareExe ran $MODDIR/init.d/S* one at a time in the foreground, and that
loop WAS the boot order. Three of the seven were already thin wrappers over a
supervisor that was already running, so what the loop still carried was not
supervision, it was sequencing -- expressed as filenames, in a directory
nothing but the loop read.
This ships s6-rc and moves the sequencing into a database compiled at build
time. firmwareExe now starts the scanner as its own child, initialises s6-rc,
asks for one machine state, and blocks on `wait "$SCANNER_PID"`.
WHAT THE GRAPH ACTUALLY SAYS
mcu-bringup -> klipper -> ff-startup -> ui
^
moonraker
ok-all = wifi nginx moonraker camera klipper ff-startup ui
Those are the first real edges the plan has ever had; it predicted they would
not arrive until phase 7. bin/ff-startup.py became two services, which is what
made them possible: the MCU bring-up has to precede klipper and the readiness
wait has to follow it, and both facts now live where a program can read them.
Deliberately still absent: nginx before moonraker. S60nginx's header wants it,
but dependencies.d/nginx under moonraker means `s6-rc -d change nginx` stops
moonraker too -- the exact failure splitting S60web into two scripts prevented.
The cost of not writing it is a second where a reloading browser gets a refused
connection instead of a 502.
AND WHAT THEY DO NOT SAY. klipper has no notification-fd, so s6-rc calls it up
when it is forked, not when klippy is usable. ff-startup keeps its own poll of
moonraker's API. moonraker and camera DO carry one, so their readiness is real.
The asymmetry is written down at each site rather than glossed.
init.d/ IS DELETED, REVERSING PHASE 4
Phase 4 promised the S* scripts would survive as thin wrappers, because
`S60nginx restart` is in people's hands. They did not survive, and the reason
is the argument nginx/run already made: a service that only works when a shell
script happened to run first is not supervised, it is lucky. Every prerequisite
a wrapper did belongs in the run script anyway, because s6 restarts a service
without going near a shell. Once they moved, the wrappers were verb translation
in a directory whose S<NN> numbers meant nothing. s6-rc is the interface now.
anvil-service.sh goes with them -- 625 lines whose last caller was firmwareExe,
and a shared library for one consumer is the redundancy the wrappers were.
So do the fallbacks. S60nginx and S62moonraker each carried a second,
unsupervised implementation reachable only via MOD_S6=0; that switch is gone,
and with it the argument for a code path exercised only on broken printers. The
cost is stated plainly in anvil.conf: a printer whose s6 fails now has nothing.
klipperDaemon is replaced with a no-op `start`. It is FlashForge's script and
off-repo callers remain -- stock start.sh, the UI's restart path, habits over
ssh -- and each of them would fork a second klippy beside the supervised one.
WHAT SUPERVISES THE SUPERVISOR, ANSWERED
Phase 3 left this open and guessed a dead scanner was a dead printer either
way. It is not. firmwareExe's tail used to be `while true; do sleep 3600; done`
purely to satisfy app_startup.sh's watchdog, which greps ps for our name 5s in.
It waits on the scanner instead, so exiting IS the recovery: app_startup.sh --
FlashForge's own init, which we never patch -- re-execs us and the tree comes
back. Not a respawn loop in ash, which phase 3 rightly refused. It restarts
everything rather than the scanner alone, and that is written down.
MEASURED, NOT READ OFF THE MANUAL
Host- and target-compiled databases are byte-identical (db md5
80f5261437191fa591744ada6087c6f7 both ways), so the database is built here and
shipped. /run is tmpfs and writable, so the live directory needs no fallback.
Both were BLOCKING UNKNOWNs in the note and are now settled sections.
Three traps the replica found that reading could not:
* s6-svscan resolves s6-supervise through PATH. Without $MODDIR/bin on it the
scanner answers s6-svscanctl -a perfectly and then fails every spawn with
ENOENT for ever, and s6-rc-init HANGS rather than failing.
* s6-rc-init creates one symlink per service in the scandir and dies with
"File exists" if the name is taken. So the cutover is not additive, and
run-append.sh sweeps.
* s6-rc-compile does not reject a `down` file. It accepts it and SILENTLY
DROPS it -- the servicedir is byte-identical. "Down by default" can only
mean absent from the bundle, which is why mcu-bringup is the one service
outside ok-all and arrives as klipper's dependency.
execline turned out not to be optional: s6-rc GENERATES two servicedirs whose
run scripts are execline, and every oneshot's `up` goes through one of them.
The binary list was read off a real compile, not guessed -- seven execline
binaries of 53, plus six s6 ones we built and did not ship.
Two build bugs fell out. fetch-assets.sh compared against a stamp patch.sh
never wrote, so every build pulled the ~100MB toolchain the skip exists to
avoid; S6_STAMP is one string in common.sh now. And `chmod +x .../bin/s6-*`
would have missed all seven execline binaries.
qa: 83 static. The suite gained the s6-rc run scripts and klipperDaemon, which
no glob had ever covered, and lost a dead payload/init.d/S* glob -- exactly the
silently-retired check that file's own docstring warns about. The oneshots'
up/down are NOT in it: they are execline carrying shell inside a /bin/sh -c
argument, so both checkers would parse the wrapper and pass.
Not verified here: no config.env and no stock package on this machine, so the
mipsel cross-build of execline/s6-rc and the ABI gate over 33 binaries have
never run. The native compile and the database are proven; that half is not.
The replica gate is the next commit, and it still owes an answer on whether
helix-screen runs under s6-supervise at all.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||
|
|
1c89a9cd80 |
Neither Alpine nor OpenWrt has a binary we can install. That picks opkg.
The evaluation asked for apk vs opkg. The comparison turns on one fact that makes most of the usual argument irrelevant here: Alpine has no MIPS port at all, and OpenWrt's mipsel_24kc is musl against this printer's glibc 2.29. So no upstream repository is reachable either way, every package is cross-built here regardless, and a package manager buys packaging and installing -- not software. That reduces the choice to which format is cheapest to PRODUCE on every build, and .ipk wins by a distance: ar + two tarballs + a three-byte version file, no keypair, no Alpine host, no abuild. apk would need the signed three-concatenated-gzip layout implemented by hand or abuild imported into a Debian build image, paid per recipe forever, for a format with an in-progress v2->v3 transition and no users on our architecture. The full table, the two honest points for apk, and the phased plan are in docs/notes/85-packaging.md. The proof of concept is libsodium, end to end and measured rather than described: make packages -> libsodium_1.0.20-1_mipsel_xburst2.ipk + a feed index Verified against real opkg 0.6.3 built from source -- offline-root install, both symlinks preserved as symlinks, list-installed, files, remove leaving nothing behind. dpkg-deb reads it too; the container format is Debian's. A full recompile from a cold cache produces a byte-identical package, so a pin bump is reviewable as a hash diff. libsodium is now compiled ONCE. patch.sh section 5d's build block moved to pkg/libsodium/build.sh and patch.sh runs it, so the tarball's copy and the packaged copy cannot become different libraries wearing one version number -- asserted, not trusted. MODDIR, PY_HOST, PY_TOOLCHAIN_DIR and mips_abi_gate moved to common.sh for the same reason: a package is a shipping vehicle and gets ABI-gated like one, on a machine that may never run patch.sh. Nothing ships. make build is unchanged and still produces one anvil.tar.xz; make packages needs no stock FlashForge package, which is the point -- a packaging gate that only runs where the proprietary firmware is stops being a gate. Two findings worth more than the code. opkg bakes its state directory in at COMPILE time, so a phase-2 opkg must be configured --prefix=/usr/data/anvil or it comes up believing nothing is installed -- the s6 libexecdir trap one layer up. And opkg 0.6.3 will not configure without libarchive, which is what phase 2 actually costs; the opkg binary itself is 33KB. So pkg/ipk-install walks the ar headers with tail/head instead: this busybox is 1.31.1 built small, with no timeout, nc or ionice applet, and betting an installer on an ar nobody has checked for is how an update fails at the last step. qa/static/test_ipk.py is 18 tests that build their own package from a synthetic tree -- no toolchain, no firmware -- so the lane keeps the property qa/conftest.py insists on and can never be skipped into looking green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
bcd38fae28 |
Six of the ten library paths were never loaded by anything
ANVIL_LIBS handed every mod process ten /usr/prog library directories. Four of
them are load-bearing and six have no consumer at all, which was established
by measurement rather than argument: /proc/PID/maps of the processes the mod
actually runs, plus a DT_NEEDED walk over every ELF in
/usr/prog/{PROGRAM,bin,nginx,mjpg-streamer,klipper,module,modules,wifi},
/usr/bin, /usr/sbin, /bin and /sbin.
That walk found exactly ONE consumer of ffmpeg, x264, opencv, nim and libzip
between them: /usr/prog/PROGRAM/software/firmwareExe -- FlashForge's Qt
binary, which this mod replaces with a shell script. curl is the interesting
one, because it did have a Python consumer: pycurl, in site-packages, which
does not import on this firmware at all. "undefined symbol:
curl_global_sslset", a symbol that arrived in curl 7.56 while /usr/prog ships
7.55.1, and the shipped pycurl links libssl.so.1.1 where this curl links
1.0.0.
The comment that stood here is replaced rather than deleted. It argued the
list should be the union, because which subset a script needs is a property of
the component set in moonraker.conf and the user edits that file. The
measurement empties that defence: no moonraker.conf edit can reach a library
through a module that cannot be imported in the first place. What replaces it
says what was measured, and what would have to be true to put an entry back --
some process the mod runs has to map it, which is a measurement and not an
argument.
Each of the four that stay now names the failure it prevents, and openssl's is
worth having written down: it is NOT for `import ssl`. _ssl and zlib are
BUILTIN on this interpreter, statically linked into libpython, so
libpython3.8.so.1.0 itself carries DT_NEEDED on libssl.so.1.0.0 -- take the
directory away and the interpreter does not start, before it has read a line
of anyone's python. The rootfs has 1.1 only, a different soname that cannot
stand in.
case-libpath.sh is the measurement, kept as a gate and running in both
directions. Each of the four is removed one at a time and required to produce
a real failure -- two of them the interpreter refusing to start, one an
ImportError from ctypes, one an OSError from libnacl, which is the shape that
once made a missing library read as a component crash. And a probe is run
twice: on the four shipped entries, and again on the full old ten-entry path.
The second run is the one that carries the weight. With every directory
available it still maps none of the six, while it does map libffi and
libsodium -- so it was loading libraries, it just never wanted those.
Two of the case's checks SKIP in the replica, honestly and for the same reason
case-moonraker.sh skips the identical one: only the payload directory is
mounted, so FlashForge's 2022 Moonraker tree stands in and has no
moonraker.server. The component-import proof needs a checkout where patch.sh
has staged the mod's own tree.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|