Retire the probe profile: there is one package now

The probe profile was a bring-up aid. It set BUILD_APPLY=0, reinstalled
the stock component byte-for-byte and wrote a report to the stick, so you
could prove the update chain worked on your own machine -- the filename
glob, the DES3 key, the md5 gate, run.sh execution -- before flashing
anything that changed a file. That rehearsal has served its purpose.

With it gone there was exactly one profile left, so the whole PROFILE
indirection goes too: profiles/ is deleted and its six BUILD_* flags are
plain defaults in bin/common.sh, which also means config.env now
overrides them instead of being silently overridden. make default folds
into make build; all-profiles and RECOVERY_PROFILE are gone with it.

What is NOT gone is the undo button: flashing the stock FlashForge
package still restores every file the mod touches, and case-recovery.sh
still proves it. What is gone is the dry run.

Note the loss: PROG_MB/DATA_MB used to come out of the probe report.
The docs now point at df -h over ssh, which you only have after a
successful flash.
This commit is contained in:
Oleksandr Shyshatskyi committed 2026-08-23 22:15:26 +03:00
1 parent 3369a5ee1b
commit 12bbd1e7dc
21 files changed
+143 -370

No files matched your search

-1
View File
@@ -132,7 +132,6 @@ jobs:
STOCK_TGZ_CREATOR5="$PWD/stock/Creator5-stock.tgz"
STOCK_TGZ=""
KLIPPER_FORK=""
DEFAULT_PROFILE=probe
ROOT_PW_HASH='\$6\$ci\$abcdefghijklmnopqrstuvwxyz'
FF_KEY='FFP0331&*%root'
CFG
+1 -10
View File
@@ -25,12 +25,6 @@ on:
push:
tags: [ 'v*' ]
workflow_dispatch:
inputs:
profile:
description: 'Profile to build'
default: default
type: choice
options: [ default, probe ]
permissions:
contents: write
@@ -38,8 +32,6 @@ permissions:
jobs:
release:
runs-on: ubuntu-latest
env:
PROFILE: ${{ inputs.profile || 'default' }}
steps:
- uses: actions/checkout@v4
@@ -107,7 +99,6 @@ jobs:
STOCK_TGZ_CREATOR5="$PWD/stock/Creator5-stock.tgz"
STOCK_TGZ=""
KLIPPER_FORK=""
DEFAULT_PROFILE=$PROFILE
ROOT_PW_HASH='${{ secrets.ROOT_PW_HASH }}'
FF_KEY='FFP0331&*%root'
CFG
@@ -125,7 +116,7 @@ jobs:
creator5-fw-build ./test/run-tests.sh
- name: Build both models
run: make release PROFILE=$PROFILE ASSET_ROOT=$PWD
run: make release ASSET_ROOT=$PWD
- name: End-to-end update test on the packages that will actually ship
# The suite above tested a package built from the same tree; this
+24 -32
View File
@@ -4,14 +4,12 @@
# 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 pre-flight package (changes nothing)
# make default build the firmware package
# make build build the firmware package
# 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 ?= default
# Packages carry only the software component by default: the stock installer
# skips absent components, so the kernel and the MCU/board firmware are left
# alone. FULL=1 carries all four (and reflashes the MCU).
@@ -31,7 +29,7 @@ DOCKER_SOCK := /var/run/docker.sock
# directories. Keeping the path identical on both sides avoids that entirely.
# config.env usually points at a stock package OUTSIDE this repo, so the
# directory holding it must be mounted too. Override when yours lives
# elsewhere: make ASSET_ROOT=/path/to/parent default
# elsewhere: make ASSET_ROOT=/path/to/parent build
ASSET_ROOT ?= $(firstword $(wildcard /mnt/c /Users /home))
# Two runners, because the two lanes need different things:
@@ -45,7 +43,7 @@ ASSET_ROOT ?= $(firstword $(wildcard /mnt/c /Users /home))
DOCKER_BASE = $(DOCKER) run --rm -i \
-v "$(CURDIR)":"$(CURDIR)" -w "$(CURDIR)" \
$(if $(ASSET_ROOT),-v "$(ASSET_ROOT)":"$(ASSET_ROOT)",) \
-e PROFILE -e MODEL -e TARGET_MACHINE -e CONFIG_ENV -e ALLOW_STALE_CHELPER
-e MODEL -e TARGET_MACHINE -e CONFIG_ENV -e ALLOW_STALE_CHELPER
ifeq ($(LOCAL),)
RUN = $(DOCKER_BASE) $(IMAGE)
@@ -62,23 +60,21 @@ else
endif
.DEFAULT_GOAL := help
.PHONY: help image shell build vendor probe default all-profiles \
.PHONY: help image shell build vendor \
rootfs verify test test-lint test-install test-applets \
printer-image printer-image-push \
test-recovery test-ui test-mcu test-ash test-abi test-chelper test-macros test-chamber test-model release clean distclean
test-recovery test-ui test-mcu test-ash test-abi test-chelper test-macros test-chamber test-basecfg test-model release clean distclean
help:
@echo 'creator5-custom-firmware -- everything runs in Docker'
@echo
@echo 'Build (flash these in order -- see docs/hardware-testing.md):'
@echo ' make probe pre-flight changes nothing, reports back on a USB stick'
@echo ' make default the firmware Klipper fork, toolchanger, Mainsail,'
@echo 'Build (see docs/hardware-testing.md before you flash):'
@echo ' make build the firmware Klipper fork, toolchanger, Mainsail,'
@echo ' ssh and HelixScreen'
@echo ' make all-profiles'
@echo ' make release PROFILE=<p> build BOTH models into dist/'
@echo ' make release build BOTH models into dist/'
@echo
@echo 'Models: packages are model-specific and refuse to install on the'
@echo 'other one. MODEL=Creator5 make default builds the non-Pro variant.'
@echo 'other one. MODEL=Creator5 make build builds the non-Pro variant.'
@echo
@echo 'Recovery: keep a copy of the STOCK FlashForge .tgz on a spare stick.'
@echo 'Flashing it restores every file the mod touches (see make test-recovery).'
@@ -97,6 +93,7 @@ help:
@echo ' make test-chelper c_helper.so exports everything klippy declares'
@echo ' make test-macros the ff-*.cfg gcode macros parse as Jinja'
@echo ' make test-chamber the chamber heater is off on the Creator 5'
@echo ' make test-basecfg our printer.base.cfg still matches the stock one'
@echo
@echo 'test-install, test-recovery, test-ui, test-mcu and test-ash run inside'
@echo 'a replica of the printer: the real rootfs.squashfs under qemu-mipsel, with'
@@ -131,33 +128,26 @@ shell: image
@$(RUNTTY) bash
# ===========================================================================
# BUILD LANE -- produces the package you flash. Reads config.env + profiles/,
# ships payload/ and assets/, and touches nothing under test/.
# BUILD LANE -- produces the package you flash. Reads config.env, ships
# payload/ and assets/, and touches nothing under test/.
# ===========================================================================
# Mainsail and HelixScreen are not vendored in the repo. bin/build.sh fetches
# whatever the profile needs; this target pre-fetches everything, and is a
# what the build needs; this target pre-fetches everything, and is a
# no-op once vendor/ holds files with the sha256 that versions.env pins.
vendor: image config.env
@$(RUN) ./bin/fetch-assets.sh --all
build: image config.env
@PROFILE=$(PROFILE) $(RUN) ./bin/build.sh $(PACKARGS)
probe default: image config.env
@PROFILE=$@ $(RUN) ./bin/build.sh $(PACKARGS)
all-profiles: image config.env
@for p in probe default; do \
echo "=== $$p ==="; PROFILE=$$p $(RUN) ./bin/build.sh || exit 1; done
@$(RUN) ./bin/build.sh $(PACKARGS)
# One package per model, collected in dist/. They cannot share content: the
# two stock packages ship different firmwareExe binaries.
release: image config.env
@rm -rf dist && mkdir -p dist
@for m in Creator5Pro Creator5; do \
echo "=== $$m / $(PROFILE) ==="; \
MODEL=$$m PROFILE=$(PROFILE) $(RUN) ./bin/build.sh $(PACKARGS) || exit 1; \
echo "=== $$m ==="; \
MODEL=$$m $(RUN) ./bin/build.sh $(PACKARGS) || exit 1; \
MODEL=$$m $(RUN) ./bin/verify.sh || exit 1; \
cp work/out/$$m-*.tgz dist/ || exit 1; \
done
@@ -223,6 +213,9 @@ test-macros: image
test-chamber: image
@$(RUN) python3 ./test/test-chamber.py
test-basecfg: image
@$(RUN) python3 ./test/test-base-cfg.py
test-applets: image
@$(RUN) python3 ./test/test-applets.py
@@ -237,22 +230,21 @@ test-mcu: image
# there). Look in both.
test-install: image
@$(RUNSIM) bash -c 'pkg=$$(ls -1 dist/$(or $(MODEL),Creator5Pro)-*.tgz work/out/$(or $(MODEL),Creator5Pro)-*.tgz 2>/dev/null | head -1); \
[ -n "$$pkg" ] || { echo "build a package first: make default (or make release)"; exit 1; }; \
[ -n "$$pkg" ] || { echo "build a package first: make build (or make release)"; exit 1; }; \
echo "package: $$pkg"; ./test/sim-install.sh "$$pkg"'
# Recovery = flash the stock package you already have. This proves it works.
#
# It builds its own package rather than reusing whatever is lying around: the
# test is meaningless against a profile that does not replace the UI, and
# `probe` deliberately does not. RECOVERY_PROFILE=<p> to test another one.
RECOVERY_PROFILE ?= default
# test is only meaningful against a package that really does replace the UI,
# and whatever is in work/out may be anything.
test-recovery: image config.env
@MODEL=$(or $(MODEL),Creator5Pro) PROFILE=$(RECOVERY_PROFILE) $(RUNSIM) bash -c '. ./bin/common.sh; \
@MODEL=$(or $(MODEL),Creator5Pro) $(RUNSIM) bash -c '. ./bin/common.sh; \
./bin/build.sh $(PACKARGS) >/dev/null || exit 1; \
m=$$(ls -1 work/out/$$TARGET_MACHINE-*.tgz 2>/dev/null | head -1); \
[ -n "$$m" ] || { echo "no package built for $$TARGET_MACHINE"; exit 1; }; \
[ -f "$$STOCK_TGZ" ] || { echo "no stock package for $$TARGET_MACHINE"; exit 1; }; \
echo "mod: $$m ($(RECOVERY_PROFILE))"; echo "stock: $$STOCK_TGZ"; \
echo "mod: $$m"; echo "stock: $$STOCK_TGZ"; \
./test/sim-roundtrip.sh "$$m" "$$STOCK_TGZ"'
clean:
+3 -3
View File
@@ -74,10 +74,10 @@ Not sure which you have? On the printer: **Settings → About**.
Afterwards, `http://<printer-ip>/` gets you Mainsail. The install log is on
the printer at `/usr/data/anvil-install.log`.
### Before you do
### Two things to do first
One file per model, one flash — but do two things first, because they are the
cheap version of every problem people hit:
One file per model, one flash — but do these before you start, because they
are the cheap version of every problem people hit:
* **Check the model.** A `Creator5Pro-` package will not install on a Creator 5
and vice versa; the printer refuses it outright, which is annoying rather
+3 -8
View File
@@ -1,14 +1,9 @@
#!/usr/bin/env bash
# One-shot: fetch -> unpack -> patch -> pack, for one profile.
# One-shot: fetch -> unpack -> patch -> pack.
#
# ./bin/build.sh the profile from config.env (DEFAULT_PROFILE)
# ./bin/build.sh probe pre-flight: changes nothing
# ./bin/build.sh default --slim pass-through options go to pack.sh
# ./bin/build.sh build the package
# ./bin/build.sh --slim options are passed through to pack.sh
set -euo pipefail
case "${1:-}" in
''|-*) : ;;
*) export PROFILE="$1"; shift ;;
esac
D="$(dirname "$0")"
"$D/fetch-assets.sh"
"$D/unpack.sh"
+22 -14
View File
@@ -1,10 +1,5 @@
# Sourced by every script in bin/. Resolves the repo root, loads config.env
# and the selected profile, and exports the feature flags.
#
# Profile selection, in order of precedence:
# PROFILE=probe ./bin/build.sh environment
# ./bin/build.sh --profile probe argument (parsed by the caller)
# PROFILE in config.env default
# and exports the feature flags.
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")/.." && pwd)"
cd "$ROOT"
@@ -21,14 +16,27 @@ fi
# shellcheck disable=SC1090
. "$CONFIG_ENV"
PROFILE="${PROFILE:-${DEFAULT_PROFILE:-default}}"
if [ -f "profiles/$PROFILE.env" ]; then
# shellcheck disable=SC1090
. "profiles/$PROFILE.env"
else
echo "unknown profile '$PROFILE' (have: $(ls profiles/*.env 2>/dev/null | xargs -n1 basename 2>/dev/null | sed 's/\.env$//' | tr '\n' ' '))" >&2
exit 1
fi
# WHAT GOES INTO THE PACKAGE. There is exactly one build -- the firmware:
# forked Klipper with toolchanger support, Mainsail/Moonraker, ssh, and
# HelixScreen driving the touchscreen in place of FlashForge's UI.
#
# These used to live in profiles/*.env, chosen by a PROFILE variable, back
# when a second "changes nothing, writes a report" package existed alongside
# this one. With one build left the indirection bought nothing but a layer to
# read through, so the flags are plain defaults here and config.env -- sourced
# above -- still overrides any of them.
#
# FlashForge's firmwareExe is REPLACED, not kept: HelixScreen is the only UI.
# If it crash-loops, S80ui latches SAFE-MODE after 3 failed boots and the
# printer boots headless instead of looping. ssh and Mainsail are your
# recovery path if the screen is dark, and a USB stick with the STOCK
# FlashForge package on it is the uninstall (proven by `make test-recovery`).
BUILD_KLIPPER="${BUILD_KLIPPER:-fork}"
BUILD_TOOLCHANGE="${BUILD_TOOLCHANGE:-1}"
BUILD_MAINSAIL="${BUILD_MAINSAIL:-1}"
BUILD_HELIX="${BUILD_HELIX:-1}"
MOD_SSH="${MOD_SSH:-1}"
MOD_WEB="${MOD_WEB:-1}"
# Third-party payload pieces (Mainsail, HelixScreen). They are downloaded on
# demand rather than vendored, so the repo carries no binaries and no binary
+22 -49
View File
@@ -7,9 +7,6 @@ set -euo pipefail
SW=work/software
[ -d "$SW" ] || { echo "run bin/unpack.sh first" >&2; exit 1; }
echo "profile: $PROFILE -- ${PROFILE_DESC:-}"
echo
MARK_BEGIN="# >>> anvil begin >>>"
MARK_END="# <<< anvil end <<<"
@@ -29,35 +26,6 @@ MP=work/modpayload
rm -rf "$MP" "$SW/mod" # $SW/mod: leftover from an older layout
mkdir -p "$MP/bin" "$MP/nginx" "$MP/www" "$MP/config"
# ---------------------------------------------------------------------------
# BUILD_APPLY=0 (the "probe" profile): reinstall the stock component untouched
# and add only a diagnostic report. Nothing on the printer changes.
# ---------------------------------------------------------------------------
if [ "${BUILD_APPLY:-1}" = "0" ]; then
say "BUILD_APPLY=0 -- stock component left byte-for-byte unmodified"
if [ "${BUILD_REPORT:-0}" = "1" ]; then
say "adding diagnostic report step"
python3 - "$SW/run.sh" payload/report.sh <<'PY2'
import sys, re
run, extra = sys.argv[1], sys.argv[2]
B, E = "# >>> anvil begin >>>", "# <<< anvil end <<<"
s = open(run, encoding='utf-8', errors='surrogateescape').read()
s = re.sub(re.escape(B) + r".*?" + re.escape(E) + r"\n?", "", s, flags=re.S)
block = open(extra, encoding='utf-8').read()
m = list(re.finditer(r"^exit 0\s*$", s, flags=re.M))
i = m[-1].start() if m else len(s)
s = s[:i] + B + "\n" + block + E + "\n\n" + s[i:]
open(run, 'w', encoding='utf-8', errors='surrogateescape').write(s)
PY2
fi
rm -rf "$MP"
echo
echo "Patched (probe profile -- no changes to the printer)."
echo " software component: $(du -sh "$SW" | cut -f1)"
echo "Now run bin/pack.sh"
exit 0
fi
# ---------------------------------------------------------------- 1. Klipper
if [ "${BUILD_KLIPPER:-fork}" = "fork" ] && [ -d "${KLIPPER_FORK:-}/klippy" ]; then
say "Klipper: installing fork tree from $KLIPPER_FORK"
@@ -126,22 +94,34 @@ if [ "${BUILD_TOOLCHANGE:-1}" = "1" ]; then
# .cfg files belong on the data partition; run.sh installs them without
# clobbering a config the user already tuned.
cp -f payload/klipper/config/ff-*.cfg "$MP/config/"
# Our printer.base.cfg is FlashForge's with the chamber block replaced by
# [include printer.chamber.cfg] -- Klipper can override an option but
# cannot un-declare a section, and the plain Creator 5 has no chamber
# heating element, so its heater has to be absent rather than neutralised.
# test/test-base-cfg.py reconstructs the stock file from ours plus the
# Pro's chamber file and fails if FlashForge's has changed.
cp -f payload/klipper/config/printer.base.cfg "$SW/klipper/config/printer.base.cfg"
# Anything that differs between models exists once per model, named
# <file>.creator5 / <file>.creator5pro, and the matching one is installed
# under its real name. Nothing is edited: the suffixed file IS the
# difference. The suffixed names do not match the ff-*.cfg glob above, so
# a variant cannot leak into the wrong package.
# difference. printer.*.cfg belongs beside printer.base.cfg on the program
# partition; ff-*.cfg belongs on the data partition with the rest.
SUFFIX=$(printf '%s' "$TARGET_MACHINE" | tr 'A-Z' 'a-z')
for variant in payload/klipper/config/*."$SUFFIX"; do
[ -e "$variant" ] || continue
cp -f "$variant" "$MP/config/$(basename "$variant" ".$SUFFIX")"
say "Model: $(basename "$variant" ".$SUFFIX") for $TARGET_MACHINE"
base=$(basename "$variant" ".$SUFFIX")
case "$base" in
printer.*) dest="$SW/klipper/config/$base" ;;
*) dest="$MP/config/$base" ;;
esac
cp -f "$variant" "$dest"
say "Model: $base for $TARGET_MACHINE"
done
# ff-model.cfg is what tells ff-chamber.cfg whether this machine has a
# chamber heater. Missing, every macro that reads it fails at runtime, so
# this is a broken build rather than a silent default.
[ -f "$MP/config/ff-model.cfg" ] \
|| { echo "no ff-model.cfg.$SUFFIX for TARGET_MACHINE=$TARGET_MACHINE" >&2; exit 1; }
# printer.base.cfg includes it unconditionally, so without it klippy will
# not start at all. A broken build, not a silent default.
[ -f "$SW/klipper/config/printer.chamber.cfg" ] \
|| { echo "no printer.chamber.cfg.$SUFFIX for TARGET_MACHINE=$TARGET_MACHINE" >&2; exit 1; }
else
skip "Toolchange"
fi
@@ -241,16 +221,13 @@ mkdir -p "$MP/init.d"
cp -f payload/init.d/S* "$MP/init.d/"
chmod +x "$MP/init.d"/S*
sed -e "s/^MOD_WEB=.*/MOD_WEB=${MOD_WEB:-1}/" \
-e "s/^MOD_CAM=.*/MOD_CAM=${MOD_CAM:-1}/" \
-e "s/^MOD_SSH=.*/MOD_SSH=${MOD_SSH:-1}/" \
-e "s/^MOD_WIFI=.*/MOD_WIFI=${MOD_WIFI:-1}/" \
payload/anvil.conf > "$MP/anvil.conf"
# --------------------------------------------------- 10. run.sh install step
say "run.sh: injecting mod install blocks (pre + post)"
# The diagnostic report is a DEBUG payload: report.sh copies /etc, passwd and
# shadow onto the USB stick. Only the probe profile turns it on -- see
# profiles/*.env. It rides at the end of the post block rather than in one of
# its own, so there is still exactly one injected region to strip on re-run.
POST=work/.run-post.sh
# 1 only when ssh is on and nothing was baked in: a package is one file that
# many people flash, so a baked-in default would be the same password on every
@@ -263,10 +240,6 @@ else
fi
sed -e "s/^MOD_PW_AUTO=.*/MOD_PW_AUTO=$PW_AUTO/" \
payload/run-append.sh > "$POST"
if [ "${BUILD_REPORT:-0}" = "1" ]; then
say "run.sh: including the diagnostic report step (BUILD_REPORT=1)"
cat payload/report.sh >> "$POST"
fi
python3 - "$SW/run.sh" payload/run-pre.sh "$POST" <<'PY'
import sys, re
run, pre_f, post_f = sys.argv[1], sys.argv[2], sys.argv[3]
+4 -4
View File
@@ -160,8 +160,8 @@ if [ -n "$FN_MACHINE" ]; then
fi
# 10 --------------------------------------------------- the ship boundary
# Only payload/ and assets/ may end up on a printer; bin/, test/, docker/ and
# profiles/ are host-side. Nothing enforces that except this check.
# Only payload/ and assets/ may end up on a printer; bin/, test/ and docker/
# are host-side. Nothing enforces that except this check.
#
# It compares by CONTENT, not by name: a name blacklist would trip over any
# file Mainsail or HelixScreen happens to call common.sh, while a byte-for-byte
@@ -173,13 +173,13 @@ if [ -f "$T/anvil.tar.xz" ]; then
xz -dc "$T/anvil.tar.xz" 2>/dev/null | tar -xf - -C "$T/ship/.anvil" 2>/dev/null \
|| tar -xf "$T/anvil.tar.xz" -C "$T/ship/.anvil" 2>/dev/null || true
fi
find bin test docker profiles -type f 2>/dev/null | xargs -r md5sum 2>/dev/null \
find bin test docker -type f 2>/dev/null | xargs -r md5sum 2>/dev/null \
| awk '{print $1}' | sort -u > "$T/host.md5"
find "$T/ship" -type f 2>/dev/null | xargs -r md5sum 2>/dev/null > "$T/ship.md5"
LEAK=$(awk 'NR==FNR{h[$1];next} $1 in h {print}' "$T/host.md5" "$T/ship.md5" \
| sed "s|$T/ship/||")
if [ -z "$LEAK" ]; then
ok "package carries nothing from bin/, test/, docker/ or profiles/"
ok "package carries nothing from bin/, test/ or docker/"
else
bad "host-side files leaked into the package:"
echo "$LEAK" | head -10 | sed 's/^/ /'
+26 -38
View File
@@ -6,12 +6,11 @@ it, it does not replace it. Everything else is Docker.
```sh
cp config.env.example config.env # what to build, and what ships
$EDITOR config.env # point it at your stock package
make probe # pre-flight: changes nothing, reports back
make default # the firmware
make build # the firmware
```
The package lands in `work/out/`. `make release PROFILE=<p>` builds both
models into `dist/`.
The package lands in `work/out/`. `make release` builds both models into
`dist/`.
## Requirements
@@ -41,31 +40,27 @@ MCU/board firmware are left untouched — MCU flashing is the riskiest thing in
a package and there is no reason to run it for a userspace mod. `FULL=1 make
<target>` carries all four.
## Profiles
## One build
There are two, and only two. `profiles/*.env` set the `BUILD_*` flags:
There is one package: the forked Klipper with toolchanger support,
Mainsail/Moonraker, ssh, and HelixScreen as the UI. `bin/common.sh` defaults
the `BUILD_*` flags to exactly that, and `config.env` can override any of
them if you want a piece left out.
| Profile | Klipper | Mainsail | HelixScreen | ssh | Report |
|---|---|---|---|---|---|
| `probe` | stock | – | – | – | ✓ |
| `default` | fork | ✓ | ✓ (as the UI) | ✓ | – |
`probe` is a pre-flight check: it reinstalls the stock software byte-for-byte
and writes a diagnostic report to the USB stick, proving the update chain
works on *your* machine before anything changes. `default` is the firmware.
There used to be five profiles — `ssh`, `web`, `full` and `helix` climbing one
feature at a time. They are collapsed into `default`: the intermediate rungs
each needed their own flash-and-verify cycle, and the recovery story is the
same at every rung (flash the stock package), so the extra loops bought
caution nobody was spending.
There used to be a `profiles/` directory choosing between builds — five of
them at one point (`ssh`, `web`, `full`, `helix`) climbing one feature at a
time, then two (`probe`, `default`). The intermediate rungs each cost their
own flash-and-verify cycle while the recovery story stayed the same at every
one of them (flash the stock package), so they bought caution nobody was
spending; the last of them, `probe`, changed nothing on the printer and only
wrote a diagnostic report to the USB stick, which was a bring-up aid rather
than something to keep shipping. With one build left, the selection layer went
with them.
```sh
make probe # pre-flight
make default # the firmware
make all-profiles # both
make release PROFILE=default # both models, into dist/
MODEL=Creator5 make default # just the non-Pro
make build # the firmware
make release # both models, into dist/
MODEL=Creator5 make build # just the non-Pro
```
## Two kinds of flag
@@ -73,18 +68,13 @@ MODEL=Creator5 make default # just the non-Pro
This distinction is the one to keep straight:
* **`BUILD_*`** decides what goes *into* a package. Read at build time only,
owned by `profiles/*.env`, never present on the printer.
(`BUILD_APPLY`, `BUILD_REPORT`, `BUILD_KLIPPER`, `BUILD_TOOLCHANGE`,
`BUILD_MAINSAIL`, `BUILD_HELIX`.)
defaulted in `bin/common.sh`, never present on the printer.
(`BUILD_KLIPPER`, `BUILD_TOOLCHANGE`, `BUILD_MAINSAIL`, `BUILD_HELIX`.)
* **`MOD_*`** are runtime switches. They are written into
`/usr/data/anvil/anvil.conf`, which the printer re-reads at every boot, so
they can be changed over ssh afterwards and survive a mod update.
(`MOD_WEB`, `MOD_SSH`, `MOD_WIFI`.)
`BUILD_REPORT` is the diagnostic report, and it is a debug payload:
`payload/report.sh` copies `/etc`, `passwd` and `shadow` onto the USB stick.
Only the `probe` profile turns it on.
## Third-party pieces are downloaded, not vendored
Mainsail and the HelixScreen build for this printer are large binaries, so the
@@ -108,8 +98,8 @@ Setting `MAINSAIL_ZIP` or `HELIX_TGZ` in `config.env` overrides the download
and builds against your own local copy. An explicit path is used as-is and is
never checksummed.
If a profile asks for Mainsail or HelixScreen and the file is not there, the
build fails. It used to skip silently, which shipped a package with an empty
If the build asks for Mainsail or HelixScreen and the file is not there, it
fails. It used to skip silently, which shipped a package with an empty
web root and no way to notice.
## The root password
@@ -156,7 +146,6 @@ payload/ POSIX sh, busybox ash -- runs ON the printer
anvil.conf runtime switches, preserved across mod updates
run-pre.sh backups, injected at the TOP of the stock run.sh
run-append.sh payload install, injected before its exit
report.sh the pre-flight diagnostic (BUILD_REPORT=1 -- probe only)
assets/ nginx.conf, moonraker.conf
```
@@ -164,7 +153,6 @@ assets/ nginx.conf, moonraker.conf
```
bin/ fetch-assets -> unpack -> patch -> pack, plus verify
profiles/ probe and default
versions.env pinned Mainsail / HelixScreen versions + sha256
vendor/ where fetch-assets.sh caches them (gitignored)
config.env your paths, the root password hash, the model
@@ -185,8 +173,8 @@ docs/ the documentation
Two things keep the boundary from eroding: only `payload/` and `assets/` are
ever copied into a package by `patch.sh`, and `make verify` fails if a built
package contains any file byte-identical to one in `bin/`, `test/`, `docker/`
or `profiles/`.
package contains any file byte-identical to one in `bin/`, `test/` or
`docker/`.
## Rebuilding chelper
+9 -35
View File
@@ -6,13 +6,13 @@ CI installs the package into a replica of the printer -- the real
machine would still boot. What it cannot do is drive the screen, the MCUs, the
toolchanger or a print. This is the on-hardware procedure.
There are two flashes: `probe`, which changes nothing and proves the update
chain works on your machine, and `default`, which is the firmware.
There is one flash: the firmware package for your model.
**Rule: do the probe first, and have the stock package on a spare stick before
you flash the firmware.** The probe costs five minutes and rules out a bad
stick, the wrong model or a truncated download — the three failures that are
annoying on a probe and frightening on a real flash.
**Rule: have the stock FlashForge package for your model on a spare stick
before you flash anything.** Flashing it back is the uninstall, and it is the
only recovery step that needs nothing but a USB port — no ssh, no screen. It
is proven by `make test-recovery`, which installs the mod into the printer
replica and then flashes the stock package over it.
---
@@ -38,8 +38,8 @@ Find yours in Settings → About, or in
`TARGET_MACHINE` in `config.env`. Then:
```sh
make default && make verify # one model
make release PROFILE=default # both, into dist/
make build && make verify # one model
make release # both, into dist/
```
`verify.sh` fails loudly on a mismatch. **You must start from a stock package
@@ -65,33 +65,7 @@ boot script globs `Creator5Pro-*.tgz`, the non-Pro globs `Creator5-*.tgz`.
---
## Stage 0 — probe (`make probe`)
**Changes nothing.** Reinstalls the stock software component byte-for-byte and
writes a diagnostic report to the USB stick.
Flash it, wait for the reboot, pull the stick and read:
| File on the stick | What it tells you |
|---|---|
| `anvil-report.txt` | free space, partitions, installed versions, init layout, whether nginx/moonraker really exist |
| `anvil-stock-etc.tar` | the whole `/etc`, so the init chain can be studied offline |
| `anvil-stock-nginx.conf` | the stock nginx config (not shipped in any update package) |
| `anvil-stock-bootscripts.tar` | `app_startup.sh`, `start.sh`, `passwd`, `shadow` as they exist on YOUR unit |
**Go/no-go:** the report exists and the printer boots normally.
If the stick comes back empty, the package never installed — nothing was
changed, and you have learned that before risking anything.
**Check in the report:**
- free space on `/usr/data` is comfortably above the size of your full package
- `moonrakerDmn: yes` and `nginx binary: yes`
- the `INIT SYSTEM` block matches expectations (busybox init, `/etc/init.d/rcS`,
`S50dropbear` present)
---
## Stage 1 — the firmware (`make default`)
## The flash (`make build`)
One flash brings up everything: a root password you know, Mainsail and
moonraker, the forked Klipper with the toolchanger extras, and HelixScreen on
+1 -1
View File
@@ -187,7 +187,7 @@ logged instead.
for presence, executability and ELF-ness, never run.
* Timing-dependent boot races are not modelled.
* Partition sizes are unbounded unless you set `PROG_MB` and `DATA_MB` (in `test.env`) from
the `df -h` block in the stage-0 probe report. Until then an install that
what `df -h` reports on the printer itself. Until then an install that
runs the data partition out of space passes here and fails on the machine.
The replica says so in `/usr/prog/.SIMULATED`.
+4 -5
View File
@@ -78,17 +78,16 @@ say where it goes — `run-tests.sh` stamps every header with elapsed seconds:
```
== brick-risk lint == [0s]
== profile: probe / default (on the fixture) == [1s]
== build on the fixture == [1s]
== extracting the printer rootfs == [16s]
== applets / ash / ABI / UI safety == [18s]
== end-to-end update on the replica: probe == [28s]
== end-to-end update on the replica: default == [111s]
== end-to-end update on the replica == [111s]
== recovery: a stock package reverts the mod == [250s]
[326s]
```
What is left is real work: two full package builds (the `default` payload is
55MB through `xz`) and three replica runs. If it needs to get faster again,
What is left is real work: two full package builds (the payload is 55MB
through `xz`) and two replica runs. If it needs to get faster again,
that is where to look — not in the harness.
## What was dropped, and why
-85
View File
@@ -1,85 +0,0 @@
# Diagnostic report. Runs on the printer during install and writes everything
# we need to know to /mnt (the USB stick), which is still mounted at this
# point -- app_startup.sh unmounts it only after runFirmwareExe.sh returns.
#
# Read this BEFORE flashing anything that modifies the machine: it tells you
# the real partition sizes, what is already installed, and whether the whole
# decrypt -> md5 -> run.sh chain works on your unit.
REPORT=/mnt/anvil-report.txt
{
echo "=== anvil report ==="
echo "date: `date 2>/dev/null`"
echo "machine: `uname -a`"
echo "args: $*"
echo
echo "--- free space (the number that decides what fits) ---"
df -h 2>/dev/null || df
echo
echo "--- mounts (a mount named like a mod blocks factory restore) ---"
mount
echo
echo "--- partitions ---"
/sbin/fdisk -l /dev/mmcblk0 2>/dev/null | head -30
echo
echo "--- installed component versions ---"
for c in software library kernel control; do
echo " $c: `ls -1 /usr/prog/PROGRAM/$c 2>/dev/null | tr '\n' ' '`"
done
echo
echo "--- does the web stack really ship on stock? ---"
echo " nginx binary : `[ -x /usr/prog/nginx/sbin/nginx ] && echo yes || echo NO`"
echo " nginx conf : `[ -f /usr/prog/nginx/conf/nginx.conf ] && echo yes || echo NO`"
echo " moonraker : `[ -f /usr/prog/moonraker/moonraker/moonraker/moonraker.py ] && echo yes || echo NO`"
echo " moonrakerDmn : `[ -x /usr/prog/klipper/moonrakerDaemon ] && echo yes || echo NO`"
echo " python3 : `/usr/prog/Python-3.8.2/bin/python3 -V 2>&1`"
echo " xz : `command -v xz || echo NO`"
echo
echo "--- nginx.conf (needed to know the stock docroot/ports) ---"
cat /usr/prog/nginx/conf/nginx.conf 2>/dev/null | head -60
echo
echo "--- INIT SYSTEM (decides how we should start services) ---"
echo " PID 1 : `cat /proc/1/comm 2>/dev/null` (`readlink /proc/1/exe 2>/dev/null`)"
echo " systemctl : `command -v systemctl || echo NO`"
echo " /etc/inittab : `[ -f /etc/inittab ] && echo yes || echo NO`"
echo " /etc/init.d : `[ -d /etc/init.d ] && echo yes || echo NO`"
echo " start-stop-daemon: `command -v start-stop-daemon || echo NO`"
echo
echo " ### /etc/inittab"
cat /etc/inittab 2>/dev/null
echo " ### ls /etc/init.d"
ls -la /etc/init.d 2>/dev/null
echo " ### /etc/init.d/rcS"
cat /etc/init.d/rcS 2>/dev/null | head -40
echo " ### what invokes app_startup.sh"
grep -rl "app_startup" /etc /usr/prog/bin 2>/dev/null | head
grep -rn "app_startup" /etc/inittab /etc/init.d/* 2>/dev/null | head
echo
echo " ### running processes at install time"
ps 2>/dev/null | head -40
echo
echo "--- klipper ---"
echo " chelper: `ls -l /usr/prog/klipper/klippy/chelper/c_helper.so 2>/dev/null`"
echo " extras : `ls -1 /usr/prog/klipper/klippy/extras 2>/dev/null | wc -l` modules"
echo
echo "--- existing config on the data partition ---"
ls -la /usr/data/config 2>/dev/null
echo
echo "--- stock boot scripts (so we can diff our patched versions) ---"
echo "### /usr/prog/app_startup.sh"; cat /usr/prog/app_startup.sh 2>/dev/null
echo "### /usr/prog/klipper/start.sh"; cat /usr/prog/klipper/start.sh 2>/dev/null
echo
echo "=== end of report ==="
} > "$REPORT" 2>&1
sync
# Copy the stock nginx conf out too -- we need it verbatim to build a correct
# Mainsail vhost, and it is not in any update package.
cp -f /usr/prog/nginx/conf/nginx.conf /mnt/anvil-stock-nginx.conf 2>/dev/null
# The whole /etc, so the init layout can be studied offline instead of
# guessed at. It is a few hundred KB and answers the init.d-vs-custom
# question definitively.
tar -cf /mnt/anvil-stock-etc.tar /etc 2>/dev/null
tar -cf /mnt/anvil-stock-bootscripts.tar \
/usr/prog/app_startup.sh /usr/prog/klipper/start.sh \
/usr/prog/etc/passwd /usr/prog/etc/shadow 2>/dev/null
sync
-24
View File
@@ -1,24 +0,0 @@
# THE DEFAULT PROFILE -- the actual firmware: everything, in one flash.
#
# Forked Klipper with toolchanger support, Mainsail/Moonraker, ssh, and
# HelixScreen driving the touchscreen in place of FlashForge's UI.
#
# Do not flash this until stage 0 (`make probe`) has come back clean on YOUR
# machine, and keep a USB stick with the STOCK FlashForge package within
# reach -- flashing that is the uninstall (proven by `make test-recovery`).
#
# FlashForge's firmwareExe is REPLACED, not kept: HelixScreen is the only UI.
# If it crash-loops, S80ui latches SAFE-MODE after 3 failed boots and the
# printer boots headless instead of looping. ssh and Mainsail are your
# recovery path if the screen is dark.
PROFILE_DESC="the firmware: forked Klipper + toolchanger + Mainsail + ssh + HelixScreen"
BUILD_REPORT=0 # the report dumps /etc + shadow to the stick: probe only
BUILD_APPLY=1
BUILD_KLIPPER=fork
BUILD_TOOLCHANGE=1
BUILD_MAINSAIL=1
BUILD_HELIX=1
MOD_SSH=1
MOD_WEB=1
-23
View File
@@ -1,23 +0,0 @@
# STAGE 0 -- the first thing you should ever flash.
#
# Changes NOTHING on the printer. It reinstalls the stock software component
# byte-for-byte and adds one extra step: write a diagnostic report to the USB
# stick, then reboot.
#
# Purpose: prove the whole chain works on YOUR machine -- the filename glob,
# the DES3 key, the md5 gate, run.sh execution -- and learn the real partition
# sizes BEFORE you commit to a package that modifies anything.
#
# Pull the stick afterwards and read anvil-report.txt.
PROFILE_DESC="stage 0: no changes, writes a diagnostic report to the USB stick"
BUILD_REPORT=1 # write the report
BUILD_APPLY=0 # master switch: change nothing at all
BUILD_KLIPPER=stock
BUILD_TOOLCHANGE=0
BUILD_MAINSAIL=0
BUILD_HELIX=0
MOD_SSH=0
MOD_WEB=0
ROOT_PW_HASH=""
+1 -1
View File
@@ -38,7 +38,7 @@ PRINTER_IMAGE="monstrofil/creator5-printer:latest"
# https://github.com/ghzserg/FF/releases/download/R/Creator5Pro-factory.tar.xz
PROG_DUMP="$HOME/Desktop/Creator5Pro-factory.tar.xz"
# Real partition sizes, from the `df -h` block of the stage-0 probe report.
# Real partition sizes, as `df -h` reports them on the printer itself.
# Until these are set the replica's prog/data partitions are unbounded, so an
# install that runs the machine out of space would pass here and fail there.
PROG_MB=""
+1 -2
View File
@@ -42,7 +42,6 @@ while IFS= read -r f; do
grep -nE 'rm[[:space:]]+-[a-zA-Z]*r[a-zA-Z]*[[:space:]]+\$[A-Za-z_]+/' "$f" | head -3 | sed 's/^/ /'
fi
# Replacing the init/boot chain without restoring a UI
# Only MODIFYING app_startup.sh is a risk; report.sh merely cats it.
if grep -nE '(cp|mv|install|tee|sed[[:space:]]+-i|>>?[[:space:]]*)[^|;]*app_startup\.sh' "$f" >/dev/null \
&& ! grep -qE 'firmwareExe|helix' "$f"; then
warn "$f" "modifies app_startup.sh but never mentions a UI launcher"
@@ -84,7 +83,7 @@ else
fi
# --- the installer must not hard-fail into a boot loop ----------------------
for f in payload/run-append.sh payload/report.sh; do
for f in payload/run-append.sh; do
[ -f "$f" ] || continue
if grep -qE '^[[:space:]]*set -e' "$f"; then
bad "$f" "'set -e' in an installer fragment aborts run.sh mid-way, leaving a half-installed system"
+3 -3
View File
@@ -83,9 +83,9 @@ mount -t tmpfs tmpfs $R/run
# the suite fail on a small runner.
#
# Their SIZES are NOT modelled unless you say what they are. The real numbers
# come off the machine in the stage-0 probe report (`df -h`); until
# PROG_MB/DATA_MB are set from it these are unbounded, and an install that
# runs the machine out of space passes here and fails there.
# come off the machine (`df -h` over ssh); until PROG_MB/DATA_MB are set from
# it these are unbounded, and an install that runs the machine out of space
# passes here and fails there.
mkdir -p /parts/prog /parts/data
if [ -n "${PROG_MB:-}" ]; then mount -t tmpfs tmpfs $R/usr/prog -o size=${PROG_MB}m
else mount --bind /parts/prog $R/usr/prog; fi # "usershare"
+1 -1
View File
@@ -32,7 +32,7 @@ mkdir -p $R/usr/prog/PROGRAM/software $R/usr/prog/PROGRAM/library \
[ -f $R/usr/prog/.SIMULATED ] || : > $R/usr/prog/.SIMULATED
if [ -z "${PROG_MB:-}" ] || [ -z "${DATA_MB:-}" ]; then
echo '/usr/prog and /usr/data sizes (unbounded: set PROG_MB/DATA_MB from the probe report)' \
echo '/usr/prog and /usr/data sizes (unbounded: set PROG_MB/DATA_MB from df -h on the printer)' \
>> $R/usr/prog/.SIMULATED
fi
+17 -30
View File
@@ -104,31 +104,18 @@ KLIPPER_FORK=""
HELIX_TGZ="$FXASSETS/helixscreen.tar.gz"
MAINSAIL_ZIP="$FXASSETS/mainsail.zip"
BUSYBOX_BIN=""
DEFAULT_PROFILE=probe
TARGET_MACHINE=Creator5Pro
ROOT_PW_HASH='\$6\$ci\$abcdefghijklmnopqrstuvwxyz'
FF_KEY='FFP0331&*%root'
CFG
export CONFIG_ENV="$FIXTURE_CFG"
for PROF in probe default; do
hdr "profile: $PROF"
export PROFILE="$PROF"
run ./bin/unpack.sh
run ./bin/patch.sh
run ./bin/pack.sh
PKG=$(ls -1 work/out/Creator5Pro-*.tgz 2>/dev/null | head -n1)
if [ -n "$PKG" ]; then run ./bin/verify.sh "$PKG"; else fail "no package produced for $PROF"; fi
done
hdr "the probe profile must change nothing"
export PROFILE=probe
./bin/unpack.sh >/dev/null 2>&1
cp -r work/software "$TMP/sw-before"
./bin/patch.sh >/dev/null 2>&1
DIFF=$(diff -rq "$TMP/sw-before" work/software 2>&1 | grep -vE 'run\.sh|md5sum\.list' || true)
if [ -z "$DIFF" ]; then pass "probe touches only run.sh (adds the report step)"
else fail "probe modified more than run.sh"; echo "$DIFF" | sed 's/^/ /'; fi
hdr "build on the fixture"
run ./bin/unpack.sh
run ./bin/patch.sh
run ./bin/pack.sh
PKG=$(ls -1 work/out/Creator5Pro-*.tgz 2>/dev/null | head -n1)
if [ -n "$PKG" ]; then run ./bin/verify.sh "$PKG"; else fail "no package produced"; fi
hdr "model gates"
sub "model gate" ./test/test-model-gate.sh
@@ -136,7 +123,7 @@ sub "model gate" ./test/test-model-gate.sh
# ============================================================ the replica ====
# Back to the real config for the replica half: it needs the actual stock
# package, which no fixture can stand in for.
unset CONFIG_ENV PROFILE TARGET_MACHINE
unset CONFIG_ENV TARGET_MACHINE
# Throw away everything the fixture half built. bin/ hardcodes work/, so those
# packages land in the same work/out that a real build uses -- and a 380KB
@@ -188,22 +175,22 @@ else
hdr "chamber heater is gated by model"
sub "chamber gate" python3 ./test/test-chamber.py
hdr "our printer.base.cfg is still FlashForge's"
sub "base cfg" python3 ./test/test-base-cfg.py
hdr "MCU bring-up runs on the printer's own Python"
sub "mcu bring-up" ./test/sim-mcu-bringup.sh
hdr "UI decision and crash protection (on the printer's shell)"
sub "ui safety" ./test/sim-ui-fallback.sh
for PROF in probe default; do
hdr "end-to-end update on the printer replica: $PROF"
export PROFILE="$PROF"
run ./bin/unpack.sh
run ./bin/patch.sh
run ./bin/pack.sh
P=$(ls -1 work/out/*-*.tgz 2>/dev/null | head -n1)
if [ -n "$P" ]; then sub "boot -> install -> re-install -> boot ($PROF)" ./test/sim-install.sh "$P"
else fail "no package produced for $PROF"; fi
done
hdr "end-to-end update on the printer replica"
run ./bin/unpack.sh
run ./bin/patch.sh
run ./bin/pack.sh
P=$(ls -1 work/out/*-*.tgz 2>/dev/null | head -n1)
if [ -n "$P" ]; then sub "boot -> install -> re-install -> boot" ./test/sim-install.sh "$P"
else fail "no package produced"; fi
hdr "recovery: a stock package reverts the mod"
P=$(ls -1 work/out/*-*.tgz 2>/dev/null | head -n1)
+1 -1
View File
@@ -57,7 +57,7 @@ echo " using: $(echo "$V" | cut -c1-46)"
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
/payload/run-append.sh /payload/init.d/S*; do
[ -f "$f" ] || continue
n=$(basename "$f")
if $ASH sh -n "$f" 2>/tmp/err; then