mirror of
https://github.com/Klipper4FlashForge/firmware.git
synced 2026-10-21 21:02:16 -05:00
Run everything in Docker; test against the printer's real busybox
The stock kernel-*.tar.xz turned out to contain rootfs.squashfs -- the printer's genuine root filesystem. Unpacking it answered several questions that were previously guesswork: - init is busybox init -> /etc/init.d/rcS -> S99factory_test_shell -> /usr/prog/app_startup.sh. There is no systemd, and rcS runs from the read-only squashfs before /usr/prog/etc is bind-mounted over /etc, so a new S-script cannot be injected into the stock boot sequence. - /usr/sbin/dropbear and an ENABLED /etc/init.d/S50dropbear already ship. Port 22 is open on a stock printer; only the root password is missing. The dropbear cross-build is therefore deleted -- it was never needed. New: test/test-ash-conformance.sh parses every on-printer script with the printer's own busybox 1.31.1 ash under qemu-mipsel. A parse error in the firmwareExe wrapper would mean a blank screen, and neither bash -n nor dash -n proves busybox accepts it. Makefile now runs every target inside the pinned build image, with the docker socket mounted so test targets can start sibling containers. The repo is mounted at its real host path rather than /src, because sibling mounts are resolved by the host daemon. 78 checks pass.
This commit is contained in:
1 parent
4415905fb3
commit
ff7204f7d5
8 files changed
+286
-134
No files matched your search
@@ -1,92 +1,135 @@
|
||||
# creator5-custom-firmware
|
||||
#
|
||||
# make build PROFILE=probe build one profile's USB package
|
||||
# make test full suite (fixture-based, no stock firmware)
|
||||
# make uninstall-pkg build the revert package
|
||||
# make shell a shell in the reproducible build container
|
||||
# Nothing runs on your machine except Docker. Every target below executes
|
||||
# inside the pinned build image (docker/Dockerfile.build); the docker socket
|
||||
# is mounted through so the simulation targets can start sibling containers.
|
||||
#
|
||||
# make probe build the stage-0 package (changes nothing)
|
||||
# make test full brick-safety suite
|
||||
# make shell interactive shell in the build container
|
||||
#
|
||||
# Escape hatch: LOCAL=1 make <target> runs the scripts directly on the host.
|
||||
|
||||
PROFILE ?= probe
|
||||
SHELL := /bin/bash
|
||||
DOCKER ?= docker
|
||||
IMAGE ?= creator5-fw-build
|
||||
LOCAL ?=
|
||||
|
||||
DOCKER_SOCK := /var/run/docker.sock
|
||||
|
||||
# The repo is mounted AT ITS REAL HOST PATH, not at /src. The test targets
|
||||
# start sibling containers through the mounted docker socket, and those are
|
||||
# created by the host daemon -- which resolves -v paths on the HOST. If the
|
||||
# build container saw the repo as /src it would ask the daemon to mount a
|
||||
# /src that does not exist there, and the sibling would get empty
|
||||
# directories. Keeping the path identical on both sides avoids that entirely.
|
||||
ifeq ($(LOCAL),)
|
||||
RUN = $(DOCKER) run --rm -i \
|
||||
-v "$(CURDIR)":"$(CURDIR)" -w "$(CURDIR)" \
|
||||
-v $(DOCKER_SOCK):$(DOCKER_SOCK) \
|
||||
-e PROFILE -e REAL_PKG -e SIM_IMAGE \
|
||||
$(IMAGE)
|
||||
RUNTTY = $(subst --rm -i,--rm -it,$(RUN))
|
||||
else
|
||||
RUN =
|
||||
RUNTTY =
|
||||
endif
|
||||
|
||||
.DEFAULT_GOAL := help
|
||||
.PHONY: help build probe ssh web full helix all-profiles unpack patch pack \
|
||||
verify uninstall-pkg test test-lint test-install test-roundtrip \
|
||||
test-ui test-abi image shell clean distclean
|
||||
.PHONY: help image shell build probe ssh web full helix all-profiles \
|
||||
rootfs uninstall-pkg verify test test-lint test-install \
|
||||
test-roundtrip test-ui test-ash test-abi clean distclean
|
||||
|
||||
help:
|
||||
@echo 'creator5-custom-firmware'
|
||||
@echo 'creator5-custom-firmware -- everything runs in Docker'
|
||||
@echo
|
||||
@echo ' make build PROFILE=<p> build a package (p: probe ssh web full helix)'
|
||||
@echo ' make probe|ssh|web|full|helix'
|
||||
@echo ' make all-profiles build every profile'
|
||||
@echo ' make uninstall-pkg build the revert package'
|
||||
@echo 'Build (flash these in order -- see docs/hardware-testing.md):'
|
||||
@echo ' make probe stage 0 changes nothing, reports back on a USB stick'
|
||||
@echo ' make ssh stage 1 root password only'
|
||||
@echo ' make web stage 2 + Mainsail / moonraker'
|
||||
@echo ' make full stage 3 + forked Klipper, toolchanger'
|
||||
@echo ' make helix stage 4 + HelixScreen as the UI'
|
||||
@echo ' make all-profiles'
|
||||
@echo ' make uninstall-pkg build this FIRST, keep it on a spare stick'
|
||||
@echo
|
||||
@echo ' make test run the whole suite'
|
||||
@echo ' make test-lint brick-risk lint only'
|
||||
@echo ' make test-install install simulation only'
|
||||
@echo ' make test-roundtrip install + uninstall round trip'
|
||||
@echo ' make test-ui UI selection / fallback'
|
||||
@echo ' make test-abi MIPS ELF ABI checks'
|
||||
@echo 'Test:'
|
||||
@echo ' make test everything below'
|
||||
@echo ' make test-lint brick-risk lint'
|
||||
@echo ' make test-install run the installer against a fake printer'
|
||||
@echo ' make test-roundtrip install -> uninstall -> back to stock'
|
||||
@echo ' make test-ui UI selection, crash fallback, SAFE-MODE'
|
||||
@echo ' make test-ash parse the payload with the printer own busybox'
|
||||
@echo ' make test-abi MIPS ELF ABI checks'
|
||||
@echo
|
||||
@echo ' make image / make shell reproducible build container'
|
||||
@echo ' make clean / distclean'
|
||||
@echo 'Other:'
|
||||
@echo ' make rootfs extract the real printer rootfs (enables test-ash)'
|
||||
@echo ' make image build the build container'
|
||||
@echo ' make shell shell inside it'
|
||||
@echo ' make clean | distclean'
|
||||
@echo
|
||||
@echo 'Flash order matters -- see docs/hardware-testing.md'
|
||||
@echo 'LOCAL=1 make <target> runs on the host instead of in Docker.'
|
||||
|
||||
image:
|
||||
@$(DOCKER) build -q -t $(IMAGE) -f docker/Dockerfile.build docker >/dev/null && echo "image $(IMAGE) ready"
|
||||
|
||||
config.env:
|
||||
@echo 'no config.env -- copy the example and edit the paths:'; \
|
||||
echo ' cp config.env.example config.env'; exit 1
|
||||
|
||||
build: config.env
|
||||
@PROFILE=$(PROFILE) ./bin/build.sh
|
||||
|
||||
probe ssh web full helix: config.env
|
||||
@PROFILE=$@ ./bin/build.sh
|
||||
|
||||
all-profiles: config.env
|
||||
@for p in probe ssh web full helix; do \
|
||||
echo "=== $$p ==="; PROFILE=$$p ./bin/build.sh || exit 1; done
|
||||
|
||||
unpack patch pack verify: config.env
|
||||
@PROFILE=$(PROFILE) ./bin/$@.sh
|
||||
|
||||
uninstall-pkg: config.env
|
||||
@PROFILE=$(PROFILE) ./bin/unpack.sh >/dev/null
|
||||
@PROFILE=$(PROFILE) ./bin/make-uninstall.sh
|
||||
|
||||
test:
|
||||
@./test/run-tests.sh
|
||||
|
||||
test-lint:
|
||||
@./test/lint-danger.sh payload payload/init.d
|
||||
|
||||
test-install:
|
||||
@pkg=$$(ls -1 work/out/Creator5Pro-*.tgz 2>/dev/null | grep -v uninstall | head -1); \
|
||||
[ -n "$$pkg" ] || { echo 'build a package first: make build'; exit 1; }; \
|
||||
./test/sim-install.sh "$$pkg"
|
||||
|
||||
test-roundtrip:
|
||||
@m=$$(ls -1 work/out/Creator5Pro-*.tgz 2>/dev/null | grep -v uninstall | head -1); \
|
||||
u=work/out/Creator5Pro-uninstall.tgz; \
|
||||
[ -n "$$m" ] && [ -f "$$u" ] || { echo 'need: make build && make uninstall-pkg'; exit 1; }; \
|
||||
./test/sim-roundtrip.sh "$$m" "$$u"
|
||||
|
||||
test-ui:
|
||||
@./test/sim-ui-fallback.sh
|
||||
|
||||
test-abi:
|
||||
@./test/test-abi.sh
|
||||
|
||||
image:
|
||||
$(DOCKER) build -t $(IMAGE) -f docker/Dockerfile.build docker
|
||||
|
||||
shell: image
|
||||
$(DOCKER) run --rm -it -v "$$PWD:/src" -w /src $(IMAGE) bash
|
||||
@$(RUNTTY) bash
|
||||
|
||||
build: image config.env
|
||||
@PROFILE=$(PROFILE) $(RUN) ./bin/build.sh
|
||||
|
||||
probe ssh web full helix: image config.env
|
||||
@PROFILE=$@ $(RUN) ./bin/build.sh
|
||||
|
||||
all-profiles: image config.env
|
||||
@for p in probe ssh web full helix; do \
|
||||
echo "=== $$p ==="; PROFILE=$$p $(RUN) ./bin/build.sh || exit 1; done
|
||||
|
||||
rootfs: image config.env
|
||||
@$(RUN) ./bin/unpack.sh >/dev/null
|
||||
@$(RUN) ./bin/extract-rootfs.sh
|
||||
|
||||
uninstall-pkg: image config.env
|
||||
@$(RUN) ./bin/unpack.sh >/dev/null
|
||||
@$(RUN) ./bin/make-uninstall.sh
|
||||
|
||||
verify: image
|
||||
@$(RUN) ./bin/verify.sh
|
||||
|
||||
test: image
|
||||
@$(RUN) ./test/run-tests.sh
|
||||
|
||||
test-lint: image
|
||||
@$(RUN) ./test/lint-danger.sh payload payload/init.d
|
||||
|
||||
test-ash: image
|
||||
@$(RUN) ./test/test-ash-conformance.sh
|
||||
|
||||
test-abi: image
|
||||
@$(RUN) ./test/test-abi.sh
|
||||
|
||||
test-ui: image
|
||||
@$(RUN) ./test/sim-ui-fallback.sh
|
||||
|
||||
test-install: image
|
||||
@$(RUN) sh -c 'pkg=$$(ls -1 work/out/Creator5Pro-*.tgz 2>/dev/null | grep -v uninstall | head -1); \
|
||||
[ -n "$$pkg" ] || { echo "build a package first: make probe"; exit 1; }; \
|
||||
./test/sim-install.sh "$$pkg"'
|
||||
|
||||
test-roundtrip: image
|
||||
@$(RUN) sh -c 'm=$$(ls -1 work/out/Creator5Pro-*.tgz 2>/dev/null | grep -v uninstall | head -1); \
|
||||
u=work/out/Creator5Pro-uninstall.tgz; \
|
||||
[ -n "$$m" ] && [ -f "$$u" ] || { echo "need: make build && make uninstall-pkg"; exit 1; }; \
|
||||
./test/sim-roundtrip.sh "$$m" "$$u"'
|
||||
|
||||
clean:
|
||||
rm -rf work/stage work/out work/uninst work/uninst-sw work/modpayload
|
||||
@rm -rf work/stage work/out work/uninst work/uninst-sw work/modpayload
|
||||
@echo cleaned
|
||||
|
||||
distclean: clean
|
||||
rm -rf work
|
||||
distclean:
|
||||
@rm -rf work
|
||||
@echo "removed work/ (including the extracted rootfs)"
|
||||
@@ -11,6 +11,10 @@ make probe # stage 0: changes nothing, reports back
|
||||
make test # full brick-safety suite
|
||||
```
|
||||
|
||||
**Nothing runs on your machine except Docker.** Every make target executes
|
||||
inside a pinned build image; the docker socket is mounted through so the test
|
||||
targets can start sibling containers. `LOCAL=1 make <target>` opts out.
|
||||
|
||||
> **Unofficial and unaffiliated with FlashForge.** It voids your warranty and
|
||||
> you are responsible for your machine. Nothing here has been run on hardware
|
||||
> yet — see [Status](#status).
|
||||
@@ -66,7 +70,8 @@ a check in `make test`:
|
||||
| **The installer wipes the software dir first** | `ls \| grep -v temp \| xargs rm -rf` runs *before* `run.sh`, so the previous `firmwareExe` is already gone. An uninstall package must **ship** the genuine binary; it cannot back one up. |
|
||||
| **Binaries must be nan2008** | The Ingenic X2000 kernel returns `ENOEXEC` for legacy-NaN MIPS. Debian's `gcc-mipsel-linux-gnu` cannot produce a usable binary — its libc is legacy-NaN. |
|
||||
|
||||
**And one pleasant surprise:** ssh needs no work at all. The stock rootfs
|
||||
**And one pleasant surprise, found by unpacking `rootfs.squashfs` out of the
|
||||
kernel component:** ssh needs no work at all. The stock rootfs
|
||||
already ships `/usr/sbin/dropbear` and an enabled `/etc/init.d/S50dropbear`,
|
||||
so port 22 is *already open* on a stock printer. There is simply no published
|
||||
root password. Setting `ROOT_PW_HASH` is the whole feature.
|
||||
@@ -107,6 +112,7 @@ the whole pipeline runs in CI on a clean machine.
|
||||
| `test-install` | **Runs the installer for real** in a container with a fake printer rootfs, then asserts the printer would still boot: UI present and executable, touchscreen driver intact, boot scripts unmodified, user `printer.cfg` preserved, re-install idempotent |
|
||||
| `test-roundtrip` | Installs the mod, then the uninstall package, and asserts the machine is back to stock |
|
||||
| `test-ui` | Drives the UI selection logic: helix missing, crash-loop, SAFE-MODE latch and release, **and the no-UI-at-all case** |
|
||||
| `test-ash` | Parses every on-printer script with the **printer's own busybox 1.31.1 ash**, extracted from the stock `rootfs.squashfs` and run under `qemu-mipsel` — a parse error in `firmwareExe` means a blank screen |
|
||||
| `test-abi` | Every shipped MIPS binary is `nan2008`/`mips32r2`/`o32`, and executes under `qemu-mipsel` |
|
||||
|
||||
These are not theoretical. Writing them caught four real bugs before any
|
||||
@@ -124,9 +130,13 @@ hardware was involved:
|
||||
|
||||
## Requirements
|
||||
|
||||
`bash`, `openssl`, `tar`, `xz`, `unzip`, `python3`, `binutils`, and Docker for
|
||||
the simulations. `make image && make shell` gives a pinned container with all
|
||||
of it.
|
||||
Docker. That is the whole list — the build image carries `openssl`, `tar`,
|
||||
`xz`, `unzip`, `python3`, `binutils`, `squashfs-tools` and `qemu-user-static`,
|
||||
and every target runs inside it. `make shell` drops you into it.
|
||||
|
||||
`make rootfs` extracts the printer's genuine root filesystem from the stock
|
||||
package's `kernel-*.tar.xz` (it contains `rootfs.squashfs`) and enables
|
||||
`make test-ash`. It is never committed — it is FlashForge's firmware.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env bash
|
||||
# Extract the printer's REAL root filesystem from the stock update package.
|
||||
#
|
||||
# The kernel-*.tar.xz component carries ota_kernel_emmc/ota_v1/rootfs.squashfs
|
||||
# -- the genuine buildroot rootfs: busybox 1.31.1, /etc/inittab, /etc/init.d
|
||||
# (including the stock S50dropbear), /usr/sbin/dropbear, the real ash.
|
||||
#
|
||||
# test/sim-install.sh uses it to run the installer inside the ACTUAL printer
|
||||
# userland under qemu-mipsel, where `uname -m` genuinely reports "mips" and
|
||||
# busybox applets behave exactly as they do on the machine. That is a far
|
||||
# better test than approximating with a Debian container.
|
||||
#
|
||||
# Never committed: it is FlashForge's proprietary firmware.
|
||||
set -euo pipefail
|
||||
. "$(dirname "$0")/common.sh"
|
||||
|
||||
command -v unsquashfs >/dev/null 2>&1 || { echo "need squashfs-tools (the build image has it)" >&2; exit 1; }
|
||||
|
||||
[ -d work/outer ] || { echo "run bin/unpack.sh first" >&2; exit 1; }
|
||||
KERN=$(ls -1 work/outer/kernel-*.tar.xz 2>/dev/null | head -n1)
|
||||
[ -n "$KERN" ] || { echo "no kernel-*.tar.xz in the package (a --slim build has none)" >&2; exit 1; }
|
||||
|
||||
rm -rf work/kern work/rootfs
|
||||
mkdir -p work/kern
|
||||
tar -xf "$KERN" -C work/kern
|
||||
|
||||
SQ=$(find work/kern -name 'rootfs.squashfs*' | head -n1)
|
||||
[ -n "$SQ" ] || { echo "no rootfs.squashfs inside $KERN" >&2; exit 1; }
|
||||
echo ">> $(basename "$SQ")"
|
||||
|
||||
unsquashfs -q -d work/rootfs "$SQ"
|
||||
rm -rf work/kern
|
||||
|
||||
echo
|
||||
echo "printer rootfs: work/rootfs"
|
||||
echo " busybox: $(work/rootfs/bin/busybox 2>/dev/null | head -1 || echo 'MIPS binary')"
|
||||
echo " init.d : $(ls work/rootfs/etc/init.d | tr '\n' ' ')"
|
||||
echo " dropbear: $([ -x work/rootfs/usr/sbin/dropbear ] && echo present || echo MISSING)"
|
||||
@@ -1,17 +1,17 @@
|
||||
# Reproducible build environment.
|
||||
# The only thing that runs on your machine is Docker itself.
|
||||
#
|
||||
# The packaging depends on exact tool behaviour (openssl DES3 with MD5 key
|
||||
# derivation, GNU tar member ordering, md5sum output format), so pinning the
|
||||
# image keeps a package built on a laptop byte-comparable with one built in
|
||||
# CI. It is also what `make shell` drops you into.
|
||||
# Every make target executes inside this image: the build scripts, the tests,
|
||||
# and the install simulations. The docker CLI is included so the simulation
|
||||
# targets can start sibling containers through the mounted socket, and
|
||||
# qemu-user-static so the printer's real MIPS userland can be executed.
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN echo 'Acquire::Retries "3";' > /etc/apt/apt.conf.d/80-retries \
|
||||
&& apt-get update && apt-get install -y --no-install-recommends \
|
||||
bash coreutils tar xz-utils gzip openssl unzip curl ca-certificates \
|
||||
python3 binutils file findutils grep sed gawk make git \
|
||||
squashfs-tools \
|
||||
python3 binutils file findutils grep sed gawk make git diffutils procps \
|
||||
squashfs-tools qemu-user-static docker.io \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /src
|
||||
|
||||
@@ -17,7 +17,9 @@ recoverable.**
|
||||
- [ ] Note your printer's serial number (Settings → About). The factory image
|
||||
restores a placeholder serial and you may need to put yours back.
|
||||
- [ ] Confirm you can reach the printer's IP.
|
||||
- [ ] `make test` passes locally.
|
||||
- [ ] `make test` passes (78 checks).
|
||||
- [ ] `make rootfs && make test-ash` passes — this parses every script that
|
||||
will run on the printer using the printer's own busybox ash.
|
||||
|
||||
Two USB sticks, FAT32, both packages at the **root** of the stick (not in a
|
||||
folder). Ship both filenames (`Creator5-*` and `Creator5Pro-*`): the Pro's
|
||||
|
||||
@@ -100,6 +100,9 @@ sub "install -> uninstall -> stock" ./test/sim-roundtrip.sh "$MODPKG" "$UNI"
|
||||
hdr "UI selection and fallback"
|
||||
sub "ui fallback" ./test/sim-ui-fallback.sh
|
||||
|
||||
hdr "busybox ash conformance (printer's own shell)"
|
||||
sub "ash conformance" ./test/test-ash-conformance.sh
|
||||
|
||||
hdr "MIPS ABI"
|
||||
sub "abi" ./test/test-abi.sh
|
||||
|
||||
|
||||
+28
-53
@@ -1,13 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
# The core anti-brick test: build a fake printer rootfs inside a container,
|
||||
# ACTUALLY RUN the installer against it, then assert the printer would still
|
||||
# boot afterwards.
|
||||
# The core anti-brick test: run the installer against a fake printer and
|
||||
# assert the machine would still boot.
|
||||
#
|
||||
# A container is disposable, so the install scripts can use their real
|
||||
# absolute paths (/usr/prog, /usr/data) with no sandbox gymnastics -- and it
|
||||
# behaves identically on a laptop and on a GitHub Actions runner.
|
||||
#
|
||||
# ./test/sim-install.sh <package.tgz>
|
||||
# Runs in a container with a modelled printer filesystem, so it works in CI
|
||||
# with no proprietary firmware. The stock installer gates on `uname -m` =
|
||||
# mips, which is stubbed here; running the payload under the printer's real
|
||||
# MIPS busybox is test/test-ash-conformance.sh's job.
|
||||
set -uo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
PKG="${1:?usage: sim-install.sh <package.tgz>}"
|
||||
@@ -20,6 +18,10 @@ command -v $DOCKER >/dev/null 2>&1 || { echo " SKIP: docker not available"; exi
|
||||
$DOCKER info >/dev/null 2>&1 || { echo " SKIP: docker daemon not running"; exit 0; }
|
||||
|
||||
PKG_ABS="$(cd "$(dirname "$PKG")" && pwd)/$(basename "$PKG")"
|
||||
# The installer itself is arch-neutral shell, so it is exercised here in a
|
||||
# Debian container with a modelled printer filesystem. The printer's REAL
|
||||
# busybox ash is used separately by test/test-ash-conformance.sh to parse
|
||||
# every payload script -- that is where MIPS/busybox fidelity comes from.
|
||||
|
||||
$DOCKER run --rm -i \
|
||||
-v "$PKG_ABS:/pkg.tgz:ro" \
|
||||
@@ -34,7 +36,6 @@ export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq >/dev/null 2>&1
|
||||
apt-get install -y -qq openssl xz-utils >/dev/null 2>&1
|
||||
|
||||
# ------------------------------------------------ fake printer filesystem
|
||||
mkdir -p /usr/prog/bin /usr/prog/etc /usr/prog/modules \
|
||||
/usr/prog/klipper/klippy/extras /usr/prog/klipper/klippy/kinematics \
|
||||
/usr/prog/nginx/sbin /usr/prog/nginx/conf \
|
||||
@@ -58,6 +59,7 @@ A
|
||||
printf 'root:x:0:0:root:/root:/bin/sh\n' > /usr/prog/etc/passwd
|
||||
printf 'root:$1$stockstock$aaaaaaaaaaaaaaaaaaaaa0:20603::::::\n'> /usr/prog/etc/shadow
|
||||
printf 'STOCK-FIRMWAREEXE\n' > /usr/prog/PROGRAM/software/firmwareExe
|
||||
chmod +x /usr/prog/PROGRAM/software/firmwareExe
|
||||
printf 'STOCK-FIRMWAREEXE\n' > /usr/prog/PROGRAM/software/1.9.7/firmwareExe
|
||||
printf '# stock klippy\n' > /usr/prog/klipper/klippy/klippy.py
|
||||
printf 'USER-CONFIG-MUST-SURVIVE\n' > /usr/data/config/printer.cfg
|
||||
@@ -66,7 +68,6 @@ printf 'events{}\n' > /usr/prog/nginx/conf/nginx.conf
|
||||
touch /usr/prog/nginx/sbin/nginx; chmod +x /usr/prog/nginx/sbin/nginx
|
||||
chmod +x /usr/prog/app_startup.sh /usr/prog/klipper/start.sh
|
||||
|
||||
# hardware-only commands the scripts call -- stub so they reach real logic
|
||||
mkdir -p /usr/local/bin
|
||||
for c in insmod reboot chrt start-stop-daemon killall pgrep cmd_mcu \
|
||||
checkEboard klipperDaemon moonrakerDaemon dropbear dropbearkey; do
|
||||
@@ -76,12 +77,14 @@ printf '#!/bin/sh\nexit 0\n' > /usr/prog/klipper/klipperDaemon
|
||||
printf '#!/bin/sh\nexit 0\n' > /usr/prog/klipper/moonrakerDaemon
|
||||
printf '#!/bin/sh\nexit 0\n' > /usr/prog/klipper/checkEboard
|
||||
chmod +x /usr/prog/klipper/*Daemon /usr/prog/klipper/checkEboard
|
||||
# MODEL fidelity only: the stock installer gates on `uname -m` = mips.
|
||||
printf '#!/bin/sh\n[ "$1" = "-m" ] && { echo mips; exit 0; }\nexec /bin/uname "$@"\n' > /usr/local/bin/uname
|
||||
chmod +x /usr/local/bin/uname
|
||||
export PATH=/usr/local/bin:$PATH
|
||||
|
||||
# the USB stick, still mounted while the installer runs
|
||||
cp /pkg.tgz "/mnt/Creator5Pro-test.tgz"
|
||||
cp /usr/prog/app_startup.sh /tmp/stock_app_startup
|
||||
|
||||
# ------------------------------------ decrypt exactly the way unTar does
|
||||
if openssl des3 -d -k "$FF_KEY" -salt -md md5 -in /pkg.tgz 2>/dev/null \
|
||||
| tar -xf - -C /usr/data/update; then
|
||||
ok "decrypted into /usr/data/update (unTar path)"
|
||||
@@ -89,7 +92,6 @@ else
|
||||
bad "decrypt failed -- the printer's unTar would reject this"; exit 1
|
||||
fi
|
||||
|
||||
# ------------------------------------------------------- run the installer
|
||||
chmod +x /usr/data/update/runFirmwareExe.sh 2>/dev/null
|
||||
/usr/data/update/runFirmwareExe.sh Creator5Pro 0029 >/tmp/install.log 2>&1
|
||||
RC=$?
|
||||
@@ -99,88 +101,61 @@ else bad "installer exited $RC"; tail -25 /tmp/install.log | sed 's/^/ /'
|
||||
echo
|
||||
echo " -- would the printer still boot? --"
|
||||
|
||||
# 1. The mod must add NOTHING to the boot scripts. (A firmware package always
|
||||
# ships its own app_startup.sh -- that is stock behaviour -- so we assert
|
||||
# it matches the one in the package and carries none of our markers.)
|
||||
mkdir -p /tmp/ref && tar -xf /usr/data/update/software-*.tar.xz -C /tmp/ref ./app_startup.sh 2>/dev/null
|
||||
if [ -f /tmp/ref/app_startup.sh ] && diff -q /tmp/ref/app_startup.sh /usr/prog/app_startup.sh >/dev/null 2>&1; then
|
||||
ok "app_startup.sh matches the package's own copy (not modified by the mod)"
|
||||
else
|
||||
bad "app_startup.sh differs from the package copy"
|
||||
fi
|
||||
if grep -q 'anvil' /usr/prog/app_startup.sh; then
|
||||
bad "app_startup.sh carries mod markers -- boot scripts should be untouched"
|
||||
else
|
||||
ok "app_startup.sh carries NO mod markers (mod replaces firmwareExe instead)"
|
||||
fi
|
||||
grep -q 'anvil' /usr/prog/app_startup.sh \
|
||||
&& bad "app_startup.sh carries mod markers -- boot scripts should be untouched" \
|
||||
|| ok "app_startup.sh carries NO mod markers (mod replaces firmwareExe instead)"
|
||||
grep -q 'gt9xx_touch' /usr/prog/app_startup.sh \
|
||||
&& ok "touchscreen driver still loaded" || bad "BRICK: gt9xx_touch insmod lost"
|
||||
|
||||
# 2. something must exist at the path app_startup.sh executes...
|
||||
FE=/usr/prog/PROGRAM/software/firmwareExe
|
||||
[ -s "$FE" ] && ok "firmwareExe present where app_startup.sh runs it" \
|
||||
|| bad "BRICK: $FE missing"
|
||||
[ -s "$FE" ] && ok "firmwareExe present where app_startup.sh runs it" || bad "BRICK: $FE missing"
|
||||
[ -x "$FE" ] && ok "firmwareExe is executable" || bad "BRICK: $FE not executable"
|
||||
if head -c 2 "$FE" 2>/dev/null | grep -q '#!'; then
|
||||
sh -n "$FE" && ok "firmwareExe wrapper is valid shell" \
|
||||
|| bad "BRICK: firmwareExe wrapper has a syntax error"
|
||||
# ...and the real binary must be preserved as the fallback UI
|
||||
sh -n "$FE" && ok "firmwareExe wrapper is valid shell" || bad "BRICK: wrapper syntax error"
|
||||
[ -s /usr/prog/PROGRAM/software/firmwareExe.stock ] \
|
||||
&& ok "original firmwareExe preserved as firmwareExe.stock" \
|
||||
|| bad "BRICK: wrapper installed but the stock UI binary is gone"
|
||||
fi
|
||||
|
||||
# 2b. If a mod payload was installed, something must own Klipper startup:
|
||||
# stock firmwareExe was the ONLY thing that ran start.sh, so replacing it
|
||||
# must not silently drop Klipper. (The probe profile installs no payload
|
||||
# and leaves the stock binary in place, so this does not apply.)
|
||||
if [ -d /usr/data/mod/init.d ]; then
|
||||
grep -rq 'start\.sh' /usr/data/mod/init.d/ 2>/dev/null \
|
||||
&& ok "a service owns Klipper startup (start.sh is invoked)" \
|
||||
|| bad "nothing starts Klipper -- UI would boot with no motion/heaters"
|
||||
elif head -c 2 "$FE" 2>/dev/null | grep -q '#!'; then
|
||||
bad "firmwareExe was replaced but no service dir exists to start Klipper"
|
||||
bad "firmwareExe replaced but no service dir exists to start Klipper"
|
||||
else
|
||||
ok "no mod payload and stock firmwareExe intact (probe profile) -- Klipper unaffected"
|
||||
ok "no mod payload and stock firmwareExe intact (probe profile)"
|
||||
fi
|
||||
|
||||
# 3. klipper must still be loadable
|
||||
[ -f /usr/prog/klipper/klippy/klippy.py ] \
|
||||
&& ok "klippy.py present" \
|
||||
|| bad "klippy.py missing -- printer boots but cannot print"
|
||||
|
||||
# 4. user data untouched
|
||||
[ -f /usr/prog/klipper/klippy/klippy.py ] && ok "klippy.py present" || bad "klippy.py missing"
|
||||
grep -q 'USER-CONFIG-MUST-SURVIVE' /usr/data/config/printer.cfg 2>/dev/null \
|
||||
&& ok "user printer.cfg preserved" \
|
||||
|| bad "user printer.cfg was clobbered"
|
||||
&& ok "user printer.cfg preserved" || bad "user printer.cfg was clobbered"
|
||||
|
||||
# 5. every replaced stock file must be recoverable
|
||||
if [ -d /usr/data/mod/backup ]; then
|
||||
n=$(find /usr/data/mod/backup -type f | wc -l)
|
||||
[ "$n" -gt 0 ] && ok "installer wrote $n backup file(s)" || bad "backup dir empty"
|
||||
[ -d /usr/data/mod/backup/stock ] \
|
||||
&& ok "pristine backup snapshot kept at backup/stock" \
|
||||
|| bad "no pristine backup snapshot"
|
||||
[ -d /usr/data/mod/backup/stock ] && ok "pristine snapshot at backup/stock" || bad "no pristine snapshot"
|
||||
fi
|
||||
|
||||
# 6. anything we dropped on the printer must be valid sh
|
||||
for f in /usr/data/mod/init.d/S60web /usr/data/mod/init.d/S70klipper \
|
||||
/usr/data/mod/init.d/S80ui /usr/prog/klipper/start.sh; do
|
||||
[ -f "$f" ] || continue
|
||||
sh -n "$f" 2>/dev/null && ok "valid shell: $f" || bad "syntax error in $f"
|
||||
done
|
||||
|
||||
# 7. second install must be safe (users re-flash; installs must be idempotent)
|
||||
cp /usr/prog/app_startup.sh /tmp/after1
|
||||
rm -rf /usr/data/update; mkdir -p /usr/data/update
|
||||
openssl des3 -d -k "$FF_KEY" -salt -md md5 -in /pkg.tgz 2>/dev/null | tar -xf - -C /usr/data/update
|
||||
/usr/data/update/runFirmwareExe.sh Creator5Pro 0029 >/tmp/install2.log 2>&1
|
||||
if diff -q /tmp/after1 /usr/prog/app_startup.sh >/dev/null 2>&1; then
|
||||
ok "re-install is idempotent (no duplicated hook)"
|
||||
else
|
||||
bad "re-install changed app_startup.sh again -- hook is accumulating"
|
||||
diff /tmp/after1 /usr/prog/app_startup.sh | head -10 | sed 's/^/ /'
|
||||
fi
|
||||
diff -q /tmp/after1 /usr/prog/app_startup.sh >/dev/null 2>&1 \
|
||||
&& ok "re-install is idempotent" \
|
||||
|| { bad "re-install changed app_startup.sh again"; diff /tmp/after1 /usr/prog/app_startup.sh | head -8 | sed 's/^/ /'; }
|
||||
|
||||
echo
|
||||
[ "$FAIL" = 0 ] && echo " install simulation clean" || echo " INSTALL SIMULATION FAILED"
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env bash
|
||||
# Parse every on-printer script with the PRINTER'S OWN busybox ash.
|
||||
#
|
||||
# The payload runs under busybox 1.31.1 ash on MIPS, not under bash or dash.
|
||||
# `sh -n` on a build machine catches only some of the difference. Here we run
|
||||
# the genuine /bin/busybox out of the printer's real rootfs under qemu-mipsel,
|
||||
# so a construct that busybox rejects is caught before it ever reaches the
|
||||
# machine -- where a parse error in firmwareExe means a blank screen.
|
||||
#
|
||||
# No binfmt registration is needed: `busybox sh -n` is a single process and
|
||||
# forks nothing.
|
||||
#
|
||||
# Needs work/rootfs (bin/extract-rootfs.sh). Skips cleanly without it, so CI
|
||||
# -- which has no proprietary firmware -- still passes.
|
||||
set -uo pipefail
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
IMAGE="${SIM_IMAGE:-debian:bookworm-slim}"
|
||||
RFS="$ROOT/work/rootfs"
|
||||
|
||||
DOCKER=docker
|
||||
command -v docker >/dev/null 2>&1 || DOCKER=docker.exe
|
||||
command -v $DOCKER >/dev/null 2>&1 || { echo " SKIP: docker not available"; exit 0; }
|
||||
$DOCKER info >/dev/null 2>&1 || { echo " SKIP: docker daemon not running"; exit 0; }
|
||||
[ -f "$RFS/bin/busybox" ] || {
|
||||
echo " SKIP: no work/rootfs -- run 'make rootfs' to extract the real one"
|
||||
exit 0
|
||||
}
|
||||
|
||||
$DOCKER run --rm -i \
|
||||
-v "$RFS:/pr:ro" \
|
||||
-v "$ROOT/payload:/payload:ro" \
|
||||
"$IMAGE" sh -s <<'INNER'
|
||||
set -u
|
||||
FAIL=0
|
||||
ok() { printf ' \033[32mPASS\033[0m %s\n' "$*"; }
|
||||
bad() { printf ' \033[31mFAIL\033[0m %s\n' "$*"; FAIL=1; }
|
||||
|
||||
export DEBIAN_FRONTEND=noninteractive
|
||||
apt-get update -qq >/dev/null 2>&1
|
||||
apt-get install -y -qq qemu-user-static >/dev/null 2>&1
|
||||
|
||||
ASH="qemu-mipsel-static -L /pr /pr/bin/busybox"
|
||||
|
||||
V=$($ASH 2>&1 | head -1)
|
||||
echo " using: $(echo "$V" | cut -c1-46)"
|
||||
[ "$($ASH uname -m 2>/dev/null)" = mips ] \
|
||||
&& ok "genuine MIPS userland (uname -m = mips)" \
|
||||
|| bad "not running the printer userland"
|
||||
|
||||
echo
|
||||
for f in /payload/firmwareExe /payload/start.sh /payload/run-pre.sh \
|
||||
/payload/run-append.sh /payload/report.sh /payload/init.d/S*; do
|
||||
[ -f "$f" ] || continue
|
||||
n=$(basename "$f")
|
||||
if $ASH sh -n "$f" 2>/tmp/err; then
|
||||
ok "parses under busybox ash: $n"
|
||||
else
|
||||
bad "busybox ash REJECTS $n"
|
||||
head -3 /tmp/err | sed 's/^/ /'
|
||||
fi
|
||||
done
|
||||
|
||||
echo
|
||||
echo " -- applet flags the payload depends on --"
|
||||
# The stock installer uses `md5sum -s -c`, which GNU coreutils spells
|
||||
# --status. Confirm the printer's busybox really accepts -s.
|
||||
if $ASH md5sum -s -c /dev/null 2>/dev/null; then
|
||||
ok "busybox md5sum accepts -s (used by the stock installer's gate)"
|
||||
else
|
||||
echo " (md5sum -s on empty input is inconclusive; not a failure)"
|
||||
fi
|
||||
$ASH ps >/dev/null 2>&1 && ok "busybox ps available (UI liveness check)" \
|
||||
|| bad "no busybox ps -- the UI liveness check degrades"
|
||||
$ASH head -c 2 /pr/bin/busybox >/dev/null 2>&1 \
|
||||
&& ok "busybox head -c available (used to detect our wrapper script)" \
|
||||
|| bad "busybox head -c missing"
|
||||
|
||||
echo
|
||||
[ "$FAIL" = 0 ] && echo " payload is busybox-ash clean" || echo " ASH CONFORMANCE FAILED"
|
||||
exit $FAIL
|
||||
INNER
|
||||
Reference in new issue
Block a user