The SPDX 2.2 pipeline in Yocto has been stable since Honister (Yocto 3.4). It produces standards-compliant SBOMs through three core BitBake tasks: do_create_spdx, do_create_runtime_spdx, and do_create_image_spdx. Each recipe gets its own SPDX document, linked together by external document references, and bundled into a tarball at image time. Its biggest gap: no native VEX support.
The SPDX 2.2 implementation in the Yocto Project has been stable since the Honister release (Yocto 3.4, October 2021). It is the established, widely understood pipeline that most Yocto users have been relying on, and for good reason. It produces standards-compliant SBOMs with rich per-package metadata that you can hand off to most SBOM tooling today.
This post is part 2 of a 5-part series on how Yocto generates SBOMs. If you have not read Part 1: How Yocto Generates SBOMs Behind the Scenes, it covers why build-time SBOM generation matters and the high-level architecture.
Key Variables and Configuration
When create-spdx-2.2.bbclass is loaded, it sets up several directories and control variables:
DEPLOY_DIR_SPDX ??= "${DEPLOY_DIR}/spdx/${MACHINE}"
SPDXDIR ??= "${WORKDIR}/spdx"
SPDXDEPLOY = "${SPDXDIR}/deploy"
SPDXWORK = "${SPDXDIR}/work"
SPDX_UUID_NAMESPACE ??= "sbom.openembedded.org"
SPDX_NAMESPACE_PREFIX ??= "http://spdx.org/spdxdocs"
The SPDXDIR within each recipe’s WORKDIR is the scratch space where intermediate SPDX data is assembled. DEPLOY_DIR_SPDX is the shared location where finalized SPDX documents are staged before being rolled up into the image-level output.
The SPDX_UUID_NAMESPACE and SPDX_NAMESPACE_PREFIX are used to generate document namespaces, the globally unique URIs that identify each SPDX document. These are constructed by combining the prefix with the recipe name and a UUID generated from a hash of the recipe’s content.
If you are publishing SPDX documents generated by Yocto, it is recommended that you redefine SPDX_UUID_NAMESPACE and SPDX_NAMESPACE_PREFIX to a URL owned by your organization to differentiate them from other producers.
Task 1: do_create_spdx, the Per-Recipe Core Task
The do_create_spdx task is the workhorse. It runs after do_package (so all packaging information is available) and produces SPDX documents for the recipe and each package it generates.
Here is what happens step by step.
Step 1: Create the Recipe-Level Document
The task creates an SPDX document for the recipe itself. This “recipe document” contains:
- A document header with creation information (tool name, organization, timestamp)
- A document namespace constructed from the recipe name and a UUID
- A top-level
SPDXPackageobject representing the recipe, populated with:namefrom the recipe name (PN)versionInfofrom the recipe version (PV)supplierset toOrganization: OpenEmbeddedlicenseDeclaredfrom the recipe’sLICENSEvariableSPDXIDwhich uniquely identifies the recipe within the document
Step 2: Add Source Information
For each entry in SRC_URI, the task records the download location. If SPDX_INCLUDE_SOURCES is enabled, it goes further: it walks the unpacked source tree (${S}), calculates SHA-1 and SHA-256 checksums for each file, and creates SPDXFile objects for them. This is expensive (which is why it is off by default), but it allows you to trace any individual source file back through the SBOM.
Step 3: Process Each Package
A single recipe may produce multiple packages (for example, bash produces bash, bash-dbg, bash-doc, and so on). For each package, the task creates a separate SPDX document containing:
- A new
SPDXPackageobject representing the runtime package - An
externalDocumentReflinking back to the recipe-level document - A
GENERATED_FROMrelationship from the package back to the recipe SPDXFileobjects for every file installed by the package, including their checksums
The checksums are calculated by walking ${PKGDEST}/<package>/, the staging area where BitBake has already laid out each package’s files.
Step 4: Write Output
The recipe document is written to ${SPDXDIR}/deploy/ as recipe-<recipename>.spdx.json. Each package document is written as <packagename>.spdx.json. These are then deployed to ${DEPLOY_DIR_SPDX}.
Task 2: do_create_runtime_spdx, Runtime Dependencies
A second task, do_create_runtime_spdx, handles the dependency relationships between packages. This task exists separately because runtime dependency information is not available until all recipes have been processed (BitBake needs the full dependency graph).
This task reads the RDEPENDS for each package and adds DEPENDS_ON relationships to the package’s SPDX document via external document references. For example, if bash depends on glibc, the bash SPDX document will reference the glibc SPDX document and declare a dependency relationship.
Task 3: do_create_image_spdx, Image Aggregation
When building an image recipe, the create-spdx-image class adds a do_create_image_spdx task that aggregates all per-recipe SPDX documents into the final deliverable.
Here is what the image task produces:
IMAGE-MACHINE.spdx.json: The top-level SPDX document describing the image. It contains a singleSPDXPackagefor the image itself,externalDocumentRefspointing to every included package, andCONTAINSrelationships linking the image package to each package.IMAGE-MACHINE.spdx.index.json: An index file listing all the individual SPDX documents that make up the complete SBOM.IMAGE-MACHINE.spdx.tar.zst: A compressed archive containing all individual SPDX documents (recipe-level and package-level).
The Document-Linking Challenge in SPDX 2.2
One of the key architectural decisions in the SPDX 2.2 implementation is the use of separate documents linked by external document references. Rather than producing one monolithic SPDX file, Yocto creates hundreds of individual SPDX documents (one per recipe, one per package) and links them together by SPDX external document references, then places all of the individual documents into a tarball with an index file for the final deliverable.
This was a pragmatic choice driven by some limitations in SPDX 2.2. The namespace and document structure of SPDX 2.2 makes it hard to combine documents without risking ID collisions or losing relationship information. The tarball approach side-steps this by simply bundling everything together and relying on the index file for navigation.
The downside is that tooling consuming these SBOMs needs to understand the document-linking model. You cannot just load a single JSON file and see everything. You need to extract the archive and follow the external document references.
The VEX Gap in SPDX 2.2
One significant limitation of SPDX 2.2 is that it has no standard way to include vulnerability information. There is simply no mechanism in the SPDX 2.2 specification to embed VEX data or CVE assessments within the SBOM itself.
This means that if you are using SPDX 2.2 and want vulnerability tracking, you need a separate workflow. You can use the cve-check class to generate a standalone CVE report (typically as cve-summary.json), but that report lives outside of the SBOM and needs to be correlated with it manually or through external tooling. You can also produce a separate VEX document (for example, in OpenVEX or CycloneDX VEX format) and distribute it alongside your SPDX 2.2 SBOM, but the two are not structurally connected.
This separation is one of the primary motivations for moving to SPDX 3.0, which addresses the problem head-on.
What the SPDX 2.2 Output Looks Like
A typical package SPDX document (e.g., bash.spdx.json) looks something like:
{
"SPDXID": "SPDXRef-DOCUMENT",
"creationInfo": {
"comment": "This document was created by analyzing packages created during the build.",
"created": "2025-02-18T21:58:47Z",
"creators": [
"Tool: OpenEmbedded Core create-spdx.bbclass",
"Organization: OpenEmbedded ()",
"Person: N/A ()"
],
"licenseListVersion": "3.14"
},
"dataLicense": "CC0-1.0",
"documentNamespace": "http://spdx.org/spdxdocs/bash-0139ef99-a375-59f8-9ada-833f50f987d3",
"externalDocumentRefs": [
{
"checksum": {
"algorithm": "SHA1",
"checksumValue": "e9353b8e26447ef425aa740060f57411420c817a"
},
"externalDocumentId": "DocumentRef-recipe-bash",
"spdxDocument": "http://spdx.org/spdxdocs/recipe-bash-ad5747dc-7b5a-562d-aabf-dfb516e7095d"
}
],
"packages": [...],
"files": [...],
"relationships": [...]
}
Each file within the package gets a detailed entry with checksums:
{
"SPDXID": "SPDXRef-PackagedFile-bash-1",
"checksums": [
{ "algorithm": "SHA1", "checksumValue": "14cd85414db85903029caca727345272de829a69" },
{ "algorithm": "SHA256", "checksumValue": "..." }
],
"fileName": "/usr/bin/bash"
}
Series: How Yocto Generates SBOMs Behind the Scenes
- Part 1: How Yocto Generates SBOMs Behind the Scenes
- Part 2: A Deep Dive into Yocto’s SPDX 2.2 Pipeline (this post)
- Part 3: SPDX 3.0 in Yocto: What Changed and Why It Matters (coming soon)
- Part 4: VEX in the SBOM: How Yocto Embeds Vulnerability Assessments (coming soon)
- Part 5: Yocto SBOM in Production: Configuration, Tooling, and What’s Still Missing (coming soon)
Found an error or typo? File a PR against this file.