Files
firmware/config.env.example
T
Oleksandr ShyshatskyiandClaude Opus 5 9973019ec8 A printer ships knowing where its updates come from
Phase 3 of docs/notes/85-packaging.md was three things: signing, a URL the
printer asks, and a tree at that URL. The migration landed the first. This is
the second, and the tooling for the third.

THE POINTER HAS TO SHIP BEFORE THE HOST EXISTS, which is why this lands with
nothing serving yet. A feed cannot tell a printer where the feed is: a machine
installed without etc/apk/repositories needs a USB stick before it can ever
fetch anything, which is the thing packages were adopted to stop. So the file
travels in the .tgz, naming a host that does not answer yet, and every printer
installed from today starts fetching the day one does.

The cost of that is bounded, and was read out of apk's source rather than
assumed: an unreachable repository fails `apk upgrade` and nothing else --
apk_db_repository_check is called from app_upgrade.c and not from app_add.c,
so `apk add ./file.apk` does not consult a repository at all.

    http://reforge.8941973.xyz/apk/mipsel_xburst2/anvil.adb

bin/common.sh's FEED_URL is the base, config.env can override it for a fork,
and pkgs/anvil-core generates the file from it beside the public key it
already shipped. Three things about that URL:

  * IT NAMES THE INDEX FILE, not a directory. src/repoparser.c reads a line
    ending .adb as a v3 index in place, with the packages as siblings named by
    the pkgname-spec mkndx wrote into it; anything else sends apk looking for
    <url>/<arch>/APKINDEX.tar.gz. That is the layout this repo already builds
    and the stick already uses, so the network feed is the same shape --
    exercised by every payload build rather than only in production. The v3
    directory form would work too, but only with
    `mkndx --pkgname-spec '${arch}/${name}-${version}.apk'`.

  * THE ARCH IS A DIRECTORY. Package file names carry no architecture, so two
    architectures served from one directory would be two files with one name.

  * PLAIN HTTP. libfetch defaults to SSL_VERIFY_PEER and this printer has no
    certificate bundle -- the same gap tools/python/README.md records for
    CPython. Every package is signed, and the signature rather than the
    transport is what decides whether a printer installs one.

bin/publish-feed.sh writes the matching tree and optionally rsyncs it.
Packages go first and the index last, because the index names every package by
sha256 and a mirror carrying a new index with old packages hands printers a
checksum for a file that 404s. Nothing is deleted at the far end. It refuses
an unsigned feed unless ALLOW_UNSIGNED=1: a printer verifies against the key
that shipped in its own anvil-core, so an unsigned feed is one no printer
installs from. The release workflow attaches the same tree as
reforge-apk-feed.tar.gz, so a host can be filled from any release -- including
ones tagged before the host existed -- without rebuilding.

THE REPLICA STOPPED WRITING THE FILE IT USED TO DELETE.
case-build-payload.sh wrote its own etc/apk/repositories naming /mnt and
removed it before tarring; it now passes --repositories-file with a path on
the stick, which apk reads INSTEAD of the default (database.c consults the
default file and repositories.d only when no file is named). So what ships is
whatever anvil-core installed and the build never reads it. The `apk info`
line takes the same flag: without it that one command reads the shipped file
and reaches for the network, which a build's output must not depend on.

Measured: 47 packages built from a cold cache, the payload assembled in the
replica twice, qa/static at 319 passed. Two of those are new -- the default
URL in common.sh (read from the file, not resolved, so a fork setting
FEED_URL in config.env still passes) and the built payload carrying a
package-owned entry that ends /mipsel_xburst2/anvil.adb.

What is left is operational: a host answering at $FEED_URL, and who runs
publish-feed.sh -- a release step with a deploy secret, or a person with the
tarball.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017rgrDeu9YAUuMM2G7gKTpL
2026-09-01 14:09:34 +03:00

169 lines
8.9 KiB
Bash

# creator5-fw-builder — build configuration
# Edit paths to match your machine, then run `make build`.
# ---- identity of the package you are building -------------------------------
MOD_NAME=anvil # goes into the .tgz filename
MOD_VER="" # empty = today's UTC date, e.g. 20260823.
# Pin it to rebuild an old release, or for a
# second release on the same day (20260823b).
# ---- which printer is this for? --------------------------------------------
# Creator5Pro / 0029 or Creator5 / 0028
# Find it on the printer: Settings -> About, or in
# /usr/data/firmwareRes/config/general.json ("machineName").
#
# This MUST match the model the stock package was built for. A package's
# runFirmwareExe.sh carries its own MACHINE=/PID= gate and refuses to install
# on the other model, so a Pro cannot be recovered or modded with a non-Pro
# package (and vice versa).
TARGET_MACHINE=Creator5Pro
# Override for one command: MODEL=Creator5 make build
# Build both at once: make release
# ---- inputs -----------------------------------------------------------------
# THERE ARE NO PATHS TO EDIT HERE ANY MORE. Every source this build reads is
# downloaded into vendor/ by bin/fetch-assets.sh, pinned by name and sha256 in
# versions.env -- the stock FlashForge OTA package included, which used to be
# the one file you had to go and find yourself. `make build` fetches what it
# needs; `make vendor` fetches the lot.
#
# The stock packages are ONE PER MODEL and not interchangeable: each carries a
# different firmwareExe and refuses to install on the other model. The one
# matching TARGET_MACHINE (or MODEL=) is selected automatically.
#
# Set one of these ONLY to build against firmware you downloaded yourself.
# Unlike HELIX_TGZ below, a path you put here is never checksummed and never
# overwritten -- it is 93MB you fetched by hand, and this build will not
# replace it with one you did not ask for. That also means nothing verifies it:
# it is your file and your problem.
#STOCK_TGZ_CREATOR5PRO="/path/to/Creator5Pro-1.9.7-1.2.9-20260810.tgz"
#STOCK_TGZ_CREATOR5="/path/to/Creator5-1.9.7-1.2.9-20260810.tgz"
# Set this only to force one specific package regardless of model. It does not
# suppress the download above; point the per-model variable at your file
# instead if that is what you want.
STOCK_TGZ=""
# KLIPPER_FORK USED TO BE HERE and pointed the build at a local checkout of
# the fork. It is gone: the Klipper build is pkg/klipper now, and a recipe
# names its source exactly once -- a checkout beside the pinned tarball is the
# second source that rule forbids, and the one that let a release ship a tree
# nobody had built. To build your own fork, put its tarball in vendor/ and
# re-pin KLIPPER_VERSION and KLIPPER_SHA256 in versions.env; the sha is then
# the same guarantee every other source in this repo carries.
# The toolchanger mod (klippy-extras + configs + helixscreen db entry) used to
# be a TOOLCHANGE= path to a creator5-toolchange checkout. It now lives in this
# repo under payload/klipper/, so there is nothing to set. BUILD_TOOLCHANGE=0
# here still turns it off.
# Mainsail, the HelixScreen build for the Creator 5 (MIPS static) and Moonraker
# are NOT set here. They are downloaded on demand into vendor/ by
# bin/fetch-assets.sh, pinned by version and sha256 in versions.env, so the
# repo carries no binaries. Set any of these ONLY to build against your own
# local copy.
# NOTE: the path is still checksummed against versions.env, and on a mismatch
# fetch-assets.sh OVERWRITES it with the pinned release -- SKIP does not save
# you, it re-downloads too. To build against your own file, also put ITS
# sha256 in versions.env; only an exact match is left alone ("cached"):
#HELIX_TGZ="/path/to/helixscreen-creator5-v0.99.115.tar.gz"
#MAINSAIL_ZIP="/path/to/mainsail.zip"
#MOONRAKER_TGZ="/path/to/moonraker-v0.9.3.tar.gz"
# ---- optional extras -- leave empty to skip that feature ---------------------
# The ONE input with no pinned source, because it has no source at all: a
# busybox someone built elsewhere, for the applets FlashForge left out. Nothing
# needs it (the printer's own busybox works), so `make vendor` has nothing to
# fetch and an empty value simply leaves the package out of the feed. See
# pkgs/3rdparty/busybox/build.sh.
BUSYBOX_BIN=""
# The private key the package feed is signed with, or empty for an unsigned
# feed. A PATH, never a key: config.env is gitignored and this points outside
# the repo, so the key is never a thing the repository can leak.
#
# ./bin/apk-keygen.sh ~/.anvil/anvil.rsa makes the pair
# APK_SIGN_KEY="/home/you/.anvil/anvil.rsa"
#
# EMPTY IS A WORKING CONFIGURATION, deliberately: `make packages` has to run on
# a bare checkout with no secrets, which is what keeps the packaging lane a
# gate in CI. An unsigned feed installs with --allow-untrusted, exactly as it
# does today; a signed one does not need the flag and refuses a package whose
# signature does not match. RSA or Ed25519 only -- ECDSA signatures are
# randomised and two builds of one tree would stop being byte-identical.
#
# The PUBLIC half is generated beside the private one and ships in anvil-core,
# so a printer can verify what it is offered. Losing the private key means
# printers stop accepting new packages until one is reflashed by .tgz.
#
# The DIRECTORY holding the key is bind-mounted read-only into the build
# container by the Makefile, which is what lets this point outside the
# checkout at all. Both halves must live in it, named as apk-keygen.sh leaves
# them, and it must be a path docker is allowed to mount.
#
# In CI the same value comes from a repository secret rather than from here:
# see the signing section of docs/notes/85-packaging.md.
APK_SIGN_KEY=""
# Where that feed is SERVED, and the URL every printer this build produces will
# ask for updates. It ships inside anvil-core as $MODDIR/etc/apk/repositories,
# so it is baked into the .tgz a printer installs and cannot be changed later
# except by installing an anvil-core that names somewhere else.
#
# LEAVE IT UNSET unless you are running your own feed. The default is
# Reforge's own host, and a printer only accepts packages signed with the key
# whose public half is in the same build -- so pointing a build at somebody
# else's URL produces a printer that fetches packages it will then refuse.
# Change this and APK_SIGN_KEY together or neither.
#
# The layout it names is what ./bin/publish-feed.sh writes:
# $FEED_URL/<arch>/anvil.adb the index
# $FEED_URL/<arch>/<name>-<version>.apk
#FEED_URL="http://reforge.8941973.xyz/apk"
# ---- what the installed image does at boot ----------------------------------
# NOTHING, from here. There is no build-time knob for boot behaviour any more,
# and no runtime config file on the printer either: /usr/data/anvil/anvil.conf
# is gone. Every component that is installed runs, and the settings that used
# to live in that file -- the splash, the startup wait and its timeout, the
# first-boot import, the camera resolution, the nice values -- are now stated
# once each, in the service that uses them, under
# pkgs/anvil-core/payload/etc/s6-rc/source/.
#
# A printer that should not run a component does not have it: leave it out of
# the build with the BUILD_* flags, or `apk del` it on the printer. What
# there is no longer a way to ask for is a component that is installed and
# switched off -- a half-installed mod nobody chose on purpose, that everything
# downstream had to keep a second answer for.
#
# The BUILD_* flags that decide what goes INTO a package (BUILD_TOOLCHANGE,
# BUILD_MAINSAIL, BUILD_MOONRAKER, BUILD_HELIX) are not set
# here: bin/common.sh defaults them to the full firmware, and setting one in
# this file overrides that. Only touch them if you want a piece left out.
# ---- root login shipped in the image ---------------------------------------
# This is the whole "enable ssh" feature. The stock rootfs already runs
# dropbear from /etc/init.d/S50dropbear, so port 22 is ALREADY open on a
# stock printer -- there is simply no published root password.
#
# Empty = the INSTALLER picks a random password on the printer -- on the
# FIRST install only, updates keep the one the machine already has -- and
# writes it to anvil-password.txt on the USB stick you flashed from. Every
# machine gets a different one, which a hash baked into a shared package
# never could.
# Set one here to choose your own: make passwd
# (it prompts, and runs inside the build image -- the same python everywhere,
# where a host openssl may lack `passwd -6` entirely)
ROOT_PW_HASH=""
# ---- crypto (from /usr/prog/bin/unTar on the printer) -----------------------
FF_KEY='FFP0331&*%root'
# ---- the printer replica -----------------------------------------------------
# Nothing here. The replica's settings -- the factory image, the partition
# sizes, a prebuilt printer image -- live in test.env, because they exist only
# for the tests and must never reach a printer:
#
# cp test.env.example test.env