Files
forgefirm/meta-forgefirm/classes/forgefirm-manifest.bbclass
T
ScottW514 9b5558dfbc meta-forgefirm: the image manifest (/etc/forgefirm-manifest.json)
Every image records the identity of its build inputs. forgefirm-manifest.bbclass
gives a component recipe a per-file source fingerprint (git ls-tree blob ids,
submodules recursed; file:// sources hashed with git hash-object); the kernel
and glowforge.ko publish theirs through do_deploy because kernel-module-split
leaves the module recipe's main package empty. forgefirm-image-manifest.bbclass
assembles the entries with the platform identity - machine, modules directory,
device tree hashes, layer content hashes (meta-forgefirm and the meta-openglow
layers by content, the kas-managed layers by revision) - into
/etc/forgefirm-manifest.json and deploys a copy next to the image. Layer
revisions and dirty flags stay outside the identity, so a rebuild from an
unchanged tree keeps its content hash and the release and dev images of one
build share it. The acceptance tool and the release gate read this file.
2026-08-15 15:57:10 -04:00

133 lines
5.9 KiB
Plaintext

# forgefirm-manifest.bbclass - source fingerprint of one ForgeFIRM component
#
# A component recipe that inherits this class records what source it was
# built from: the component name, the pinned revision, and one
# [path, blob-id] pair per source file (git blob ids, so equal content
# means equal ids regardless of the commit that carried it). The image
# collects the entries into /etc/forgefirm-manifest.json
# (forgefirm-image-manifest.bbclass); the acceptance tool (forgetest) and
# the release gate (scripts/acceptance-gate.py) read that file to decide
# which acceptance results still apply to a build.
#
# Modes (FORGEFIRM_MANIFEST_MODE):
# rootfs (default) the entry is installed as
# /etc/forgefirm-manifest.d/<PN>.json in the recipe's main package
# deploy the recipe writes the entry into ${DEPLOYDIR} itself (see the
# kernel-module bbappend: its .ko is packaged by kernel-module-split
# into a versioned package, so a rootfs file from the recipe would
# not reach the image); this class then only provides the helpers.
#
# Sources: a git checkout in ${S} is fingerprinted with `git ls-tree`
# (submodules recursed, the gitlink kept). Non-git sources (file:// recipes)
# set FORGEFIRM_MANIFEST_SRC to the directory to fingerprint; the ids are
# computed with `git hash-object`, so they compare with tree ids.
FORGEFIRM_MANIFEST_NAME ?= "${PN}"
FORGEFIRM_MANIFEST_MODE ?= "rootfs"
FORGEFIRM_MANIFEST_SRC ?= ""
FORGEFIRM_MANIFEST_DIR = "${sysconfdir}/forgefirm-manifest.d"
def forgefirm_manifest_git(args, cwd):
import subprocess
return subprocess.check_output(['git'] + args, cwd=cwd,
stderr=subprocess.STDOUT).decode('utf-8', 'replace')
def forgefirm_manifest_tree(files, repo, prefix):
import os
out = forgefirm_manifest_git(['ls-tree', '-r', '--full-tree', 'HEAD'], repo)
for line in out.splitlines():
if not line.strip():
continue
meta, path = line.split('\t', 1)
fields = meta.split()
typ, obj = fields[1], fields[2]
files.append([prefix + path, obj])
if typ == 'commit':
sub = os.path.join(repo, path)
if os.path.exists(os.path.join(sub, '.git')):
forgefirm_manifest_tree(files, sub, prefix + path + '/')
def forgefirm_manifest_git_prefix(cwd):
"""`git hash-object --stdin-paths` resolves paths against the repository
top level, not the cwd; this is the cwd's prefix inside the enclosing
repository ('' when there is none)."""
import subprocess
try:
return subprocess.check_output(['git', 'rev-parse', '--show-prefix'], cwd=cwd,
stderr=subprocess.DEVNULL).decode().strip()
except (subprocess.CalledProcessError, OSError):
return ''
def forgefirm_manifest_hash_dir(src):
import os, subprocess
paths = []
for root, dirs, fns in os.walk(src):
dirs[:] = sorted(x for x in dirs if x not in ('.git', '__pycache__'))
for fn in fns:
if fn.endswith(('.pyc', '.pyo')):
continue
p = os.path.join(root, fn)
if os.path.isfile(p) and not os.path.islink(p):
paths.append(os.path.relpath(p, src))
paths.sort()
if not paths:
return []
prefix = forgefirm_manifest_git_prefix(src)
out = subprocess.run(['git', 'hash-object', '--stdin-paths'], cwd=src,
input=('\n'.join(prefix + p for p in paths) + '\n').encode(),
stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True)
ids = out.stdout.decode().split()
if len(ids) != len(paths):
bb.fatal("forgefirm-manifest: hash-object returned %d ids for %d paths"
% (len(ids), len(paths)))
return [[p, i] for p, i in zip(paths, ids)]
def forgefirm_manifest_entry(d):
import os
pn = d.getVar('PN')
entry = {'component': d.getVar('FORGEFIRM_MANIFEST_NAME'),
'recipe': pn, 'pv': d.getVar('PV'), 'files': []}
src = (d.getVar('FORGEFIRM_MANIFEST_SRC') or '').strip()
s = d.getVar('S')
if src:
if not os.path.isdir(src):
bb.fatal("forgefirm-manifest: %s: FORGEFIRM_MANIFEST_SRC '%s' is not a directory" % (pn, src))
entry['srcrev'] = None
entry['source'] = 'files'
entry['files'] = forgefirm_manifest_hash_dir(src)
elif os.path.exists(os.path.join(s, '.git')):
entry['srcrev'] = forgefirm_manifest_git(['rev-parse', 'HEAD'], s).strip()
uri = (d.getVar('SRC_URI') or '').split()
entry['source'] = uri[0].split(';')[0] if uri else None
forgefirm_manifest_tree(entry['files'], s, '')
else:
bb.fatal("forgefirm-manifest: %s: ${S} is not a git checkout and FORGEFIRM_MANIFEST_SRC is unset" % pn)
entry['files'].sort()
return entry
def forgefirm_manifest_write(entry, path):
import json, os
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, 'w') as f:
json.dump(entry, f, sort_keys=True, separators=(',', ':'))
f.write('\n')
os.chmod(path, 0o644)
fakeroot python do_forgefirm_manifest() {
import os
entry = forgefirm_manifest_entry(d)
forgefirm_manifest_write(entry, os.path.join(d.getVar('D') + d.getVar('FORGEFIRM_MANIFEST_DIR'),
d.getVar('PN') + '.json'))
}
do_forgefirm_manifest[depends] += "virtual/fakeroot-native:do_populate_sysroot"
do_forgefirm_manifest[vardeps] += "FORGEFIRM_MANIFEST_NAME FORGEFIRM_MANIFEST_SRC SRCREV SRC_URI \
forgefirm_manifest_entry forgefirm_manifest_tree forgefirm_manifest_git \
forgefirm_manifest_hash_dir forgefirm_manifest_git_prefix forgefirm_manifest_write"
python __anonymous() {
if d.getVar('FORGEFIRM_MANIFEST_MODE') == 'rootfs':
bb.build.addtask('do_forgefirm_manifest', 'do_package do_populate_sysroot', 'do_install', d)
}
FILES:${PN}:append = " ${FORGEFIRM_MANIFEST_DIR}"