blob: b0aef80db16e466620bdf9fdaee59c855ef02f77 [file] [log] [blame]
Patrick Williams7784c422022-11-17 07:29:11 -06001#
2# Copyright OpenEmbedded Contributors
3#
4# SPDX-License-Identifier: GPL-2.0-only
5#
6
Patrick Williamsb542dec2023-06-09 01:26:37 -05007DEPLOY_DIR_SPDX ??= "${DEPLOY_DIR}/spdx"
Patrick Williams7784c422022-11-17 07:29:11 -06008
9# The product name that the CVE database uses. Defaults to BPN, but may need to
10# be overriden per recipe (for example tiff.bb sets CVE_PRODUCT=libtiff).
11CVE_PRODUCT ??= "${BPN}"
12CVE_VERSION ??= "${PV}"
13
14SPDXDIR ??= "${WORKDIR}/spdx"
15SPDXDEPLOY = "${SPDXDIR}/deploy"
16SPDXWORK = "${SPDXDIR}/work"
Andrew Geissler6aa7eec2023-03-03 12:41:14 -060017SPDXIMAGEWORK = "${SPDXDIR}/image-work"
18SPDXSDKWORK = "${SPDXDIR}/sdk-work"
Patrick Williamsb542dec2023-06-09 01:26:37 -050019SPDXDEPS = "${SPDXDIR}/deps.json"
Patrick Williams7784c422022-11-17 07:29:11 -060020
21SPDX_TOOL_NAME ??= "oe-spdx-creator"
22SPDX_TOOL_VERSION ??= "1.0"
23
24SPDXRUNTIMEDEPLOY = "${SPDXDIR}/runtime-deploy"
25
26SPDX_INCLUDE_SOURCES ??= "0"
27SPDX_ARCHIVE_SOURCES ??= "0"
28SPDX_ARCHIVE_PACKAGED ??= "0"
29
30SPDX_UUID_NAMESPACE ??= "sbom.openembedded.org"
31SPDX_NAMESPACE_PREFIX ??= "http://spdx.org/spdxdoc"
32SPDX_PRETTY ??= "0"
33
34SPDX_LICENSES ??= "${COREBASE}/meta/files/spdx-licenses.json"
35
Andrew Geissler6aa7eec2023-03-03 12:41:14 -060036SPDX_CUSTOM_ANNOTATION_VARS ??= ""
37
Patrick Williams7784c422022-11-17 07:29:11 -060038SPDX_ORG ??= "OpenEmbedded ()"
39SPDX_SUPPLIER ??= "Organization: ${SPDX_ORG}"
40SPDX_SUPPLIER[doc] = "The SPDX PackageSupplier field for SPDX packages created from \
41 this recipe. For SPDX documents create using this class during the build, this \
42 is the contact information for the person or organization who is doing the \
43 build."
44
45def extract_licenses(filename):
46 import re
47
48 lic_regex = re.compile(rb'^\W*SPDX-License-Identifier:\s*([ \w\d.()+-]+?)(?:\s+\W*)?$', re.MULTILINE)
49
50 try:
51 with open(filename, 'rb') as f:
52 size = min(15000, os.stat(filename).st_size)
53 txt = f.read(size)
54 licenses = re.findall(lic_regex, txt)
55 if licenses:
56 ascii_licenses = [lic.decode('ascii') for lic in licenses]
57 return ascii_licenses
58 except Exception as e:
59 bb.warn(f"Exception reading {filename}: {e}")
60 return None
61
62def get_doc_namespace(d, doc):
63 import uuid
64 namespace_uuid = uuid.uuid5(uuid.NAMESPACE_DNS, d.getVar("SPDX_UUID_NAMESPACE"))
65 return "%s/%s-%s" % (d.getVar("SPDX_NAMESPACE_PREFIX"), doc.name, str(uuid.uuid5(namespace_uuid, doc.name)))
66
67def create_annotation(d, comment):
68 from datetime import datetime, timezone
69
70 creation_time = datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
71 annotation = oe.spdx.SPDXAnnotation()
72 annotation.annotationDate = creation_time
73 annotation.annotationType = "OTHER"
74 annotation.annotator = "Tool: %s - %s" % (d.getVar("SPDX_TOOL_NAME"), d.getVar("SPDX_TOOL_VERSION"))
75 annotation.comment = comment
76 return annotation
77
78def recipe_spdx_is_native(d, recipe):
79 return any(a.annotationType == "OTHER" and
80 a.annotator == "Tool: %s - %s" % (d.getVar("SPDX_TOOL_NAME"), d.getVar("SPDX_TOOL_VERSION")) and
81 a.comment == "isNative" for a in recipe.annotations)
82
83def is_work_shared_spdx(d):
84 return bb.data.inherits_class('kernel', d) or ('work-shared' in d.getVar('WORKDIR'))
85
86def get_json_indent(d):
87 if d.getVar("SPDX_PRETTY") == "1":
88 return 2
89 return None
90
91python() {
92 import json
93 if d.getVar("SPDX_LICENSE_DATA"):
94 return
95
96 with open(d.getVar("SPDX_LICENSES"), "r") as f:
97 data = json.load(f)
98 # Transform the license array to a dictionary
99 data["licenses"] = {l["licenseId"]: l for l in data["licenses"]}
100 d.setVar("SPDX_LICENSE_DATA", data)
101}
102
103def convert_license_to_spdx(lic, document, d, existing={}):
104 from pathlib import Path
105 import oe.spdx
106
107 license_data = d.getVar("SPDX_LICENSE_DATA")
108 extracted = {}
109
110 def add_extracted_license(ident, name):
111 nonlocal document
112
113 if name in extracted:
114 return
115
116 extracted_info = oe.spdx.SPDXExtractedLicensingInfo()
117 extracted_info.name = name
118 extracted_info.licenseId = ident
119 extracted_info.extractedText = None
120
121 if name == "PD":
122 # Special-case this.
123 extracted_info.extractedText = "Software released to the public domain"
124 else:
125 # Seach for the license in COMMON_LICENSE_DIR and LICENSE_PATH
126 for directory in [d.getVar('COMMON_LICENSE_DIR')] + (d.getVar('LICENSE_PATH') or '').split():
127 try:
128 with (Path(directory) / name).open(errors="replace") as f:
129 extracted_info.extractedText = f.read()
130 break
131 except FileNotFoundError:
132 pass
133 if extracted_info.extractedText is None:
134 # If it's not SPDX or PD, then NO_GENERIC_LICENSE must be set
135 filename = d.getVarFlag('NO_GENERIC_LICENSE', name)
136 if filename:
137 filename = d.expand("${S}/" + filename)
138 with open(filename, errors="replace") as f:
139 extracted_info.extractedText = f.read()
140 else:
Patrick Williamsb542dec2023-06-09 01:26:37 -0500141 bb.fatal("Cannot find any text for license %s" % name)
Patrick Williams7784c422022-11-17 07:29:11 -0600142
143 extracted[name] = extracted_info
144 document.hasExtractedLicensingInfos.append(extracted_info)
145
146 def convert(l):
147 if l == "(" or l == ")":
148 return l
149
150 if l == "&":
151 return "AND"
152
153 if l == "|":
154 return "OR"
155
156 if l == "CLOSED":
157 return "NONE"
158
159 spdx_license = d.getVarFlag("SPDXLICENSEMAP", l) or l
160 if spdx_license in license_data["licenses"]:
161 return spdx_license
162
163 try:
164 spdx_license = existing[l]
165 except KeyError:
166 spdx_license = "LicenseRef-" + l
167 add_extracted_license(spdx_license, l)
168
169 return spdx_license
170
Patrick Williamsb542dec2023-06-09 01:26:37 -0500171 lic_split = lic.replace("(", " ( ").replace(")", " ) ").replace("|", " | ").replace("&", " & ").split()
Patrick Williams7784c422022-11-17 07:29:11 -0600172
173 return ' '.join(convert(l) for l in lic_split)
174
175def process_sources(d):
176 pn = d.getVar('PN')
177 assume_provided = (d.getVar("ASSUME_PROVIDED") or "").split()
178 if pn in assume_provided:
179 for p in d.getVar("PROVIDES").split():
180 if p != pn:
181 pn = p
182 break
183
184 # glibc-locale: do_fetch, do_unpack and do_patch tasks have been deleted,
185 # so avoid archiving source here.
186 if pn.startswith('glibc-locale'):
187 return False
188 if d.getVar('PN') == "libtool-cross":
189 return False
190 if d.getVar('PN') == "libgcc-initial":
191 return False
192 if d.getVar('PN') == "shadow-sysroot":
193 return False
194
195 # We just archive gcc-source for all the gcc related recipes
196 if d.getVar('BPN') in ['gcc', 'libgcc']:
197 bb.debug(1, 'spdx: There is bug in scan of %s is, do nothing' % pn)
198 return False
199
200 return True
201
202
203def add_package_files(d, doc, spdx_pkg, topdir, get_spdxid, get_types, *, archive=None, ignore_dirs=[], ignore_top_level_dirs=[]):
204 from pathlib import Path
205 import oe.spdx
206 import hashlib
207
208 source_date_epoch = d.getVar("SOURCE_DATE_EPOCH")
209 if source_date_epoch:
210 source_date_epoch = int(source_date_epoch)
211
212 sha1s = []
213 spdx_files = []
214
215 file_counter = 1
216 for subdir, dirs, files in os.walk(topdir):
217 dirs[:] = [d for d in dirs if d not in ignore_dirs]
218 if subdir == str(topdir):
219 dirs[:] = [d for d in dirs if d not in ignore_top_level_dirs]
220
221 for file in files:
222 filepath = Path(subdir) / file
223 filename = str(filepath.relative_to(topdir))
224
225 if not filepath.is_symlink() and filepath.is_file():
226 spdx_file = oe.spdx.SPDXFile()
227 spdx_file.SPDXID = get_spdxid(file_counter)
228 for t in get_types(filepath):
229 spdx_file.fileTypes.append(t)
230 spdx_file.fileName = filename
231
232 if archive is not None:
233 with filepath.open("rb") as f:
234 info = archive.gettarinfo(fileobj=f)
235 info.name = filename
236 info.uid = 0
237 info.gid = 0
238 info.uname = "root"
239 info.gname = "root"
240
241 if source_date_epoch is not None and info.mtime > source_date_epoch:
242 info.mtime = source_date_epoch
243
244 archive.addfile(info, f)
245
246 sha1 = bb.utils.sha1_file(filepath)
247 sha1s.append(sha1)
248 spdx_file.checksums.append(oe.spdx.SPDXChecksum(
249 algorithm="SHA1",
250 checksumValue=sha1,
251 ))
252 spdx_file.checksums.append(oe.spdx.SPDXChecksum(
253 algorithm="SHA256",
254 checksumValue=bb.utils.sha256_file(filepath),
255 ))
256
257 if "SOURCE" in spdx_file.fileTypes:
258 extracted_lics = extract_licenses(filepath)
259 if extracted_lics:
260 spdx_file.licenseInfoInFiles = extracted_lics
261
262 doc.files.append(spdx_file)
263 doc.add_relationship(spdx_pkg, "CONTAINS", spdx_file)
264 spdx_pkg.hasFiles.append(spdx_file.SPDXID)
265
266 spdx_files.append(spdx_file)
267
268 file_counter += 1
269
270 sha1s.sort()
271 verifier = hashlib.sha1()
272 for v in sha1s:
273 verifier.update(v.encode("utf-8"))
274 spdx_pkg.packageVerificationCode.packageVerificationCodeValue = verifier.hexdigest()
275
276 return spdx_files
277
278
279def add_package_sources_from_debug(d, package_doc, spdx_package, package, package_files, sources):
280 from pathlib import Path
281 import hashlib
282 import oe.packagedata
283 import oe.spdx
284
285 debug_search_paths = [
286 Path(d.getVar('PKGD')),
287 Path(d.getVar('STAGING_DIR_TARGET')),
288 Path(d.getVar('STAGING_DIR_NATIVE')),
289 Path(d.getVar('STAGING_KERNEL_DIR')),
290 ]
291
292 pkg_data = oe.packagedata.read_subpkgdata_extended(package, d)
293
294 if pkg_data is None:
295 return
296
297 for file_path, file_data in pkg_data["files_info"].items():
298 if not "debugsrc" in file_data:
299 continue
300
301 for pkg_file in package_files:
302 if file_path.lstrip("/") == pkg_file.fileName.lstrip("/"):
303 break
304 else:
Andrew Geissler220dafd2023-10-04 10:18:08 -0500305 bb.fatal("No package file found for %s in %s; SPDX found: %s" % (str(file_path), package,
306 " ".join(p.fileName for p in package_files)))
Patrick Williams7784c422022-11-17 07:29:11 -0600307 continue
308
309 for debugsrc in file_data["debugsrc"]:
310 ref_id = "NOASSERTION"
311 for search in debug_search_paths:
312 if debugsrc.startswith("/usr/src/kernel"):
313 debugsrc_path = search / debugsrc.replace('/usr/src/kernel/', '')
314 else:
315 debugsrc_path = search / debugsrc.lstrip("/")
316 if not debugsrc_path.exists():
317 continue
318
319 file_sha256 = bb.utils.sha256_file(debugsrc_path)
320
321 if file_sha256 in sources:
322 source_file = sources[file_sha256]
323
324 doc_ref = package_doc.find_external_document_ref(source_file.doc.documentNamespace)
325 if doc_ref is None:
326 doc_ref = oe.spdx.SPDXExternalDocumentRef()
327 doc_ref.externalDocumentId = "DocumentRef-dependency-" + source_file.doc.name
328 doc_ref.spdxDocument = source_file.doc.documentNamespace
329 doc_ref.checksum.algorithm = "SHA1"
330 doc_ref.checksum.checksumValue = source_file.doc_sha1
331 package_doc.externalDocumentRefs.append(doc_ref)
332
333 ref_id = "%s:%s" % (doc_ref.externalDocumentId, source_file.file.SPDXID)
334 else:
335 bb.debug(1, "Debug source %s with SHA256 %s not found in any dependency" % (str(debugsrc_path), file_sha256))
336 break
337 else:
338 bb.debug(1, "Debug source %s not found" % debugsrc)
339
340 package_doc.add_relationship(pkg_file, "GENERATED_FROM", ref_id, comment=debugsrc)
341
Patrick Williamsb542dec2023-06-09 01:26:37 -0500342add_package_sources_from_debug[vardepsexclude] += "STAGING_KERNEL_DIR"
343
Patrick Williams7784c422022-11-17 07:29:11 -0600344def collect_dep_recipes(d, doc, spdx_recipe):
Patrick Williamsb542dec2023-06-09 01:26:37 -0500345 import json
Patrick Williams7784c422022-11-17 07:29:11 -0600346 from pathlib import Path
347 import oe.sbom
348 import oe.spdx
349
350 deploy_dir_spdx = Path(d.getVar("DEPLOY_DIR_SPDX"))
Patrick Williamsb542dec2023-06-09 01:26:37 -0500351 spdx_deps_file = Path(d.getVar("SPDXDEPS"))
Andrew Geissler220dafd2023-10-04 10:18:08 -0500352 package_archs = d.getVar("SSTATE_ARCHS").split()
353 package_archs.reverse()
Patrick Williams7784c422022-11-17 07:29:11 -0600354
355 dep_recipes = []
Patrick Williamsb542dec2023-06-09 01:26:37 -0500356
357 with spdx_deps_file.open("r") as f:
358 deps = json.load(f)
359
360 for dep_pn, dep_hashfn in deps:
Andrew Geissler220dafd2023-10-04 10:18:08 -0500361 dep_recipe_path = oe.sbom.doc_find_by_hashfn(deploy_dir_spdx, package_archs, "recipe-" + dep_pn, dep_hashfn)
362 if not dep_recipe_path:
363 bb.fatal("Cannot find any SPDX file for recipe %s, %s" % (dep_pn, dep_hashfn))
Patrick Williams7784c422022-11-17 07:29:11 -0600364
365 spdx_dep_doc, spdx_dep_sha1 = oe.sbom.read_doc(dep_recipe_path)
366
367 for pkg in spdx_dep_doc.packages:
368 if pkg.name == dep_pn:
369 spdx_dep_recipe = pkg
370 break
371 else:
372 continue
373
374 dep_recipes.append(oe.sbom.DepRecipe(spdx_dep_doc, spdx_dep_sha1, spdx_dep_recipe))
375
376 dep_recipe_ref = oe.spdx.SPDXExternalDocumentRef()
377 dep_recipe_ref.externalDocumentId = "DocumentRef-dependency-" + spdx_dep_doc.name
378 dep_recipe_ref.spdxDocument = spdx_dep_doc.documentNamespace
379 dep_recipe_ref.checksum.algorithm = "SHA1"
380 dep_recipe_ref.checksum.checksumValue = spdx_dep_sha1
381
382 doc.externalDocumentRefs.append(dep_recipe_ref)
383
384 doc.add_relationship(
385 "%s:%s" % (dep_recipe_ref.externalDocumentId, spdx_dep_recipe.SPDXID),
386 "BUILD_DEPENDENCY_OF",
387 spdx_recipe
388 )
389
390 return dep_recipes
391
Andrew Geissler220dafd2023-10-04 10:18:08 -0500392collect_dep_recipes[vardepsexclude] = "SSTATE_ARCHS"
Patrick Williams7784c422022-11-17 07:29:11 -0600393
394def collect_dep_sources(d, dep_recipes):
395 import oe.sbom
396
397 sources = {}
398 for dep in dep_recipes:
399 # Don't collect sources from native recipes as they
400 # match non-native sources also.
401 if recipe_spdx_is_native(d, dep.recipe):
402 continue
403 recipe_files = set(dep.recipe.hasFiles)
404
405 for spdx_file in dep.doc.files:
406 if spdx_file.SPDXID not in recipe_files:
407 continue
408
409 if "SOURCE" in spdx_file.fileTypes:
410 for checksum in spdx_file.checksums:
411 if checksum.algorithm == "SHA256":
412 sources[checksum.checksumValue] = oe.sbom.DepSource(dep.doc, dep.doc_sha1, dep.recipe, spdx_file)
413 break
414
415 return sources
416
Andrew Geissler6aa7eec2023-03-03 12:41:14 -0600417def add_download_packages(d, doc, recipe):
418 import os.path
419 from bb.fetch2 import decodeurl, CHECKSUM_LIST
420 import bb.process
421 import oe.spdx
422 import oe.sbom
423
424 for download_idx, src_uri in enumerate(d.getVar('SRC_URI').split()):
425 f = bb.fetch2.FetchData(src_uri, d)
426
427 for name in f.names:
428 package = oe.spdx.SPDXPackage()
429 package.name = "%s-source-%d" % (d.getVar("PN"), download_idx + 1)
430 package.SPDXID = oe.sbom.get_download_spdxid(d, download_idx + 1)
431
432 if f.type == "file":
433 continue
434
435 uri = f.type
436 proto = getattr(f, "proto", None)
437 if proto is not None:
438 uri = uri + "+" + proto
439 uri = uri + "://" + f.host + f.path
440
441 if f.method.supports_srcrev():
442 uri = uri + "@" + f.revisions[name]
443
444 if f.method.supports_checksum(f):
445 for checksum_id in CHECKSUM_LIST:
446 if checksum_id.upper() not in oe.spdx.SPDXPackage.ALLOWED_CHECKSUMS:
447 continue
448
449 expected_checksum = getattr(f, "%s_expected" % checksum_id)
450 if expected_checksum is None:
451 continue
452
453 c = oe.spdx.SPDXChecksum()
454 c.algorithm = checksum_id.upper()
455 c.checksumValue = expected_checksum
456 package.checksums.append(c)
457
458 package.downloadLocation = uri
459 doc.packages.append(package)
460 doc.add_relationship(doc, "DESCRIBES", package)
461 # In the future, we might be able to do more fancy dependencies,
462 # but this should be sufficient for now
463 doc.add_relationship(package, "BUILD_DEPENDENCY_OF", recipe)
Patrick Williams7784c422022-11-17 07:29:11 -0600464
Patrick Williamsb542dec2023-06-09 01:26:37 -0500465def collect_direct_deps(d, dep_task):
466 current_task = "do_" + d.getVar("BB_CURRENTTASK")
467 pn = d.getVar("PN")
468
469 taskdepdata = d.getVar("BB_TASKDEPDATA", False)
470
471 for this_dep in taskdepdata.values():
472 if this_dep[0] == pn and this_dep[1] == current_task:
473 break
474 else:
475 bb.fatal(f"Unable to find this {pn}:{current_task} in taskdepdata")
476
477 deps = set()
478 for dep_name in this_dep[3]:
479 dep_data = taskdepdata[dep_name]
480 if dep_data[1] == dep_task and dep_data[0] != pn:
481 deps.add((dep_data[0], dep_data[7]))
482
483 return sorted(deps)
484
485collect_direct_deps[vardepsexclude] += "BB_TASKDEPDATA"
486collect_direct_deps[vardeps] += "DEPENDS"
487
488python do_collect_spdx_deps() {
489 # This task calculates the build time dependencies of the recipe, and is
490 # required because while a task can deptask on itself, those dependencies
491 # do not show up in BB_TASKDEPDATA. To work around that, this task does the
492 # deptask on do_create_spdx and writes out the dependencies it finds, then
493 # do_create_spdx reads in the found dependencies when writing the actual
494 # SPDX document
495 import json
496 from pathlib import Path
497
498 spdx_deps_file = Path(d.getVar("SPDXDEPS"))
499
500 deps = collect_direct_deps(d, "do_create_spdx")
501
502 with spdx_deps_file.open("w") as f:
503 json.dump(deps, f)
504}
505# NOTE: depending on do_unpack is a hack that is necessary to get it's dependencies for archive the source
506addtask do_collect_spdx_deps after do_unpack
507do_collect_spdx_deps[depends] += "${PATCHDEPENDENCY}"
508do_collect_spdx_deps[deptask] = "do_create_spdx"
509do_collect_spdx_deps[dirs] = "${SPDXDIR}"
510
Patrick Williams7784c422022-11-17 07:29:11 -0600511python do_create_spdx() {
512 from datetime import datetime, timezone
513 import oe.sbom
514 import oe.spdx
515 import uuid
516 from pathlib import Path
517 from contextlib import contextmanager
518 import oe.cve_check
519
520 @contextmanager
521 def optional_tarfile(name, guard, mode="w"):
522 import tarfile
523 import bb.compress.zstd
524
525 num_threads = int(d.getVar("BB_NUMBER_THREADS"))
526
527 if guard:
528 name.parent.mkdir(parents=True, exist_ok=True)
529 with bb.compress.zstd.open(name, mode=mode + "b", num_threads=num_threads) as f:
530 with tarfile.open(fileobj=f, mode=mode + "|") as tf:
531 yield tf
532 else:
533 yield None
534
535
536 deploy_dir_spdx = Path(d.getVar("DEPLOY_DIR_SPDX"))
537 spdx_workdir = Path(d.getVar("SPDXWORK"))
538 include_sources = d.getVar("SPDX_INCLUDE_SOURCES") == "1"
539 archive_sources = d.getVar("SPDX_ARCHIVE_SOURCES") == "1"
540 archive_packaged = d.getVar("SPDX_ARCHIVE_PACKAGED") == "1"
Andrew Geissler220dafd2023-10-04 10:18:08 -0500541 pkg_arch = d.getVar("SSTATE_PKGARCH")
Patrick Williams7784c422022-11-17 07:29:11 -0600542
543 creation_time = datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
544
545 doc = oe.spdx.SPDXDocument()
546
547 doc.name = "recipe-" + d.getVar("PN")
548 doc.documentNamespace = get_doc_namespace(d, doc)
549 doc.creationInfo.created = creation_time
550 doc.creationInfo.comment = "This document was created by analyzing recipe files during the build."
551 doc.creationInfo.licenseListVersion = d.getVar("SPDX_LICENSE_DATA")["licenseListVersion"]
552 doc.creationInfo.creators.append("Tool: OpenEmbedded Core create-spdx.bbclass")
553 doc.creationInfo.creators.append("Organization: %s" % d.getVar("SPDX_ORG"))
554 doc.creationInfo.creators.append("Person: N/A ()")
555
556 recipe = oe.spdx.SPDXPackage()
557 recipe.name = d.getVar("PN")
558 recipe.versionInfo = d.getVar("PV")
559 recipe.SPDXID = oe.sbom.get_recipe_spdxid(d)
560 recipe.supplier = d.getVar("SPDX_SUPPLIER")
561 if bb.data.inherits_class("native", d) or bb.data.inherits_class("cross", d):
562 recipe.annotations.append(create_annotation(d, "isNative"))
563
Patrick Williams7784c422022-11-17 07:29:11 -0600564 homepage = d.getVar("HOMEPAGE")
565 if homepage:
566 recipe.homepage = homepage
567
568 license = d.getVar("LICENSE")
569 if license:
570 recipe.licenseDeclared = convert_license_to_spdx(license, doc, d)
571
572 summary = d.getVar("SUMMARY")
573 if summary:
574 recipe.summary = summary
575
576 description = d.getVar("DESCRIPTION")
577 if description:
578 recipe.description = description
579
Andrew Geissler6aa7eec2023-03-03 12:41:14 -0600580 if d.getVar("SPDX_CUSTOM_ANNOTATION_VARS"):
581 for var in d.getVar('SPDX_CUSTOM_ANNOTATION_VARS').split():
582 recipe.annotations.append(create_annotation(d, var + "=" + d.getVar(var)))
583
Patrick Williams7784c422022-11-17 07:29:11 -0600584 # Some CVEs may be patched during the build process without incrementing the version number,
585 # so querying for CVEs based on the CPE id can lead to false positives. To account for this,
586 # save the CVEs fixed by patches to source information field in the SPDX.
587 patched_cves = oe.cve_check.get_patched_cves(d)
588 patched_cves = list(patched_cves)
589 patched_cves = ' '.join(patched_cves)
590 if patched_cves:
591 recipe.sourceInfo = "CVEs fixed: " + patched_cves
592
593 cpe_ids = oe.cve_check.get_cpe_ids(d.getVar("CVE_PRODUCT"), d.getVar("CVE_VERSION"))
594 if cpe_ids:
595 for cpe_id in cpe_ids:
596 cpe = oe.spdx.SPDXExternalReference()
597 cpe.referenceCategory = "SECURITY"
598 cpe.referenceType = "http://spdx.org/rdf/references/cpe23Type"
599 cpe.referenceLocator = cpe_id
600 recipe.externalRefs.append(cpe)
601
602 doc.packages.append(recipe)
603 doc.add_relationship(doc, "DESCRIBES", recipe)
604
Andrew Geissler6aa7eec2023-03-03 12:41:14 -0600605 add_download_packages(d, doc, recipe)
606
Patrick Williams7784c422022-11-17 07:29:11 -0600607 if process_sources(d) and include_sources:
608 recipe_archive = deploy_dir_spdx / "recipes" / (doc.name + ".tar.zst")
609 with optional_tarfile(recipe_archive, archive_sources) as archive:
610 spdx_get_src(d)
611
612 add_package_files(
613 d,
614 doc,
615 recipe,
616 spdx_workdir,
617 lambda file_counter: "SPDXRef-SourceFile-%s-%d" % (d.getVar("PN"), file_counter),
618 lambda filepath: ["SOURCE"],
619 ignore_dirs=[".git"],
620 ignore_top_level_dirs=["temp"],
621 archive=archive,
622 )
623
624 if archive is not None:
625 recipe.packageFileName = str(recipe_archive.name)
626
627 dep_recipes = collect_dep_recipes(d, doc, recipe)
628
Andrew Geissler220dafd2023-10-04 10:18:08 -0500629 doc_sha1 = oe.sbom.write_doc(d, doc, pkg_arch, "recipes", indent=get_json_indent(d))
Patrick Williams7784c422022-11-17 07:29:11 -0600630 dep_recipes.append(oe.sbom.DepRecipe(doc, doc_sha1, recipe))
631
632 recipe_ref = oe.spdx.SPDXExternalDocumentRef()
633 recipe_ref.externalDocumentId = "DocumentRef-recipe-" + recipe.name
634 recipe_ref.spdxDocument = doc.documentNamespace
635 recipe_ref.checksum.algorithm = "SHA1"
636 recipe_ref.checksum.checksumValue = doc_sha1
637
638 sources = collect_dep_sources(d, dep_recipes)
639 found_licenses = {license.name:recipe_ref.externalDocumentId + ":" + license.licenseId for license in doc.hasExtractedLicensingInfos}
640
641 if not recipe_spdx_is_native(d, recipe):
642 bb.build.exec_func("read_subpackage_metadata", d)
643
644 pkgdest = Path(d.getVar("PKGDEST"))
645 for package in d.getVar("PACKAGES").split():
646 if not oe.packagedata.packaged(package, d):
647 continue
648
649 package_doc = oe.spdx.SPDXDocument()
650 pkg_name = d.getVar("PKG:%s" % package) or package
651 package_doc.name = pkg_name
652 package_doc.documentNamespace = get_doc_namespace(d, package_doc)
653 package_doc.creationInfo.created = creation_time
654 package_doc.creationInfo.comment = "This document was created by analyzing packages created during the build."
655 package_doc.creationInfo.licenseListVersion = d.getVar("SPDX_LICENSE_DATA")["licenseListVersion"]
656 package_doc.creationInfo.creators.append("Tool: OpenEmbedded Core create-spdx.bbclass")
657 package_doc.creationInfo.creators.append("Organization: %s" % d.getVar("SPDX_ORG"))
658 package_doc.creationInfo.creators.append("Person: N/A ()")
659 package_doc.externalDocumentRefs.append(recipe_ref)
660
661 package_license = d.getVar("LICENSE:%s" % package) or d.getVar("LICENSE")
662
663 spdx_package = oe.spdx.SPDXPackage()
664
665 spdx_package.SPDXID = oe.sbom.get_package_spdxid(pkg_name)
666 spdx_package.name = pkg_name
667 spdx_package.versionInfo = d.getVar("PV")
668 spdx_package.licenseDeclared = convert_license_to_spdx(package_license, package_doc, d, found_licenses)
669 spdx_package.supplier = d.getVar("SPDX_SUPPLIER")
670
671 package_doc.packages.append(spdx_package)
672
673 package_doc.add_relationship(spdx_package, "GENERATED_FROM", "%s:%s" % (recipe_ref.externalDocumentId, recipe.SPDXID))
674 package_doc.add_relationship(package_doc, "DESCRIBES", spdx_package)
675
676 package_archive = deploy_dir_spdx / "packages" / (package_doc.name + ".tar.zst")
677 with optional_tarfile(package_archive, archive_packaged) as archive:
678 package_files = add_package_files(
679 d,
680 package_doc,
681 spdx_package,
682 pkgdest / package,
683 lambda file_counter: oe.sbom.get_packaged_file_spdxid(pkg_name, file_counter),
684 lambda filepath: ["BINARY"],
685 ignore_top_level_dirs=['CONTROL', 'DEBIAN'],
686 archive=archive,
687 )
688
689 if archive is not None:
690 spdx_package.packageFileName = str(package_archive.name)
691
692 add_package_sources_from_debug(d, package_doc, spdx_package, package, package_files, sources)
693
Andrew Geissler220dafd2023-10-04 10:18:08 -0500694 oe.sbom.write_doc(d, package_doc, pkg_arch, "packages", indent=get_json_indent(d))
Patrick Williams7784c422022-11-17 07:29:11 -0600695}
Patrick Williamsb542dec2023-06-09 01:26:37 -0500696do_create_spdx[vardepsexclude] += "BB_NUMBER_THREADS"
Patrick Williams7784c422022-11-17 07:29:11 -0600697# NOTE: depending on do_unpack is a hack that is necessary to get it's dependencies for archive the source
Patrick Williamsb542dec2023-06-09 01:26:37 -0500698addtask do_create_spdx after do_package do_packagedata do_unpack do_collect_spdx_deps before do_populate_sdk do_build do_rm_work
Patrick Williams7784c422022-11-17 07:29:11 -0600699
700SSTATETASKS += "do_create_spdx"
701do_create_spdx[sstate-inputdirs] = "${SPDXDEPLOY}"
702do_create_spdx[sstate-outputdirs] = "${DEPLOY_DIR_SPDX}"
703
704python do_create_spdx_setscene () {
705 sstate_setscene(d)
706}
707addtask do_create_spdx_setscene
708
709do_create_spdx[dirs] = "${SPDXWORK}"
710do_create_spdx[cleandirs] = "${SPDXDEPLOY} ${SPDXWORK}"
711do_create_spdx[depends] += "${PATCHDEPENDENCY}"
Patrick Williams7784c422022-11-17 07:29:11 -0600712
713def collect_package_providers(d):
714 from pathlib import Path
715 import oe.sbom
716 import oe.spdx
717 import json
718
719 deploy_dir_spdx = Path(d.getVar("DEPLOY_DIR_SPDX"))
720
721 providers = {}
722
Patrick Williamsb542dec2023-06-09 01:26:37 -0500723 deps = collect_direct_deps(d, "do_create_spdx")
724 deps.append((d.getVar("PN"), d.getVar("BB_HASHFILENAME")))
Patrick Williams7784c422022-11-17 07:29:11 -0600725
Patrick Williamsb542dec2023-06-09 01:26:37 -0500726 for dep_pn, dep_hashfn in deps:
727 localdata = d
728 recipe_data = oe.packagedata.read_pkgdata(dep_pn, localdata)
729 if not recipe_data:
730 localdata = bb.data.createCopy(d)
731 localdata.setVar("PKGDATA_DIR", "${PKGDATA_DIR_SDK}")
732 recipe_data = oe.packagedata.read_pkgdata(dep_pn, localdata)
Patrick Williams7784c422022-11-17 07:29:11 -0600733
734 for pkg in recipe_data.get("PACKAGES", "").split():
735
Patrick Williamsb542dec2023-06-09 01:26:37 -0500736 pkg_data = oe.packagedata.read_subpkgdata_dict(pkg, localdata)
Patrick Williams7784c422022-11-17 07:29:11 -0600737 rprovides = set(n for n, _ in bb.utils.explode_dep_versions2(pkg_data.get("RPROVIDES", "")).items())
738 rprovides.add(pkg)
739
Patrick Williamsb542dec2023-06-09 01:26:37 -0500740 if "PKG" in pkg_data:
741 pkg = pkg_data["PKG"]
742 rprovides.add(pkg)
743
Patrick Williams7784c422022-11-17 07:29:11 -0600744 for r in rprovides:
Patrick Williamsb542dec2023-06-09 01:26:37 -0500745 providers[r] = (pkg, dep_hashfn)
Patrick Williams7784c422022-11-17 07:29:11 -0600746
747 return providers
748
749collect_package_providers[vardepsexclude] += "BB_TASKDEPDATA"
750
751python do_create_runtime_spdx() {
752 from datetime import datetime, timezone
753 import oe.sbom
754 import oe.spdx
755 import oe.packagedata
756 from pathlib import Path
757
758 deploy_dir_spdx = Path(d.getVar("DEPLOY_DIR_SPDX"))
759 spdx_deploy = Path(d.getVar("SPDXRUNTIMEDEPLOY"))
760 is_native = bb.data.inherits_class("native", d) or bb.data.inherits_class("cross", d)
761
762 creation_time = datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
763
764 providers = collect_package_providers(d)
Andrew Geissler220dafd2023-10-04 10:18:08 -0500765 pkg_arch = d.getVar("SSTATE_PKGARCH")
766 package_archs = d.getVar("SSTATE_ARCHS").split()
767 package_archs.reverse()
Patrick Williams7784c422022-11-17 07:29:11 -0600768
769 if not is_native:
770 bb.build.exec_func("read_subpackage_metadata", d)
771
772 dep_package_cache = {}
773
774 pkgdest = Path(d.getVar("PKGDEST"))
775 for package in d.getVar("PACKAGES").split():
776 localdata = bb.data.createCopy(d)
777 pkg_name = d.getVar("PKG:%s" % package) or package
778 localdata.setVar("PKG", pkg_name)
779 localdata.setVar('OVERRIDES', d.getVar("OVERRIDES", False) + ":" + package)
780
781 if not oe.packagedata.packaged(package, localdata):
782 continue
783
Andrew Geissler220dafd2023-10-04 10:18:08 -0500784 pkg_spdx_path = oe.sbom.doc_path(deploy_dir_spdx, pkg_name, pkg_arch, "packages")
Patrick Williams7784c422022-11-17 07:29:11 -0600785
786 package_doc, package_doc_sha1 = oe.sbom.read_doc(pkg_spdx_path)
787
788 for p in package_doc.packages:
789 if p.name == pkg_name:
790 spdx_package = p
791 break
792 else:
793 bb.fatal("Package '%s' not found in %s" % (pkg_name, pkg_spdx_path))
794
795 runtime_doc = oe.spdx.SPDXDocument()
796 runtime_doc.name = "runtime-" + pkg_name
797 runtime_doc.documentNamespace = get_doc_namespace(localdata, runtime_doc)
798 runtime_doc.creationInfo.created = creation_time
799 runtime_doc.creationInfo.comment = "This document was created by analyzing package runtime dependencies."
800 runtime_doc.creationInfo.licenseListVersion = d.getVar("SPDX_LICENSE_DATA")["licenseListVersion"]
801 runtime_doc.creationInfo.creators.append("Tool: OpenEmbedded Core create-spdx.bbclass")
802 runtime_doc.creationInfo.creators.append("Organization: %s" % d.getVar("SPDX_ORG"))
803 runtime_doc.creationInfo.creators.append("Person: N/A ()")
804
805 package_ref = oe.spdx.SPDXExternalDocumentRef()
806 package_ref.externalDocumentId = "DocumentRef-package-" + package
807 package_ref.spdxDocument = package_doc.documentNamespace
808 package_ref.checksum.algorithm = "SHA1"
809 package_ref.checksum.checksumValue = package_doc_sha1
810
811 runtime_doc.externalDocumentRefs.append(package_ref)
812
813 runtime_doc.add_relationship(
814 runtime_doc.SPDXID,
815 "AMENDS",
816 "%s:%s" % (package_ref.externalDocumentId, package_doc.SPDXID)
817 )
818
819 deps = bb.utils.explode_dep_versions2(localdata.getVar("RDEPENDS") or "")
820 seen_deps = set()
821 for dep, _ in deps.items():
822 if dep in seen_deps:
823 continue
824
825 if dep not in providers:
826 continue
827
Patrick Williamsb542dec2023-06-09 01:26:37 -0500828 (dep, dep_hashfn) = providers[dep]
Patrick Williams7784c422022-11-17 07:29:11 -0600829
830 if not oe.packagedata.packaged(dep, localdata):
831 continue
832
833 dep_pkg_data = oe.packagedata.read_subpkgdata_dict(dep, d)
834 dep_pkg = dep_pkg_data["PKG"]
835
836 if dep in dep_package_cache:
837 (dep_spdx_package, dep_package_ref) = dep_package_cache[dep]
838 else:
Andrew Geissler220dafd2023-10-04 10:18:08 -0500839 dep_path = oe.sbom.doc_find_by_hashfn(deploy_dir_spdx, package_archs, dep_pkg, dep_hashfn)
840 if not dep_path:
841 bb.fatal("No SPDX file found for package %s, %s" % (dep_pkg, dep_hashfn))
Patrick Williams7784c422022-11-17 07:29:11 -0600842
843 spdx_dep_doc, spdx_dep_sha1 = oe.sbom.read_doc(dep_path)
844
845 for pkg in spdx_dep_doc.packages:
846 if pkg.name == dep_pkg:
847 dep_spdx_package = pkg
848 break
849 else:
850 bb.fatal("Package '%s' not found in %s" % (dep_pkg, dep_path))
851
852 dep_package_ref = oe.spdx.SPDXExternalDocumentRef()
853 dep_package_ref.externalDocumentId = "DocumentRef-runtime-dependency-" + spdx_dep_doc.name
854 dep_package_ref.spdxDocument = spdx_dep_doc.documentNamespace
855 dep_package_ref.checksum.algorithm = "SHA1"
856 dep_package_ref.checksum.checksumValue = spdx_dep_sha1
857
858 dep_package_cache[dep] = (dep_spdx_package, dep_package_ref)
859
860 runtime_doc.externalDocumentRefs.append(dep_package_ref)
861
862 runtime_doc.add_relationship(
863 "%s:%s" % (dep_package_ref.externalDocumentId, dep_spdx_package.SPDXID),
864 "RUNTIME_DEPENDENCY_OF",
865 "%s:%s" % (package_ref.externalDocumentId, spdx_package.SPDXID)
866 )
867 seen_deps.add(dep)
868
Andrew Geissler220dafd2023-10-04 10:18:08 -0500869 oe.sbom.write_doc(d, runtime_doc, pkg_arch, "runtime", spdx_deploy, indent=get_json_indent(d))
Patrick Williams7784c422022-11-17 07:29:11 -0600870}
871
Andrew Geissler220dafd2023-10-04 10:18:08 -0500872do_create_runtime_spdx[vardepsexclude] += "OVERRIDES SSTATE_ARCHS"
Patrick Williamsb542dec2023-06-09 01:26:37 -0500873
Patrick Williams7784c422022-11-17 07:29:11 -0600874addtask do_create_runtime_spdx after do_create_spdx before do_build do_rm_work
875SSTATETASKS += "do_create_runtime_spdx"
876do_create_runtime_spdx[sstate-inputdirs] = "${SPDXRUNTIMEDEPLOY}"
877do_create_runtime_spdx[sstate-outputdirs] = "${DEPLOY_DIR_SPDX}"
878
879python do_create_runtime_spdx_setscene () {
880 sstate_setscene(d)
881}
882addtask do_create_runtime_spdx_setscene
883
884do_create_runtime_spdx[dirs] = "${SPDXRUNTIMEDEPLOY}"
885do_create_runtime_spdx[cleandirs] = "${SPDXRUNTIMEDEPLOY}"
886do_create_runtime_spdx[rdeptask] = "do_create_spdx"
887
888def spdx_get_src(d):
889 """
890 save patched source of the recipe in SPDX_WORKDIR.
891 """
892 import shutil
893 spdx_workdir = d.getVar('SPDXWORK')
894 spdx_sysroot_native = d.getVar('STAGING_DIR_NATIVE')
895 pn = d.getVar('PN')
896
897 workdir = d.getVar("WORKDIR")
898
899 try:
900 # The kernel class functions require it to be on work-shared, so we dont change WORKDIR
901 if not is_work_shared_spdx(d):
902 # Change the WORKDIR to make do_unpack do_patch run in another dir.
903 d.setVar('WORKDIR', spdx_workdir)
904 # Restore the original path to recipe's native sysroot (it's relative to WORKDIR).
905 d.setVar('STAGING_DIR_NATIVE', spdx_sysroot_native)
906
907 # The changed 'WORKDIR' also caused 'B' changed, create dir 'B' for the
908 # possibly requiring of the following tasks (such as some recipes's
909 # do_patch required 'B' existed).
910 bb.utils.mkdirhier(d.getVar('B'))
911
912 bb.build.exec_func('do_unpack', d)
913 # Copy source of kernel to spdx_workdir
914 if is_work_shared_spdx(d):
915 share_src = d.getVar('WORKDIR')
916 d.setVar('WORKDIR', spdx_workdir)
917 d.setVar('STAGING_DIR_NATIVE', spdx_sysroot_native)
918 src_dir = spdx_workdir + "/" + d.getVar('PN')+ "-" + d.getVar('PV') + "-" + d.getVar('PR')
919 bb.utils.mkdirhier(src_dir)
920 if bb.data.inherits_class('kernel',d):
921 share_src = d.getVar('STAGING_KERNEL_DIR')
922 cmd_copy_share = "cp -rf " + share_src + "/* " + src_dir + "/"
923 cmd_copy_shared_res = os.popen(cmd_copy_share).read()
924 bb.note("cmd_copy_shared_result = " + cmd_copy_shared_res)
925
926 git_path = src_dir + "/.git"
927 if os.path.exists(git_path):
928 shutils.rmtree(git_path)
929
930 # Make sure gcc and kernel sources are patched only once
931 if not (d.getVar('SRC_URI') == "" or is_work_shared_spdx(d)):
932 bb.build.exec_func('do_patch', d)
933
934 # Some userland has no source.
935 if not os.path.exists( spdx_workdir ):
936 bb.utils.mkdirhier(spdx_workdir)
937 finally:
938 d.setVar("WORKDIR", workdir)
939
Patrick Williamsb542dec2023-06-09 01:26:37 -0500940spdx_get_src[vardepsexclude] += "STAGING_KERNEL_DIR"
941
Patrick Williams7784c422022-11-17 07:29:11 -0600942do_rootfs[recrdeptask] += "do_create_spdx do_create_runtime_spdx"
Andrew Geissler6aa7eec2023-03-03 12:41:14 -0600943do_rootfs[cleandirs] += "${SPDXIMAGEWORK}"
Patrick Williams7784c422022-11-17 07:29:11 -0600944
Andrew Geissler5082cc72023-09-11 08:41:39 -0400945ROOTFS_POSTUNINSTALL_COMMAND =+ "image_combine_spdx"
Patrick Williams7784c422022-11-17 07:29:11 -0600946
947do_populate_sdk[recrdeptask] += "do_create_spdx do_create_runtime_spdx"
Andrew Geissler6aa7eec2023-03-03 12:41:14 -0600948do_populate_sdk[cleandirs] += "${SPDXSDKWORK}"
Andrew Geissler5082cc72023-09-11 08:41:39 -0400949POPULATE_SDK_POST_HOST_COMMAND:append:task-populate-sdk = " sdk_host_combine_spdx"
950POPULATE_SDK_POST_TARGET_COMMAND:append:task-populate-sdk = " sdk_target_combine_spdx"
Patrick Williams7784c422022-11-17 07:29:11 -0600951
952python image_combine_spdx() {
953 import os
954 import oe.sbom
955 from pathlib import Path
956 from oe.rootfs import image_list_installed_packages
957
958 image_name = d.getVar("IMAGE_NAME")
959 image_link_name = d.getVar("IMAGE_LINK_NAME")
960 imgdeploydir = Path(d.getVar("IMGDEPLOYDIR"))
961 img_spdxid = oe.sbom.get_image_spdxid(image_name)
962 packages = image_list_installed_packages(d)
963
Andrew Geissler6aa7eec2023-03-03 12:41:14 -0600964 combine_spdx(d, image_name, imgdeploydir, img_spdxid, packages, Path(d.getVar("SPDXIMAGEWORK")))
Patrick Williams7784c422022-11-17 07:29:11 -0600965
966 def make_image_link(target_path, suffix):
967 if image_link_name:
968 link = imgdeploydir / (image_link_name + suffix)
969 if link != target_path:
970 link.symlink_to(os.path.relpath(target_path, link.parent))
971
Patrick Williams7784c422022-11-17 07:29:11 -0600972 spdx_tar_path = imgdeploydir / (image_name + ".spdx.tar.zst")
973 make_image_link(spdx_tar_path, ".spdx.tar.zst")
Patrick Williams7784c422022-11-17 07:29:11 -0600974}
975
976python sdk_host_combine_spdx() {
977 sdk_combine_spdx(d, "host")
978}
979
980python sdk_target_combine_spdx() {
981 sdk_combine_spdx(d, "target")
982}
983
984def sdk_combine_spdx(d, sdk_type):
985 import oe.sbom
986 from pathlib import Path
987 from oe.sdk import sdk_list_installed_packages
988
Andrew Geissler220dafd2023-10-04 10:18:08 -0500989 sdk_name = d.getVar("TOOLCHAIN_OUTPUTNAME") + "-" + sdk_type
Patrick Williams7784c422022-11-17 07:29:11 -0600990 sdk_deploydir = Path(d.getVar("SDKDEPLOYDIR"))
991 sdk_spdxid = oe.sbom.get_sdk_spdxid(sdk_name)
992 sdk_packages = sdk_list_installed_packages(d, sdk_type == "target")
Andrew Geissler6aa7eec2023-03-03 12:41:14 -0600993 combine_spdx(d, sdk_name, sdk_deploydir, sdk_spdxid, sdk_packages, Path(d.getVar('SPDXSDKWORK')))
Patrick Williams7784c422022-11-17 07:29:11 -0600994
Andrew Geissler6aa7eec2023-03-03 12:41:14 -0600995def combine_spdx(d, rootfs_name, rootfs_deploydir, rootfs_spdxid, packages, spdx_workdir):
Patrick Williams7784c422022-11-17 07:29:11 -0600996 import os
997 import oe.spdx
998 import oe.sbom
999 import io
1000 import json
1001 from datetime import timezone, datetime
1002 from pathlib import Path
1003 import tarfile
1004 import bb.compress.zstd
1005
Patrick Williamsb542dec2023-06-09 01:26:37 -05001006 providers = collect_package_providers(d)
Andrew Geissler220dafd2023-10-04 10:18:08 -05001007 package_archs = d.getVar("SSTATE_ARCHS").split()
1008 package_archs.reverse()
Patrick Williamsb542dec2023-06-09 01:26:37 -05001009
Patrick Williams7784c422022-11-17 07:29:11 -06001010 creation_time = datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
1011 deploy_dir_spdx = Path(d.getVar("DEPLOY_DIR_SPDX"))
1012 source_date_epoch = d.getVar("SOURCE_DATE_EPOCH")
1013
1014 doc = oe.spdx.SPDXDocument()
1015 doc.name = rootfs_name
1016 doc.documentNamespace = get_doc_namespace(d, doc)
1017 doc.creationInfo.created = creation_time
1018 doc.creationInfo.comment = "This document was created by analyzing the source of the Yocto recipe during the build."
1019 doc.creationInfo.licenseListVersion = d.getVar("SPDX_LICENSE_DATA")["licenseListVersion"]
1020 doc.creationInfo.creators.append("Tool: OpenEmbedded Core create-spdx.bbclass")
1021 doc.creationInfo.creators.append("Organization: %s" % d.getVar("SPDX_ORG"))
1022 doc.creationInfo.creators.append("Person: N/A ()")
1023
1024 image = oe.spdx.SPDXPackage()
1025 image.name = d.getVar("PN")
1026 image.versionInfo = d.getVar("PV")
1027 image.SPDXID = rootfs_spdxid
1028 image.supplier = d.getVar("SPDX_SUPPLIER")
1029
1030 doc.packages.append(image)
1031
1032 for name in sorted(packages.keys()):
Patrick Williamsb542dec2023-06-09 01:26:37 -05001033 if name not in providers:
Andrew Geissler220dafd2023-10-04 10:18:08 -05001034 bb.fatal("Unable to find SPDX provider for '%s'" % name)
Patrick Williamsb542dec2023-06-09 01:26:37 -05001035
1036 pkg_name, pkg_hashfn = providers[name]
1037
Andrew Geissler220dafd2023-10-04 10:18:08 -05001038 pkg_spdx_path = oe.sbom.doc_find_by_hashfn(deploy_dir_spdx, package_archs, pkg_name, pkg_hashfn)
1039 if not pkg_spdx_path:
1040 bb.fatal("No SPDX file found for package %s, %s" % (pkg_name, pkg_hashfn))
1041
Patrick Williams7784c422022-11-17 07:29:11 -06001042 pkg_doc, pkg_doc_sha1 = oe.sbom.read_doc(pkg_spdx_path)
1043
1044 for p in pkg_doc.packages:
1045 if p.name == name:
1046 pkg_ref = oe.spdx.SPDXExternalDocumentRef()
1047 pkg_ref.externalDocumentId = "DocumentRef-%s" % pkg_doc.name
1048 pkg_ref.spdxDocument = pkg_doc.documentNamespace
1049 pkg_ref.checksum.algorithm = "SHA1"
1050 pkg_ref.checksum.checksumValue = pkg_doc_sha1
1051
1052 doc.externalDocumentRefs.append(pkg_ref)
1053 doc.add_relationship(image, "CONTAINS", "%s:%s" % (pkg_ref.externalDocumentId, p.SPDXID))
1054 break
1055 else:
1056 bb.fatal("Unable to find package with name '%s' in SPDX file %s" % (name, pkg_spdx_path))
1057
Andrew Geissler220dafd2023-10-04 10:18:08 -05001058 runtime_spdx_path = oe.sbom.doc_find_by_hashfn(deploy_dir_spdx, package_archs, "runtime-" + name, pkg_hashfn)
1059 if not runtime_spdx_path:
1060 bb.fatal("No runtime SPDX document found for %s, %s" % (name, pkg_hashfn))
1061
Patrick Williams7784c422022-11-17 07:29:11 -06001062 runtime_doc, runtime_doc_sha1 = oe.sbom.read_doc(runtime_spdx_path)
1063
1064 runtime_ref = oe.spdx.SPDXExternalDocumentRef()
1065 runtime_ref.externalDocumentId = "DocumentRef-%s" % runtime_doc.name
1066 runtime_ref.spdxDocument = runtime_doc.documentNamespace
1067 runtime_ref.checksum.algorithm = "SHA1"
1068 runtime_ref.checksum.checksumValue = runtime_doc_sha1
1069
1070 # "OTHER" isn't ideal here, but I can't find a relationship that makes sense
1071 doc.externalDocumentRefs.append(runtime_ref)
1072 doc.add_relationship(
1073 image,
1074 "OTHER",
1075 "%s:%s" % (runtime_ref.externalDocumentId, runtime_doc.SPDXID),
1076 comment="Runtime dependencies for %s" % name
1077 )
1078
Andrew Geissler6aa7eec2023-03-03 12:41:14 -06001079 image_spdx_path = spdx_workdir / (rootfs_name + ".spdx.json")
Patrick Williams7784c422022-11-17 07:29:11 -06001080
1081 with image_spdx_path.open("wb") as f:
1082 doc.to_json(f, sort_keys=True, indent=get_json_indent(d))
1083
1084 num_threads = int(d.getVar("BB_NUMBER_THREADS"))
1085
1086 visited_docs = set()
1087
1088 index = {"documents": []}
1089
1090 spdx_tar_path = rootfs_deploydir / (rootfs_name + ".spdx.tar.zst")
1091 with bb.compress.zstd.open(spdx_tar_path, "w", num_threads=num_threads) as f:
1092 with tarfile.open(fileobj=f, mode="w|") as tar:
1093 def collect_spdx_document(path):
1094 nonlocal tar
1095 nonlocal deploy_dir_spdx
1096 nonlocal source_date_epoch
1097 nonlocal index
1098
1099 if path in visited_docs:
1100 return
1101
1102 visited_docs.add(path)
1103
1104 with path.open("rb") as f:
1105 doc, sha1 = oe.sbom.read_doc(f)
1106 f.seek(0)
1107
1108 if doc.documentNamespace in visited_docs:
1109 return
1110
1111 bb.note("Adding SPDX document %s" % path)
1112 visited_docs.add(doc.documentNamespace)
1113 info = tar.gettarinfo(fileobj=f)
1114
1115 info.name = doc.name + ".spdx.json"
1116 info.uid = 0
1117 info.gid = 0
1118 info.uname = "root"
1119 info.gname = "root"
1120
1121 if source_date_epoch is not None and info.mtime > int(source_date_epoch):
1122 info.mtime = int(source_date_epoch)
1123
1124 tar.addfile(info, f)
1125
1126 index["documents"].append({
1127 "filename": info.name,
1128 "documentNamespace": doc.documentNamespace,
1129 "sha1": sha1,
1130 })
1131
1132 for ref in doc.externalDocumentRefs:
Andrew Geissler220dafd2023-10-04 10:18:08 -05001133 ref_path = oe.sbom.doc_find_by_namespace(deploy_dir_spdx, package_archs, ref.spdxDocument)
1134 if not ref_path:
1135 bb.fatal("Cannot find any SPDX file for document %s" % ref.spdxDocument)
Patrick Williams7784c422022-11-17 07:29:11 -06001136 collect_spdx_document(ref_path)
1137
1138 collect_spdx_document(image_spdx_path)
1139
1140 index["documents"].sort(key=lambda x: x["filename"])
1141
1142 index_str = io.BytesIO(json.dumps(
1143 index,
1144 sort_keys=True,
1145 indent=get_json_indent(d),
1146 ).encode("utf-8"))
1147
1148 info = tarfile.TarInfo()
1149 info.name = "index.json"
1150 info.size = len(index_str.getvalue())
1151 info.uid = 0
1152 info.gid = 0
1153 info.uname = "root"
1154 info.gname = "root"
1155
1156 tar.addfile(info, fileobj=index_str)
Patrick Williamsb542dec2023-06-09 01:26:37 -05001157
Andrew Geissler220dafd2023-10-04 10:18:08 -05001158combine_spdx[vardepsexclude] += "BB_NUMBER_THREADS SSTATE_ARCHS"