mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-28 09:11:11 -07:00
231 lines
11 KiB
Python
231 lines
11 KiB
Python
# Copyright 2026 514 LLC d/b/a OpenGlow
|
|
# Written by Scott Wiederhold
|
|
# https://community.openglow.org
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
"""scripts/source-bundle.py - the source that a release publishes.
|
|
|
|
The bundle must hold the source of every recipe of the image whose license
|
|
makes source necessary. The list comes from the license manifests that the
|
|
build writes, so a package cannot reach a machine with its source left
|
|
behind. These tests hold the decision (which recipe needs source), the
|
|
choice of archive (the version of the image, not an older build), and the
|
|
refusal that stops a release with source missing."""
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
import tarfile
|
|
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", "source-bundle.py")
|
|
META_OPENGLOW = os.path.join(REPO, "..", "meta-openglow")
|
|
IMAGE = "forgefirm-image-glowforge.rootfs-20260101000000"
|
|
|
|
|
|
def load_script():
|
|
spec = importlib.util.spec_from_file_location("source_bundle", SCRIPT)
|
|
mod = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(mod)
|
|
return mod
|
|
|
|
|
|
def package_block(recipe, version, license_, package=None):
|
|
return ("PACKAGE NAME: %s\nPACKAGE VERSION: %s\nRECIPE NAME: %s\nLICENSE: %s\n\n"
|
|
% (package or recipe, version, recipe, license_))
|
|
|
|
|
|
def write(path, text):
|
|
os.makedirs(os.path.dirname(path), exist_ok=True)
|
|
with open(path, "w", encoding="utf-8") as f:
|
|
f.write(text)
|
|
|
|
|
|
def make_deploy(root, packages, archives, image_packages=()):
|
|
"""A deploy directory with the license manifests of one image and the
|
|
archive directories of the archiver pass."""
|
|
licdir = os.path.join(root, "licenses", "glowforge", IMAGE)
|
|
# The bundle answers for one image: its license manifests and its build
|
|
# identity must be in the deploy directory.
|
|
write(os.path.join(root, "images", "glowforge",
|
|
IMAGE + ".forgefirm-manifest.json"),
|
|
'{"content_sha256": "0"}\n')
|
|
write(os.path.join(licdir, "license.manifest"),
|
|
"".join(package_block(*p) for p in packages))
|
|
write(os.path.join(licdir, "image_license.manifest"),
|
|
"".join("RECIPE NAME: %s\nVERSION: %s\nLICENSE: %s\nFILES: x\n\n" % p
|
|
for p in image_packages))
|
|
for name in archives:
|
|
write(os.path.join(root, "sources", "arm-fslc-linux-gnueabi", name,
|
|
"%s.tar.gz" % name), "source of %s\n" % name)
|
|
return licdir
|
|
|
|
|
|
class Policy(unittest.TestCase):
|
|
"""The archiver pass and the check read one license list."""
|
|
|
|
def setUp(self):
|
|
self.mod = load_script()
|
|
self.policy = self.mod.read_policy(self.mod.KAS_OVERLAY)
|
|
|
|
def test_the_overlay_carries_the_filter(self):
|
|
self.assertIn("GPL*", self.policy["COPYLEFT_LICENSE_INCLUDE"])
|
|
self.assertIn("LGPL*", self.policy["COPYLEFT_LICENSE_INCLUDE"])
|
|
self.assertIn("Proprietary", self.policy["COPYLEFT_LICENSE_EXCLUDE"])
|
|
# The ForgeFIRM components are MIT and travel with the release too.
|
|
self.assertIn("forgectrl", self.policy["COPYLEFT_PN_INCLUDE"])
|
|
self.assertIn("gfcloud", self.policy["COPYLEFT_PN_INCLUDE"])
|
|
|
|
def test_a_copyleft_license_needs_source(self):
|
|
for expr in ("GPL-2.0-only",
|
|
"LGPL-2.1-or-later",
|
|
"GPL-2.0-only & bzip2-1.0.4",
|
|
"AFL-2.1 | GPL-2.0-or-later",
|
|
"(GPL-2.0-or-later | LGPL-3.0-only) & Unicode-DFS-2016",
|
|
"MIT ; LGPL-2.1-or-later",
|
|
"GPL-3.0-with-GCC-exception"):
|
|
self.assertTrue(self.mod.source_needed(expr, self.policy), expr)
|
|
|
|
def test_a_permissive_license_does_not(self):
|
|
for expr in ("MIT", "BSD-3-Clause", "Apache-2.0", "ISC", "PSF-2.0",
|
|
"Proprietary", "Firmware-imx-sdma_firmware", "PD"):
|
|
self.assertFalse(self.mod.source_needed(expr, self.policy), expr)
|
|
|
|
|
|
class Manifests(unittest.TestCase):
|
|
|
|
def setUp(self):
|
|
self.mod = load_script()
|
|
|
|
def test_a_recipe_collects_the_licenses_of_its_packages(self):
|
|
path = os.path.join(self.tmp(), "license.manifest")
|
|
write(path, package_block("avahi", "0.8", "LGPL-2.1-or-later", "avahi-daemon")
|
|
+ package_block("avahi", "0.8", "GPL-2.0-or-later & LGPL-2.1-or-later",
|
|
"avahi-locale-en-gb"))
|
|
recipes = self.mod.parse_manifest(path, "PACKAGE VERSION")
|
|
self.assertEqual(list(recipes), ["avahi"])
|
|
self.assertEqual(recipes["avahi"]["version"], "0.8")
|
|
self.assertEqual(len(recipes["avahi"]["licenses"]), 2)
|
|
|
|
def test_the_archive_of_the_image_version_wins(self):
|
|
index = {"linux-fslc": [("6.11.0", "/old"), ("6.12.20+git0+844aa34", "/new")],
|
|
"busybox": [("1.36.1", "/b1"), ("1.35.0", "/b0")],
|
|
"forgectrl": [("1_0.1.4", "/fc")]}
|
|
# An exact version first.
|
|
self.assertEqual(self.mod.select_archives(index, "busybox", "1.36.1"), ["/b1"])
|
|
# Then a version that starts with it: the kernel carries the
|
|
# revision of its git source in PV, and the manifest does not.
|
|
self.assertEqual(self.mod.select_archives(index, "linux-fslc", "6.12.20+git"),
|
|
["/new"])
|
|
# The name of an archive carries the epoch of the recipe, and a
|
|
# license manifest does not.
|
|
self.assertEqual(self.mod.select_archives(index, "forgectrl", "0.1.4"), ["/fc"])
|
|
self.assertEqual(self.mod.select_archives(index, "curl", "8.7.1"), [])
|
|
|
|
def test_a_directory_belongs_to_the_longest_recipe_name(self):
|
|
root = self.tmp()
|
|
make_deploy(root, [], ["python3-3.12.13-r0", "python3-certifi-2024.2.2-r0"])
|
|
index = self.mod.index_archives(os.path.join(root, "sources"),
|
|
{"python3", "python3-certifi"})
|
|
self.assertEqual([v for v, _ in index["python3"]], ["3.12.13"])
|
|
self.assertEqual([v for v, _ in index["python3-certifi"]], ["2024.2.2"])
|
|
|
|
def tmp(self):
|
|
import tempfile
|
|
d = tempfile.mkdtemp()
|
|
self.addCleanup(__import__("shutil").rmtree, d, True)
|
|
return d
|
|
|
|
|
|
@unittest.skipUnless(os.path.isdir(META_OPENGLOW),
|
|
"the meta-openglow sibling checkout is not here")
|
|
class Pack(unittest.TestCase):
|
|
"""The bundle end to end, on a deploy directory of made-up recipes."""
|
|
|
|
def setUp(self):
|
|
import tempfile
|
|
self.mod = load_script()
|
|
self.root = tempfile.mkdtemp()
|
|
self.addCleanup(__import__("shutil").rmtree, self.root, True)
|
|
self.deploy = os.path.join(self.root, "deploy")
|
|
self.out = os.path.join(self.root, "out")
|
|
|
|
def run_main(self, packages, archives, image_packages=()):
|
|
make_deploy(self.deploy, packages, archives, image_packages)
|
|
import sys
|
|
argv = sys.argv
|
|
sys.argv = ["source-bundle.py", "0.0.1", "--deploy", self.deploy,
|
|
"--image-name", IMAGE, "--out", self.out]
|
|
try:
|
|
return self.mod.main()
|
|
finally:
|
|
sys.argv = argv
|
|
|
|
def test_the_bundle_holds_the_source_and_the_accounting(self):
|
|
self.run_main(
|
|
packages=[("busybox", "1.36.1", "GPL-2.0-only"),
|
|
("jansson", "2.14", "MIT"),
|
|
("forgectrl", "0.1.4", "MIT")],
|
|
archives=["busybox-1.36.1-r0", "forgectrl-0.1.4-r0", "jansson-2.14-r0",
|
|
"u-boot-2020.01-r0"],
|
|
image_packages=[("u-boot", "2020.01", "GPL-2.0-or-later")])
|
|
path = os.path.join(self.out, "forgefirm-source-v0.0.1.tar.gz")
|
|
self.assertTrue(os.path.isfile(path))
|
|
with tarfile.open(path) as tar:
|
|
names = tar.getnames()
|
|
manifest = json.loads(tar.extractfile(
|
|
"forgefirm-source-v0.0.1/MANIFEST.json").read().decode())
|
|
sums = tar.extractfile(
|
|
"forgefirm-source-v0.0.1/sha256sums.txt").read().decode()
|
|
# The copyleft recipe and the ForgeFIRM component are in, the
|
|
# permissive third party is not.
|
|
self.assertIn("forgefirm-source-v0.0.1/sources/busybox-1.36.1-r0/busybox-1.36.1-r0.tar.gz",
|
|
names)
|
|
self.assertIn("forgefirm-source-v0.0.1/sources/forgectrl-0.1.4-r0/forgectrl-0.1.4-r0.tar.gz",
|
|
names)
|
|
self.assertNotIn("forgefirm-source-v0.0.1/sources/jansson-2.14-r0/jansson-2.14-r0.tar.gz",
|
|
names)
|
|
# The accounting travels with the source.
|
|
for name in ("README.md", "SOURCES.txt", "MANIFEST.json",
|
|
"licenses/license.manifest", "licenses/image_license.manifest",
|
|
"metadata/LAYERS.txt", "metadata/meta-forgefirm.tar.gz",
|
|
"metadata/kas/source-bundle.yml"):
|
|
self.assertIn("forgefirm-source-v0.0.1/" + name, names)
|
|
self.assertEqual(manifest["release"], "v0.0.1")
|
|
self.assertEqual(sorted(r["recipe"] for r in manifest["recipes"]),
|
|
["busybox", "forgectrl", "u-boot"])
|
|
self.assertEqual(manifest["recipes_without_source"],
|
|
[{"recipe": "jansson", "version": "2.14", "license": "MIT"}])
|
|
# Every file of the bundle is in the checksums, and only files that
|
|
# the bundle holds.
|
|
listed = sorted(line.split(" ", 1)[1] for line in sums.splitlines())
|
|
held = sorted(n.split("/", 1)[1] for n in names
|
|
if n != "forgefirm-source-v0.0.1/sha256sums.txt")
|
|
self.assertEqual(listed, held)
|
|
|
|
def test_a_recipe_with_no_source_stops_the_release(self):
|
|
with self.assertRaises(SystemExit) as caught:
|
|
self.run_main(packages=[("busybox", "1.36.1", "GPL-2.0-only"),
|
|
("gnutls", "3.8.4", "LGPL-2.1-or-later")],
|
|
archives=["busybox-1.36.1-r0"])
|
|
self.assertEqual(caught.exception.code, 1)
|
|
|
|
def test_the_source_of_gcc_covers_libgcc(self):
|
|
self.run_main(
|
|
packages=[("libgcc", "13.4.0", "GPL-3.0-with-GCC-exception"),
|
|
("glibc", "2.39+git", "GPL-2.0-only & LGPL-2.1-or-later"),
|
|
("glibc-locale", "2.39+git", "GPL-2.0-only & LGPL-2.1-or-later")],
|
|
archives=["gcc-source-13.4.0-13.4.0-r0", "glibc-2.39+git-r0"])
|
|
with tarfile.open(os.path.join(self.out, "forgefirm-source-v0.0.1.tar.gz")) as tar:
|
|
manifest = json.loads(tar.extractfile(
|
|
"forgefirm-source-v0.0.1/MANIFEST.json").read().decode())
|
|
holders = {r["recipe"]: r["holder"] for r in manifest["recipes"]}
|
|
self.assertEqual(holders["libgcc"], "gcc-source-13.4.0")
|
|
self.assertEqual(holders["glibc-locale"], "glibc")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|