# creator5-custom-firmware # # 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 build build the firmware package # make qa full brick-safety suite # make shell interactive shell in the build container # # Escape hatch: LOCAL=1 make runs the scripts directly on the host. # A package carries our installer and our payload and no FlashForge component # at all: the stock installer skips absent ones, so /usr/prog, the kernel and # the MCU/board firmware are left alone. FULL=1 carries the kernel, control and # library components too (and reflashes the MCU). FULL ?= PACKARGS = $(if $(FULL),--full,) 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. # 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 build ASSET_ROOT ?= $(firstword $(wildcard /mnt/c /Users /home)) # Three runners, because the lanes need different things: # # RUN the parts of a build that touch no daemon: the feed, unpack, # pack. No docker socket, no replica settings. # RUNSIM the test replica. It starts SIBLING containers through the # mounted socket, so it needs that plus the replica's own knobs. # RUNBUILD `make build`, which needs both: the payload is assembled inside # the replica, and the output has to belong to you. Defined below # the two it borrows from. # # Test-only variables never enter a build, which is the point of the split. DOCKER_BASE = $(DOCKER) run --rm -i \ -v "$(CURDIR)":"$(CURDIR)" -w "$(CURDIR)" \ $(if $(ASSET_ROOT),-v "$(ASSET_ROOT)":"$(ASSET_ROOT)",) \ -e MODEL -e TARGET_MACHINE -e CONFIG_ENV # THE BUILD LANE RUNS AS YOU, NOT AS root. Without this every `make build` and # `make packages` fills work/ with root-owned files that the user who started # it cannot delete -- so the next `rm -rf work/pkg` fails, and the tempting fix # is to run the build outside the container instead. That is how a project ends # up with two build environments and packages that differ depending on which # one produced them, which is exactly what this image exists to prevent. # # HOME is set because a container user with no passwd entry has none, and # anything that wants a dotfile (perl, pip) writes to / and fails. # # BUILD LANE ONLY. RUNSIM below keeps root: it talks to the docker socket to # start sibling containers, and the socket's ownership inside the container is # not the host user's. DOCKER_USER = --user $(shell id -u):$(shell id -g) -e HOME=/tmp ifeq ($(LOCAL),) RUN = $(DOCKER_BASE) $(DOCKER_USER) $(IMAGE) RUNSIM = $(DOCKER_BASE) \ -v $(DOCKER_SOCK):$(DOCKER_SOCK) \ -e TEST_ENV -e PRINTER_IMAGE -e REAL_PKG -e SIM_IMAGE \ -e SIM_VERBOSE -e PROG_MB -e DATA_MB -e REQUIRE_PRINTER_SIM \ $(IMAGE) RUNTTY = $(subst --rm -i,--rm -it,$(RUNSIM)) else RUN = RUNSIM = RUNTTY = endif # The build-lane runner with a tty, for targets that prompt. Derived from RUN, # not RUNSIM: prompting needs no docker socket and no replica knobs. RUNBLDTTY = $(subst --rm -i,--rm -it,$(RUN)) .DEFAULT_GOAL := help .PHONY: help image shell passwd build vendor packages \ printer-image printer-image-push \ boot-screen boot-screen-sim \ qa qa-static qa-replica \ release clean distclean help: @echo 'creator5-custom-firmware -- everything runs in Docker' @echo @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 release build BOTH models into dist/' @echo ' make packages .ipk packages + feed index into work/packages/' @echo ' (make build INSTALLS these to make the payload,' @echo ' so run this first; needs no stock package.' @echo ' PKG= builds that recipe and the ones' @echo ' it builds against -- see' @echo ' docs/notes/85-packaging.md)' @echo @echo 'Models: packages are model-specific and refuse to install on the' @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.' @echo @echo 'Test:' @echo ' make qa both lanes' @echo ' make qa-static needs nothing: parses, names, packaging, the boot graph' @echo ' make qa-replica needs docker + the firmware: install, upgrade, boot' @echo @echo 'Look at things:' @echo ' make boot-screen render the first-boot screen to work/boot-screen/*.png' @echo ' make boot-screen-sim the same, drawn by the printer own python in the replica' @echo @echo 'qa-replica runs inside a replica of the printer: the real' @echo 'rootfs.squashfs under qemu-mipsel, with the package' @echo 'installed by the printer own app_startup.sh off a real FAT filesystem' @echo 'at /dev/sda1. It needs make build, plus PRINTER_IMAGE in test.env' @echo '(test.env.example carries the published one).' @echo @echo 'qa has no ALLOW_SKIP: a missing tool, daemon or image FAILS at the' @echo 'point that needs it, so a gate that did not run cannot look green.' @echo @echo 'Other:' @echo ' make passwd a ROOT_PW_HASH for config.env (prompts, echoes the hash)' @echo ' make vendor download every pinned source, stock firmware included' @echo ' make image build the build container' @echo ' make shell shell inside it' @echo ' make clean | distclean' @echo @echo 'Packages carry the installer and the payload; /usr/prog, the kernel' @echo 'and the MCU are left untouched. FULL=1 also flashes the kernel and MCU.' @echo @echo 'Config: config.env is the BUILD config (what ships). test.env holds' @echo 'the replica settings, which never reach a printer. Copy the .example' @echo 'of each. LOCAL=1 make 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 shell: image @$(RUNTTY) bash # A ROOT_PW_HASH for config.env, generated INSIDE the pinned build image so # the same python makes the same $6$ sha512-crypt everywhere. Host tools are # not dependable here: LibreSSL's openssl has no `passwd -6` at all. passwd: image @$(RUNBLDTTY) python3 -W ignore -c 'import crypt, getpass; print(crypt.crypt(getpass.getpass("password: "), crypt.mksalt(crypt.METHOD_SHA512)))' # =========================================================================== # BUILD LANE -- produces the package you flash. Reads config.env, ships # payload/ and assets/, and touches nothing under qa/. # =========================================================================== # Nothing third-party is vendored in the repo -- not Mainsail, HelixScreen and # Moonraker, and not the stock FlashForge package either. bin/build.sh fetches # 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. # # --stock adds both models' stock firmware, ~186MB. It is a separate flag from # --all because the packaging lane wants none of it: `./bin/fetch-assets.sh # --all` is every payload piece and no FlashForge package, which is what keeps # `make packages` runnable on a bare checkout. vendor: image config.env @$(RUN) ./bin/fetch-assets.sh --all --stock # A THIRD LANE, because the build needs both halves of the split above. # bin/payload.sh assembles the payload by starting the printer replica, so this # lane needs the docker socket -- and it still has to run AS YOU, or every # build leaves a root-owned work/ the next one cannot delete. # # --group-add is what makes those compatible: the socket is root:docker and a # --user container has no supplementary groups, so it gets the socket's own # gid instead of root. Read from the socket rather than assumed -- it is 999 # on some distributions and 1001 here. RUNBUILD = $(DOCKER_BASE) $(DOCKER_USER) \ --group-add $(shell stat -c %g $(DOCKER_SOCK)) \ -v $(DOCKER_SOCK):$(DOCKER_SOCK) \ -e TEST_ENV -e PRINTER_IMAGE -e SIM_VERBOSE -e PROG_MB -e DATA_MB \ $(IMAGE) build: image config.env @$(RUNBUILD) ./bin/build.sh $(PACKARGS) # The package feed (docs/notes/85-packaging.md). Builds every # recipe under pkgs/ into work/packages/ as .ipk files plus the feed index that # makes that directory an opkg repository. # # THE RELEASE PATH IS BUILT ON THIS. pkgs/3rdparty/python declares seven build # dependencies, pkg_deps resolves them by unpacking their .ipk out of # work/packages, and bin/payload.sh builds none of the seven. `make build` on a # cold checkout does not work without this target. # bin/payload.sh now checks for the feed up front and names this command. # # It still, unlike `build`, needs NO stock FlashForge package -- which is most # of the point: packaging has to be runnable in CI on a bare checkout, or the # gate only runs where the proprietary firmware is and stops being a gate. packages: image config.env @$(RUN) ./bin/build-packages.sh $(PKG) # One package per model, collected in dist/. They cannot share content: the # two stock packages ship different firmwareExe binaries. # # It checks nothing itself: `make qa-replica` has the printer perform its own # install, so THAT is the gate before a release goes out. # The feed is built ONCE, outside the loop: nothing in it is model-specific # (bin/payload.sh says so where it picks the roots -- TARGET_MACHINE names only # the output file), so building it twice would only cost time. bin/build.sh # is run with RUNBUILD and not RUN because bin/payload.sh assembles the payload # INSIDE the replica, which needs the docker socket; `build` above has always # used it and `release` was left behind on the runner that has no socket. release: image config.env @rm -rf dist && mkdir -p dist @$(RUN) ./bin/build-packages.sh @for m in Creator5Pro Creator5; do \ echo "=== $$m ==="; \ MODEL=$$m $(RUNBUILD) ./bin/build.sh $(PACKARGS) || exit 1; \ cp work/out/$$m-*.tgz dist/ || exit 1; \ done @echo; echo "dist/:"; ls -lh dist | awk 'NR>1{print " "$$9" "$$5}' @echo; echo "Each file installs ONLY on the model in its name." # =========================================================================== # TEST LANE -- never ships. Reads test.env for the replica settings; the only # targets allowed to reach the docker daemon ($(RUNSIM)). # =========================================================================== # --------------------------------------------------------------------------- # THE qa SUITE # # Same machine, same gates, one framework. `make qa` is the suite; see # qa/conftest.py for the lanes. # # make qa both lanes # make qa-static needs nothing: parses, bashisms, names, the probes # make qa-replica needs docker + the firmware # # There is no strictness flag and there is nothing to remember to pass. A # missing tool, daemon, replica image or package FAILS, at the point that # needs it, with the fix in the message. Skips are reserved for a question # that does not apply to this configuration, and there are currently none. # # The replica lane needs two things, and says so if either is absent: # # a base replica PRINTER_IMAGE in test.env (see test.env.example) # a package work/out/*.tgz, from `make build` # # There is ONE way to get a replica, and it is that image. `make rootfs` # used to be a second: it unsquashed rootfs.squashfs out of the stock # package into work/rootfs and a thin container bind-mounted it. Every # caller set PRINTER_IMAGE anyway -- CI included, which extracted a rootfs # on every push and read none of it -- and `make printer-image` builds the # image from the same public firmware without needing a stock package at # all, so the fallback was a second path to the same place that nothing # took. Rebuild the image when it goes stale; that is the one lever. # # It needs the package because it INSTALLS it, through the printer's own # app_startup.sh off a real FAT stick, rather than hand-placing payload/. # That way the install is under test too, and the payload under test is the # built artefact -- s6 and CPython included, neither of which exists in # payload/. Baked into an image once per package and cached on its md5. # --------------------------------------------------------------------------- qa: image @$(RUNSIM) python3 -m pytest ./qa -q qa-static: image @$(RUN) python3 -m pytest ./qa/static -q qa-replica: image @$(RUNSIM) python3 -m pytest ./qa/replica -q # A Docker image that IS the printer: real rootfs, real /usr/prog and # /usr/data, with the stock package already installed on top of them. # # Both of those are per-run costs it removes. Measured on this repo: # # unpacking the 182MB factory image 22s every run # installing the stock package under qemu 37s every run # with the image 0.7s once, at build time # # make printer-image build (one image serves both models) # make printer-image-push build and push to Docker Hub # # Point PRINTER_IMAGE in test.env at it to use it. # # The image contains proprietary FlashForge firmware. printer-image: image @$(RUNSIM) ./tools/replica/build-printer-image.sh printer-image-push: image @$(RUNSIM) ./tools/replica/build-printer-image.sh --push boot-screen: @./bin/preview-boot-screen.py # The same frames, drawn by the PRINTER's python inside the replica. Slower, # and the only render that proves what the panel would really show. boot-screen-sim: image @$(RUNSIM) ./tools/replica/sim-boot-screen.py clean: @rm -rf work/stage work/out work/modpayload-root @echo cleaned distclean: @rm -rf work @echo "removed work/"