mirror of
https://github.com/Klipper4FlashForge/firmware.git
synced 2026-10-21 12:52:16 -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
160 lines
6.4 KiB
Python
160 lines
6.4 KiB
Python
"""config.env and test.env, read the way the build reads them.
|
|
|
|
Lifted from tools/replica/ffsim/config.py rather than imported from it. The
|
|
endgame is that ffsim is deleted, and a qa/ that imports from it would block
|
|
that deletion or break on it. The file format is stable and the parser is
|
|
thirty lines; the coupling would cost more than the duplication.
|
|
|
|
The two files are shell, and bin/*.sh still sources them, so they stay shell:
|
|
they use quoting, `$ROOT`, and occasionally a command substitution, and a
|
|
hand-rolled KEY=VALUE parser would agree with bash right up until the day it
|
|
did not. So bash reads them and reports back what it ended up with. That keeps
|
|
exactly one definition of what these files mean.
|
|
|
|
config.env the BUILD config. Everything that can reach a printer.
|
|
test.env replica-only: the factory image, the partition sizes.
|
|
Kept apart so nothing in it can end up inside a package.
|
|
|
|
THE ENVIRONMENT WINS OVER BOTH FILES. An empty `PRINTER_IMAGE=` sitting in
|
|
test.env used to overwrite an explicit `PRINTER_IMAGE=foo ...` on the command
|
|
line, so the override silently did nothing and the run tested a different image
|
|
than the one it was told to. The rule is about being SET, not about being
|
|
non-empty: `PRINTER_IMAGE= make qa-replica` deliberately means "build locally",
|
|
not "use whatever the file says".
|
|
"""
|
|
import os
|
|
import subprocess
|
|
|
|
from .paths import ROOT
|
|
|
|
# What the harness reads. Anything else in those files belongs to the build.
|
|
WANTED = (
|
|
"FF_KEY",
|
|
"STOCK_TGZ", "STOCK_TGZ_CREATOR5PRO", "STOCK_TGZ_CREATOR5",
|
|
# Not config at all -- the pinned filenames out of versions.env, so that
|
|
# stock_for can look where fetch-assets.sh actually put the download. The
|
|
# alternative was spelling "vendor/<pinned name>" a second time here, and a
|
|
# default that exists twice is a default that drifts.
|
|
"STOCK_FILE_CREATOR5PRO", "STOCK_FILE_CREATOR5",
|
|
"PRINTER_IMAGE", "PROG_MB", "DATA_MB",
|
|
)
|
|
|
|
# Of those, the ones a caller is expected to override per-run.
|
|
OVERRIDABLE = ("PRINTER_IMAGE", "PROG_MB", "DATA_MB", "FF_KEY")
|
|
|
|
# `.` on a file with a syntax error returns non-zero and does NOT stop the
|
|
# shell, so the && form quietly carried on with half the file applied. That is
|
|
# the disease being cured here: a broken config that looks like an absent one.
|
|
# Each source is checked, and its own exit code says which file.
|
|
_DUMP = r'''
|
|
set -a
|
|
if [ -f "$1" ]; then . "$1" || exit 90; fi
|
|
if [ -f "$2" ]; then . "$2" || exit 91; fi
|
|
if [ -f "$3" ]; then . "$3" || exit 92; fi
|
|
set +a
|
|
for key in %s; do
|
|
eval "value=\${$key-}"
|
|
printf '%%s=%%s\0' "$key" "$value"
|
|
done
|
|
''' % " ".join(WANTED)
|
|
|
|
|
|
class ConfigError(Exception):
|
|
"""The config itself is broken -- not absent, broken.
|
|
|
|
Deliberately not a skip. A syntax error in config.env leaves STOCK_TGZ_*
|
|
empty, and a harness that treats that as "nothing to test here" reports a
|
|
clean skip on a machine that could have run everything. Naming the file
|
|
that failed is the entire point.
|
|
"""
|
|
|
|
|
|
class Config:
|
|
"""What the two files say, with the environment layered back on top."""
|
|
|
|
def __init__(self, values, config_env, test_env):
|
|
self.values = values
|
|
self.root = ROOT
|
|
self.config_env = config_env
|
|
self.test_env = test_env
|
|
|
|
@classmethod
|
|
def load(cls):
|
|
config_env = os.environ.get("CONFIG_ENV") or str(ROOT / "config.env")
|
|
test_env = os.environ.get("TEST_ENV") or str(ROOT / "test.env")
|
|
|
|
versions_env = str(ROOT / "versions.env")
|
|
|
|
# versions.env FIRST, so config.env still wins on anything both name.
|
|
# It is read for the pinned stock filenames alone -- see WANTED.
|
|
dumped = subprocess.run(
|
|
["bash", "-c", _DUMP, "qa", versions_env, config_env, test_env],
|
|
cwd=str(ROOT), capture_output=True, text=True)
|
|
if dumped.returncode != 0:
|
|
which = {90: versions_env, 91: config_env,
|
|
92: test_env}.get(dumped.returncode)
|
|
raise ConfigError(
|
|
"could not read %s:\n%s"
|
|
% (which or ("%s / %s / %s"
|
|
% (versions_env, config_env, test_env)),
|
|
dumped.stderr.strip()))
|
|
|
|
values = {}
|
|
for item in dumped.stdout.split("\0"):
|
|
if "=" in item:
|
|
key, _, value = item.partition("=")
|
|
values[key] = value
|
|
|
|
# The environment wins -- see the module docstring. Being SET is what
|
|
# counts, so this tests membership rather than truthiness.
|
|
for key in OVERRIDABLE:
|
|
if key in os.environ:
|
|
values[key] = os.environ[key]
|
|
|
|
return cls(values, config_env, test_env)
|
|
|
|
def get(self, key, default=""):
|
|
return self.values.get(key, default) or default
|
|
|
|
@property
|
|
def ff_key(self):
|
|
return self.get("FF_KEY", "FFP0331&*%root")
|
|
|
|
def _vendored_stock(self, key):
|
|
"""Where bin/fetch-assets.sh puts the stock package for that key.
|
|
|
|
The common case now, and on a fresh checkout the only case: config.env
|
|
carries no path at all and `make vendor` has downloaded the pin. An
|
|
explicit STOCK_TGZ_* in config.env still comes first -- that is what it
|
|
is for -- so this is a fallback, not an override.
|
|
"""
|
|
# Only the per-model keys have a pin. Bare STOCK_TGZ is a path someone
|
|
# typed to force one package, and has no vendored counterpart.
|
|
if not key.startswith("STOCK_TGZ_"):
|
|
return ""
|
|
name = self.get(key.replace("STOCK_TGZ_", "STOCK_FILE_", 1))
|
|
return str(self.root / "vendor" / name) if name else ""
|
|
|
|
def stock_for(self, package_name=None):
|
|
"""The stock package that is the authentic baseline for this build.
|
|
|
|
With a package name, pick by its model prefix: the two models ship
|
|
different firmwareExe binaries and each refuses to install on the
|
|
other, so the baseline has to match.
|
|
"""
|
|
if package_name:
|
|
if package_name.startswith("Creator5Pro-"):
|
|
keys = ("STOCK_TGZ_CREATOR5PRO",)
|
|
elif package_name.startswith("Creator5-"):
|
|
keys = ("STOCK_TGZ_CREATOR5",)
|
|
else:
|
|
keys = ("STOCK_TGZ",)
|
|
else:
|
|
keys = ("STOCK_TGZ_CREATOR5PRO", "STOCK_TGZ")
|
|
|
|
for k in keys:
|
|
for path in (self.get(k), self._vendored_stock(k)):
|
|
if path and os.path.isfile(path):
|
|
return path
|
|
return ""
|