Files
poky/meta/lib/oeqa/selftest/cases/spdx.py
Benjamin Robin (Schneider Electric) 2d57a09792 meta: fix generation of kernel CONFIG_ in SPDX3
With the current solution, using a separate task
(do_create_kernel_config_spdx) there is a dependency issue. Sometimes
the final rootfs SBOM does not contain the CONFIG_ values.

do_create_kernel_config_spdx is executed after do_create_spdx which
deploys the SPDX file. do_create_kernel_config_spdx calls
oe.sbom30.find_root_obj_in_jsonld to read from the deploy directory,
which is OK, but the do_create_kernel_config_spdx ends up writing to
this deployed file (updating it).

do_create_rootfs_spdx has an explicit dependency to all do_create_spdx
tasks, but there is nothing that prevents executing
do_create_kernel_config_spdx after do_create_rootfs_spdx.

To fix it, instead, now read from the workdir, and write to the
workdir, and do the processing from the do_create_spdx task:
we append to the do_create_spdx task.
Furthermore, update oeqa selftest to execute do_create_spdx instead
of removed function.

Also only execute this task if create-spdx-3.0 was inherited,
previously this code could be executed if create-spdx-2.2 is
inherited.

(cherry picked from commit 8417f4a186e78a9d309541f5d0e711178bb80488)

Fixes: 1fff29a04287 ("kernel.bbclass: Add task to export kernel configuration to SPDX")
(From OE-Core rev: 22e8bc2bcfe762c83c00b73a33384e63548e82c0)

Signed-off-by: Benjamin Robin (Schneider Electric) <benjamin.robin@bootlin.com>
Reviewed-by: Joshua Watt <JPEWhacker@gmail.com>
Signed-off-by: Yoann Congal <yoann.congal@smile.fr>
Signed-off-by: Paul Barker <paul@pbarker.dev>
2026-06-19 12:49:07 +01:00

346 lines
11 KiB
Python

#
# Copyright OpenEmbedded Contributors
#
# SPDX-License-Identifier: MIT
#
import json
import os
import textwrap
import hashlib
from pathlib import Path
from oeqa.selftest.case import OESelftestTestCase
from oeqa.utils.commands import bitbake, get_bb_var, get_bb_vars, runCmd
import oe.spdx30
class SPDX22Check(OESelftestTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
bitbake("python3-spdx-tools-native")
bitbake("-c addto_recipe_sysroot python3-spdx-tools-native")
def check_recipe_spdx(self, high_level_dir, spdx_file, target_name):
config = textwrap.dedent(
"""\
INHERIT:remove = "create-spdx"
INHERIT += "create-spdx-2.2"
"""
)
self.write_config(config)
deploy_dir = get_bb_var("DEPLOY_DIR")
arch_dir = get_bb_var("PACKAGE_ARCH", target_name)
spdx_version = get_bb_var("SPDX_VERSION")
# qemux86-64 creates the directory qemux86_64
#arch_dir = arch_var.replace("-", "_")
full_file_path = os.path.join(
deploy_dir, "spdx", spdx_version, arch_dir, high_level_dir, spdx_file
)
try:
os.remove(full_file_path)
except FileNotFoundError:
pass
bitbake("%s -c create_spdx" % target_name)
def check_spdx_json(filename):
with open(filename) as f:
report = json.load(f)
self.assertNotEqual(report, None)
self.assertNotEqual(report["SPDXID"], None)
python = os.path.join(
get_bb_var("STAGING_BINDIR", "python3-spdx-tools-native"),
"nativepython3",
)
validator = os.path.join(
get_bb_var("STAGING_BINDIR", "python3-spdx-tools-native"), "pyspdxtools"
)
result = runCmd("{} {} -i {}".format(python, validator, filename))
self.assertExists(full_file_path)
result = check_spdx_json(full_file_path)
def test_spdx_base_files(self):
self.check_recipe_spdx("packages", "base-files.spdx.json", "base-files")
def test_spdx_tar(self):
self.check_recipe_spdx("packages", "tar.spdx.json", "tar")
class SPDX3CheckBase(object):
"""
Base class for checking SPDX 3 based tests
"""
def check_spdx_file(self, filename):
self.assertExists(filename)
# Read the file
objset = oe.spdx30.SHACLObjectSet()
with open(filename, "r") as f:
d = oe.spdx30.JSONLDDeserializer()
d.read(f, objset)
return objset
def check_recipe_spdx(self, target_name, spdx_path, *, task=None, extraconf=""):
config = (
textwrap.dedent(
f"""\
INHERIT:remove = "create-spdx"
INHERIT += "{self.SPDX_CLASS}"
"""
)
+ textwrap.dedent(extraconf)
)
self.write_config(config)
if task:
bitbake(f"-c {task} {target_name}")
else:
bitbake(target_name)
filename = spdx_path.format(
**get_bb_vars(
[
"DEPLOY_DIR_IMAGE",
"DEPLOY_DIR_SPDX",
"MACHINE",
"MACHINE_ARCH",
"SDKMACHINE",
"SDK_DEPLOY",
"SPDX_VERSION",
"SSTATE_PKGARCH",
"TOOLCHAIN_OUTPUTNAME",
],
target_name,
)
)
return self.check_spdx_file(filename)
def check_objset_missing_ids(self, objset):
for o in objset.foreach_type(oe.spdx30.SpdxDocument):
doc = o
break
else:
self.assertTrue(False, "Unable to find SpdxDocument")
missing_ids = objset.missing_ids - set(i.externalSpdxId for i in doc.import_)
if missing_ids:
self.assertTrue(
False,
"The following SPDXIDs are unresolved:\n " + "\n ".join(missing_ids),
)
class SPDX30Check(SPDX3CheckBase, OESelftestTestCase):
SPDX_CLASS = "create-spdx-3.0"
def test_base_files(self):
self.check_recipe_spdx(
"base-files",
"{DEPLOY_DIR_SPDX}/{MACHINE_ARCH}/packages/package-base-files.spdx.json",
)
def test_gcc_include_source(self):
objset = self.check_recipe_spdx(
"gcc",
"{DEPLOY_DIR_SPDX}/{SSTATE_PKGARCH}/recipes/recipe-gcc.spdx.json",
extraconf="""\
SPDX_INCLUDE_SOURCES = "1"
""",
)
gcc_pv = get_bb_var("PV", "gcc")
filename = f"gcc-{gcc_pv}/README"
found = False
for software_file in objset.foreach_type(oe.spdx30.software_File):
if software_file.name == filename:
found = True
self.logger.info(
f"The spdxId of {filename} in recipe-gcc.spdx.json is {software_file.spdxId}"
)
break
self.assertTrue(
found, f"Not found source file {filename} in recipe-gcc.spdx.json\n"
)
def test_core_image_minimal(self):
objset = self.check_recipe_spdx(
"core-image-minimal",
"{DEPLOY_DIR_IMAGE}/core-image-minimal-{MACHINE}.rootfs.spdx.json",
)
# Document should be fully linked
self.check_objset_missing_ids(objset)
def test_core_image_minimal_sdk(self):
objset = self.check_recipe_spdx(
"core-image-minimal",
"{SDK_DEPLOY}/{TOOLCHAIN_OUTPUTNAME}.spdx.json",
task="populate_sdk",
)
# Document should be fully linked
self.check_objset_missing_ids(objset)
def test_baremetal_helloworld(self):
objset = self.check_recipe_spdx(
"baremetal-helloworld",
"{DEPLOY_DIR_IMAGE}/baremetal-helloworld-image-{MACHINE}.spdx.json",
extraconf="""\
TCLIBC = "baremetal"
""",
)
# Document should be fully linked
self.check_objset_missing_ids(objset)
def test_extra_opts(self):
HOST_SPDXID = "http://foo.bar/spdx/bar2"
EXTRACONF = textwrap.dedent(
f"""\
SPDX_INVOKED_BY_name = "CI Tool"
SPDX_INVOKED_BY_type = "software"
SPDX_ON_BEHALF_OF_name = "John Doe"
SPDX_ON_BEHALF_OF_type = "person"
SPDX_ON_BEHALF_OF_id_email = "John.Doe@noreply.com"
SPDX_PACKAGE_SUPPLIER_name = "ACME Embedded Widgets"
SPDX_PACKAGE_SUPPLIER_type = "organization"
SPDX_AUTHORS += "authorA"
SPDX_AUTHORS_authorA_ref = "SPDX_ON_BEHALF_OF"
SPDX_BUILD_HOST = "host"
SPDX_IMPORTS += "host"
SPDX_IMPORTS_host_spdxid = "{HOST_SPDXID}"
SPDX_INCLUDE_BUILD_VARIABLES = "1"
SPDX_INCLUDE_BITBAKE_PARENT_BUILD = "1"
SPDX_INCLUDE_TIMESTAMPS = "1"
SPDX_PRETTY = "1"
"""
)
extraconf_hash = hashlib.sha1(EXTRACONF.encode("utf-8")).hexdigest()
objset = self.check_recipe_spdx(
"core-image-minimal",
"{DEPLOY_DIR_IMAGE}/core-image-minimal-{MACHINE}.rootfs.spdx.json",
# Many SPDX variables do not trigger a rebuild, since they are
# intended to record information at the time of the build. As such,
# the extra configuration alone may not trigger a rebuild, and even
# if it does, the task hash won't necessarily be unique. In order
# to make sure rebuilds happen, but still allow these test objects
# to be pulled from sstate (e.g. remain reproducible), change the
# namespace prefix to include the hash of the extra configuration
extraconf=textwrap.dedent(
f"""\
SPDX_NAMESPACE_PREFIX = "http://spdx.org/spdxdocs/{extraconf_hash}"
"""
)
+ EXTRACONF,
)
# Document should be fully linked
self.check_objset_missing_ids(objset)
for o in objset.foreach_type(oe.spdx30.SoftwareAgent):
if o.name == "CI Tool":
break
else:
self.assertTrue(False, "Unable to find software tool")
for o in objset.foreach_type(oe.spdx30.Person):
if o.name == "John Doe":
break
else:
self.assertTrue(False, "Unable to find person")
for o in objset.foreach_type(oe.spdx30.Organization):
if o.name == "ACME Embedded Widgets":
break
else:
self.assertTrue(False, "Unable to find organization")
for o in objset.foreach_type(oe.spdx30.SpdxDocument):
doc = o
break
else:
self.assertTrue(False, "Unable to find SpdxDocument")
for i in doc.import_:
if i.externalSpdxId == HOST_SPDXID:
break
else:
self.assertTrue(False, "Unable to find imported Host SpdxID")
def test_kernel_config_spdx(self):
kernel_recipe = get_bb_var("PREFERRED_PROVIDER_virtual/kernel")
spdx_file = f"recipe-{kernel_recipe}.spdx.json"
spdx_path = f"{{DEPLOY_DIR_SPDX}}/{{SSTATE_PKGARCH}}/recipes/{spdx_file}"
# Make sure kernel is configured first
bitbake(f"-c configure {kernel_recipe}")
objset = self.check_recipe_spdx(
kernel_recipe,
spdx_path,
task="do_create_spdx",
extraconf="""\
INHERIT += "create-spdx"
SPDX_INCLUDE_KERNEL_CONFIG = "1"
""",
)
# Check that at least one CONFIG_* entry exists
found_kernel_config = False
for build_obj in objset.foreach_type(oe.spdx30.build_Build):
if getattr(build_obj, "build_buildType", "") == "https://openembedded.org/kernel-configuration":
found_kernel_config = True
self.assertTrue(
len(getattr(build_obj, "build_parameter", [])) > 0,
"Kernel configuration build_Build has no CONFIG_* entries"
)
break
self.assertTrue(found_kernel_config, "Kernel configuration build_Build not found in SPDX output")
def test_packageconfig_spdx(self):
objset = self.check_recipe_spdx(
"tar",
"{DEPLOY_DIR_SPDX}/{SSTATE_PKGARCH}/recipes/recipe-tar.spdx.json",
extraconf="""\
SPDX_INCLUDE_PACKAGECONFIG = "1"
""",
)
found_entries = []
for build_obj in objset.foreach_type(oe.spdx30.build_Build):
for param in getattr(build_obj, "build_parameter", []):
if param.key.startswith("PACKAGECONFIG:"):
found_entries.append((param.key, param.value))
self.assertTrue(
found_entries,
"No PACKAGECONFIG entries found in SPDX output for 'tar'"
)
for key, value in found_entries:
self.assertIn(
value, ["enabled", "disabled"],
f"Unexpected PACKAGECONFIG value '{value}' for {key}"
)