@@ -30,7 +30,7 @@ python do_create_rootfs_spdx() {
import oe.spdx30_tasks
oe.spdx30_tasks.create_rootfs_spdx(d)
}
-addtask do_create_rootfs_spdx after do_rootfs before do_image
+addtask do_create_rootfs_spdx after do_rootfs do_create_recipe_spdx before do_image
SSTATETASKS += "do_create_rootfs_spdx"
do_create_rootfs_spdx[sstate-inputdirs] = "${SPDXROOTFSDEPLOY}"
do_create_rootfs_spdx[sstate-outputdirs] = "${DEPLOY_DIR_SPDX}"
@@ -47,7 +47,7 @@ python do_create_image_spdx() {
import oe.spdx30_tasks
oe.spdx30_tasks.create_image_spdx(d)
}
-addtask do_create_image_spdx after do_image_complete do_create_rootfs_spdx before do_build
+addtask do_create_image_spdx after do_image_complete do_create_rootfs_spdx do_create_recipe_spdx before do_build
SSTATETASKS += "do_create_image_spdx"
SSTATE_SKIP_CREATION:task-create-image-spdx = "1"
do_create_image_spdx[sstate-inputdirs] = "${SPDXIMAGEWORK}"
@@ -6,6 +6,7 @@
DEPLOYDIR = "${WORKDIR}/deploy-${PN}"
SSTATETASKS += "do_deploy"
+SPDX_DEPLOY_ARTIFACTS_DIR:task-deploy = "${DEPLOYDIR}"
do_deploy[sstate-inputdirs] = "${DEPLOYDIR}"
do_deploy[sstate-outputdirs] = "${DEPLOY_DIR_IMAGE}"
@@ -11,3 +11,4 @@ deltask do_create_package_spdx
deltask do_create_rootfs_spdx
deltask do_create_image_spdx
deltask do_create_image_sbom
+deltask do_create_deploy_sbom
@@ -163,6 +163,34 @@ SPDX_GIT_PURL_MAPPINGS[doc] = "A space separated list of domain:purl_type \
on gitlab.example.com to the pkg:gitlab PURL type. \
github.com is always mapped to pkg:github by default."
+SPDX_DEPLOY_TASKS ?= ""
+SPDX_DEPLOY_TASKS[doc] = "A space separated list of sstate tasks that produce \
+ deployed output (usually written to DEPLOY_DIR_IMAGE). Tasks in this list \
+ will produce SPDX documents that describe the deployed output. If a task \
+ is in this list, see the SPDX_DEPLOY_ARITFACTS and SPDX_DEPLOY_ARTIFACTS_DIR \
+ for how to configure its SPDX output.\
+ \
+ Dependencies of deploy tasks that produce SPDX data will be automatically \
+ linked in as a build time dependency of the deploy task's SBoM. (for \
+ example, if one do_deploy depends on another recipes do_deploy, this will \
+ be reflected in the SPDX data). If the deploy task should depend on the \
+ primary build process of the recipe, the task should be declared \
+ 'after do_create_spdx'.\
+ "
+
+SPDX_DEPLOY_ARTIFACTS = "AUTO"
+SPDX_DEPLOY_ARITFACTS[doc] = "A space separated list of deployed artifacts, \
+ relative to SPDX_DEPLOY_ARTIFACTS_DIR that should be included in the SBoM. \
+ If 'AUTO' (the default), all files in SPDX_DEPLOY_ARITFACTS_DIR will be \
+ added. A :task- override *must* be used to set this value so that it is \
+ scoped to a specific task"
+
+SPDX_DEPLOY_ARTIFACTS_DIR = ""
+SPDX_DEPLOY_ARTIFACTS_DIR[doc] = "The directory recipe specific directory \
+ where artifacts are deployed for staging to sstate (e.g. for do_deploy, \
+ this is DEPLOY_DIR). A :task- override *must* be used to set this value so \
+ that it is scoped to a specific task."
+
IMAGE_CLASSES:append = " create-spdx-image-3.0"
SDK_CLASSES += "create-spdx-sdk-3.0"
@@ -291,3 +319,133 @@ python spdx30_build_started_handler () {
addhandler spdx30_build_started_handler
spdx30_build_started_handler[eventmask] = "bb.event.BuildStarted"
+python create_deploy_spdx() {
+ import oe.spdx30_tasks
+ from pathlib import Path
+ current_task = "do_" + d.getVar("BB_CURRENTTASK")
+
+ spdxdeploydir = Path(d.getVar("SPDXDIR") + "/deploy-" + current_task)
+
+ artifactsdir = d.getVar("SPDX_DEPLOY_ARTIFACTS_DIR")
+ if not artifactsdir:
+ bb.fatal(f"{pn}: spdx-artifactsdir must be set for task {current_task}")
+ return
+
+ artifacts = d.getVar("SPDX_DEPLOY_ARTIFACTS")
+
+ oe.spdx30_tasks.create_deploy_spdx(d, spdxdeploydir, artifactsdir, artifacts)
+}
+oe.spdx30_tasks.find_build_dep_objsets[vardepsexclude] += "BB_TASKDEPDATA"
+
+python () {
+ # Most recipes generate SPDX output in a distinct task from the task that
+ # actually is the relevant dependency. As such, we need to map the task
+ # that we care about to the task that generates the corresponding SPDX
+ # output so that we can rely on the SPDX output being present when the time
+ # comes to use it downstream.
+ #
+ # The down side of this is that only the first level of dependencies (e.g
+ # tasks listed in SPDX_DEPLOY_TASKS) will have the mapping done and thus
+ # find the dependencies. Transitive dependencies will not be mapped and
+ # thus the SPDX data will not be linked in.
+ #
+ # Ideally, this will be able to go away once more tasks directly generate
+ # SPDX files for their output instead of combining it into monolithic
+ # functions; tasks listed in this map are the best candidates to have this
+ # done first.
+ TASK_MAP = {
+ # If a task requires the RSS be extended, depend on the SPDX build task
+ # for the recipe, at least until it's possible for do_populate_sysroot
+ # to describe it's own output.
+ "do_populate_sysroot": "do_create_spdx",
+ # If an image is needed, also depend on the task to create the SBoM for
+ # the image
+ "do_image_complete": "do_create_image_spdx",
+ }
+
+ def map_task_deps(task, flag):
+ task_flags= (d.getVarFlag(task, flag) or "").split()
+ for t in task_flags:
+ if t in TASK_MAP and TASK_MAP[t] not in task_flags:
+ d.appendVarFlag(task, flag, f" {TASK_MAP[t]}")
+
+ def before_postfunc(f):
+ return f == "sstate_task_postfunc" or "buildhistory" in f
+
+ if bb.data.inherits_class("nospdx", d):
+ return
+
+ sstate_tasks = set((d.getVar("SSTATETASKS") or "").split())
+ spdx_tasks = (d.getVar("SPDX_DEPLOY_TASKS") or "").split()
+ deploy_sbom_tasks = []
+ for task in spdx_tasks:
+ if ":" in task:
+ task, func = task.split(":")
+ else:
+ func = "create_deploy_spdx"
+
+ deploy_sbom_tasks.append(task)
+
+ if task not in sstate_tasks:
+ bb.fatal(f"{task} is not an sstate task")
+
+ spdx_deploy = "${SPDXDIR}/deploy-" + task
+
+ # Ensure function is sorted properly. It should be right before
+ # sstate_task_postfunc
+ postfuncs = (d.getVarFlag(task, "postfuncs") or "").split()
+ d.setVarFlag(task, "postfuncs", " ".join(
+ [f for f in postfuncs if not before_postfunc(f)] +
+ [func] +
+ [f for f in postfuncs if before_postfunc(f)]
+ ))
+ d.prependVarFlag(task, "sstate-inputdirs", f"{spdx_deploy} ")
+ d.prependVarFlag(task, "sstate-outputdirs", "${DEPLOY_DIR_SPDX} ")
+ d.prependVarFlag(task, "file-checksums", "${SPDX3_DEP_FILES} ")
+ d.prependVarFlag(task, "dirs", f"{spdx_deploy} ")
+ d.prependVarFlag(task, "cleandirs", f"{spdx_deploy} ")
+
+ deps = (d.getVarFlag(task, "depends") or "").split()
+ extra_deps = ["${PN}:do_create_recipe_spdx"]
+ for dep in deps:
+ _, fn, taskname = bb.runqueue.split_tid(dep)
+ if taskname in TASK_MAP:
+ extra_deps.append(f"{fn}:{TASK_MAP[taskname]}")
+
+ d.prependVarFlag(task, "depends", " ".join(extra_deps) + " ")
+
+ map_task_deps(task, "deptask")
+ map_task_deps(task, "rdeptask")
+ map_task_deps(task, "recrdeptask")
+
+ # For now, if a recipe is directly built, deploy all of it's deploy tasks
+ # into a single SBoM. We may need an option in the future to have tasks
+ # that don't do this (e.g. because they do not deploy to a location that is
+ # intended to be consumed by the user)
+ if spdx_tasks:
+ bb.build.addtask("do_create_deploy_sbom", "do_build", " ".join(deploy_sbom_tasks), d)
+}
+
+python do_create_deploy_sbom() {
+ import oe.spdx30_tasks
+ from pathlib import Path
+ deploydir = Path(d.getVar("SPDXDEPLOYSBOMDEPLOY"))
+ deploy_tasks = []
+ for task in (d.getVar("SPDX_DEPLOY_TASKS") or "").split():
+ if ":" in task:
+ task, _ = task.split(":")
+ deploy_tasks.append(task)
+
+ oe.spdx30_tasks.create_deploy_sbom(d, deploydir, deploy_tasks)
+}
+do_create_deploy_sbom[sstate-inputdirs] = "${SPDXDEPLOYSBOMDEPLOY}"
+do_create_deploy_sbom[sstate-outputdirs] = "${DEPLOY_DIR_IMAGE}"
+do_create_deploy_sbom[recrdeptask] += "do_create_recipe_spdx do_create_spdx"
+do_create_deploy_sbom[cleandirs] += "${SPDXDEPLOYSBOMDEPLOY}"
+do_create_deploy_sbom[file-checksums] += "${SPDX3_DEP_FILES}"
+
+SSTATETASKS += "do_create_deploy_sbom"
+python do_create_deploy_sbom_setscene() {
+ sstate_setscene(d)
+}
+addtask do_create_deploy_sbom_setscene
@@ -26,6 +26,7 @@ SPDX_TOOL_VERSION ??= "1.0"
SPDXRECIPEDEPLOY = "${SPDXDIR}/recipe-deploy"
SPDXRUNTIMEDEPLOY = "${SPDXDIR}/runtime-deploy"
SPDXRECIPESBOMDEPLOY = "${SPDXDIR}/recipes-bom-deploy"
+SPDXDEPLOYSBOMDEPLOY = "${SPDXDIR}/deploy-bom-deploy"
SPDX_INCLUDE_SOURCES ??= "0"
SPDX_INCLUDE_SOURCES[doc] = "If set to '1', include source code files in the \
@@ -1048,6 +1048,25 @@ def write_jsonld_doc(d, objset, dest):
objset.objects.remove(objset.doc)
+def make_jsonld_link(d, fn, subdir, name, deploydir):
+ pkg_arch = d.getVar("SSTATE_PKGARCH")
+
+ link_name = jsonld_arch_path(
+ d,
+ pkg_arch,
+ subdir,
+ name,
+ deploydir=deploydir,
+ )
+ try:
+ link_name.parent.mkdir(exist_ok=True, parents=True)
+ link_name.symlink_to(os.path.relpath(fn, link_name.parent))
+ except:
+ target = link_name.readlink()
+ bb.warn(f"Unable to link {fn} as {link_name}. Already points to {target}")
+ raise
+
+
def write_recipe_jsonld_doc(
d,
objset,
@@ -1055,6 +1074,7 @@ def write_recipe_jsonld_doc(
deploydir,
*,
create_spdx_id_links=True,
+ create_task_link=False,
):
pkg_arch = d.getVar("SSTATE_PKGARCH")
@@ -1062,23 +1082,7 @@ def write_recipe_jsonld_doc(
def link_id(_id):
hash_path = jsonld_hash_path(hash_id(_id))
-
- link_name = jsonld_arch_path(
- d,
- pkg_arch,
- *hash_path,
- deploydir=deploydir,
- )
- try:
- link_name.parent.mkdir(exist_ok=True, parents=True)
- link_name.symlink_to(os.path.relpath(dest, link_name.parent))
- except:
- target = link_name.readlink()
- bb.warn(
- f"Unable to link {_id} in {dest} as {link_name}. Already points to {target}"
- )
- raise
-
+ make_jsonld_link(d, dest, *hash_path, deploydir)
return hash_path[-1]
objset.add_aliases()
@@ -1094,6 +1098,14 @@ def write_recipe_jsonld_doc(
# out, so always do that even if there is an error making the links
write_jsonld_doc(d, objset, dest)
+ if create_task_link:
+ pn = d.getVar("PN")
+ current_task = "do_" + d.getVar("BB_CURRENTTASK")
+
+ make_jsonld_link(d, dest, "by-task", f"{pn}:{current_task}", deploydir)
+
+ return dest
+
def find_root_obj_in_jsonld(d, subdir, fn_name, obj_type, **attr_filter):
objset, fn = find_jsonld(d, subdir, fn_name, required=True)
@@ -605,6 +605,135 @@ def get_is_native(d):
return bb.data.inherits_class("native", d) or bb.data.inherits_class("cross", d)
+def set_var_field(d, var, obj, name, package=None):
+ val = None
+ if package:
+ val = d.getVar("%s:%s" % (var, package))
+
+ if not val:
+ val = d.getVar(var)
+
+ if val:
+ setattr(obj, name, val)
+
+
+def find_build_dep_objsets(d, start_task):
+ def find_deps(d, taskdepdata, current_dep, start_dep, visited, depth=0):
+ key = f"{current_dep.pn}:{current_dep.taskname}"
+
+ dep_objsets = []
+
+ if key not in visited:
+ visited.add(key)
+
+ for n in current_dep.deps:
+ dep = taskdepdata[n]
+ dep_name = f"{dep.pn}:{dep.taskname}"
+
+ dep_objset, dep_path = oe.sbom30.find_jsonld(d, "by-task", dep_name)
+ if dep_objset:
+ dep_objsets.append(dep_objset)
+
+ elif dep.pn == start_dep.pn:
+ # If this task is still part of the same recipe, continue
+ # searching up the dependency tree until a valid dependency
+ # is found. This detects transitive dependencies that may
+ # have been pulled in by previous tasks in the same recipe.
+ dep_objsets.extend(
+ find_deps(d, taskdepdata, dep, start_dep, visited, depth + 1)
+ )
+
+ return dep_objsets
+
+ pn = d.getVar("PN")
+ taskdepdata = d.getVar("BB_TASKDEPDATA", False)
+ for dep in taskdepdata.values():
+ if dep.pn == pn and dep.taskname == start_task:
+ start_dep = dep
+ break
+ else:
+ bb.fatal(f"Unable to find {pn}:{start_task} in taskdepdata")
+
+ return find_deps(d, taskdepdata, start_dep, start_dep, set())
+
+
+def create_deploy_package(d, objset, build, spdxid, name, start_task, files, **attrs):
+ recipe, _ = load_recipe_spdx(d)
+
+ deploy_package = objset.add_root(
+ oe.spdx30.software_Package(
+ _id=spdxid,
+ creationInfo=objset.doc.creationInfo,
+ name=name,
+ software_packageVersion=d.getVar("PV"),
+ )
+ )
+
+ objset.new_scoped_relationship(
+ [oe.sbom30.get_element_link_id(recipe)],
+ oe.spdx30.RelationshipType.generates,
+ oe.spdx30.LifecycleScopeType.build,
+ [deploy_package],
+ )
+
+ set_var_field(d, "HOMEPAGE", deploy_package, "software_homePage")
+ set_var_field(d, "SUMMARY", deploy_package, "summary")
+ set_var_field(d, "DESCRIPTION", deploy_package, "description")
+
+ set_purls(deploy_package, (d.getVar("SPDX_PACKAGE_URLS") or "").split())
+
+ set_timestamp_now(d, deploy_package, "builtTime")
+
+ supplier = objset.new_agent("SPDX_PACKAGE_SUPPLIER")
+ if supplier is not None:
+ deploy_package.suppliedBy = (
+ supplier if isinstance(supplier, str) else supplier._id
+ )
+
+ if files:
+ objset.new_relationship(
+ [deploy_package],
+ oe.spdx30.RelationshipType.contains,
+ sorted(list(files)),
+ )
+
+ objset.new_scoped_relationship(
+ [build],
+ oe.spdx30.RelationshipType.hasOutput,
+ oe.spdx30.LifecycleScopeType.build,
+ sorted(list(files) + [deploy_package]),
+ )
+
+ # Collect dependencies
+ if start_task is not None:
+ dep_builds = set()
+ dep_packages = set()
+ for o in find_build_dep_objsets(d, start_task):
+ if obj := o.find_root(oe.spdx30.software_Package):
+ dep_packages.add(oe.sbom30.get_element_link_id(obj))
+
+ if obj := o.find_root(oe.spdx30.build_Build):
+ dep_builds.add(oe.sbom30.get_element_link_id(obj))
+
+ if dep_packages:
+ objset.new_scoped_relationship(
+ [deploy_package],
+ oe.spdx30.RelationshipType.dependsOn,
+ oe.spdx30.LifecycleScopeType.build,
+ sorted(list(dep_packages)),
+ )
+
+ if dep_builds:
+ objset.new_scoped_relationship(
+ [build],
+ oe.spdx30.RelationshipType.dependsOn,
+ oe.spdx30.LifecycleScopeType.build,
+ sorted(list(dep_builds)),
+ )
+
+ return deploy_package
+
+
def create_recipe_spdx(d):
deploydir = Path(d.getVar("SPDXRECIPEDEPLOY"))
pn = d.getVar("PN")
@@ -795,7 +924,9 @@ def create_recipe_spdx(d):
sorted(list(all_cves)),
)
- oe.sbom30.write_recipe_jsonld_doc(d, recipe_objset, "static", deploydir)
+ oe.sbom30.write_recipe_jsonld_doc(
+ d, recipe_objset, "static", deploydir, create_task_link=True
+ )
def load_recipe_spdx(d):
@@ -809,17 +940,6 @@ def load_recipe_spdx(d):
def create_spdx(d):
- def set_var_field(var, obj, name, package=None):
- val = None
- if package:
- val = d.getVar("%s:%s" % (var, package))
-
- if not val:
- val = d.getVar(var)
-
- if val:
- setattr(obj, name, val)
-
license_data = oe.spdx_common.load_spdx_license_data(d)
pn = d.getVar("PN")
@@ -947,10 +1067,12 @@ def create_spdx(d):
)
set_var_field(
- "HOMEPAGE", spdx_package, "software_homePage", package=package
+ d, "HOMEPAGE", spdx_package, "software_homePage", package=package
+ )
+ set_var_field(d, "SUMMARY", spdx_package, "summary", package=package)
+ set_var_field(
+ d, "DESCRIPTION", spdx_package, "description", package=package
)
- set_var_field("SUMMARY", spdx_package, "summary", package=package)
- set_var_field("DESCRIPTION", spdx_package, "description", package=package)
purls = (
d.getVar("SPDX_PACKAGE_URLS:%s" % package)
@@ -1130,7 +1252,9 @@ def create_spdx(d):
f"Added PACKAGECONFIG entries: {len(enabled)} enabled, {len(disabled)} disabled"
)
- oe.sbom30.write_recipe_jsonld_doc(d, build_objset, "builds", deploydir)
+ oe.sbom30.write_recipe_jsonld_doc(
+ d, build_objset, "builds", deploydir, create_task_link=True
+ )
def create_package_spdx(d):
@@ -1364,26 +1488,9 @@ def create_rootfs_spdx(d):
d, "%s-%s-rootfs" % (image_basename, machine)
)
- rootfs = objset.add_root(
- oe.spdx30.software_Package(
- _id=objset.new_spdxid("rootfs", image_basename),
- creationInfo=objset.doc.creationInfo,
- name=image_basename,
- software_primaryPurpose=oe.spdx30.software_SoftwarePurpose.archive,
- )
- )
- set_timestamp_now(d, rootfs, "builtTime")
-
rootfs_build = objset.add_root(objset.new_task_build("rootfs", "rootfs"))
set_timestamp_now(d, rootfs_build, "build_buildEndTime")
- objset.new_scoped_relationship(
- [rootfs_build],
- oe.spdx30.RelationshipType.hasOutput,
- oe.spdx30.LifecycleScopeType.build,
- [rootfs],
- )
-
files_by_hash = {}
collect_build_package_inputs(d, objset, rootfs_build, packages, files_by_hash)
@@ -1416,14 +1523,20 @@ def create_rootfs_spdx(d):
)
)
- if files:
- objset.new_relationship(
- [rootfs],
- oe.spdx30.RelationshipType.contains,
- sorted(list(files)),
- )
+ rootfs = create_deploy_package(
+ d,
+ objset,
+ rootfs_build,
+ objset.new_spdxid("rootfs", image_basename),
+ image_basename,
+ None,
+ files,
+ )
+ rootfs.software_primaryPurpose = oe.spdx30.software_SoftwarePurpose.archive
- oe.sbom30.write_recipe_jsonld_doc(d, objset, "rootfs", deploydir)
+ oe.sbom30.write_recipe_jsonld_doc(
+ d, objset, "rootfs", deploydir, create_task_link=True
+ )
def create_image_spdx(d):
@@ -1503,10 +1616,13 @@ def create_image_spdx(d):
set_timestamp_now(d, a, "builtTime")
if artifacts:
- objset.new_scoped_relationship(
- [image_build],
- oe.spdx30.RelationshipType.hasOutput,
- oe.spdx30.LifecycleScopeType.build,
+ create_deploy_package(
+ d,
+ objset,
+ image_build,
+ objset.new_spdxid(taskname, "image", imagetype),
+ "image",
+ f"do_{taskname}",
artifacts,
)
@@ -1527,7 +1643,9 @@ def create_image_spdx(d):
objset.add_aliases()
objset.link()
- oe.sbom30.write_recipe_jsonld_doc(d, objset, "image", spdx_work_dir)
+ oe.sbom30.write_recipe_jsonld_doc(
+ d, objset, "image", spdx_work_dir, create_task_link=True
+ )
def create_image_sbom_spdx(d):
@@ -1705,3 +1823,77 @@ def create_recipe_sbom(d, deploydir):
objset, sbom = oe.sbom30.create_sbom(d, sbom_name, [recipe], [recipe_objset])
oe.sbom30.write_jsonld_doc(d, objset, deploydir / (sbom_name + ".spdx.json"))
+
+
+def create_deploy_spdx(d, spdxdeploydir, artifactsdir, artifacts):
+ pn = d.getVar("PN")
+ current_task = "do_" + d.getVar("BB_CURRENTTASK")
+
+ recipe, recipe_objset = load_recipe_spdx(d)
+
+ if artifacts == "AUTO":
+ artifacts = []
+ for root, dirs, files in os.walk(artifactsdir):
+ for p in [Path(os.path.join(root, f)) for f in files]:
+ if p.is_file():
+ artifacts.append(p)
+ else:
+ artifacts = [artifactsdir / p for p in artifacts.split()]
+
+ artifacts.sort(key=lambda p: (p.is_symlink(), p))
+
+ objset = oe.sbom30.ObjectSet.new_objset(d, f"{pn}-{current_task}-deploy")
+
+ build = objset.add_root(objset.new_task_build(current_task, "deploy"))
+ set_timestamp_now(d, build, "build_buildEndTime")
+ objset.set_is_native(get_is_native(d))
+
+ files = set()
+ for a in artifacts:
+ relpath = a.relative_to(artifactsdir)
+ f = objset.new_file(
+ objset.new_spdxid("deploy", str(relpath)),
+ a.name,
+ a,
+ )
+ files.add(f)
+
+ if not files:
+ bb.fatal(f"No deployed artifacts found in {artifactsdir}")
+ return
+
+ create_deploy_package(
+ d,
+ objset,
+ build,
+ objset.new_spdxid("deploy", pn, current_task),
+ pn,
+ current_task,
+ files,
+ )
+
+ # Create document
+ dest = oe.sbom30.write_recipe_jsonld_doc(
+ d,
+ objset,
+ "deploy",
+ spdxdeploydir,
+ create_task_link=True,
+ )
+
+
+def create_deploy_sbom(d, deploydir, deploy_tasks):
+ pn = d.getVar("PN")
+ sbom_name = f"{pn}-deploy-sbom"
+
+ objsets = []
+ for t in deploy_tasks:
+ o, _ = oe.sbom30.find_jsonld(d, "deploy", f"{pn}-{t}-deploy", required=True)
+ objsets.append(o)
+
+ root_objs = []
+ for o in objsets:
+ root_objs.extend(o.doc.rootElement)
+
+ objset, sbom = oe.sbom30.create_sbom(d, sbom_name, root_objs, objsets)
+ oe.sbom30.write_jsonld_doc(d, objset, deploydir / (sbom_name + ".spdx.json"))
@@ -113,7 +113,7 @@ def collect_direct_deps(d, dep_task):
)
for this_dep in taskdepdata.values():
- if this_dep[0] == pn and this_dep[1] == current_task:
+ if this_dep.pn == pn and this_dep.taskname == current_task:
break
else:
bb.fatal(f"Unable to find this {pn}:{current_task} in taskdepdata")
Adds support for "deploy" tasks (like do_deploy) to write out SPDX documents that describe what has been deployed. Deploy tasks will automatically detect many dependencies on other recipes; specifically they will correctly detect dependencies on any do_create_spdx task, and also other deploy tasks that generate SPDX output. The only known notable exception are transitive (e.g. originating from other upstream tasks) dependencies on do_image_complete, and do_populate_sysroot. However, these are detected if a direct dependency of the deploy task (via translation of the task dependencies). This same dependency finding algorithm is now applied to the image generation SBoM; this means that if an image creation task depends on a task that generates a deploy SBoM, it will show up in the dependency graph of the image. A typical example is a wic file that consumes the kernel, u-boot, etc. will now correctly list those as a dependency, as long as their do_deploy step is added to SPDX_DEPLOY_TASKS. Signed-off-by: Joshua Watt <JPEWhacker@gmail.com> --- .../create-spdx-image-3.0.bbclass | 4 +- meta/classes-recipe/deploy.bbclass | 1 + meta/classes-recipe/nospdx.bbclass | 1 + meta/classes/create-spdx-3.0.bbclass | 158 ++++++++++ meta/classes/spdx-common.bbclass | 1 + meta/lib/oe/sbom30.py | 46 +-- meta/lib/oe/spdx30_tasks.py | 282 +++++++++++++++--- meta/lib/oe/spdx_common.py | 2 +- 8 files changed, 430 insertions(+), 65 deletions(-)