Files
forgefirm/meta-forgefirm/recipes-forgefirm/images/pack-licenses.sh
T
ScottW514 03146a1529 forgefirm-image: pack-licenses.sh refuses a license text tar cannot read
pack-licenses.sh packs the image's license tree into one tar.gz and
removes the tree. It piped tar into gzip under /bin/sh (dash, no
pipefail): the pipeline's status is gzip's, and tar's is lost. The
check that follows proves only that the bundle holds license.manifest.
So a member tar cannot read leaves a bundle without that text, the
build green, and the tree removed, the only copy of the text with it.

tar now runs the compressor itself (-I 'gzip -9 -n'), so its exit
status covers the read and the write. A failed pack removes the
partial bundle and the script refuses, with the tree left for the
build log; pack_licenses in forgefirm-image.bb fails do_rootfs on the
refusal. The bundle's bytes do not change: tar feeds the same stream
to the same gzip, and a tree packed by both forms gives one sha256.

Proof: forgetest-ci "Unit tests" (`python -B -m unittest discover -s
tests -v` in forgetest), on Ubuntu 24.04 with python 3.12 as an
unprivileged user: test_image.py's PackLicensesTests gain
test_a_license_text_tar_cannot_read_is_refused_and_the_tree_left (a
text under zlib/ with mode 0: a nonzero exit, a message on stderr, no
bundle, the manifest left); the class's 5 tests OK. Negative control
on a scratch copy: the new case against the script without this change
fails at assertRefused with an exit status of 0. At the shell, on a
fixture tree with the unreadable member, the script without this
change exits 0, leaves the bundle without the text and removes the
tree; with it, the script exits 1, leaves no bundle and leaves the
tree. On a readable tree both forms write byte-identical bundles (one
sha256 for both). pyflakes reports no undefined name.

Acceptance: every catalog test. pack-licenses.sh is under
meta-forgefirm, whose content hash is part of the platform in every
fingerprint, so the image that carries this change needs the whole
catalog on the bench.
2026-10-15 21:12:04 -04:00

43 lines
1.8 KiB
Bash
Executable File

#!/bin/sh
# Copyright 2026 514 LLC d/b/a OpenGlow
# Written by Scott Wiederhold
# https://community.openglow.org
# SPDX-License-Identifier: MIT
#
# pack-licenses.sh <common-licenses directory> <tar.gz>
#
# Packs the image's license tree into one reproducible tar.gz (sorted
# names, no timestamps, no owners), its members under the tree's own
# name, and removes the tree. It refuses, before it writes the tar or
# removes anything, a tree that is missing, a tree with no
# license.manifest, and a manifest that does not name the project's own
# recipes: forgectrl, grblhal-glowforge and kernel-module-glowforge.
# forgefirm-image.bb's pack_licenses runs it; forgetest's test_image.py
# runs it on a fixture.
dir=${1:?common-licenses directory}
out=${2:?output tar.gz}
fail() { echo "pack-licenses: $*" >&2; exit 1; }
[ -d "$dir" ] || fail "$dir is missing (COPY_LIC_DIRS off?)"
manifest=$dir/license.manifest
[ -f "$manifest" ] || fail "no license.manifest in $dir"
# A line counts with its newline: an unterminated last line is not read.
lines=$(wc -l < "$manifest")
for name in forgectrl grblhal-glowforge kernel-module-glowforge; do
head -n "$lines" "$manifest" | grep -Fxq "RECIPE NAME: $name" \
|| fail "$manifest does not name the recipe $name"
done
base=$(basename "$dir")
mkdir -p "$(dirname "$out")" || fail "cannot make the directory of $out"
# tar runs the compressor itself, so its exit status covers both: a
# member it cannot read fails the pack, and a failed pack leaves no bundle.
tar -C "$(dirname "$dir")" --sort=name --mtime=@0 --owner=0 --group=0 \
--numeric-owner -I 'gzip -9 -n' -cf "$out" "$base" \
|| { rm -f "$out"; fail "cannot pack $dir into $out"; }
# the tree goes only once the bundle holds its manifest
tar -tzf "$out" "$base/license.manifest" > /dev/null 2>&1 \
|| fail "$out holds no $base/license.manifest"
rm -rf "$dir"