west: spdx: allow to generate for different SPDX versions

When support for SPDX 2.3 was added, it effectively dropped support for
SPDX 2.2, which in retrospect was a bad idea since SPDX 2.2 is the
version that is the current ISO/IEC standard.
This commit adds a `--spdx-version` option to the `west spdx` command
so that users can generate SPDX 2.2 documents if they want.
Default is 2.3 given that's effectively what shipped for a few releases
now, including latest LTS.

Signed-off-by: Benjamin Cabé <benjamin@zephyrproject.org>
This commit is contained in:
Benjamin Cabé 2025-05-28 17:13:27 +02:00 committed by Benjamin Cabé
commit be504b000d
5 changed files with 68 additions and 18 deletions

View file

@ -75,7 +75,7 @@ See :zephyr_file:`share/zephyr-package/cmake` for details.
Software bill of materials: ``west spdx`` Software bill of materials: ``west spdx``
***************************************** *****************************************
This command generates SPDX 2.3 tag-value documents, creating relationships This command generates SPDX 2.2 or 2.3 tag-value documents, creating relationships
from source files to the corresponding generated build files. from source files to the corresponding generated build files.
``SPDX-License-Identifier`` comments in source files are scanned and filled ``SPDX-License-Identifier`` comments in source files are scanned and filled
into the SPDX documents. into the SPDX documents.
@ -105,6 +105,12 @@ To use this command:
west spdx -d BUILD_DIR west spdx -d BUILD_DIR
By default, this generates SPDX 2.3 documents. To generate SPDX 2.2 documents instead:
.. code-block:: bash
west spdx -d BUILD_DIR --spdx-version 2.2
.. note:: .. note::
When building with :ref:`sysbuild`, make sure you target the actual application When building with :ref:`sysbuild`, make sure you target the actual application
@ -144,6 +150,10 @@ source files that are compiled to generate the built library files.
- ``-s SPDX_DIR``: specifies an alternate directory where the SPDX documents - ``-s SPDX_DIR``: specifies an alternate directory where the SPDX documents
should be written instead of :file:`BUILD_DIR/spdx/`. should be written instead of :file:`BUILD_DIR/spdx/`.
- ``--spdx-version {2.2,2.3}``: specifies which SPDX specification version to use.
Defaults to ``2.3``. SPDX 2.3 includes additional fields like ``PrimaryPackagePurpose``
that are not available in SPDX 2.2.
- ``--analyze-includes``: in addition to recording the compiled source code - ``--analyze-includes``: in addition to recording the compiled source code
files (e.g. ``.c``, ``.S``) in the bills-of-materials, also attempt to files (e.g. ``.c``, ``.S``) in the bills-of-materials, also attempt to
determine the specific header files that are included for each ``.c`` file. determine the specific header files that are included for each ``.c`` file.

View file

@ -6,11 +6,11 @@ import os
import uuid import uuid
from west.commands import WestCommand from west.commands import WestCommand
from zspdx.sbom import SBOMConfig, makeSPDX, setupCmakeQuery from zspdx.sbom import SBOMConfig, makeSPDX, setupCmakeQuery
from zspdx.version import SPDX_VERSION_2_3, SUPPORTED_SPDX_VERSIONS, parse
SPDX_DESCRIPTION = """\ SPDX_DESCRIPTION = """\
This command creates an SPDX 2.2 tag-value bill of materials This command creates an SPDX 2.2 or 2.3 tag-value bill of materials
following the completion of a Zephyr build. following the completion of a Zephyr build.
Prior to the build, an empty file must be created at Prior to the build, an empty file must be created at
@ -41,6 +41,9 @@ class ZephyrSpdx(WestCommand):
help="namespace prefix") help="namespace prefix")
parser.add_argument('-s', '--spdx-dir', parser.add_argument('-s', '--spdx-dir',
help="SPDX output directory") help="SPDX output directory")
parser.add_argument('--spdx-version', choices=[str(v) for v in SUPPORTED_SPDX_VERSIONS],
default=str(SPDX_VERSION_2_3),
help="SPDX specification version to use (default: 2.3)")
parser.add_argument('--analyze-includes', action="store_true", parser.add_argument('--analyze-includes', action="store_true",
help="also analyze included header files") help="also analyze included header files")
parser.add_argument('--include-sdk', action="store_true", parser.add_argument('--include-sdk', action="store_true",
@ -55,6 +58,7 @@ class ZephyrSpdx(WestCommand):
self.dbg(" --build-dir is", args.build_dir) self.dbg(" --build-dir is", args.build_dir)
self.dbg(" --namespace-prefix is", args.namespace_prefix) self.dbg(" --namespace-prefix is", args.namespace_prefix)
self.dbg(" --spdx-dir is", args.spdx_dir) self.dbg(" --spdx-dir is", args.spdx_dir)
self.dbg(" --spdx-version is", args.spdx_version)
self.dbg(" --analyze-includes is", args.analyze_includes) self.dbg(" --analyze-includes is", args.analyze_includes)
self.dbg(" --include-sdk is", args.include_sdk) self.dbg(" --include-sdk is", args.include_sdk)
@ -85,6 +89,11 @@ class ZephyrSpdx(WestCommand):
# create the SPDX files # create the SPDX files
cfg = SBOMConfig() cfg = SBOMConfig()
cfg.buildDir = args.build_dir cfg.buildDir = args.build_dir
try:
version_obj = parse(args.spdx_version)
except Exception:
self.die(f"Invalid SPDX version: {args.spdx_version}")
cfg.spdxVersion = version_obj
if args.namespace_prefix: if args.namespace_prefix:
cfg.namespacePrefix = args.namespace_prefix cfg.namespacePrefix = args.namespace_prefix
else: else:

View file

@ -7,6 +7,7 @@ import os
from west import log from west import log
from zspdx.scanner import ScannerConfig, scanDocument from zspdx.scanner import ScannerConfig, scanDocument
from zspdx.version import SPDX_VERSION_2_3
from zspdx.walker import Walker, WalkerConfig from zspdx.walker import Walker, WalkerConfig
from zspdx.writer import writeSPDX from zspdx.writer import writeSPDX
@ -26,6 +27,9 @@ class SBOMConfig:
# location of SPDX document output directory # location of SPDX document output directory
self.spdxDir = "" self.spdxDir = ""
# SPDX specification version to use
self.spdxVersion = SPDX_VERSION_2_3
# should also analyze for included header files? # should also analyze for included header files?
self.analyzeIncludes = False self.analyzeIncludes = False
@ -101,31 +105,33 @@ def makeSPDX(cfg):
# write SDK document, if we made one # write SDK document, if we made one
if cfg.includeSDK: if cfg.includeSDK:
retval = writeSPDX(os.path.join(cfg.spdxDir, "sdk.spdx"), w.docSDK) retval = writeSPDX(os.path.join(cfg.spdxDir, "sdk.spdx"), w.docSDK, cfg.spdxVersion)
if not retval: if not retval:
log.err("SPDX writer failed for SDK document; bailing") log.err("SPDX writer failed for SDK document; bailing")
return False return False
# write app document # write app document
retval = writeSPDX(os.path.join(cfg.spdxDir, "app.spdx"), w.docApp) retval = writeSPDX(os.path.join(cfg.spdxDir, "app.spdx"), w.docApp, cfg.spdxVersion)
if not retval: if not retval:
log.err("SPDX writer failed for app document; bailing") log.err("SPDX writer failed for app document; bailing")
return False return False
# write zephyr document # write zephyr document
retval = writeSPDX(os.path.join(cfg.spdxDir, "zephyr.spdx"), w.docZephyr) retval = writeSPDX(os.path.join(cfg.spdxDir, "zephyr.spdx"), w.docZephyr, cfg.spdxVersion)
if not retval: if not retval:
log.err("SPDX writer failed for zephyr document; bailing") log.err("SPDX writer failed for zephyr document; bailing")
return False return False
# write build document # write build document
retval = writeSPDX(os.path.join(cfg.spdxDir, "build.spdx"), w.docBuild) retval = writeSPDX(os.path.join(cfg.spdxDir, "build.spdx"), w.docBuild, cfg.spdxVersion)
if not retval: if not retval:
log.err("SPDX writer failed for build document; bailing") log.err("SPDX writer failed for build document; bailing")
return False return False
# write modules document # write modules document
retval = writeSPDX(os.path.join(cfg.spdxDir, "modules-deps.spdx"), w.docModulesExtRefs) retval = writeSPDX(
os.path.join(cfg.spdxDir, "modules-deps.spdx"), w.docModulesExtRefs, cfg.spdxVersion
)
if not retval: if not retval:
log.err("SPDX writer failed for modules-deps document; bailing") log.err("SPDX writer failed for modules-deps document; bailing")
return False return False

View file

@ -0,0 +1,20 @@
# Copyright (c) 2025 The Linux Foundation
#
# SPDX-License-Identifier: Apache-2.0
from packaging.version import Version
SPDX_VERSION_2_2 = Version("2.2")
SPDX_VERSION_2_3 = Version("2.3")
SUPPORTED_SPDX_VERSIONS = [
SPDX_VERSION_2_2,
SPDX_VERSION_2_3,
]
def parse(version_str):
v = Version(version_str)
if v not in SUPPORTED_SPDX_VERSIONS:
raise ValueError(f"Unsupported SPDX version: {version_str}")
return v

View file

@ -8,6 +8,7 @@ from datetime import datetime
from west import log from west import log
from zspdx.util import getHashes from zspdx.util import getHashes
from zspdx.version import SPDX_VERSION_2_3
CPE23TYPE_REGEX = ( CPE23TYPE_REGEX = (
r'^cpe:2\.3:[aho\*\-](:(((\?*|\*?)([a-zA-Z0-9\-\._]|(\\[\\\*\?!"#$$%&\'\(\)\+,\/:;<=>@\[\]\^' r'^cpe:2\.3:[aho\*\-](:(((\?*|\*?)([a-zA-Z0-9\-\._]|(\\[\\\*\?!"#$$%&\'\(\)\+,\/:;<=>@\[\]\^'
@ -67,11 +68,12 @@ def generateDowloadUrl(url, revision):
return f'git+{url}@{revision}' return f'git+{url}@{revision}'
# Output tag-value SPDX 2.3 content for the given Package object. # Output tag-value SPDX content for the given Package object.
# Arguments: # Arguments:
# 1) f: file handle for SPDX document # 1) f: file handle for SPDX document
# 2) pkg: Package object being described # 2) pkg: Package object being described
def writePackageSPDX(f, pkg): # 3) spdx_version: SPDX specification version
def writePackageSPDX(f, pkg, spdx_version=SPDX_VERSION_2_3):
spdx_normalized_name = _normalize_spdx_name(pkg.cfg.name) spdx_normalized_name = _normalize_spdx_name(pkg.cfg.name)
spdx_normalize_spdx_id = _normalize_spdx_name(pkg.cfg.spdxID) spdx_normalize_spdx_id = _normalize_spdx_name(pkg.cfg.spdxID)
@ -85,7 +87,8 @@ PackageLicenseConcluded: {pkg.concludedLicense}
PackageCopyrightText: {pkg.cfg.copyrightText} PackageCopyrightText: {pkg.cfg.copyrightText}
""") """)
if pkg.cfg.primaryPurpose != "": # PrimaryPackagePurpose is only available in SPDX 2.3 and later
if spdx_version >= SPDX_VERSION_2_3 and pkg.cfg.primaryPurpose != "":
f.write(f"PrimaryPackagePurpose: {pkg.cfg.primaryPurpose}\n") f.write(f"PrimaryPackagePurpose: {pkg.cfg.primaryPurpose}\n")
if len(pkg.cfg.url) > 0: if len(pkg.cfg.url) > 0:
@ -142,14 +145,15 @@ LicenseName: {lic}
LicenseComment: Corresponds to the license ID `{lic}` detected in an SPDX-License-Identifier: tag. LicenseComment: Corresponds to the license ID `{lic}` detected in an SPDX-License-Identifier: tag.
""") """)
# Output tag-value SPDX 2.3 content for the given Document object. # Output tag-value SPDX content for the given Document object.
# Arguments: # Arguments:
# 1) f: file handle for SPDX document # 1) f: file handle for SPDX document
# 2) doc: Document object being described # 2) doc: Document object being described
def writeDocumentSPDX(f, doc): # 3) spdx_version: SPDX specification version
def writeDocumentSPDX(f, doc, spdx_version=SPDX_VERSION_2_3):
spdx_normalized_name = _normalize_spdx_name(doc.cfg.name) spdx_normalized_name = _normalize_spdx_name(doc.cfg.name)
f.write(f"""SPDXVersion: SPDX-2.3 f.write(f"""SPDXVersion: SPDX-{spdx_version}
DataLicense: CC0-1.0 DataLicense: CC0-1.0
SPDXID: SPDXRef-DOCUMENT SPDXID: SPDXRef-DOCUMENT
DocumentName: {spdx_normalized_name} DocumentName: {spdx_normalized_name}
@ -178,7 +182,7 @@ Created: {datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")}
# write packages # write packages
for pkg in doc.pkgs.values(): for pkg in doc.pkgs.values():
writePackageSPDX(f, pkg) writePackageSPDX(f, pkg, spdx_version)
# write other license info, if any # write other license info, if any
if len(doc.customLicenseIDs) > 0: if len(doc.customLicenseIDs) > 0:
@ -190,12 +194,13 @@ Created: {datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")}
# Arguments: # Arguments:
# 1) spdxPath: path to write SPDX document # 1) spdxPath: path to write SPDX document
# 2) doc: SPDX Document object to write # 2) doc: SPDX Document object to write
def writeSPDX(spdxPath, doc): # 3) spdx_version: SPDX specification version
def writeSPDX(spdxPath, doc, spdx_version=SPDX_VERSION_2_3):
# create and write document to disk # create and write document to disk
try: try:
log.inf(f"Writing SPDX document {doc.cfg.name} to {spdxPath}") log.inf(f"Writing SPDX {spdx_version} document {doc.cfg.name} to {spdxPath}")
with open(spdxPath, "w") as f: with open(spdxPath, "w") as f:
writeDocumentSPDX(f, doc) writeDocumentSPDX(f, doc, spdx_version)
except OSError as e: except OSError as e:
log.err(f"Error: Unable to write to {spdxPath}: {str(e)}") log.err(f"Error: Unable to write to {spdxPath}: {str(e)}")
return False return False