mirror of
https://github.com/Klipper4FlashForge/firmware.git
synced 2026-10-21 21:02:16 -05:00
`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 a1b4ea6, 45 commits after ci.yml was last touched.
Both ran unpack/patch/pack straight into that guard.
* `make release` ran bin/build.sh on $(RUN), which has no docker socket,
while patch.sh assembles the payload inside the replica. `build` has
used RUNBUILD all along; `release` was left behind. It also built no
feed. It now builds one, once, outside the per-model loop -- nothing in
it is model-specific.
* ci.yml's shellcheck gate named pkgs/*/payload/init.d/S* and
pkgs/*/prog/*.sh, neither of which exists any more; shellcheck exits 2
on a path it cannot open. It is a `find` with an asserted count now,
because the opposite failure is worse: a glob that quietly matches
nothing checks nothing and still reports green.
Both workflows now go through `make` instead of hand-rolling `docker run`.
Hand-rolling meant CI exercised entry points no human uses, which is exactly
why the `release` runner bug could sit there unseen; it also ran as root and
left a work/ that the later `make release` could not write to.
ci.yml's test job builds the feed before the static lane. A good part of that
lane asks what is inside an .ipk and those questions do not fail without one
-- they return, and the lane reports a pass having never opened a package.
build-packages.sh needs no stock firmware, which is what makes that possible.
Docs, and the packages skill, which still documented pkg_buildopkg and
work/.opkg-host as how the payload is assembled -- the host opkg went in
dcd037c and the printer's own installs the feed now -- and still claimed
qa/static holds an ABI gate over every ELF in every package. It does not:
qa/replica/test_abi.py is the only one, and qa/static asserts a seventh does
not grow back.
Verified: qa/static is 122 passed / 110 errors here, identical to master --
every error is `vendor/opkg-utils is missing` in a checkout that has not run
`make vendor`. mkdocs build --strict passes. The new shellcheck gate was run
as written. The workflows themselves were not executed.
tools/replica/printer/*.sh live inside the published image, so those edits
take effect on the next `make printer-image`; behaviour is unchanged either
way, because nothing sets PROG_DUMP any more.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TrhjKabxDihvVjNQGGT4MK
100 lines
3.9 KiB
Python
100 lines
3.9 KiB
Python
"""config.env and test.env, read the way the build reads them.
|
|
|
|
The two files are shell, and bin/*.sh still sources them, so they stay shell:
|
|
they use quoting, `$ROOT`, and occasionally a command substitution, and a
|
|
hand-rolled `KEY=VALUE` parser would agree with bash right up until the day it
|
|
did not. So bash reads them and reports back what it ended up with. That keeps
|
|
exactly one definition of what these files mean.
|
|
|
|
config.env the BUILD config. Everything that can reach a printer.
|
|
test.env replica-only: the factory image, the partition sizes.
|
|
Kept apart so nothing in it can end up inside a package.
|
|
|
|
THE ENVIRONMENT WINS OVER BOTH FILES, which is a rule the shell version had to
|
|
work for with a save-and-restore dance around the sourcing. An empty
|
|
`PRINTER_IMAGE=` sitting in test.env used to overwrite an explicit
|
|
`PRINTER_IMAGE=foo make boot-screen-sim`, so the override
|
|
silently did nothing and the run tested a different image than the one it was
|
|
told to. Note the rule is about being SET, not about being non-empty: an empty
|
|
string in the environment is still a decision, and `PRINTER_IMAGE= make
|
|
boot-screen-sim` deliberately means "build locally", not "use whatever the
|
|
file says".
|
|
"""
|
|
import os
|
|
import subprocess
|
|
|
|
from . import Fail, repo_root
|
|
|
|
# What the harness reads. Anything else in those files belongs to the build.
|
|
WANTED = (
|
|
"FF_KEY",
|
|
"PRINTER_IMAGE", "PROG_MB", "DATA_MB",
|
|
)
|
|
|
|
# Of those, the ones a caller is expected to override per-run.
|
|
OVERRIDABLE = ("PRINTER_IMAGE", "PROG_MB", "DATA_MB", "FF_KEY")
|
|
|
|
# `.` on a file with a syntax error returns non-zero and does NOT stop the
|
|
# shell, so the && form quietly carried on with half the file applied. That is
|
|
# the whole disease being cured here: a broken config that looks like an
|
|
# absent one. Each source is checked, and its own exit code says which file.
|
|
_DUMP = r'''
|
|
set -a
|
|
if [ -f "$1" ]; then . "$1" || exit 91; fi
|
|
if [ -f "$2" ]; then . "$2" || exit 92; fi
|
|
set +a
|
|
for key in %s; do
|
|
eval "value=\${$key-}"
|
|
printf '%%s=%%s\0' "$key" "$value"
|
|
done
|
|
''' % " ".join(WANTED)
|
|
|
|
|
|
class Config:
|
|
"""What the two files say, with the environment layered back on top."""
|
|
|
|
def __init__(self, values, root):
|
|
self.values = values
|
|
self.root = root
|
|
|
|
@classmethod
|
|
def load(cls, root=None):
|
|
root = root or repo_root()
|
|
config_env = os.environ.get("CONFIG_ENV") or str(root / "config.env")
|
|
test_env = os.environ.get("TEST_ENV") or str(root / "test.env")
|
|
|
|
dumped = subprocess.run(
|
|
["bash", "-c", _DUMP, "ffsim", config_env, test_env],
|
|
cwd=str(root), capture_output=True, text=True)
|
|
if dumped.returncode != 0:
|
|
# A syntax error in config.env is a BROKEN harness, not a missing
|
|
# precondition. The shell version could not tell the two apart:
|
|
# the half-sourced file left the settings empty, the launcher
|
|
# concluded there was nothing to test, and it reported a clean
|
|
# skip. Naming the file that failed is the entire point.
|
|
which = {91: config_env, 92: test_env}.get(dumped.returncode)
|
|
raise Fail("could not read %s:\n%s"
|
|
% (which or ("%s / %s" % (config_env, test_env)),
|
|
dumped.stderr.strip()))
|
|
|
|
values = {}
|
|
for item in dumped.stdout.split("\0"):
|
|
if "=" in item:
|
|
key, _, value = item.partition("=")
|
|
values[key] = value
|
|
|
|
# The environment wins -- see the module docstring. Being SET is what
|
|
# counts, so this tests membership rather than truthiness.
|
|
for key in OVERRIDABLE:
|
|
if key in os.environ:
|
|
values[key] = os.environ[key]
|
|
|
|
return cls(values, root)
|
|
|
|
def get(self, key, default=""):
|
|
return self.values.get(key, default) or default
|
|
|
|
@property
|
|
def ff_key(self):
|
|
return self.get("FF_KEY", "FFP0331&*%root")
|