Files
forgefirm/forgetest/tests/test_tree_manifest.py
T
ScottW514 b51e695fb1 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.
2026-08-16 16:06:58 -04:00

108 lines
4.3 KiB
Python

"""scripts/manifest-from-tree.py - the workstation/CI mirror of the image
manifest: recipe pins are read through their pin files, and a layer's
content hash leaves the pin files out (a component bump is not a platform
change; a recipe-body change is)."""
import importlib.util
import os
import shutil
import subprocess
import tempfile
import unittest
import helpers # noqa: F401 (sys.path)
REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
SCRIPT = os.path.join(REPO, "scripts", "manifest-from-tree.py")
def load_script():
spec = importlib.util.spec_from_file_location("manifest_from_tree", SCRIPT)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
def git_ok():
try:
subprocess.run(["git", "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
return True
except (OSError, subprocess.CalledProcessError):
return False
RECIPE = '''DESCRIPTION = "component"
LICENSE = "MIT"
SRC_URI = "git://github.com/example/comp.git;protocol=https;branch=main"
# SRCREV and PV live in the pin file.
require comp-pin.inc
S = "${WORKDIR}/git"
'''
PIN_A = 'SRCREV = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"\nPV = "1.0"\n'
PIN_B = 'SRCREV = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"\nPV = "1.1"\n'
@unittest.skipUnless(git_ok(), "git not available")
class TreeManifestTests(unittest.TestCase):
def setUp(self):
self.mod = load_script()
self.tmp = tempfile.mkdtemp(prefix="forgetest-tree-")
self.layer = os.path.join(self.tmp, "meta-x")
self.rdir = os.path.join(self.layer, "recipes-x", "comp")
os.makedirs(self.rdir)
subprocess.run(["git", "init", "-q"], cwd=self.tmp, check=True,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
self.write("comp.bb", RECIPE)
self.write("comp-pin.inc", PIN_A)
self.write("README.md", "# docs\n")
def tearDown(self):
shutil.rmtree(self.tmp, ignore_errors=True)
def write(self, name, text):
with open(os.path.join(self.rdir, name), "w", encoding="utf-8", newline="\n") as f:
f.write(text)
def test_parse_recipe_follows_the_pin_file(self):
url, rev = self.mod.parse_recipe(os.path.join(self.rdir, "comp.bb"))
self.assertEqual(url, "https://github.com/example/comp.git")
self.assertEqual(rev, "a" * 40)
self.write("comp-pin.inc", PIN_B)
self.assertEqual(self.mod.parse_recipe(os.path.join(self.rdir, "comp.bb"))[1], "b" * 40)
def test_parse_recipe_inline_pin_still_works(self):
self.write("comp.bb", RECIPE.replace("require comp-pin.inc\n", PIN_A))
os.remove(os.path.join(self.rdir, "comp-pin.inc"))
self.assertEqual(self.mod.parse_recipe(os.path.join(self.rdir, "comp.bb"))[1], "a" * 40)
def test_layer_hash_ignores_pin_bumps_and_docs(self):
base = self.mod.layer_content(self.layer)
self.assertIsNotNone(base)
self.write("comp-pin.inc", PIN_B)
self.assertEqual(self.mod.layer_content(self.layer), base, "a pin bump is not layer content")
self.write("README.md", "# docs, revised\n")
self.assertEqual(self.mod.layer_content(self.layer), base, "documentation is not layer content")
def test_layer_hash_sees_recipe_body_changes(self):
base = self.mod.layer_content(self.layer)
self.write("comp.bb", RECIPE + 'EXTRA_OEMAKE += "KCFLAGS=-Werror"\n')
self.assertNotEqual(self.mod.layer_content(self.layer), base)
def test_layer_hash_sees_a_pin_written_into_the_recipe(self):
# The safe direction: a pin that bypasses its pin file still hashes.
base = self.mod.layer_content(self.layer)
self.write("comp.bb", RECIPE + PIN_B)
self.assertNotEqual(self.mod.layer_content(self.layer), base)
def test_layer_hash_matches_the_bbclass_rule(self):
# The same skip list as FORGEFIRM_MANIFEST_PIN_SUFFIX + *.md in
# forgefirm-image-manifest.bbclass.
bbclass = os.path.join(REPO, "meta-forgefirm", "classes", "forgefirm-image-manifest.bbclass")
with open(bbclass, encoding="utf-8") as f:
text = f.read()
self.assertIn('FORGEFIRM_MANIFEST_PIN_SUFFIX ?= "-pin.inc"', text)
self.assertEqual(self.mod.LAYER_SKIP_SUFFIXES, (".md", "-pin.inc"))
if __name__ == "__main__":
unittest.main()