xref: /openbmc/openbmc/poky/meta/lib/oeqa/selftest/cases/spdx.py (revision c9537f57ab488bf5d90132917b0184e2527970a5)
1#
2# Copyright OpenEmbedded Contributors
3#
4# SPDX-License-Identifier: MIT
5#
6
7import json
8import os
9import textwrap
10import hashlib
11from pathlib import Path
12from oeqa.selftest.case import OESelftestTestCase
13from oeqa.utils.commands import bitbake, get_bb_var, get_bb_vars, runCmd
14import oe.spdx30
15
16
17class SPDX22Check(OESelftestTestCase):
18    @classmethod
19    def setUpClass(cls):
20        super().setUpClass()
21        bitbake("python3-spdx-tools-native")
22        bitbake("-c addto_recipe_sysroot python3-spdx-tools-native")
23
24    def check_recipe_spdx(self, high_level_dir, spdx_file, target_name):
25        config = textwrap.dedent(
26            """\
27            INHERIT:remove = "create-spdx"
28            INHERIT += "create-spdx-2.2"
29            """
30        )
31        self.write_config(config)
32
33        deploy_dir = get_bb_var("DEPLOY_DIR")
34        arch_dir = get_bb_var("PACKAGE_ARCH", target_name)
35        spdx_version = get_bb_var("SPDX_VERSION")
36        # qemux86-64 creates the directory qemux86_64
37        #arch_dir = arch_var.replace("-", "_")
38
39        full_file_path = os.path.join(
40            deploy_dir, "spdx", spdx_version, arch_dir, high_level_dir, spdx_file
41        )
42
43        try:
44            os.remove(full_file_path)
45        except FileNotFoundError:
46            pass
47
48        bitbake("%s -c create_spdx" % target_name)
49
50        def check_spdx_json(filename):
51            with open(filename) as f:
52                report = json.load(f)
53                self.assertNotEqual(report, None)
54                self.assertNotEqual(report["SPDXID"], None)
55
56            python = os.path.join(
57                get_bb_var("STAGING_BINDIR", "python3-spdx-tools-native"),
58                "nativepython3",
59            )
60            validator = os.path.join(
61                get_bb_var("STAGING_BINDIR", "python3-spdx-tools-native"), "pyspdxtools"
62            )
63            result = runCmd("{} {} -i {}".format(python, validator, filename))
64
65        self.assertExists(full_file_path)
66        result = check_spdx_json(full_file_path)
67
68    def test_spdx_base_files(self):
69        self.check_recipe_spdx("packages", "base-files.spdx.json", "base-files")
70
71    def test_spdx_tar(self):
72        self.check_recipe_spdx("packages", "tar.spdx.json", "tar")
73
74
75class SPDX3CheckBase(object):
76    """
77    Base class for checking SPDX 3 based tests
78    """
79
80    def check_spdx_file(self, filename):
81        self.assertExists(filename)
82
83        # Read the file
84        objset = oe.spdx30.SHACLObjectSet()
85        with open(filename, "r") as f:
86            d = oe.spdx30.JSONLDDeserializer()
87            d.read(f, objset)
88
89        return objset
90
91    def check_recipe_spdx(self, target_name, spdx_path, *, task=None, extraconf=""):
92        config = (
93            textwrap.dedent(
94                f"""\
95                INHERIT:remove = "create-spdx"
96                INHERIT += "{self.SPDX_CLASS}"
97                """
98            )
99            + textwrap.dedent(extraconf)
100        )
101
102        self.write_config(config)
103
104        if task:
105            bitbake(f"-c {task} {target_name}")
106        else:
107            bitbake(target_name)
108
109        filename = spdx_path.format(
110            **get_bb_vars(
111                [
112                    "DEPLOY_DIR_IMAGE",
113                    "DEPLOY_DIR_SPDX",
114                    "MACHINE",
115                    "MACHINE_ARCH",
116                    "SDKMACHINE",
117                    "SDK_DEPLOY",
118                    "SPDX_VERSION",
119                    "SSTATE_PKGARCH",
120                    "TOOLCHAIN_OUTPUTNAME",
121                ],
122                target_name,
123            )
124        )
125
126        return self.check_spdx_file(filename)
127
128    def check_objset_missing_ids(self, objset):
129        for o in objset.foreach_type(oe.spdx30.SpdxDocument):
130            doc = o
131            break
132        else:
133            self.assertTrue(False, "Unable to find SpdxDocument")
134
135        missing_ids = objset.missing_ids - set(i.externalSpdxId for i in doc.import_)
136        if missing_ids:
137            self.assertTrue(
138                False,
139                "The following SPDXIDs are unresolved:\n  " + "\n  ".join(missing_ids),
140            )
141
142
143class SPDX30Check(SPDX3CheckBase, OESelftestTestCase):
144    SPDX_CLASS = "create-spdx-3.0"
145
146    def test_base_files(self):
147        self.check_recipe_spdx(
148            "base-files",
149            "{DEPLOY_DIR_SPDX}/{MACHINE_ARCH}/packages/package-base-files.spdx.json",
150        )
151
152    def test_gcc_include_source(self):
153        objset = self.check_recipe_spdx(
154            "gcc",
155            "{DEPLOY_DIR_SPDX}/{SSTATE_PKGARCH}/recipes/recipe-gcc.spdx.json",
156            extraconf="""\
157                SPDX_INCLUDE_SOURCES = "1"
158                """,
159        )
160
161        gcc_pv = get_bb_var("PV", "gcc")
162        filename = f"gcc-{gcc_pv}/README"
163        found = False
164        for software_file in objset.foreach_type(oe.spdx30.software_File):
165            if software_file.name == filename:
166                found = True
167                self.logger.info(
168                    f"The spdxId of {filename} in recipe-gcc.spdx.json is {software_file.spdxId}"
169                )
170                break
171
172        self.assertTrue(
173            found, f"Not found source file {filename} in recipe-gcc.spdx.json\n"
174        )
175
176    def test_core_image_minimal(self):
177        objset = self.check_recipe_spdx(
178            "core-image-minimal",
179            "{DEPLOY_DIR_IMAGE}/core-image-minimal-{MACHINE}.rootfs.spdx.json",
180        )
181
182        # Document should be fully linked
183        self.check_objset_missing_ids(objset)
184
185    def test_core_image_minimal_sdk(self):
186        objset = self.check_recipe_spdx(
187            "core-image-minimal",
188            "{SDK_DEPLOY}/{TOOLCHAIN_OUTPUTNAME}.spdx.json",
189            task="populate_sdk",
190        )
191
192        # Document should be fully linked
193        self.check_objset_missing_ids(objset)
194
195    def test_baremetal_helloworld(self):
196        objset = self.check_recipe_spdx(
197            "baremetal-helloworld",
198            "{DEPLOY_DIR_IMAGE}/baremetal-helloworld-image-{MACHINE}.spdx.json",
199            extraconf="""\
200                TCLIBC = "baremetal"
201                """,
202        )
203
204        # Document should be fully linked
205        self.check_objset_missing_ids(objset)
206
207    def test_extra_opts(self):
208        HOST_SPDXID = "http://foo.bar/spdx/bar2"
209
210        EXTRACONF = textwrap.dedent(
211            f"""\
212            SPDX_INVOKED_BY_name = "CI Tool"
213            SPDX_INVOKED_BY_type = "software"
214
215            SPDX_ON_BEHALF_OF_name = "John Doe"
216            SPDX_ON_BEHALF_OF_type = "person"
217            SPDX_ON_BEHALF_OF_id_email = "John.Doe@noreply.com"
218
219            SPDX_PACKAGE_SUPPLIER_name = "ACME Embedded Widgets"
220            SPDX_PACKAGE_SUPPLIER_type = "organization"
221
222            SPDX_AUTHORS += "authorA"
223            SPDX_AUTHORS_authorA_ref = "SPDX_ON_BEHALF_OF"
224
225            SPDX_BUILD_HOST = "host"
226
227            SPDX_IMPORTS += "host"
228            SPDX_IMPORTS_host_spdxid = "{HOST_SPDXID}"
229
230            SPDX_INCLUDE_BUILD_VARIABLES = "1"
231            SPDX_INCLUDE_BITBAKE_PARENT_BUILD = "1"
232            SPDX_INCLUDE_TIMESTAMPS = "1"
233
234            SPDX_PRETTY = "1"
235            """
236        )
237        extraconf_hash = hashlib.sha1(EXTRACONF.encode("utf-8")).hexdigest()
238
239        objset = self.check_recipe_spdx(
240            "core-image-minimal",
241            "{DEPLOY_DIR_IMAGE}/core-image-minimal-{MACHINE}.rootfs.spdx.json",
242            # Many SPDX variables do not trigger a rebuild, since they are
243            # intended to record information at the time of the build. As such,
244            # the extra configuration alone may not trigger a rebuild, and even
245            # if it does, the task hash won't necessarily be unique. In order
246            # to make sure rebuilds happen, but still allow these test objects
247            # to be pulled from sstate (e.g. remain reproducible), change the
248            # namespace prefix to include the hash of the extra configuration
249            extraconf=textwrap.dedent(
250                f"""\
251                SPDX_NAMESPACE_PREFIX = "http://spdx.org/spdxdocs/{extraconf_hash}"
252                """
253            )
254            + EXTRACONF,
255        )
256
257        # Document should be fully linked
258        self.check_objset_missing_ids(objset)
259
260        for o in objset.foreach_type(oe.spdx30.SoftwareAgent):
261            if o.name == "CI Tool":
262                break
263        else:
264            self.assertTrue(False, "Unable to find software tool")
265
266        for o in objset.foreach_type(oe.spdx30.Person):
267            if o.name == "John Doe":
268                break
269        else:
270            self.assertTrue(False, "Unable to find person")
271
272        for o in objset.foreach_type(oe.spdx30.Organization):
273            if o.name == "ACME Embedded Widgets":
274                break
275        else:
276            self.assertTrue(False, "Unable to find organization")
277
278        for o in objset.foreach_type(oe.spdx30.SpdxDocument):
279            doc = o
280            break
281        else:
282            self.assertTrue(False, "Unable to find SpdxDocument")
283
284        for i in doc.import_:
285            if i.externalSpdxId == HOST_SPDXID:
286                break
287        else:
288            self.assertTrue(False, "Unable to find imported Host SpdxID")
289