spdx.py 9.0 KB

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