diff --git a/.ruff-excludes.toml b/.ruff-excludes.toml index e7d51e2294f09..4ea5ee5bd666b 100644 --- a/.ruff-excludes.toml +++ b/.ruff-excludes.toml @@ -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 ] diff --git a/doc/develop/west/zephyr-cmds.rst b/doc/develop/west/zephyr-cmds.rst index e40093c3dd16d..100e05f9044e4 100644 --- a/doc/develop/west/zephyr-cmds.rst +++ b/doc/develop/west/zephyr-cmds.rst @@ -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. @@ -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 @@ -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. diff --git a/scripts/west_commands/spdx.py b/scripts/west_commands/spdx.py index d4d366480ac84..d28c99af1820b 100644 --- a/scripts/west_commands/spdx.py +++ b/scripts/west_commands/spdx.py @@ -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 @@ -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", @@ -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) if args.init: self.do_run_init(args) @@ -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: diff --git a/scripts/west_commands/zspdx/sbom.py b/scripts/west_commands/zspdx/sbom.py index 459210eba8308..9728dc5301801 100644 --- a/scripts/west_commands/zspdx/sbom.py +++ b/scripts/west_commands/zspdx/sbom.py @@ -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 @@ -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 @@ -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 diff --git a/scripts/west_commands/zspdx/version.py b/scripts/west_commands/zspdx/version.py new file mode 100644 index 0000000000000..11088dc29fa30 --- /dev/null +++ b/scripts/west_commands/zspdx/version.py @@ -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 diff --git a/scripts/west_commands/zspdx/writer.py b/scripts/west_commands/zspdx/writer.py index 8223d0ec1eb92..ac1562619ee86 100644 --- a/scripts/west_commands/zspdx/writer.py +++ b/scripts/west_commands/zspdx/writer.py @@ -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\-\._]|(\\[\\\*\?!"#$$%&\'\(\)\+,\/:;<=>@\[\]\^' @@ -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) @@ -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: @@ -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} @@ -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: @@ -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