zephyr/scripts/west_commands/zspdx/sbom.py
Steve Winslow fd31b9b4ac west: spdx: Generate SPDX 2.2 tag-value documents
This adds support to generate SPDX 2.2 tag-value documents via the
new west spdx command. The CMake file-based APIs are leveraged to
create 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.

Before `west build` is run, a specific file must be created in the
build directory so that the CMake API reply will run. This can be
done by running:

    west spdx --init -d BUILD_DIR

After `west build` is run, SPDX generation is then activated by
calling `west spdx`; currently this requires passing the build
directory as a parameter again:

    west spdx -d BUILD_DIR

This will generate three SPDX documents in `BUILD_DIR/spdx/`:

1) `app.spdx`: This contains the bill-of-materials for the
application source files used for the build.

2) `zephyr.spdx`: This contains the bill-of-materials for the
specific Zephyr source code files that are used for the build.

3) `build.spdx`: This contains the bill-of-materials for the built
output files.

Each file in the bill-of-materials is scanned, so that its hashes
(SHA256 and SHA1) can be recorded, along with any detected licenses
if an `SPDX-License-Identifier` appears in the file.

SPDX Relationships are created to indicate dependencies between
CMake build targets; build targets that are linked together; and
source files that are compiled to generate the built library files.

`west spdx` can be called with optional parameters for further
configuration:

* `-n PREFIX`: specifies a prefix for the Document Namespaces that
will be included in the generated SPDX documents. See SPDX spec 2.2
section 2.5 at
https://spdx.github.io/spdx-spec/2-document-creation-information/.
If -n is omitted, a default namespace will be generated according
to the default format described in section 2.5 using a random UUID.

* `-s SPDX_DIR`: specifies an alternate directory where the SPDX
documents should be written. If not specified, they will be saved
in `BUILD_DIR/spdx/`.

* `--analyze-includes`: in addition to recording the compiled
source code files (e.g. `.c`, `.S`) in the bills-of-materials, if
this flag is specified, `west spdx` will attempt to determine the
specific header files that are included for each `.c` file. This
will take longer, as it performs a dry run using the C compiler
for each `.c` file (using the same arguments that were passed to it
for the actual build).

* `--include-sdk`: if `--analyze-includes` is used, then adding
`--include-sdk` will create a fourth SPDX document, `sdk.spdx`,
which will list any header files included from the SDK.

Signed-off-by: Steve Winslow <steve@swinslow.net>
2021-05-05 11:14:06 -04:00

124 lines
4.0 KiB
Python

# Copyright (c) 2020, 2021 The Linux Foundation
#
# SPDX-License-Identifier: Apache-2.0
import os
from west import log
from zspdx.walker import WalkerConfig, Walker
from zspdx.scanner import ScannerConfig, scanDocument
from zspdx.writer import writeSPDX
# SBOMConfig contains settings that will be passed along to the various
# SBOM maker subcomponents.
class SBOMConfig:
def __init__(self):
super(SBOMConfig, self).__init__()
# prefix for Document namespaces; should not end with "/"
self.namespacePrefix = ""
# location of build directory
self.buildDir = ""
# location of SPDX document output directory
self.spdxDir = ""
# should also analyze for included header files?
self.analyzeIncludes = False
# should also add an SPDX document for the SDK?
self.includeSDK = False
# create Cmake file-based API directories and query file
# Arguments:
# 1) build_dir: build directory
def setupCmakeQuery(build_dir):
# check that query dir exists as a directory, or else create it
cmakeApiDirPath = os.path.join(build_dir, ".cmake", "api", "v1", "query")
if os.path.exists(cmakeApiDirPath):
if not os.path.isdir(cmakeApiDirPath):
log.err(f'cmake api query directory {cmakeApiDirPath} exists and is not a directory')
return False
# directory exists, we're good
else:
# create the directory
os.makedirs(cmakeApiDirPath, exist_ok=False)
# check that codemodel-v2 exists as a file, or else create it
queryFilePath = os.path.join(cmakeApiDirPath, "codemodel-v2")
if os.path.exists(queryFilePath):
if not os.path.isfile(queryFilePath):
log.err(f'cmake api query file {queryFilePath} exists and is not a directory')
return False
# file exists, we're good
return True
else:
# file doesn't exist, let's create it
os.mknod(queryFilePath)
return True
# main entry point for SBOM maker
# Arguments:
# 1) cfg: SBOMConfig
def makeSPDX(cfg):
# report any odd configuration settings
if cfg.analyzeIncludes and not cfg.includeSDK:
log.wrn(f"config: requested to analyze includes but not to generate SDK SPDX document;")
log.wrn(f"config: will proceed but will discard detected includes for SDK header files")
# set up walker configuration
walkerCfg = WalkerConfig()
walkerCfg.namespacePrefix = cfg.namespacePrefix
walkerCfg.buildDir = cfg.buildDir
walkerCfg.analyzeIncludes = cfg.analyzeIncludes
walkerCfg.includeSDK = cfg.includeSDK
# make and run the walker
w = Walker(walkerCfg)
retval = w.makeDocuments()
if not retval:
log.err("SPDX walker failed; bailing")
return False
# set up scanner configuration
scannerCfg = ScannerConfig()
# scan each document from walker
if cfg.includeSDK:
scanDocument(scannerCfg, w.docSDK)
scanDocument(scannerCfg, w.docApp)
scanDocument(scannerCfg, w.docZephyr)
scanDocument(scannerCfg, w.docBuild)
# write each document, in this particular order so that the
# hashes for external references are calculated
# write SDK document, if we made one
if cfg.includeSDK:
retval = writeSPDX(os.path.join(cfg.spdxDir, "sdk.spdx"), w.docSDK)
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)
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)
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)
if not retval:
log.err("SPDX writer failed for build document; bailing")
return False
return True