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