mirror of
https://github.com/Klipper4FlashForge/firmware.git
synced 2026-10-22 05:12:16 -05:00
Two strands, both landing here because they had grown into each other in the
working tree.
The printer replica: the printer's real rootfs.squashfs chrooted under
qemu-mipsel, with /usr/prog and /usr/data from the factory image, so a package
is installed by FlashForge's own updater running on the printer's own busybox,
tar, md5sum and unTar. There are no stubs -- if the real prog partition is
missing the harness refuses to run rather than test against mocks. A release
workflow is blocked on it.
The lanes: a file's directory now says whether it can ever reach a printer.
payload/, assets/ ship
bin/, profiles/ build the package
test/, docs/ neither, ever
* the replica's own infrastructure moved out of bin/ and docker/ and in
beside the harness it belongs to (test/extract-rootfs.sh,
test/build-printer-image.sh, test/printer/Dockerfile{,.full})
* the Makefile has two runners: only RUNSIM gets the docker socket and the
replica's variables, so a build cannot reach the docker daemon at all
* replica settings moved to test.env, which nothing in a package reads
* build-time flags renamed BUILD_* (APPLY, REPORT, KLIPPER, TOOLCHANGE,
MAINSAIL, HELIX). MOD_* now means only the runtime switches written into
anvil.conf and re-read by the printer at every boot
* verify.sh fails if any file in a package is byte-identical to one in
bin/, test/, docker/ or profiles/ -- by content, because a name blacklist
would trip over whatever Mainsail happens to call common.sh
Fixed while mapping it:
* BUILD_REPORT was read only inside the BUILD_APPLY=0 branch, so four
profiles declared it and got nothing. It is honoured on both paths now,
and off outside probe: report.sh copies /etc, passwd and shadow onto the
USB stick, which is a diagnostic, not something to hand out.
* three checks in the brick lint had been quietly dead. Two were guarded on
a payload/boot.sh replaced by the firmwareExe wrapper long ago; the third
was the file glob itself, which matched neither firmwareExe nor the
init.d/S* scripts -- the UI wrapper and every service went unscanned. A
missing file is now a failure rather than a silent pass.
* run-tests.sh overwrote config.env and copied it back, leaving the config
you edited one crashed run away from being a fixture. It uses CONFIG_ENV.
* bin/make-uninstall.sh does not exist and never did; README, run-pre.sh and
full.env all pointed at it. Recovery is the stock package, which is what
test-recovery actually proves.
README is now for someone who wants to flash their printer: what it is, which
file, how to flash, what to do when it goes wrong. The rest moved to
docs/how-it-works.md, docs/building.md and docs/testing.md.
make test: 89 passed, 0 failed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
136 lines
4.5 KiB
Python
136 lines
4.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Every command the payload runs must actually exist on the printer.
|
|
|
|
The printer's busybox is a 1.31.1 build with a specific applet set. It has no
|
|
`timeout`, no `bash`, no `systemctl`. A script that reaches for one of those
|
|
fails at boot, on the machine, with the screen already blank -- so the check
|
|
belongs here, against the real extracted rootfs.
|
|
"""
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
RFS = os.path.join(ROOT, "work", "rootfs")
|
|
|
|
KEYWORDS = set("""
|
|
if then else elif fi for while until do done case esac in function select time
|
|
return exit local export readonly set unset shift eval exec trap break continue
|
|
true false echo cd test read printf source wait umask alias command type hash
|
|
getopts times ulimit jobs fg bg kill let
|
|
""".split())
|
|
|
|
SEPARATORS = re.compile(r"[;&|\n]|\$\(|`|\(\s|\bthen\b|\bdo\b|\belse\b")
|
|
WORD = re.compile(r"[A-Za-z_][A-Za-z0-9_.:-]*")
|
|
|
|
|
|
def strip_quotes_and_comments(text):
|
|
"""Blank out comments and quoted text so prose cannot look like a command."""
|
|
out, i, n = [], 0, len(text)
|
|
quote = None
|
|
while i < n:
|
|
c = text[i]
|
|
if quote is None and c == "#" and (not out or out[-1] in " \t\n;&|(`"):
|
|
while i < n and text[i] != "\n":
|
|
i += 1
|
|
continue
|
|
if quote is None and c in "'\"":
|
|
quote = c
|
|
out.append(" ")
|
|
i += 1
|
|
continue
|
|
if quote is not None:
|
|
if c == "\\" and quote == '"':
|
|
out.append(" ")
|
|
i += 2
|
|
continue
|
|
if c == quote:
|
|
quote = None
|
|
out.append(" ")
|
|
i += 1
|
|
continue
|
|
# keep $(...) and `...` inside double quotes: they are real commands
|
|
out.append(c if (quote == '"' and c in "$()`") else " ")
|
|
i += 1
|
|
continue
|
|
out.append(c)
|
|
i += 1
|
|
return "".join(out)
|
|
|
|
|
|
ARITH = re.compile(r"\$\(\([^()]*\)\)")
|
|
|
|
|
|
def command_words(text):
|
|
text = strip_quotes_and_comments(text)
|
|
while ARITH.search(text): # $((...)) is arithmetic, not a command
|
|
text = ARITH.sub(" ", text)
|
|
words = []
|
|
for segment in SEPARATORS.split(text):
|
|
segment = segment.lstrip(" \t()")
|
|
m = WORD.match(segment)
|
|
if not m:
|
|
continue
|
|
word = m.group(0)
|
|
rest = segment[m.end():].lstrip()
|
|
if rest.startswith("=") or rest.startswith(")"):
|
|
continue # assignment, or a case label
|
|
words.append(word)
|
|
return words
|
|
|
|
|
|
def main():
|
|
if not os.path.isdir(os.path.join(RFS, "bin")):
|
|
print(" SKIP: no printer rootfs -- run 'make rootfs' first")
|
|
return 0
|
|
|
|
avail = set()
|
|
for d in ("bin", "sbin", "usr/bin", "usr/sbin"):
|
|
p = os.path.join(RFS, d)
|
|
if os.path.isdir(p):
|
|
avail.update(os.listdir(p))
|
|
|
|
allow = set()
|
|
allow_file = os.path.join(ROOT, "test", "applets.allow")
|
|
if os.path.exists(allow_file):
|
|
for line in open(allow_file):
|
|
line = line.split("#")[0].strip()
|
|
if line:
|
|
allow.add(line)
|
|
|
|
files = []
|
|
for dirpath, _, names in os.walk(os.path.join(ROOT, "payload")):
|
|
for n in names:
|
|
if n.endswith(".sh") or n == "firmwareExe" or re.match(r"^S\d", n):
|
|
files.append(os.path.join(dirpath, n))
|
|
files.sort()
|
|
|
|
fail = False
|
|
for f in files:
|
|
src = open(f, encoding="utf-8", errors="replace").read()
|
|
funcs = set(re.findall(r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*\(\)", src, re.M))
|
|
rel = os.path.relpath(f, ROOT)
|
|
for w in sorted(set(command_words(src))):
|
|
if w in KEYWORDS or w in funcs or w in avail or w in allow:
|
|
continue
|
|
print(f" FAIL {rel}: \"{w}\" is not on the printer "
|
|
f"and not in test/applets.allow")
|
|
fail = True
|
|
# Absolute paths into the rootfs must exist. /usr/prog and /usr/data
|
|
# live on partitions the rootfs does not contain; the install
|
|
# simulation covers those.
|
|
for p in sorted(set(re.findall(
|
|
r"(?<![\w.])(/(?:bin|sbin|etc)/[A-Za-z0-9_./-]+|"
|
|
r"/usr/(?:bin|sbin)/[A-Za-z0-9_./-]+)", src))):
|
|
if not os.path.exists(RFS + p):
|
|
print(f" FAIL {rel}: {p} does not exist on the printer")
|
|
fail = True
|
|
|
|
if not fail:
|
|
print(" every command the payload uses exists on the printer")
|
|
return 1 if fail else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|