mirror of
https://github.com/Klipper4FlashForge/firmware.git
synced 2026-10-19 03:49:50 -05:00
config.env.example opened with DESKTOP="$HOME/Desktop", and four settings hung off it. Nothing in bin/, pkgs/, qa/ or installer/ ever read it: three of the four were commented-out overrides for files `make vendor` already fetches, and the fourth, BUSYBOX_BIN, names a binary with no source at all. What DESKTOP really did was carry the stock FlashForge .tgz -- the one input a person had to go and find before they could build anything. It did not have to be. .github/workflows/ci.yml has been fetching that exact file for as long as printer-sim has run, from ghzserg/FF, pinned by sha256, with its own get(), its own cache and its own secrets -- a second spelling of "download the firmware and check the hash" that no human path shared. The pins here are those pins, verified byte-for-byte against a fresh download of both assets. So versions.env pins the stock package the way it pins everything else, bin/fetch-assets.sh downloads it into vendor/, and bin/common.sh defaults STOCK_TGZ_* there. A cold checkout is now `cp config.env.example config.env && make build`, with nothing to edit. --stock is a separate flag from --all, not folded into it. The packaging lane wants no FlashForge package -- that is most of the point of it being runnable in CI on a bare checkout -- so `--all` stays "every payload piece, no firmware", `make build` asks for the one model it is building, and `make vendor` (--all --stock) takes both. The one download that will not overwrite an override. HELIX_TGZ and friends name a public release anyone can re-fetch in a minute; STOCK_TGZ_* names 93MB of proprietary firmware someone downloaded by hand, and clobbering it on a hash mismatch would destroy the only copy on the machine. It is skipped instead, and unchecked -- their file, their problem. CI loses its duplicate fetcher, its stock/ cache and the STOCK_URL_* secrets. A fork testing its own firmware re-pins versions.env, which works locally too; the secrets only ever worked in CI. qa/lib/config.py reads versions.env for the pinned filenames so stock_for() looks where the download actually lands. An explicit STOCK_TGZ_* still wins. An existing config.env keeps working untouched: its paths are now overrides, so the build skips the download and uses the file it always used. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DPU6RR4UJH2Qf9DR5hb2gg
319 lines
15 KiB
Makefile
319 lines
15 KiB
Makefile
# 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 <target> 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=<name> 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 <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
|
|
|
|
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/"
|