Skip to content

west: spdx: allow to generate for different SPDX versions #90753

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .ruff-excludes.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1117,10 +1117,6 @@
"UP008", # https://docs.astral.sh/ruff/rules/super-call-with-parameters
"UP032", # https://docs.astral.sh/ruff/rules/f-string
]
"./scripts/west_commands/spdx.py" = [
"F541", # https://docs.astral.sh/ruff/rules/f-string-missing-placeholders
"I001", # https://docs.astral.sh/ruff/rules/unsorted-imports
]
"./scripts/west_commands/tests/conftest.py" = [
"I001", # https://docs.astral.sh/ruff/rules/unsorted-imports
]
Expand Down
12 changes: 11 additions & 1 deletion doc/develop/west/zephyr-cmds.rst
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ See :zephyr_file:`share/zephyr-package/cmake` for details.
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.
``SPDX-License-Identifier`` comments in source files are scanned and filled
into the SPDX documents.
Expand Down Expand Up @@ -105,6 +105,12 @@ To use this command:

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::

When building with :ref:`sysbuild`, make sure you target the actual application
Expand Down Expand Up @@ -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
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
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.
Expand Down
27 changes: 18 additions & 9 deletions scripts/west_commands/spdx.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
import uuid

from west.commands import WestCommand

from zspdx.sbom import SBOMConfig, makeSPDX, setupCmakeQuery
from zspdx.version import SPDX_VERSION_2_3, SUPPORTED_SPDX_VERSIONS, parse

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.

Prior to the build, an empty file must be created at
Expand Down Expand Up @@ -41,6 +41,9 @@ def do_add_parser(self, parser_adder):
help="namespace prefix")
parser.add_argument('-s', '--spdx-dir',
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",
help="also analyze included header files")
parser.add_argument('--include-sdk', action="store_true",
Expand All @@ -49,14 +52,15 @@ def do_add_parser(self, parser_adder):
return parser

def do_run(self, args, unknown_args):
self.dbg(f"running zephyr SPDX generator")
self.dbg("running zephyr SPDX generator")

self.dbg(f" --init is", args.init)
self.dbg(f" --build-dir is", args.build_dir)
self.dbg(f" --namespace-prefix is", args.namespace_prefix)
self.dbg(f" --spdx-dir is", args.spdx_dir)
self.dbg(f" --analyze-includes is", args.analyze_includes)
self.dbg(f" --include-sdk is", args.include_sdk)
self.dbg(" --init is", args.init)
self.dbg(" --build-dir is", args.build_dir)
self.dbg(" --namespace-prefix is", args.namespace_prefix)
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(" --include-sdk is", args.include_sdk)
Comment on lines +57 to +63
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could've used the f-strings here:
self.dbg(" --init is {args.init}")


if args.init:
self.do_run_init(args)
Expand Down Expand Up @@ -85,6 +89,11 @@ def do_run_spdx(self, args):
# create the SPDX files
cfg = SBOMConfig()
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:
cfg.namespacePrefix = args.namespace_prefix
else:
Expand Down
16 changes: 11 additions & 5 deletions scripts/west_commands/zspdx/sbom.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from west import log

from zspdx.scanner import ScannerConfig, scanDocument
from zspdx.version import SPDX_VERSION_2_3
from zspdx.walker import Walker, WalkerConfig
from zspdx.writer import writeSPDX

Expand All @@ -26,6 +27,9 @@ def __init__(self):
# location of SPDX document output directory
self.spdxDir = ""

# SPDX specification version to use
self.spdxVersion = SPDX_VERSION_2_3

# should also analyze for included header files?
self.analyzeIncludes = False

Expand Down Expand Up @@ -101,31 +105,33 @@ def makeSPDX(cfg):

# write SDK document, if we made one
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:
log.err("SPDX writer failed for SDK document; bailing")
return False

# 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:
log.err("SPDX writer failed for app document; bailing")
return False

# write zephyr document
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:
log.err("SPDX writer failed for zephyr document; bailing")
return False

# write build document
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:
log.err("SPDX writer failed for build document; bailing")
return False

# write modules document
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:
log.err("SPDX writer failed for modules-deps document; bailing")
return False
Expand Down
20 changes: 20 additions & 0 deletions scripts/west_commands/zspdx/version.py
Original file line number Diff line number Diff line change
@@ -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
25 changes: 15 additions & 10 deletions scripts/west_commands/zspdx/writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from west import log

from zspdx.util import getHashes
from zspdx.version import SPDX_VERSION_2_3

CPE23TYPE_REGEX = (
r'^cpe:2\.3:[aho\*\-](:(((\?*|\*?)([a-zA-Z0-9\-\._]|(\\[\\\*\?!"#$$%&\'\(\)\+,\/:;<=>@\[\]\^'
Expand Down Expand Up @@ -67,11 +68,12 @@ def generateDowloadUrl(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:
# 1) f: file handle for SPDX document
# 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_normalize_spdx_id = _normalize_spdx_name(pkg.cfg.spdxID)

Expand All @@ -85,7 +87,8 @@ def writePackageSPDX(f, pkg):
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")

if len(pkg.cfg.url) > 0:
Expand Down Expand Up @@ -142,14 +145,15 @@ def writeOtherLicenseSPDX(f, lic):
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:
# 1) f: file handle for SPDX document
# 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)

f.write(f"""SPDXVersion: SPDX-2.3
f.write(f"""SPDXVersion: SPDX-{spdx_version}
DataLicense: CC0-1.0
SPDXID: SPDXRef-DOCUMENT
DocumentName: {spdx_normalized_name}
Expand Down Expand Up @@ -178,7 +182,7 @@ def writeDocumentSPDX(f, doc):

# write packages
for pkg in doc.pkgs.values():
writePackageSPDX(f, pkg)
writePackageSPDX(f, pkg, spdx_version)

# write other license info, if any
if len(doc.customLicenseIDs) > 0:
Expand All @@ -190,12 +194,13 @@ def writeDocumentSPDX(f, doc):
# Arguments:
# 1) spdxPath: path to write SPDX document
# 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
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:
writeDocumentSPDX(f, doc)
writeDocumentSPDX(f, doc, spdx_version)
except OSError as e:
log.err(f"Error: Unable to write to {spdxPath}: {str(e)}")
return False
Expand Down
Loading