manifest: component pins are not layer content

A component pin bump counted as a platform change: the layer content hash
in the platform identity covered the recipe carrying the SRCREV, the
platform is folded into every acceptance fingerprint, so every image
that carried any component update invalidated the whole catalog (dev
image 20260816191951: every test domain-changed after a one-line
forgectrl bump; the two manifests differ only in
platform.layers.meta-forgefirm). The component entry already identifies
the pinned source file by file; the pin double-counted it.

Component pins now live in <recipe>-pin.inc (SRCREV and the PV that
moves with it, nothing else) - forgectrl, grblhal-glowforge and
forgefirm-app here, the BSP components in meta-openglow - and
forgefirm-image-manifest.bbclass leaves *-pin.inc out of the layer
content (FORGEFIRM_MANIFEST_PIN_SUFFIX). Recipe bodies, patches, config
fragments, init scripts and third-party pins with no manifest entry stay
layer content; a pin written into a recipe body still hashes (the safe
direction). manifest-from-tree.py mirrors the rule and reads pins
through the recipe's requires; test_tree_manifest.py proves both
(pin bump: hash unchanged; recipe body or inline pin: changed).
Bitbake resolves the same SRCREV/PV for every pinned recipe.

Docs: ACCEPTANCE.md (what layer content is), kas/README.md (the pin
files in the push order), BRINGUP.md (the finding and the bench
consequence: the first image built with the pin files is itself a
platform change, so its campaign is a full one; pin bumps inherit
after it).

No catalog consequence: nothing in the image's behavior changes; the
change is to the acceptance identity computation, proven by the unit
tests and the CI lint on the tree manifest.
This commit is contained in:
ScottW514
2026-08-16 16:06:58 -04:00
parent bf066d4e27
commit b51e695fb1
12 changed files with 254 additions and 36 deletions
+32 -6
View File
@@ -15,10 +15,13 @@
# [--cache DIR] [--kernel-srcrev REV]
#
# Component revisions come from the recipes in meta-forgefirm and the sibling
# meta-openglow checkout (default ../meta-openglow relative to this repo).
# Each pinned commit is fetched shallowly into --cache (default
# .manifest-cache/, gitignored) and listed with `git ls-tree`; a submodule
# gitlink is followed through .gitmodules.
# meta-openglow checkout (default ../meta-openglow relative to this repo); a
# recipe's `require`d files in its own directory are read too, which is
# where the pins live (<recipe>-pin.inc). Each pinned commit is fetched
# shallowly into --cache (default .manifest-cache/, gitignored) and listed
# with `git ls-tree`; a submodule gitlink is followed through .gitmodules.
# The layer content hash mirrors forgefirm-image-manifest.bbclass: every
# file under the layer except *.md and *-pin.inc.
import argparse
import hashlib
import json
@@ -43,6 +46,9 @@ RECIPES = [
CONTENT_LAYERS = {"meta-forgefirm": ("forgefirm", "meta-forgefirm"),
"meta-glowforge-bsp": ("meta-openglow", "meta-glowforge-bsp"),
"meta-openglow-core": ("meta-openglow", "meta-openglow-core")}
# Left out of a layer's content, as in forgefirm-image-manifest.bbclass
# (FORGEFIRM_MANIFEST_PIN_SUFFIX): documentation and the component pin files.
LAYER_SKIP_SUFFIXES = (".md", "-pin.inc")
def git(args, cwd=None, input=None):
@@ -50,8 +56,27 @@ def git(args, cwd=None, input=None):
stderr=subprocess.PIPE, check=True).stdout
def recipe_text(path, seen=None):
"""The recipe's text with its `require`/`include`d files from the same
directory appended (bitbake resolves a relative name against the
including file's directory first). Only local files are followed;
anything else is left to BBPATH and skipped here."""
seen = seen if seen is not None else set()
path = os.path.abspath(path)
if path in seen:
return ""
seen.add(path)
with open(path, encoding="utf-8") as f:
text = f.read()
for m in re.finditer(r'^\s*(?:require|include)\s+(\S+)\s*$', text, re.M):
cand = os.path.join(os.path.dirname(path), m.group(1))
if os.path.isfile(cand):
text += "\n" + recipe_text(cand, seen)
return text
def parse_recipe(path):
text = open(path, encoding="utf-8").read()
text = recipe_text(path)
uri = re.search(r'^SRC_URI\s*\+?=\s*"([^"]+)"', text, re.M)
rev = re.search(r'^SRCREV\s*\??=\s*"([0-9a-fA-F]+)"', text, re.M)
if not uri or not rev:
@@ -116,7 +141,8 @@ def ls_tree(repo, rev, url, cache, prefix, files):
def layer_content(path):
out = git(["ls-files", "-z", "--cached", "--others", "--exclude-standard", "--", "."], cwd=path)
paths = sorted(set(p.decode("utf-8", "replace") for p in out.split(b"\0") if p))
paths = [p for p in paths if os.path.isfile(os.path.join(path, p)) and not p.endswith(".md")]
paths = [p for p in paths
if os.path.isfile(os.path.join(path, p)) and not p.endswith(LAYER_SKIP_SUFFIXES)]
if not paths:
return None
# hash-object --stdin-paths resolves against the repository top level