spdx_common.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. #
  2. # Copyright OpenEmbedded Contributors
  3. #
  4. # SPDX-License-Identifier: GPL-2.0-only
  5. #
  6. import bb
  7. import collections
  8. import json
  9. import oe.packagedata
  10. import re
  11. import shutil
  12. from pathlib import Path
  13. from dataclasses import dataclass
  14. LIC_REGEX = re.compile(
  15. rb"^\W*SPDX-License-Identifier:\s*([ \w\d.()+-]+?)(?:\s+\W*)?$",
  16. re.MULTILINE,
  17. )
  18. def extract_licenses(filename):
  19. """
  20. Extract SPDX License identifiers from a file
  21. """
  22. try:
  23. with open(filename, "rb") as f:
  24. size = min(15000, os.stat(filename).st_size)
  25. txt = f.read(size)
  26. licenses = re.findall(LIC_REGEX, txt)
  27. if licenses:
  28. ascii_licenses = [lic.decode("ascii") for lic in licenses]
  29. return ascii_licenses
  30. except Exception as e:
  31. bb.warn(f"Exception reading {filename}: {e}")
  32. return []
  33. def is_work_shared_spdx(d):
  34. return '/work-shared/' in d.getVar('S')
  35. def load_spdx_license_data(d):
  36. with open(d.getVar("SPDX_LICENSES"), "r") as f:
  37. data = json.load(f)
  38. # Transform the license array to a dictionary
  39. data["licenses"] = {l["licenseId"]: l for l in data["licenses"]}
  40. return data
  41. def process_sources(d):
  42. """
  43. Returns True if the sources for this recipe should be included in the SPDX
  44. or False if not
  45. """
  46. pn = d.getVar("PN")
  47. assume_provided = (d.getVar("ASSUME_PROVIDED") or "").split()
  48. if pn in assume_provided:
  49. for p in d.getVar("PROVIDES").split():
  50. if p != pn:
  51. pn = p
  52. break
  53. # glibc-locale: do_fetch, do_unpack and do_patch tasks have been deleted,
  54. # so avoid archiving source here.
  55. if pn.startswith("glibc-locale"):
  56. return False
  57. if d.getVar("PN") == "libtool-cross":
  58. return False
  59. if d.getVar("PN") == "libgcc-initial":
  60. return False
  61. if d.getVar("PN") == "shadow-sysroot":
  62. return False
  63. return True
  64. @dataclass(frozen=True)
  65. class Dep(object):
  66. pn: str
  67. hashfn: str
  68. in_taskhash: bool
  69. def collect_direct_deps(d, dep_task):
  70. """
  71. Find direct dependencies of current task
  72. Returns the list of recipes that have a dep_task that the current task
  73. depends on
  74. """
  75. current_task = "do_" + d.getVar("BB_CURRENTTASK")
  76. pn = d.getVar("PN")
  77. taskdepdata = d.getVar("BB_TASKDEPDATA", False)
  78. for this_dep in taskdepdata.values():
  79. if this_dep[0] == pn and this_dep[1] == current_task:
  80. break
  81. else:
  82. bb.fatal(f"Unable to find this {pn}:{current_task} in taskdepdata")
  83. deps = set()
  84. for dep_name in this_dep.deps:
  85. dep_data = taskdepdata[dep_name]
  86. if dep_data.taskname == dep_task and dep_data.pn != pn:
  87. deps.add((dep_data.pn, dep_data.hashfn, dep_name in this_dep.taskhash_deps))
  88. return sorted(deps)
  89. def get_spdx_deps(d):
  90. """
  91. Reads the SPDX dependencies JSON file and returns the data
  92. """
  93. spdx_deps_file = Path(d.getVar("SPDXDEPS"))
  94. deps = []
  95. with spdx_deps_file.open("r") as f:
  96. for d in json.load(f):
  97. deps.append(Dep(*d))
  98. return deps
  99. def collect_package_providers(d):
  100. """
  101. Returns a dictionary where each RPROVIDES is mapped to the package that
  102. provides it
  103. """
  104. deploy_dir_spdx = Path(d.getVar("DEPLOY_DIR_SPDX"))
  105. providers = {}
  106. deps = collect_direct_deps(d, "do_create_spdx")
  107. deps.append((d.getVar("PN"), d.getVar("BB_HASHFILENAME"), True))
  108. for dep_pn, dep_hashfn, _ in deps:
  109. localdata = d
  110. recipe_data = oe.packagedata.read_pkgdata(dep_pn, localdata)
  111. if not recipe_data:
  112. localdata = bb.data.createCopy(d)
  113. localdata.setVar("PKGDATA_DIR", "${PKGDATA_DIR_SDK}")
  114. recipe_data = oe.packagedata.read_pkgdata(dep_pn, localdata)
  115. for pkg in recipe_data.get("PACKAGES", "").split():
  116. pkg_data = oe.packagedata.read_subpkgdata_dict(pkg, localdata)
  117. rprovides = set(
  118. n
  119. for n, _ in bb.utils.explode_dep_versions2(
  120. pkg_data.get("RPROVIDES", "")
  121. ).items()
  122. )
  123. rprovides.add(pkg)
  124. if "PKG" in pkg_data:
  125. pkg = pkg_data["PKG"]
  126. rprovides.add(pkg)
  127. for r in rprovides:
  128. providers[r] = (pkg, dep_hashfn)
  129. return providers
  130. def get_patched_src(d):
  131. """
  132. Save patched source of the recipe in SPDX_WORKDIR.
  133. """
  134. spdx_workdir = d.getVar("SPDXWORK")
  135. spdx_sysroot_native = d.getVar("STAGING_DIR_NATIVE")
  136. pn = d.getVar("PN")
  137. workdir = d.getVar("WORKDIR")
  138. try:
  139. # The kernel class functions require it to be on work-shared, so we dont change WORKDIR
  140. if not is_work_shared_spdx(d):
  141. # Change the WORKDIR to make do_unpack do_patch run in another dir.
  142. d.setVar("WORKDIR", spdx_workdir)
  143. # Restore the original path to recipe's native sysroot (it's relative to WORKDIR).
  144. d.setVar("STAGING_DIR_NATIVE", spdx_sysroot_native)
  145. # The changed 'WORKDIR' also caused 'B' changed, create dir 'B' for the
  146. # possibly requiring of the following tasks (such as some recipes's
  147. # do_patch required 'B' existed).
  148. bb.utils.mkdirhier(d.getVar("B"))
  149. bb.build.exec_func("do_unpack", d)
  150. if d.getVar("SRC_URI") != "":
  151. if bb.data.inherits_class('dos2unix', d):
  152. bb.build.exec_func('do_convert_crlf_to_lf', d)
  153. bb.build.exec_func("do_patch", d)
  154. # Copy source from work-share to spdx_workdir
  155. if is_work_shared_spdx(d):
  156. share_src = d.getVar('S')
  157. d.setVar("WORKDIR", spdx_workdir)
  158. d.setVar("STAGING_DIR_NATIVE", spdx_sysroot_native)
  159. # Copy source to ${SPDXWORK}, same basename dir of ${S};
  160. src_dir = (
  161. spdx_workdir
  162. + "/"
  163. + os.path.basename(share_src)
  164. )
  165. # For kernel souce, rename suffix dir 'kernel-source'
  166. # to ${BP} (${BPN}-${PV})
  167. if bb.data.inherits_class("kernel", d):
  168. src_dir = spdx_workdir + "/" + d.getVar('BP')
  169. bb.note(f"copyhardlinktree {share_src} to {src_dir}")
  170. oe.path.copyhardlinktree(share_src, src_dir)
  171. # Some userland has no source.
  172. if not os.path.exists(spdx_workdir):
  173. bb.utils.mkdirhier(spdx_workdir)
  174. finally:
  175. d.setVar("WORKDIR", workdir)
  176. def has_task(d, task):
  177. return bool(d.getVarFlag(task, "task", False)) and not bool(d.getVarFlag(task, "noexec", False))
  178. def fetch_data_to_uri(fd, name):
  179. """
  180. Translates a bitbake FetchData to a string URI
  181. """
  182. uri = fd.type
  183. # Map gitsm to git, since gitsm:// is not a valid URI protocol
  184. if uri == "gitsm":
  185. uri = "git"
  186. proto = getattr(fd, "proto", None)
  187. if proto is not None:
  188. uri = uri + "+" + proto
  189. uri = uri + "://" + fd.host + fd.path
  190. if fd.method.supports_srcrev():
  191. uri = uri + "@" + fd.revision
  192. return uri