Skip to content

Commit bba9d63

Browse files
committed
west: spdx: allow to generate for different SPDX version
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>
1 parent 3641c72 commit bba9d63

File tree

6 files changed

+82
-25
lines changed

6 files changed

+82
-25
lines changed

doc/develop/west/zephyr-cmds.rst

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ See :zephyr_file:`share/zephyr-package/cmake` for details.
7575
Software bill of materials: ``west spdx``
7676
*****************************************
7777

78-
This command generates SPDX 2.3 tag-value documents, creating relationships
78+
This command generates SPDX 2.2 or 2.3 tag-value documents, creating relationships
7979
from source files to the corresponding generated build files.
8080
``SPDX-License-Identifier`` comments in source files are scanned and filled
8181
into the SPDX documents.
@@ -105,6 +105,12 @@ To use this command:
105105
106106
west spdx -d BUILD_DIR
107107
108+
By default, this generates SPDX 2.3 documents. To generate SPDX 2.2 documents instead:
109+
110+
.. code-block:: bash
111+
112+
west spdx -d BUILD_DIR --spdx-version 2.2
113+
108114
.. note::
109115

110116
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.
144150
- ``-s SPDX_DIR``: specifies an alternate directory where the SPDX documents
145151
should be written instead of :file:`BUILD_DIR/spdx/`.
146152

153+
- ``--spdx-version {2.2,2.3}``: specifies which SPDX specification version to use.
154+
Defaults to ``2.3``. SPDX 2.3 includes additional fields like ``PrimaryPackagePurpose``
155+
that are not available in SPDX 2.2.
156+
147157
- ``--analyze-includes``: in addition to recording the compiled source code
148158
files (e.g. ``.c``, ``.S``) in the bills-of-materials, also attempt to
149159
determine the specific header files that are included for each ``.c`` file.

scripts/west_commands/spdx.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
import uuid
77

88
from west.commands import WestCommand
9-
9+
from zspdx.version import parse
1010
from zspdx.sbom import SBOMConfig, makeSPDX, setupCmakeQuery
1111

1212
SPDX_DESCRIPTION = """\
13-
This command creates an SPDX 2.2 tag-value bill of materials
13+
This command creates an SPDX 2.2 or 2.3 tag-value bill of materials
1414
following the completion of a Zephyr build.
1515
1616
Prior to the build, an empty file must be created at
@@ -41,6 +41,8 @@ def do_add_parser(self, parser_adder):
4141
help="namespace prefix")
4242
parser.add_argument('-s', '--spdx-dir',
4343
help="SPDX output directory")
44+
parser.add_argument('--spdx-version', choices=['2.2', '2.3'], default='2.3',
45+
help="SPDX specification version to use (default: 2.3)")
4446
parser.add_argument('--analyze-includes', action="store_true",
4547
help="also analyze included header files")
4648
parser.add_argument('--include-sdk', action="store_true",
@@ -55,6 +57,7 @@ def do_run(self, args, unknown_args):
5557
self.dbg(f" --build-dir is", args.build_dir)
5658
self.dbg(f" --namespace-prefix is", args.namespace_prefix)
5759
self.dbg(f" --spdx-dir is", args.spdx_dir)
60+
self.dbg(f" --spdx-version is", args.spdx_version)
5861
self.dbg(f" --analyze-includes is", args.analyze_includes)
5962
self.dbg(f" --include-sdk is", args.include_sdk)
6063

@@ -85,6 +88,11 @@ def do_run_spdx(self, args):
8588
# create the SPDX files
8689
cfg = SBOMConfig()
8790
cfg.buildDir = args.build_dir
91+
try:
92+
version_obj = parse(args.spdx_version)
93+
except Exception:
94+
self.die(f"Invalid SPDX version: {args.spdx_version}")
95+
cfg.spdxVersion = version_obj
8896
if args.namespace_prefix:
8997
cfg.namespacePrefix = args.namespace_prefix
9098
else:

scripts/west_commands/zspdx/sbom.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from zspdx.walker import WalkerConfig, Walker
1010
from zspdx.scanner import ScannerConfig, scanDocument
1111
from zspdx.writer import writeSPDX
12+
from zspdx.version import SPDX_VERSION_2_3
1213

1314

1415
# SBOMConfig contains settings that will be passed along to the various
@@ -26,6 +27,9 @@ def __init__(self):
2627
# location of SPDX document output directory
2728
self.spdxDir = ""
2829

30+
# SPDX specification version to use
31+
self.spdxVersion = SPDX_VERSION_2_3
32+
2933
# should also analyze for included header files?
3034
self.analyzeIncludes = False
3135

@@ -76,6 +80,7 @@ def makeSPDX(cfg):
7680
walkerCfg = WalkerConfig()
7781
walkerCfg.namespacePrefix = cfg.namespacePrefix
7882
walkerCfg.buildDir = cfg.buildDir
83+
walkerCfg.spdxVersion = cfg.spdxVersion
7984
walkerCfg.analyzeIncludes = cfg.analyzeIncludes
8085
walkerCfg.includeSDK = cfg.includeSDK
8186

@@ -101,31 +106,33 @@ def makeSPDX(cfg):
101106

102107
# write SDK document, if we made one
103108
if cfg.includeSDK:
104-
retval = writeSPDX(os.path.join(cfg.spdxDir, "sdk.spdx"), w.docSDK)
109+
retval = writeSPDX(os.path.join(cfg.spdxDir, "sdk.spdx"), w.docSDK, cfg.spdxVersion)
105110
if not retval:
106111
log.err("SPDX writer failed for SDK document; bailing")
107112
return False
108113

109114
# write app document
110-
retval = writeSPDX(os.path.join(cfg.spdxDir, "app.spdx"), w.docApp)
115+
retval = writeSPDX(os.path.join(cfg.spdxDir, "app.spdx"), w.docApp, cfg.spdxVersion)
111116
if not retval:
112117
log.err("SPDX writer failed for app document; bailing")
113118
return False
114119

115120
# write zephyr document
116-
retval = writeSPDX(os.path.join(cfg.spdxDir, "zephyr.spdx"), w.docZephyr)
121+
retval = writeSPDX(os.path.join(cfg.spdxDir, "zephyr.spdx"), w.docZephyr, cfg.spdxVersion)
117122
if not retval:
118123
log.err("SPDX writer failed for zephyr document; bailing")
119124
return False
120125

121126
# write build document
122-
retval = writeSPDX(os.path.join(cfg.spdxDir, "build.spdx"), w.docBuild)
127+
retval = writeSPDX(os.path.join(cfg.spdxDir, "build.spdx"), w.docBuild, cfg.spdxVersion)
123128
if not retval:
124129
log.err("SPDX writer failed for build document; bailing")
125130
return False
126131

127132
# write modules document
128-
retval = writeSPDX(os.path.join(cfg.spdxDir, "modules-deps.spdx"), w.docModulesExtRefs)
133+
retval = writeSPDX(
134+
os.path.join(cfg.spdxDir, "modules-deps.spdx"), w.docModulesExtRefs, cfg.spdxVersion
135+
)
129136
if not retval:
130137
log.err("SPDX writer failed for modules-deps document; bailing")
131138
return False
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Copyright (c) 2025 The Linux Foundation
2+
#
3+
# SPDX-License-Identifier: Apache-2.0
4+
5+
from packaging.version import Version
6+
7+
SPDX_VERSION_2_2 = Version("2.2")
8+
SPDX_VERSION_2_3 = Version("2.3")
9+
10+
SUPPORTED_SPDX_VERSIONS = [
11+
SPDX_VERSION_2_2,
12+
SPDX_VERSION_2_3,
13+
]
14+
15+
def parse(version_str):
16+
v = Version(version_str)
17+
if v not in SUPPORTED_SPDX_VERSIONS:
18+
raise ValueError(f"Unsupported SPDX version: {version_str}")
19+
return v

scripts/west_commands/zspdx/walker.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from zspdx.datatypes import DocumentConfig, Document, File, PackageConfig, Package, RelationshipDataElementType, RelationshipData, Relationship
1515
from zspdx.getincludes import getCIncludes
1616
import zspdx.spdxids
17+
from zspdx.version import SPDX_VERSION_2_3
1718

1819
# WalkerConfig contains configuration data for the Walker.
1920
class WalkerConfig:
@@ -26,6 +27,9 @@ def __init__(self):
2627
# location of build directory
2728
self.buildDir = ""
2829

30+
# SPDX specification version to use (e.g., "2.2" or "2.3")
31+
self.spdxVersion = "2.3"
32+
2933
# should also analyze for included header files?
3034
self.analyzeIncludes = False
3135

@@ -189,7 +193,9 @@ def setupAppDocument(self):
189193
cfgPackageApp = PackageConfig()
190194
cfgPackageApp.name = "app-sources"
191195
cfgPackageApp.spdxID = "SPDXRef-app-sources"
192-
cfgPackageApp.primaryPurpose = "SOURCE"
196+
# PrimaryPackagePurpose is only available in SPDX 2.3 and later
197+
if self.cfg.spdxVersion == SPDX_VERSION_2_3:
198+
cfgPackageApp.primaryPurpose = "SOURCE"
193199
# relativeBaseDir is app sources dir
194200
cfgPackageApp.relativeBaseDir = self.cm.paths_source
195201
pkgApp = Package(cfgPackageApp, self.docApp)
@@ -250,7 +256,7 @@ def setupZephyrDocument(self, zephyr, modules):
250256
purl = None
251257
zephyr_tags = zephyr.get("tags", "")
252258
if zephyr_tags:
253-
# Find tag vX.Y.Z
259+
# Find tag vX.Y.Z
254260
for tag in zephyr_tags:
255261
version = re.fullmatch(r'^v(?P<version>\d+\.\d+\.\d+)$', tag)
256262
purl = self._build_purl(zephyr_url, tag)
@@ -286,7 +292,8 @@ def setupZephyrDocument(self, zephyr, modules):
286292
cfgPackageZephyrModule.name = module_name + "-sources"
287293
cfgPackageZephyrModule.spdxID = "SPDXRef-" + module_name + "-sources"
288294
cfgPackageZephyrModule.relativeBaseDir = module_path
289-
cfgPackageZephyrModule.primaryPurpose = "SOURCE"
295+
if self.cfg.spdxVersion == SPDX_VERSION_2_3:
296+
cfgPackageZephyrModule.primaryPurpose = "SOURCE"
290297

291298
if module_revision:
292299
cfgPackageZephyrModule.revision = module_revision
@@ -401,10 +408,11 @@ def walkTargets(self):
401408
if len(cfgTarget.target.artifacts) > 0:
402409
# add its build file
403410
bf = self.addBuildFile(cfgTarget, pkg)
404-
if pkg.cfg.name == "zephyr_final":
405-
pkg.cfg.primaryPurpose = "APPLICATION"
406-
else:
407-
pkg.cfg.primaryPurpose = "LIBRARY"
411+
if self.cfg.spdxVersion == SPDX_VERSION_2_3:
412+
if pkg.cfg.name == "zephyr_final":
413+
pkg.cfg.primaryPurpose = "APPLICATION"
414+
else:
415+
pkg.cfg.primaryPurpose = "LIBRARY"
408416

409417
# get its source files if build file is found
410418
if bf:

scripts/west_commands/zspdx/writer.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
# SPDX-License-Identifier: Apache-2.0
44

55
from datetime import datetime
6+
from zspdx.version import SPDX_VERSION_2_3
67

78
from west import log
89

@@ -65,11 +66,12 @@ def generateDowloadUrl(url, revision):
6566

6667
return f'git+{url}@{revision}'
6768

68-
# Output tag-value SPDX 2.3 content for the given Package object.
69+
# Output tag-value SPDX content for the given Package object.
6970
# Arguments:
7071
# 1) f: file handle for SPDX document
7172
# 2) pkg: Package object being described
72-
def writePackageSPDX(f, pkg):
73+
# 3) spdx_version: SPDX specification version
74+
def writePackageSPDX(f, pkg, spdx_version=SPDX_VERSION_2_3):
7375
spdx_normalized_name = _normalize_spdx_name(pkg.cfg.name)
7476
spdx_normalize_spdx_id = _normalize_spdx_name(pkg.cfg.spdxID)
7577

@@ -83,7 +85,8 @@ def writePackageSPDX(f, pkg):
8385
PackageCopyrightText: {pkg.cfg.copyrightText}
8486
""")
8587

86-
if pkg.cfg.primaryPurpose != "":
88+
# PrimaryPackagePurpose is only available in SPDX 2.3 and later
89+
if spdx_version >= SPDX_VERSION_2_3 and pkg.cfg.primaryPurpose != "":
8790
f.write(f"PrimaryPackagePurpose: {pkg.cfg.primaryPurpose}\n")
8891

8992
if len(pkg.cfg.url) > 0:
@@ -140,14 +143,15 @@ def writeOtherLicenseSPDX(f, lic):
140143
LicenseComment: Corresponds to the license ID `{lic}` detected in an SPDX-License-Identifier: tag.
141144
""")
142145

143-
# Output tag-value SPDX 2.3 content for the given Document object.
146+
# Output tag-value SPDX content for the given Document object.
144147
# Arguments:
145148
# 1) f: file handle for SPDX document
146149
# 2) doc: Document object being described
147-
def writeDocumentSPDX(f, doc):
150+
# 3) spdx_version: SPDX specification version
151+
def writeDocumentSPDX(f, doc, spdx_version=SPDX_VERSION_2_3):
148152
spdx_normalized_name = _normalize_spdx_name(doc.cfg.name)
149153

150-
f.write(f"""SPDXVersion: SPDX-2.3
154+
f.write(f"""SPDXVersion: SPDX-{spdx_version}
151155
DataLicense: CC0-1.0
152156
SPDXID: SPDXRef-DOCUMENT
153157
DocumentName: {spdx_normalized_name}
@@ -173,7 +177,7 @@ def writeDocumentSPDX(f, doc):
173177

174178
# write packages
175179
for pkg in doc.pkgs.values():
176-
writePackageSPDX(f, pkg)
180+
writePackageSPDX(f, pkg, spdx_version)
177181

178182
# write other license info, if any
179183
if len(doc.customLicenseIDs) > 0:
@@ -185,12 +189,13 @@ def writeDocumentSPDX(f, doc):
185189
# Arguments:
186190
# 1) spdxPath: path to write SPDX document
187191
# 2) doc: SPDX Document object to write
188-
def writeSPDX(spdxPath, doc):
192+
# 3) spdx_version: SPDX specification version
193+
def writeSPDX(spdxPath, doc, spdx_version=SPDX_VERSION_2_3):
189194
# create and write document to disk
190195
try:
191-
log.inf(f"Writing SPDX document {doc.cfg.name} to {spdxPath}")
196+
log.inf(f"Writing SPDX {spdx_version} document {doc.cfg.name} to {spdxPath}")
192197
with open(spdxPath, "w") as f:
193-
writeDocumentSPDX(f, doc)
198+
writeDocumentSPDX(f, doc, spdx_version)
194199
except OSError as e:
195200
log.err(f"Error: Unable to write to {spdxPath}: {str(e)}")
196201
return False

0 commit comments

Comments
 (0)