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>
This commit is contained in:
parent
167e83df49
commit
fd31b9b4ac
16 changed files with 2946 additions and 0 deletions
|
@ -1506,6 +1506,7 @@ West:
|
||||||
- mbolivar-nordic
|
- mbolivar-nordic
|
||||||
collaborators:
|
collaborators:
|
||||||
- carlescufi
|
- carlescufi
|
||||||
|
- swinslow
|
||||||
files:
|
files:
|
||||||
- scripts/west-commands.yml
|
- scripts/west-commands.yml
|
||||||
- scripts/west_commands/
|
- scripts/west_commands/
|
||||||
|
|
|
@ -41,3 +41,8 @@ west-commands:
|
||||||
- name: zephyr-export
|
- name: zephyr-export
|
||||||
class: ZephyrExport
|
class: ZephyrExport
|
||||||
help: export Zephyr installation as a CMake config package
|
help: export Zephyr installation as a CMake config package
|
||||||
|
- file: scripts/west_commands/spdx.py
|
||||||
|
commands:
|
||||||
|
- name: spdx
|
||||||
|
class: ZephyrSpdx
|
||||||
|
help: create SPDX bill of materials
|
||||||
|
|
112
scripts/west_commands/spdx.py
Normal file
112
scripts/west_commands/spdx.py
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
# Copyright (c) 2021 The Linux Foundation
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import os
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
from west.commands import WestCommand
|
||||||
|
from west import log
|
||||||
|
|
||||||
|
from zspdx.sbom import SBOMConfig, makeSPDX, setupCmakeQuery
|
||||||
|
|
||||||
|
SPDX_DESCRIPTION = """\
|
||||||
|
This command creates an SPDX 2.2 tag-value bill of materials
|
||||||
|
following the completion of a Zephyr build.
|
||||||
|
|
||||||
|
Prior to the build, an empty file must be created at
|
||||||
|
BUILDDIR/.cmake/api/v1/query/codemodel-v2 in order to enable
|
||||||
|
the CMake file-based API, which the SPDX command relies upon.
|
||||||
|
This can be done by calling `west spdx --init` prior to
|
||||||
|
calling `west build`."""
|
||||||
|
|
||||||
|
class ZephyrSpdx(WestCommand):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
'spdx',
|
||||||
|
'create SPDX bill of materials',
|
||||||
|
SPDX_DESCRIPTION)
|
||||||
|
|
||||||
|
def do_add_parser(self, parser_adder):
|
||||||
|
parser = parser_adder.add_parser(self.name,
|
||||||
|
help=self.help,
|
||||||
|
description = self.description)
|
||||||
|
|
||||||
|
parser.add_argument('-i', '--init', action="store_true",
|
||||||
|
help="initialize CMake file-based API")
|
||||||
|
parser.add_argument('-d', '--build-dir',
|
||||||
|
help="build directory")
|
||||||
|
parser.add_argument('-n', '--namespace-prefix',
|
||||||
|
help="namespace prefix")
|
||||||
|
parser.add_argument('-s', '--spdx-dir',
|
||||||
|
help="SPDX output directory")
|
||||||
|
parser.add_argument('--analyze-includes', action="store_true",
|
||||||
|
help="also analyze included header files")
|
||||||
|
parser.add_argument('--include-sdk', action="store_true",
|
||||||
|
help="also generate SPDX document for SDK")
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
def do_run(self, args, unknown_args):
|
||||||
|
log.dbg(f"running zephyr SPDX generator")
|
||||||
|
|
||||||
|
log.dbg(f" --init is", args.init)
|
||||||
|
log.dbg(f" --build-dir is", args.build_dir)
|
||||||
|
log.dbg(f" --namespace-prefix is", args.namespace_prefix)
|
||||||
|
log.dbg(f" --spdx-dir is", args.spdx_dir)
|
||||||
|
log.dbg(f" --analyze-includes is", args.analyze_includes)
|
||||||
|
log.dbg(f" --include-sdk is", args.include_sdk)
|
||||||
|
|
||||||
|
if args.init:
|
||||||
|
do_run_init(args)
|
||||||
|
else:
|
||||||
|
do_run_spdx(args)
|
||||||
|
|
||||||
|
def do_run_init(args):
|
||||||
|
log.inf("initializing Cmake file-based API prior to build")
|
||||||
|
|
||||||
|
if not args.build_dir:
|
||||||
|
log.die("Build directory not specified; call `west spdx --init --build-dir=BUILD_DIR`")
|
||||||
|
|
||||||
|
# initialize CMake file-based API - empty query file
|
||||||
|
query_ready = setupCmakeQuery(args.build_dir)
|
||||||
|
if query_ready:
|
||||||
|
log.inf("initialized; run `west build` then run `west spdx`")
|
||||||
|
else:
|
||||||
|
log.err("Couldn't create Cmake file-based API query directory")
|
||||||
|
log.err("You can manually create an empty file at $BUILDDIR/.cmake/api/v1/query/codemodel-v2")
|
||||||
|
|
||||||
|
def do_run_spdx(args):
|
||||||
|
if not args.build_dir:
|
||||||
|
log.die("Build directory not specified; call `west spdx --build-dir=BUILD_DIR`")
|
||||||
|
|
||||||
|
# create the SPDX files
|
||||||
|
cfg = SBOMConfig()
|
||||||
|
cfg.buildDir = args.build_dir
|
||||||
|
if args.namespace_prefix:
|
||||||
|
cfg.namespacePrefix = args.namespace_prefix
|
||||||
|
else:
|
||||||
|
# create default namespace according to SPDX spec
|
||||||
|
# note that this is intentionally _not_ an actual URL where
|
||||||
|
# this document will be stored
|
||||||
|
cfg.namespacePrefix = f"http://spdx.org/spdxdocs/zephyr-{str(uuid.uuid4())}"
|
||||||
|
if args.spdx_dir:
|
||||||
|
cfg.spdxDir = args.spdx_dir
|
||||||
|
else:
|
||||||
|
cfg.spdxDir = os.path.join(args.build_dir, "spdx")
|
||||||
|
if args.analyze_includes:
|
||||||
|
cfg.analyzeIncludes = True
|
||||||
|
if args.include_sdk:
|
||||||
|
cfg.includeSDK = True
|
||||||
|
|
||||||
|
# make sure SPDX directory exists, or create it if it doesn't
|
||||||
|
if os.path.exists(cfg.spdxDir):
|
||||||
|
if not os.path.isdir(cfg.spdxDir):
|
||||||
|
log.err(f'SPDX output directory {cfg.spdxDir} exists but is not a directory')
|
||||||
|
return
|
||||||
|
# directory exists, we're good
|
||||||
|
else:
|
||||||
|
# create the directory
|
||||||
|
os.makedirs(cfg.spdxDir, exist_ok=False)
|
||||||
|
|
||||||
|
makeSPDX(cfg)
|
3
scripts/west_commands/zspdx/__init__.py
Normal file
3
scripts/west_commands/zspdx/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Copyright (c) 2020 The Linux Foundation
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
38
scripts/west_commands/zspdx/cmakecache.py
Normal file
38
scripts/west_commands/zspdx/cmakecache.py
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# Copyright (c) 2021 The Linux Foundation
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from west import log
|
||||||
|
|
||||||
|
# Parse a CMakeCache file and return a dict of key:value (discarding
|
||||||
|
# type hints).
|
||||||
|
def parseCMakeCacheFile(filePath):
|
||||||
|
log.dbg(f"parsing CMake cache file at {filePath}")
|
||||||
|
kv = {}
|
||||||
|
try:
|
||||||
|
with open(filePath, "r") as f:
|
||||||
|
# should be a short file, so we'll use readlines
|
||||||
|
lines = f.readlines()
|
||||||
|
|
||||||
|
# walk through and look for non-comment, non-empty lines
|
||||||
|
for line in lines:
|
||||||
|
sline = line.strip()
|
||||||
|
if sline == "":
|
||||||
|
continue
|
||||||
|
if sline.startswith("#") or sline.startswith("//"):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# parse out : and = characters
|
||||||
|
pline1 = sline.split(":", maxsplit=1)
|
||||||
|
if len(pline1) != 2:
|
||||||
|
continue
|
||||||
|
pline2 = pline1[1].split("=", maxsplit=1)
|
||||||
|
if len(pline2) != 2:
|
||||||
|
continue
|
||||||
|
kv[pline1[0]] = pline2[1]
|
||||||
|
|
||||||
|
return kv
|
||||||
|
|
||||||
|
except OSError as e:
|
||||||
|
log.err(f"Error loading {filePath}: {str(e)}")
|
||||||
|
return {}
|
306
scripts/west_commands/zspdx/cmakefileapi.py
Normal file
306
scripts/west_commands/zspdx/cmakefileapi.py
Normal file
|
@ -0,0 +1,306 @@
|
||||||
|
# Copyright (c) 2020 The Linux Foundation
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
class Codemodel:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(Codemodel, self).__init__()
|
||||||
|
|
||||||
|
self.paths_source = ""
|
||||||
|
self.paths_build = ""
|
||||||
|
self.configurations = []
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"Codemodel: source {self.paths_source}, build {self.paths_build}"
|
||||||
|
|
||||||
|
# A member of the codemodel configurations array
|
||||||
|
class Config:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(Config, self).__init__()
|
||||||
|
|
||||||
|
self.name = ""
|
||||||
|
self.directories = []
|
||||||
|
self.projects = []
|
||||||
|
self.configTargets = []
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
if self.name == "":
|
||||||
|
return f"Config: [no name]"
|
||||||
|
else:
|
||||||
|
return f"Config: {self.name}"
|
||||||
|
|
||||||
|
# A member of the configuration.directories array
|
||||||
|
class ConfigDir:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(ConfigDir, self).__init__()
|
||||||
|
|
||||||
|
self.source = ""
|
||||||
|
self.build = ""
|
||||||
|
self.parentIndex = -1
|
||||||
|
self.childIndexes = []
|
||||||
|
self.projectIndex = -1
|
||||||
|
self.targetIndexes = []
|
||||||
|
self.minimumCMakeVersion = ""
|
||||||
|
self.hasInstallRule = False
|
||||||
|
|
||||||
|
# actual items, calculated from indices after loading
|
||||||
|
self.parent = None
|
||||||
|
self.children = []
|
||||||
|
self.project = None
|
||||||
|
self.targets = []
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"ConfigDir: source {self.source}, build {self.build}"
|
||||||
|
|
||||||
|
# A member of the configuration.projects array
|
||||||
|
class ConfigProject:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(ConfigProject, self).__init__()
|
||||||
|
|
||||||
|
self.name = ""
|
||||||
|
self.parentIndex = -1
|
||||||
|
self.childIndexes = []
|
||||||
|
self.directoryIndexes = []
|
||||||
|
self.targetIndexes = []
|
||||||
|
|
||||||
|
# actual items, calculated from indices after loading
|
||||||
|
self.parent = None
|
||||||
|
self.children = []
|
||||||
|
self.directories = []
|
||||||
|
self.targets = []
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"ConfigProject: {self.name}"
|
||||||
|
|
||||||
|
# A member of the configuration.configTargets array
|
||||||
|
class ConfigTarget:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(ConfigTarget, self).__init__()
|
||||||
|
|
||||||
|
self.name = ""
|
||||||
|
self.id = ""
|
||||||
|
self.directoryIndex = -1
|
||||||
|
self.projectIndex = -1
|
||||||
|
self.jsonFile = ""
|
||||||
|
|
||||||
|
# actual target data, loaded from self.jsonFile
|
||||||
|
self.target = None
|
||||||
|
|
||||||
|
# actual items, calculated from indices after loading
|
||||||
|
self.directory = None
|
||||||
|
self.project = None
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"ConfigTarget: {self.name}"
|
||||||
|
|
||||||
|
# The available values for Target.type
|
||||||
|
class TargetType(Enum):
|
||||||
|
UNKNOWN = 0
|
||||||
|
EXECUTABLE = 1
|
||||||
|
STATIC_LIBRARY = 2
|
||||||
|
SHARED_LIBRARY = 3
|
||||||
|
MODULE_LIBRARY = 4
|
||||||
|
OBJECT_LIBRARY = 5
|
||||||
|
UTILITY = 6
|
||||||
|
|
||||||
|
# A member of the target.install_destinations array
|
||||||
|
class TargetInstallDestination:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(TargetInstallDestination, self).__init__()
|
||||||
|
|
||||||
|
self.path = ""
|
||||||
|
self.backtrace = -1
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"TargetInstallDestination: {self.path}"
|
||||||
|
|
||||||
|
# A member of the target.link_commandFragments and
|
||||||
|
# archive_commandFragments array
|
||||||
|
class TargetCommandFragment:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(TargetCommandFragment, self).__init__()
|
||||||
|
|
||||||
|
self.fragment = ""
|
||||||
|
self.role = ""
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"TargetCommandFragment: {self.fragment}"
|
||||||
|
|
||||||
|
# A member of the target.dependencies array
|
||||||
|
class TargetDependency:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(TargetDependency, self).__init__()
|
||||||
|
|
||||||
|
self.id = ""
|
||||||
|
self.backtrace = -1
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"TargetDependency: {self.id}"
|
||||||
|
|
||||||
|
# A member of the target.sources array
|
||||||
|
class TargetSource:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(TargetSource, self).__init__()
|
||||||
|
|
||||||
|
self.path = ""
|
||||||
|
self.compileGroupIndex = -1
|
||||||
|
self.sourceGroupIndex = -1
|
||||||
|
self.isGenerated = False
|
||||||
|
self.backtrace = -1
|
||||||
|
|
||||||
|
# actual items, calculated from indices after loading
|
||||||
|
self.compileGroup = None
|
||||||
|
self.sourceGroup = None
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"TargetSource: {self.path}"
|
||||||
|
|
||||||
|
# A member of the target.sourceGroups array
|
||||||
|
class TargetSourceGroup:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(TargetSourceGroup, self).__init__()
|
||||||
|
|
||||||
|
self.name = ""
|
||||||
|
self.sourceIndexes = []
|
||||||
|
|
||||||
|
# actual items, calculated from indices after loading
|
||||||
|
self.sources = []
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"TargetSourceGroup: {self.name}"
|
||||||
|
|
||||||
|
# A member of the target.compileGroups.includes array
|
||||||
|
class TargetCompileGroupInclude:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(TargetCompileGroupInclude, self).__init__()
|
||||||
|
|
||||||
|
self.path = ""
|
||||||
|
self.isSystem = False
|
||||||
|
self.backtrace = -1
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"TargetCompileGroupInclude: {self.path}"
|
||||||
|
|
||||||
|
# A member of the target.compileGroups.precompileHeaders array
|
||||||
|
class TargetCompileGroupPrecompileHeader:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(TargetCompileGroupPrecompileHeader, self).__init__()
|
||||||
|
|
||||||
|
self.header = ""
|
||||||
|
self.backtrace = -1
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"TargetCompileGroupPrecompileHeader: {self.header}"
|
||||||
|
|
||||||
|
# A member of the target.compileGroups.defines array
|
||||||
|
class TargetCompileGroupDefine:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(TargetCompileGroupDefine, self).__init__()
|
||||||
|
|
||||||
|
self.define = ""
|
||||||
|
self.backtrace = -1
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"TargetCompileGroupDefine: {self.define}"
|
||||||
|
|
||||||
|
# A member of the target.compileGroups array
|
||||||
|
class TargetCompileGroup:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(TargetCompileGroup, self).__init__()
|
||||||
|
|
||||||
|
self.sourceIndexes = []
|
||||||
|
self.language = ""
|
||||||
|
self.compileCommandFragments = []
|
||||||
|
self.includes = []
|
||||||
|
self.precompileHeaders = []
|
||||||
|
self.defines = []
|
||||||
|
self.sysroot = ""
|
||||||
|
|
||||||
|
# actual items, calculated from indices after loading
|
||||||
|
self.sources = []
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"TargetCompileGroup: {self.sources}"
|
||||||
|
|
||||||
|
# A member of the target.backtraceGraph_nodes array
|
||||||
|
class TargetBacktraceGraphNode:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(TargetBacktraceGraphNode, self).__init__()
|
||||||
|
|
||||||
|
self.file = -1
|
||||||
|
self.line = -1
|
||||||
|
self.command = -1
|
||||||
|
self.parent = -1
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"TargetBacktraceGraphNode: {self.command}"
|
||||||
|
|
||||||
|
# Actual data in config.target.target, loaded from
|
||||||
|
# config.target.jsonFile
|
||||||
|
class Target:
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(Target, self).__init__()
|
||||||
|
|
||||||
|
self.name = ""
|
||||||
|
self.id = ""
|
||||||
|
self.type = TargetType.UNKNOWN
|
||||||
|
self.backtrace = -1
|
||||||
|
self.folder = ""
|
||||||
|
self.paths_source = ""
|
||||||
|
self.paths_build = ""
|
||||||
|
self.nameOnDisk = ""
|
||||||
|
self.artifacts = []
|
||||||
|
self.isGeneratorProvided = False
|
||||||
|
|
||||||
|
# only if install rule is present
|
||||||
|
self.install_prefix = ""
|
||||||
|
self.install_destinations = []
|
||||||
|
|
||||||
|
# only for executables and shared library targets that link into
|
||||||
|
# a runtime binary
|
||||||
|
self.link_language = ""
|
||||||
|
self.link_commandFragments = []
|
||||||
|
self.link_lto = False
|
||||||
|
self.link_sysroot = ""
|
||||||
|
|
||||||
|
# only for static library targets
|
||||||
|
self.archive_commandFragments = []
|
||||||
|
self.archive_lto = False
|
||||||
|
|
||||||
|
# only if the target depends on other targets
|
||||||
|
self.dependencies = []
|
||||||
|
|
||||||
|
# corresponds to target's source files
|
||||||
|
self.sources = []
|
||||||
|
|
||||||
|
# only if sources are grouped together by source_group() or by default
|
||||||
|
self.sourceGroups = []
|
||||||
|
|
||||||
|
# only if target has sources that compile
|
||||||
|
self.compileGroups = []
|
||||||
|
|
||||||
|
# graph of backtraces referenced from elsewhere
|
||||||
|
self.backtraceGraph_nodes = []
|
||||||
|
self.backtraceGraph_commands = []
|
||||||
|
self.backtraceGraph_files = []
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"Target: {self.name}"
|
436
scripts/west_commands/zspdx/cmakefileapijson.py
Normal file
436
scripts/west_commands/zspdx/cmakefileapijson.py
Normal file
|
@ -0,0 +1,436 @@
|
||||||
|
# Copyright (c) 2020 The Linux Foundation
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
from west import log
|
||||||
|
|
||||||
|
import zspdx.cmakefileapi
|
||||||
|
|
||||||
|
def parseReply(replyIndexPath):
|
||||||
|
replyDir, _ = os.path.split(replyIndexPath)
|
||||||
|
|
||||||
|
# first we need to find the codemodel reply file
|
||||||
|
try:
|
||||||
|
with open(replyIndexPath, 'r') as indexFile:
|
||||||
|
js = json.load(indexFile)
|
||||||
|
|
||||||
|
# get reply object
|
||||||
|
reply_dict = js.get("reply", {})
|
||||||
|
if reply_dict == {}:
|
||||||
|
log.err(f"no \"reply\" field found in index file")
|
||||||
|
return None
|
||||||
|
# get codemodel object
|
||||||
|
cm_dict = reply_dict.get("codemodel-v2", {})
|
||||||
|
if cm_dict == {}:
|
||||||
|
log.err(f"no \"codemodel-v2\" field found in \"reply\" object in index file")
|
||||||
|
return None
|
||||||
|
# and get codemodel filename
|
||||||
|
jsonFile = cm_dict.get("jsonFile", "")
|
||||||
|
if jsonFile == "":
|
||||||
|
log.err(f"no \"jsonFile\" field found in \"codemodel-v2\" object in index file")
|
||||||
|
return None
|
||||||
|
|
||||||
|
return parseCodemodel(replyDir, jsonFile)
|
||||||
|
|
||||||
|
except OSError as e:
|
||||||
|
log.err(f"Error loading {replyIndexPath}: {str(e)}")
|
||||||
|
return None
|
||||||
|
except json.decoder.JSONDecodeError as e:
|
||||||
|
log.err(f"Error parsing JSON in {replyIndexPath}: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def parseCodemodel(replyDir, codemodelFile):
|
||||||
|
codemodelPath = os.path.join(replyDir, codemodelFile)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(codemodelPath, 'r') as cmFile:
|
||||||
|
js = json.load(cmFile)
|
||||||
|
|
||||||
|
cm = zspdx.cmakefileapi.Codemodel()
|
||||||
|
|
||||||
|
# for correctness, check kind and version
|
||||||
|
kind = js.get("kind", "")
|
||||||
|
if kind != "codemodel":
|
||||||
|
log.err(f"Error loading CMake API reply: expected \"kind\":\"codemodel\" in {codemodelPath}, got {kind}")
|
||||||
|
return None
|
||||||
|
version = js.get("version", {})
|
||||||
|
versionMajor = version.get("major", -1)
|
||||||
|
if versionMajor != 2:
|
||||||
|
if versionMajor == -1:
|
||||||
|
log.err(f"Error loading CMake API reply: expected major version 2 in {codemodelPath}, no version found")
|
||||||
|
return None
|
||||||
|
log.err(f"Error loading CMake API reply: expected major version 2 in {codemodelPath}, got {versionMajor}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# get paths
|
||||||
|
paths_dict = js.get("paths", {})
|
||||||
|
cm.paths_source = paths_dict.get("source", "")
|
||||||
|
cm.paths_build = paths_dict.get("build", "")
|
||||||
|
|
||||||
|
# get configurations
|
||||||
|
configs_arr = js.get("configurations", [])
|
||||||
|
for cfg_dict in configs_arr:
|
||||||
|
cfg = parseConfig(cfg_dict, replyDir)
|
||||||
|
if cfg:
|
||||||
|
cm.configurations.append(cfg)
|
||||||
|
|
||||||
|
# and after parsing is done, link all the indices
|
||||||
|
linkCodemodel(cm)
|
||||||
|
|
||||||
|
return cm
|
||||||
|
|
||||||
|
except OSError as e:
|
||||||
|
log.err(f"Error loading {codemodelPath}: {str(e)}")
|
||||||
|
return None
|
||||||
|
except json.decoder.JSONDecodeError as e:
|
||||||
|
log.err(f"Error parsing JSON in {codemodelPath}: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def parseConfig(cfg_dict, replyDir):
|
||||||
|
cfg = zspdx.cmakefileapi.Config()
|
||||||
|
cfg.name = cfg_dict.get("name", "")
|
||||||
|
|
||||||
|
# parse and add each directory
|
||||||
|
dirs_arr = cfg_dict.get("directories", [])
|
||||||
|
for dir_dict in dirs_arr:
|
||||||
|
if dir_dict != {}:
|
||||||
|
cfgdir = zspdx.cmakefileapi.ConfigDir()
|
||||||
|
cfgdir.source = dir_dict.get("source", "")
|
||||||
|
cfgdir.build = dir_dict.get("build", "")
|
||||||
|
cfgdir.parentIndex = dir_dict.get("parentIndex", -1)
|
||||||
|
cfgdir.childIndexes = dir_dict.get("childIndexes", [])
|
||||||
|
cfgdir.projectIndex = dir_dict.get("projecttIndex", -1)
|
||||||
|
cfgdir.targetIndexes = dir_dict.get("targetIndexes", [])
|
||||||
|
minCMakeVer_dict = dir_dict.get("minimumCMakeVersion", {})
|
||||||
|
cfgdir.minimumCMakeVersion = minCMakeVer_dict.get("string", "")
|
||||||
|
cfgdir.hasInstallRule = dir_dict.get("hasInstallRule", False)
|
||||||
|
cfg.directories.append(cfgdir)
|
||||||
|
|
||||||
|
# parse and add each project
|
||||||
|
projects_arr = cfg_dict.get("projects", [])
|
||||||
|
for prj_dict in projects_arr:
|
||||||
|
if prj_dict != {}:
|
||||||
|
prj = zspdx.cmakefileapi.ConfigProject()
|
||||||
|
prj.name = prj_dict.get("name", "")
|
||||||
|
prj.parentIndex = prj_dict.get("parentIndex", -1)
|
||||||
|
prj.childIndexes = prj_dict.get("childIndexes", [])
|
||||||
|
prj.directoryIndexes = prj_dict.get("directoryIndexes", [])
|
||||||
|
prj.targetIndexes = prj_dict.get("targetIndexes", [])
|
||||||
|
cfg.projects.append(prj)
|
||||||
|
|
||||||
|
# parse and add each target
|
||||||
|
cfgTargets_arr = cfg_dict.get("targets", [])
|
||||||
|
for cfgTarget_dict in cfgTargets_arr:
|
||||||
|
if cfgTarget_dict != {}:
|
||||||
|
cfgTarget = zspdx.cmakefileapi.ConfigTarget()
|
||||||
|
cfgTarget.name = cfgTarget_dict.get("name", "")
|
||||||
|
cfgTarget.id = cfgTarget_dict.get("id", "")
|
||||||
|
cfgTarget.directoryIndex = cfgTarget_dict.get("directoryIndex", -1)
|
||||||
|
cfgTarget.projectIndex = cfgTarget_dict.get("projectIndex", -1)
|
||||||
|
cfgTarget.jsonFile = cfgTarget_dict.get("jsonFile", "")
|
||||||
|
|
||||||
|
if cfgTarget.jsonFile != "":
|
||||||
|
cfgTarget.target = parseTarget(os.path.join(replyDir, cfgTarget.jsonFile))
|
||||||
|
else:
|
||||||
|
cfgTarget.target = None
|
||||||
|
|
||||||
|
cfg.configTargets.append(cfgTarget)
|
||||||
|
|
||||||
|
return cfg
|
||||||
|
|
||||||
|
def parseTarget(targetPath):
|
||||||
|
try:
|
||||||
|
with open(targetPath, 'r') as targetFile:
|
||||||
|
js = json.load(targetFile)
|
||||||
|
|
||||||
|
target = zspdx.cmakefileapi.Target()
|
||||||
|
|
||||||
|
target.name = js.get("name", "")
|
||||||
|
target.id = js.get("id", "")
|
||||||
|
target.type = parseTargetType(js.get("type", "UNKNOWN"))
|
||||||
|
target.backtrace = js.get("backtrace", -1)
|
||||||
|
target.folder = js.get("folder", "")
|
||||||
|
|
||||||
|
# get paths
|
||||||
|
paths_dict = js.get("paths", {})
|
||||||
|
target.paths_source = paths_dict.get("source", "")
|
||||||
|
target.paths_build = paths_dict.get("build", "")
|
||||||
|
|
||||||
|
target.nameOnDisk = js.get("nameOnDisk", "")
|
||||||
|
|
||||||
|
# parse artifacts if present
|
||||||
|
artifacts_arr = js.get("artifacts", [])
|
||||||
|
target.artifacts = []
|
||||||
|
for artifact_dict in artifacts_arr:
|
||||||
|
artifact_path = artifact_dict.get("path", "")
|
||||||
|
if artifact_path != "":
|
||||||
|
target.artifacts.append(artifact_path)
|
||||||
|
|
||||||
|
target.isGeneratorProvided = js.get("isGeneratorProvided", False)
|
||||||
|
|
||||||
|
# call separate functions to parse subsections
|
||||||
|
parseTargetInstall(target, js)
|
||||||
|
parseTargetLink(target, js)
|
||||||
|
parseTargetArchive(target, js)
|
||||||
|
parseTargetDependencies(target, js)
|
||||||
|
parseTargetSources(target, js)
|
||||||
|
parseTargetSourceGroups(target, js)
|
||||||
|
parseTargetCompileGroups(target, js)
|
||||||
|
parseTargetBacktraceGraph(target, js)
|
||||||
|
|
||||||
|
return target
|
||||||
|
|
||||||
|
except OSError as e:
|
||||||
|
log.err(f"Error loading {targetPath}: {str(e)}")
|
||||||
|
return None
|
||||||
|
except json.decoder.JSONDecodeError as e:
|
||||||
|
log.err(f"Error parsing JSON in {targetPath}: {str(e)}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def parseTargetType(targetType):
|
||||||
|
if targetType == "EXECUTABLE":
|
||||||
|
return zspdx.cmakefileapi.TargetType.EXECUTABLE
|
||||||
|
elif targetType == "STATIC_LIBRARY":
|
||||||
|
return zspdx.cmakefileapi.TargetType.STATIC_LIBRARY
|
||||||
|
elif targetType == "SHARED_LIBRARY":
|
||||||
|
return zspdx.cmakefileapi.TargetType.SHARED_LIBRARY
|
||||||
|
elif targetType == "MODULE_LIBRARY":
|
||||||
|
return zspdx.cmakefileapi.TargetType.MODULE_LIBRARY
|
||||||
|
elif targetType == "OBJECT_LIBRARY":
|
||||||
|
return zspdx.cmakefileapi.TargetType.OBJECT_LIBRARY
|
||||||
|
elif targetType == "UTILITY":
|
||||||
|
return zspdx.cmakefileapi.TargetType.UTILITY
|
||||||
|
else:
|
||||||
|
return zspdx.cmakefileapi.TargetType.UNKNOWN
|
||||||
|
|
||||||
|
def parseTargetInstall(target, js):
|
||||||
|
install_dict = js.get("install", {})
|
||||||
|
if install_dict == {}:
|
||||||
|
return
|
||||||
|
prefix_dict = install_dict.get("prefix", {})
|
||||||
|
target.install_prefix = prefix_dict.get("path", "")
|
||||||
|
|
||||||
|
destinations_arr = install_dict.get("destinations", [])
|
||||||
|
for destination_dict in destinations_arr:
|
||||||
|
dest = zspdx.cmakefileapi.TargetInstallDestination()
|
||||||
|
dest.path = destination_dict.get("path", "")
|
||||||
|
dest.backtrace = destination_dict.get("backtrace", -1)
|
||||||
|
target.install_destinations.append(dest)
|
||||||
|
|
||||||
|
def parseTargetLink(target, js):
|
||||||
|
link_dict = js.get("link", {})
|
||||||
|
if link_dict == {}:
|
||||||
|
return
|
||||||
|
target.link_language = link_dict.get("language", {})
|
||||||
|
target.link_lto = link_dict.get("lto", False)
|
||||||
|
sysroot_dict = link_dict.get("sysroot", {})
|
||||||
|
target.link_sysroot = sysroot_dict.get("path", "")
|
||||||
|
|
||||||
|
fragments_arr = link_dict.get("commandFragments", [])
|
||||||
|
for fragment_dict in fragments_arr:
|
||||||
|
fragment = zspdx.cmakefileapi.TargetCommandFragment()
|
||||||
|
fragment.fragment = fragment_dict.get("fragment", "")
|
||||||
|
fragment.role = fragment_dict.get("role", "")
|
||||||
|
target.link_commandFragments.append(fragment)
|
||||||
|
|
||||||
|
def parseTargetArchive(target, js):
|
||||||
|
archive_dict = js.get("archive", {})
|
||||||
|
if archive_dict == {}:
|
||||||
|
return
|
||||||
|
target.archive_lto = archive_dict.get("lto", False)
|
||||||
|
|
||||||
|
fragments_arr = archive_dict.get("commandFragments", [])
|
||||||
|
for fragment_dict in fragments_arr:
|
||||||
|
fragment = zspdx.cmakefileapi.TargetCommandFragment()
|
||||||
|
fragment.fragment = fragment_dict.get("fragment", "")
|
||||||
|
fragment.role = fragment_dict.get("role", "")
|
||||||
|
target.archive_commandFragments.append(fragment)
|
||||||
|
|
||||||
|
def parseTargetDependencies(target, js):
|
||||||
|
dependencies_arr = js.get("dependencies", [])
|
||||||
|
for dependency_dict in dependencies_arr:
|
||||||
|
dep = zspdx.cmakefileapi.TargetDependency()
|
||||||
|
dep.id = dependency_dict.get("id", "")
|
||||||
|
dep.backtrace = dependency_dict.get("backtrace", -1)
|
||||||
|
target.dependencies.append(dep)
|
||||||
|
|
||||||
|
def parseTargetSources(target, js):
|
||||||
|
sources_arr = js.get("sources", [])
|
||||||
|
for source_dict in sources_arr:
|
||||||
|
src = zspdx.cmakefileapi.TargetSource()
|
||||||
|
src.path = source_dict.get("path", "")
|
||||||
|
src.compileGroupIndex = source_dict.get("compileGroupIndex", -1)
|
||||||
|
src.sourceGroupIndex = source_dict.get("sourceGroupIndex", -1)
|
||||||
|
src.isGenerated = source_dict.get("isGenerated", False)
|
||||||
|
src.backtrace = source_dict.get("backtrace", -1)
|
||||||
|
target.sources.append(src)
|
||||||
|
|
||||||
|
def parseTargetSourceGroups(target, js):
|
||||||
|
sourceGroups_arr = js.get("sourceGroups", [])
|
||||||
|
for sourceGroup_dict in sourceGroups_arr:
|
||||||
|
srcgrp = zspdx.cmakefileapi.TargetSourceGroup()
|
||||||
|
srcgrp.name = sourceGroup_dict.get("name", "")
|
||||||
|
srcgrp.sourceIndexes = sourceGroup_dict.get("sourceIndexes", [])
|
||||||
|
target.sourceGroups.append(srcgrp)
|
||||||
|
|
||||||
|
def parseTargetCompileGroups(target, js):
|
||||||
|
compileGroups_arr = js.get("compileGroups", [])
|
||||||
|
for compileGroup_dict in compileGroups_arr:
|
||||||
|
cmpgrp = zspdx.cmakefileapi.TargetCompileGroup()
|
||||||
|
cmpgrp.sourceIndexes = compileGroup_dict.get("sourceIndexes", [])
|
||||||
|
cmpgrp.language = compileGroup_dict.get("language", "")
|
||||||
|
cmpgrp.sysroot = compileGroup_dict.get("sysroot", "")
|
||||||
|
|
||||||
|
commandFragments_arr = compileGroup_dict.get("compileCommandFragments", [])
|
||||||
|
for commandFragment_dict in commandFragments_arr:
|
||||||
|
fragment = commandFragment_dict.get("fragment", "")
|
||||||
|
if fragment != "":
|
||||||
|
cmpgrp.compileCommandFragments.append(fragment)
|
||||||
|
|
||||||
|
includes_arr = compileGroup_dict.get("includes", [])
|
||||||
|
for include_dict in includes_arr:
|
||||||
|
grpInclude = zspdx.cmakefileapi.TargetCompileGroupInclude()
|
||||||
|
grpInclude.path = include_dict.get("path", "")
|
||||||
|
grpInclude.isSystem = include_dict.get("isSystem", False)
|
||||||
|
grpInclude.backtrace = include_dict.get("backtrace", -1)
|
||||||
|
cmpgrp.includes.append(grpInclude)
|
||||||
|
|
||||||
|
precompileHeaders_arr = compileGroup_dict.get("precompileHeaders", [])
|
||||||
|
for precompileHeader_dict in precompileHeaders_arr:
|
||||||
|
grpHeader = zspdx.cmakefileapi.TargetCompileGroupPrecompileHeader()
|
||||||
|
grpHeader.header = precompileHeader_dict.get("header", "")
|
||||||
|
grpHeader.backtrace = precompileHeader_dict.get("backtrace", -1)
|
||||||
|
cmpgrp.precompileHeaders.append(grpHeader)
|
||||||
|
|
||||||
|
defines_arr = compileGroup_dict.get("defines", [])
|
||||||
|
for define_dict in defines_arr:
|
||||||
|
grpDefine = zspdx.cmakefileapi.TargetCompileGroupDefine()
|
||||||
|
grpDefine.define = define_dict.get("define", "")
|
||||||
|
grpDefine.backtrace = define_dict.get("backtrace", -1)
|
||||||
|
cmpgrp.defines.append(grpDefine)
|
||||||
|
|
||||||
|
target.compileGroups.append(cmpgrp)
|
||||||
|
|
||||||
|
def parseTargetBacktraceGraph(target, js):
|
||||||
|
backtraceGraph_dict = js.get("backtraceGraph", {})
|
||||||
|
if backtraceGraph_dict == {}:
|
||||||
|
return
|
||||||
|
target.backtraceGraph_commands = backtraceGraph_dict.get("commands", [])
|
||||||
|
target.backtraceGraph_files = backtraceGraph_dict.get("files", [])
|
||||||
|
|
||||||
|
nodes_arr = backtraceGraph_dict.get("nodes", [])
|
||||||
|
for node_dict in nodes_arr:
|
||||||
|
node = zspdx.cmakefileapi.TargetBacktraceGraphNode()
|
||||||
|
node.file = node_dict.get("file", -1)
|
||||||
|
node.line = node_dict.get("line", -1)
|
||||||
|
node.command = node_dict.get("command", -1)
|
||||||
|
node.parent = node_dict.get("parent", -1)
|
||||||
|
target.backtraceGraph_nodes.append(node)
|
||||||
|
|
||||||
|
# Create direct pointers for all Configs in Codemodel
|
||||||
|
# takes: Codemodel
|
||||||
|
def linkCodemodel(cm):
|
||||||
|
for cfg in cm.configurations:
|
||||||
|
linkConfig(cfg)
|
||||||
|
|
||||||
|
# Create direct pointers for all contents of Config
|
||||||
|
# takes: Config
|
||||||
|
def linkConfig(cfg):
|
||||||
|
for cfgDir in cfg.directories:
|
||||||
|
linkConfigDir(cfg, cfgDir)
|
||||||
|
for cfgPrj in cfg.projects:
|
||||||
|
linkConfigProject(cfg, cfgPrj)
|
||||||
|
for cfgTarget in cfg.configTargets:
|
||||||
|
linkConfigTarget(cfg, cfgTarget)
|
||||||
|
|
||||||
|
# Create direct pointers for ConfigDir indices
|
||||||
|
# takes: Config and ConfigDir
|
||||||
|
def linkConfigDir(cfg, cfgDir):
|
||||||
|
if cfgDir.parentIndex == -1:
|
||||||
|
cfgDir.parent = None
|
||||||
|
else:
|
||||||
|
cfgDir.parent = cfg.directories[cfgDir.parentIndex]
|
||||||
|
|
||||||
|
if cfgDir.projectIndex == -1:
|
||||||
|
cfgDir.project = None
|
||||||
|
else:
|
||||||
|
cfgDir.project = cfg.projects[cfgDir.projectIndex]
|
||||||
|
|
||||||
|
cfgDir.children = []
|
||||||
|
for childIndex in cfgDir.childIndexes:
|
||||||
|
cfgDir.children.append(cfg.directories[childIndex])
|
||||||
|
|
||||||
|
cfgDir.targets = []
|
||||||
|
for targetIndex in cfgDir.targetIndexes:
|
||||||
|
cfgDir.targets.append(cfg.configTargets[targetIndex])
|
||||||
|
|
||||||
|
# Create direct pointers for ConfigProject indices
|
||||||
|
# takes: Config and ConfigProject
|
||||||
|
def linkConfigProject(cfg, cfgPrj):
|
||||||
|
if cfgPrj.parentIndex == -1:
|
||||||
|
cfgPrj.parent = None
|
||||||
|
else:
|
||||||
|
cfgPrj.parent = cfg.projects[cfgPrj.parentIndex]
|
||||||
|
|
||||||
|
cfgPrj.children = []
|
||||||
|
for childIndex in cfgPrj.childIndexes:
|
||||||
|
cfgPrj.children.append(cfg.projects[childIndex])
|
||||||
|
|
||||||
|
cfgPrj.directories = []
|
||||||
|
for dirIndex in cfgPrj.directoryIndexes:
|
||||||
|
cfgPrj.directories.append(cfg.directories[dirIndex])
|
||||||
|
|
||||||
|
cfgPrj.targets = []
|
||||||
|
for targetIndex in cfgPrj.targetIndexes:
|
||||||
|
cfgPrj.targets.append(cfg.configTargets[targetIndex])
|
||||||
|
|
||||||
|
# Create direct pointers for ConfigTarget indices
|
||||||
|
# takes: Config and ConfigTarget
|
||||||
|
def linkConfigTarget(cfg, cfgTarget):
|
||||||
|
if cfgTarget.directoryIndex == -1:
|
||||||
|
cfgTarget.directory = None
|
||||||
|
else:
|
||||||
|
cfgTarget.directory = cfg.directories[cfgTarget.directoryIndex]
|
||||||
|
|
||||||
|
if cfgTarget.projectIndex == -1:
|
||||||
|
cfgTarget.project = None
|
||||||
|
else:
|
||||||
|
cfgTarget.project = cfg.projects[cfgTarget.projectIndex]
|
||||||
|
|
||||||
|
# and link target's sources and source groups
|
||||||
|
for ts in cfgTarget.target.sources:
|
||||||
|
linkTargetSource(cfgTarget.target, ts)
|
||||||
|
for tsg in cfgTarget.target.sourceGroups:
|
||||||
|
linkTargetSourceGroup(cfgTarget.target, tsg)
|
||||||
|
for tcg in cfgTarget.target.compileGroups:
|
||||||
|
linkTargetCompileGroup(cfgTarget.target, tcg)
|
||||||
|
|
||||||
|
# Create direct pointers for TargetSource indices
|
||||||
|
# takes: Target and TargetSource
|
||||||
|
def linkTargetSource(target, targetSrc):
|
||||||
|
if targetSrc.compileGroupIndex == -1:
|
||||||
|
targetSrc.compileGroup = None
|
||||||
|
else:
|
||||||
|
targetSrc.compileGroup = target.compileGroups[targetSrc.compileGroupIndex]
|
||||||
|
|
||||||
|
if targetSrc.sourceGroupIndex == -1:
|
||||||
|
targetSrc.sourceGroup = None
|
||||||
|
else:
|
||||||
|
targetSrc.sourceGroup = target.sourceGroups[targetSrc.sourceGroupIndex]
|
||||||
|
|
||||||
|
# Create direct pointers for TargetSourceGroup indices
|
||||||
|
# takes: Target and TargetSourceGroup
|
||||||
|
def linkTargetSourceGroup(target, targetSrcGrp):
|
||||||
|
targetSrcGrp.sources = []
|
||||||
|
for srcIndex in targetSrcGrp.sourceIndexes:
|
||||||
|
targetSrcGrp.sources.append(target.sources[srcIndex])
|
||||||
|
|
||||||
|
# Create direct pointers for TargetCompileGroup indices
|
||||||
|
# takes: Target and TargetCompileGroup
|
||||||
|
def linkTargetCompileGroup(target, targetCmpGrp):
|
||||||
|
targetCmpGrp.sources = []
|
||||||
|
for srcIndex in targetCmpGrp.sourceIndexes:
|
||||||
|
targetCmpGrp.sources.append(target.sources[srcIndex])
|
231
scripts/west_commands/zspdx/datatypes.py
Normal file
231
scripts/west_commands/zspdx/datatypes.py
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
# Copyright (c) 2021 The Linux Foundation
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
# DocumentConfig contains settings used to configure how the SPDX Document
|
||||||
|
# should be built.
|
||||||
|
class DocumentConfig:
|
||||||
|
def __init__(self):
|
||||||
|
super(DocumentConfig, self).__init__()
|
||||||
|
|
||||||
|
# name of document
|
||||||
|
self.name = ""
|
||||||
|
|
||||||
|
# namespace for this document
|
||||||
|
self.namespace = ""
|
||||||
|
|
||||||
|
# standardized DocumentRef- (including that prefix) that the other
|
||||||
|
# docs will use to refer to this one
|
||||||
|
self.docRefID = ""
|
||||||
|
|
||||||
|
# Document contains the data assembled by the SBOM builder, to be used to
|
||||||
|
# create the actual SPDX Document.
|
||||||
|
class Document:
|
||||||
|
# initialize with a DocumentConfig
|
||||||
|
def __init__(self, cfg):
|
||||||
|
super(Document, self).__init__()
|
||||||
|
|
||||||
|
# configuration - DocumentConfig
|
||||||
|
self.cfg = cfg
|
||||||
|
|
||||||
|
# dict of SPDX ID => Package
|
||||||
|
self.pkgs = {}
|
||||||
|
|
||||||
|
# relationships "owned" by this Document, _not_ those "owned" by its
|
||||||
|
# Packages or Files; will likely be just DESCRIBES
|
||||||
|
self.relationships = []
|
||||||
|
|
||||||
|
# dict of filename (ignoring its directory) => number of times it has
|
||||||
|
# been seen while adding files to this Document; used to calculate
|
||||||
|
# useful SPDX IDs
|
||||||
|
self.timesSeen = {}
|
||||||
|
|
||||||
|
# dict of absolute path on disk => File
|
||||||
|
self.fileLinks = {}
|
||||||
|
|
||||||
|
# set of other Documents that our elements' Relationships refer to
|
||||||
|
self.externalDocuments = set()
|
||||||
|
|
||||||
|
# set of LicenseRef- custom licenses to be declared
|
||||||
|
# may or may not include "LicenseRef-" license prefix
|
||||||
|
self.customLicenseIDs = set()
|
||||||
|
|
||||||
|
# this Document's SHA1 hash, filled in _after_ the Document has been
|
||||||
|
# written to disk, so that others can refer to it
|
||||||
|
self.myDocSHA1 = ""
|
||||||
|
|
||||||
|
# PackageConfig contains settings used to configure how an SPDX Package should
|
||||||
|
# be built.
|
||||||
|
class PackageConfig:
|
||||||
|
def __init__(self):
|
||||||
|
super(PackageConfig, self).__init__()
|
||||||
|
|
||||||
|
# package name
|
||||||
|
self.name = ""
|
||||||
|
|
||||||
|
# SPDX ID, including "SPDXRef-"
|
||||||
|
self.spdxID = ""
|
||||||
|
|
||||||
|
# the Package's declared license
|
||||||
|
self.declaredLicense = "NOASSERTION"
|
||||||
|
|
||||||
|
# the Package's copyright text
|
||||||
|
self.copyrightText = "NOASSERTION"
|
||||||
|
|
||||||
|
# absolute path of the "root" directory on disk, to be used as the
|
||||||
|
# base directory from which this Package's Files will calculate their
|
||||||
|
# relative paths
|
||||||
|
# may want to note this in a Package comment field
|
||||||
|
self.relativeBaseDir = ""
|
||||||
|
|
||||||
|
# Package contains the data assembled by the SBOM builder, to be used to
|
||||||
|
# create the actual SPDX Package.
|
||||||
|
class Package:
|
||||||
|
# initialize with:
|
||||||
|
# 1) PackageConfig
|
||||||
|
# 2) the Document that owns this Package
|
||||||
|
def __init__(self, cfg, doc):
|
||||||
|
super(Package, self).__init__()
|
||||||
|
|
||||||
|
# configuration - PackageConfig
|
||||||
|
self.cfg = cfg
|
||||||
|
|
||||||
|
# Document that owns this Package
|
||||||
|
self.doc = doc
|
||||||
|
|
||||||
|
# verification code, calculated per section 3.9 of SPDX spec v2.2
|
||||||
|
self.verificationCode = ""
|
||||||
|
|
||||||
|
# concluded license for this Package, if
|
||||||
|
# cfg.shouldConcludePackageLicense == True; NOASSERTION otherwise
|
||||||
|
self.concludedLicense = "NOASSERTION"
|
||||||
|
|
||||||
|
# list of licenses found in this Package's Files
|
||||||
|
self.licenseInfoFromFiles = []
|
||||||
|
|
||||||
|
# Files in this Package
|
||||||
|
# dict of SPDX ID => File
|
||||||
|
self.files = {}
|
||||||
|
|
||||||
|
# Relationships "owned" by this Package (e.g., this Package is left
|
||||||
|
# side)
|
||||||
|
self.rlns = []
|
||||||
|
|
||||||
|
# If this Package was a target, which File was its main build product?
|
||||||
|
self.targetBuildFile = None
|
||||||
|
|
||||||
|
# RelationshipDataElementType defines whether a RelationshipData element
|
||||||
|
# (e.g., the "owner" or the "other" element) is a File, a target Package,
|
||||||
|
# a Package's ID (as other only, and only where owner type is DOCUMENT),
|
||||||
|
# or the SPDX document itself (as owner only).
|
||||||
|
class RelationshipDataElementType(Enum):
|
||||||
|
UNKNOWN = 0
|
||||||
|
FILENAME = 1
|
||||||
|
TARGETNAME = 2
|
||||||
|
PACKAGEID = 3
|
||||||
|
DOCUMENT = 4
|
||||||
|
|
||||||
|
# RelationshipData contains the pre-analysis data about a relationship between
|
||||||
|
# Files and/or Packages/targets. It is eventually parsed into a corresponding
|
||||||
|
# Relationship after we have organized the SPDX Package and File data.
|
||||||
|
class RelationshipData:
|
||||||
|
def __init__(self):
|
||||||
|
super(RelationshipData, self).__init__()
|
||||||
|
|
||||||
|
# for the "owner" element (e.g., the left side of the Relationship),
|
||||||
|
# is it a filename or a target name (e.g., a Package in the build doc)
|
||||||
|
self.ownerType = RelationshipDataElementType.UNKNOWN
|
||||||
|
|
||||||
|
# owner file absolute path (if ownerType is FILENAME)
|
||||||
|
self.ownerFileAbspath = ""
|
||||||
|
|
||||||
|
# owner target name (if ownerType is TARGETNAME)
|
||||||
|
self.ownerTargetName = ""
|
||||||
|
|
||||||
|
# owner SPDX Document (if ownerType is DOCUMENT)
|
||||||
|
self.ownerDocument = None
|
||||||
|
|
||||||
|
# for the "other" element (e.g., the right side of the Relationship),
|
||||||
|
# is it a filename or a target name (e.g., a Package in the build doc)
|
||||||
|
self.otherType = RelationshipDataElementType.UNKNOWN
|
||||||
|
|
||||||
|
# other file absolute path (if otherType is FILENAME)
|
||||||
|
self.otherFileAbspath = ""
|
||||||
|
|
||||||
|
# other target name (if otherType is TARGETNAME)
|
||||||
|
self.otherTargetName = ""
|
||||||
|
|
||||||
|
# other package ID (if ownerType is DOCUMENT and otherType is PACKAGEID)
|
||||||
|
self.otherPackageID = ""
|
||||||
|
|
||||||
|
# text string with Relationship type
|
||||||
|
# from table in section 7.1 of SPDX spec v2.2
|
||||||
|
self.rlnType = ""
|
||||||
|
|
||||||
|
# Relationship contains the post-analysis, processed data about a relationship
|
||||||
|
# in a form suitable for creating the actual SPDX Relationship in a particular
|
||||||
|
# Document's context.
|
||||||
|
class Relationship:
|
||||||
|
def __init__(self):
|
||||||
|
super(Relationship, self).__init__()
|
||||||
|
|
||||||
|
# SPDX ID for left side of relationship
|
||||||
|
# including "SPDXRef-" as well as "DocumentRef-" if needed
|
||||||
|
self.refA = ""
|
||||||
|
|
||||||
|
# SPDX ID for right side of relationship
|
||||||
|
# including "SPDXRef-" as well as "DocumentRef-" if needed
|
||||||
|
self.refB = ""
|
||||||
|
|
||||||
|
# text string with Relationship type
|
||||||
|
# from table in section 7.1 of SPDX spec v2.2
|
||||||
|
self.rlnType = ""
|
||||||
|
|
||||||
|
# File contains the data needed to create a File element in the context of a
|
||||||
|
# particular SPDX Document and Package.
|
||||||
|
class File:
|
||||||
|
# initialize with:
|
||||||
|
# 1) Document containing this File
|
||||||
|
# 2) Package containing this File
|
||||||
|
def __init__(self, doc, pkg):
|
||||||
|
super(File, self).__init__()
|
||||||
|
|
||||||
|
# absolute path to this file on disk
|
||||||
|
self.abspath = ""
|
||||||
|
|
||||||
|
# relative path for this file, measured from the owning Package's
|
||||||
|
# cfg.relativeBaseDir
|
||||||
|
self.relpath = ""
|
||||||
|
|
||||||
|
# SPDX ID for this file, including "SPDXRef-"
|
||||||
|
self.spdxID = ""
|
||||||
|
|
||||||
|
# SHA1 hash
|
||||||
|
self.sha1 = ""
|
||||||
|
|
||||||
|
# SHA256 hash, if pkg.cfg.doSHA256 == True; empty string otherwise
|
||||||
|
self.sha256 = ""
|
||||||
|
|
||||||
|
# MD5 hash, if pkg.cfg.doMD5 == True; empty string otherwise
|
||||||
|
self.md5 = ""
|
||||||
|
|
||||||
|
# concluded license, if pkg.cfg.shouldConcludeFileLicenses == True;
|
||||||
|
# "NOASSERTION" otherwise
|
||||||
|
self.concludedLicense = "NOASSERTION"
|
||||||
|
|
||||||
|
# license info in file
|
||||||
|
self.licenseInfoInFile = []
|
||||||
|
|
||||||
|
# copyright text
|
||||||
|
self.copyrightText = "NOASSERTION"
|
||||||
|
|
||||||
|
# Relationships "owned" by this File (e.g., this File is left side)
|
||||||
|
self.rlns = []
|
||||||
|
|
||||||
|
# Package that owns this File
|
||||||
|
self.pkg = pkg
|
||||||
|
|
||||||
|
# Document that owns this File
|
||||||
|
self.doc = doc
|
65
scripts/west_commands/zspdx/getincludes.py
Normal file
65
scripts/west_commands/zspdx/getincludes.py
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
# Copyright (c) 2021 The Linux Foundation
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from subprocess import run, PIPE
|
||||||
|
|
||||||
|
from west import log
|
||||||
|
|
||||||
|
# Given a path to the applicable C compiler, a C source file, and the
|
||||||
|
# corresponding TargetCompileGroup, determine which include files would
|
||||||
|
# be used.
|
||||||
|
# Arguments:
|
||||||
|
# 1) path to applicable C compiler
|
||||||
|
# 2) C source file being analyzed
|
||||||
|
# 3) TargetCompileGroup for the current target
|
||||||
|
# Returns: list of paths to include files, or [] on error or empty findings.
|
||||||
|
def getCIncludes(compilerPath, srcFile, tcg):
|
||||||
|
log.dbg(f" - getting includes for {srcFile}")
|
||||||
|
|
||||||
|
# prepare fragments
|
||||||
|
fragments = [fr for fr in tcg.compileCommandFragments if fr.strip() != ""]
|
||||||
|
|
||||||
|
# prepare include arguments
|
||||||
|
includes = ["-I" + incl.path for incl in tcg.includes]
|
||||||
|
|
||||||
|
# prepare defines
|
||||||
|
defines = ["-D" + d.define for d in tcg.defines]
|
||||||
|
|
||||||
|
# prepare command invocation
|
||||||
|
cmd = [compilerPath, "-E", "-H"] + fragments + includes + defines + [srcFile]
|
||||||
|
|
||||||
|
cp = run(cmd, stdout=PIPE, stderr=PIPE, universal_newlines=True)
|
||||||
|
if cp.returncode != 0:
|
||||||
|
log.dbg(f" - calling {compilerPath} failed with error code {cp.returncode}")
|
||||||
|
return []
|
||||||
|
else:
|
||||||
|
# response will be in cp.stderr, not cp.stdout
|
||||||
|
return extractIncludes(cp.stderr)
|
||||||
|
|
||||||
|
# Parse the response from the CC -E -H call, to extract the include file paths
|
||||||
|
def extractIncludes(resp):
|
||||||
|
includes = set()
|
||||||
|
|
||||||
|
# lines we want will start with one or more periods, followed by
|
||||||
|
# a space and then the include file path, e.g.:
|
||||||
|
# .... /home/steve/programming/zephyr/zephyrproject/zephyr/include/kernel.h
|
||||||
|
# the number of periods indicates the depth of nesting (for transitively-
|
||||||
|
# included files), but here we aren't going to care about that. We'll
|
||||||
|
# treat everything as tied to the corresponding source file.
|
||||||
|
|
||||||
|
# once we hit the line "Multiple include guards may be useful for:",
|
||||||
|
# we're done; ignore everything after that
|
||||||
|
|
||||||
|
for rline in resp.splitlines():
|
||||||
|
if rline.startswith("Multiple include guards"):
|
||||||
|
break
|
||||||
|
if rline[0] == ".":
|
||||||
|
sline = rline.split(" ", maxsplit=1)
|
||||||
|
if len(sline) != 2:
|
||||||
|
continue
|
||||||
|
includes.add(sline[1])
|
||||||
|
|
||||||
|
includesList = list(includes)
|
||||||
|
includesList.sort()
|
||||||
|
return includesList
|
509
scripts/west_commands/zspdx/licenses.py
Normal file
509
scripts/west_commands/zspdx/licenses.py
Normal file
|
@ -0,0 +1,509 @@
|
||||||
|
# Copyright (c) 2021 The Linux Foundation
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
# from https://github.com/spdx/license-list-data/
|
||||||
|
|
||||||
|
LICENSE_LIST_VERSION = "3.12"
|
||||||
|
|
||||||
|
LICENSES = [
|
||||||
|
"0BSD",
|
||||||
|
"389-exception",
|
||||||
|
"AAL",
|
||||||
|
"ADSL",
|
||||||
|
"AFL-1.1",
|
||||||
|
"AFL-1.2",
|
||||||
|
"AFL-2.0",
|
||||||
|
"AFL-2.1",
|
||||||
|
"AFL-3.0",
|
||||||
|
"AGPL-1.0",
|
||||||
|
"AGPL-1.0-only",
|
||||||
|
"AGPL-1.0-or-later",
|
||||||
|
"AGPL-3.0",
|
||||||
|
"AGPL-3.0-only",
|
||||||
|
"AGPL-3.0-or-later",
|
||||||
|
"AMDPLPA",
|
||||||
|
"AML",
|
||||||
|
"AMPAS",
|
||||||
|
"ANTLR-PD",
|
||||||
|
"ANTLR-PD-fallback",
|
||||||
|
"APAFML",
|
||||||
|
"APL-1.0",
|
||||||
|
"APSL-1.0",
|
||||||
|
"APSL-1.1",
|
||||||
|
"APSL-1.2",
|
||||||
|
"APSL-2.0",
|
||||||
|
"Abstyles",
|
||||||
|
"Adobe-2006",
|
||||||
|
"Adobe-Glyph",
|
||||||
|
"Afmparse",
|
||||||
|
"Aladdin",
|
||||||
|
"Apache-1.0",
|
||||||
|
"Apache-1.1",
|
||||||
|
"Apache-2.0",
|
||||||
|
"Artistic-1.0",
|
||||||
|
"Artistic-1.0-Perl",
|
||||||
|
"Artistic-1.0-cl8",
|
||||||
|
"Artistic-2.0",
|
||||||
|
"Autoconf-exception-2.0",
|
||||||
|
"Autoconf-exception-3.0",
|
||||||
|
"BSD-1-Clause",
|
||||||
|
"BSD-2-Clause",
|
||||||
|
"BSD-2-Clause-FreeBSD",
|
||||||
|
"BSD-2-Clause-NetBSD",
|
||||||
|
"BSD-2-Clause-Patent",
|
||||||
|
"BSD-2-Clause-Views",
|
||||||
|
"BSD-3-Clause",
|
||||||
|
"BSD-3-Clause-Attribution",
|
||||||
|
"BSD-3-Clause-Clear",
|
||||||
|
"BSD-3-Clause-LBNL",
|
||||||
|
"BSD-3-Clause-Modification",
|
||||||
|
"BSD-3-Clause-No-Nuclear-License",
|
||||||
|
"BSD-3-Clause-No-Nuclear-License-2014",
|
||||||
|
"BSD-3-Clause-No-Nuclear-Warranty",
|
||||||
|
"BSD-3-Clause-Open-MPI",
|
||||||
|
"BSD-4-Clause",
|
||||||
|
"BSD-4-Clause-Shortened",
|
||||||
|
"BSD-4-Clause-UC",
|
||||||
|
"BSD-Protection",
|
||||||
|
"BSD-Source-Code",
|
||||||
|
"BSL-1.0",
|
||||||
|
"BUSL-1.1",
|
||||||
|
"Bahyph",
|
||||||
|
"Barr",
|
||||||
|
"Beerware",
|
||||||
|
"Bison-exception-2.2",
|
||||||
|
"BitTorrent-1.0",
|
||||||
|
"BitTorrent-1.1",
|
||||||
|
"BlueOak-1.0.0",
|
||||||
|
"Bootloader-exception",
|
||||||
|
"Borceux",
|
||||||
|
"C-UDA-1.0",
|
||||||
|
"CAL-1.0",
|
||||||
|
"CAL-1.0-Combined-Work-Exception",
|
||||||
|
"CATOSL-1.1",
|
||||||
|
"CC-BY-1.0",
|
||||||
|
"CC-BY-2.0",
|
||||||
|
"CC-BY-2.5",
|
||||||
|
"CC-BY-3.0",
|
||||||
|
"CC-BY-3.0-AT",
|
||||||
|
"CC-BY-3.0-US",
|
||||||
|
"CC-BY-4.0",
|
||||||
|
"CC-BY-NC-1.0",
|
||||||
|
"CC-BY-NC-2.0",
|
||||||
|
"CC-BY-NC-2.5",
|
||||||
|
"CC-BY-NC-3.0",
|
||||||
|
"CC-BY-NC-4.0",
|
||||||
|
"CC-BY-NC-ND-1.0",
|
||||||
|
"CC-BY-NC-ND-2.0",
|
||||||
|
"CC-BY-NC-ND-2.5",
|
||||||
|
"CC-BY-NC-ND-3.0",
|
||||||
|
"CC-BY-NC-ND-3.0-IGO",
|
||||||
|
"CC-BY-NC-ND-4.0",
|
||||||
|
"CC-BY-NC-SA-1.0",
|
||||||
|
"CC-BY-NC-SA-2.0",
|
||||||
|
"CC-BY-NC-SA-2.5",
|
||||||
|
"CC-BY-NC-SA-3.0",
|
||||||
|
"CC-BY-NC-SA-4.0",
|
||||||
|
"CC-BY-ND-1.0",
|
||||||
|
"CC-BY-ND-2.0",
|
||||||
|
"CC-BY-ND-2.5",
|
||||||
|
"CC-BY-ND-3.0",
|
||||||
|
"CC-BY-ND-4.0",
|
||||||
|
"CC-BY-SA-1.0",
|
||||||
|
"CC-BY-SA-2.0",
|
||||||
|
"CC-BY-SA-2.0-UK",
|
||||||
|
"CC-BY-SA-2.1-JP",
|
||||||
|
"CC-BY-SA-2.5",
|
||||||
|
"CC-BY-SA-3.0",
|
||||||
|
"CC-BY-SA-3.0-AT",
|
||||||
|
"CC-BY-SA-4.0",
|
||||||
|
"CC-PDDC",
|
||||||
|
"CC0-1.0",
|
||||||
|
"CDDL-1.0",
|
||||||
|
"CDDL-1.1",
|
||||||
|
"CDLA-Permissive-1.0",
|
||||||
|
"CDLA-Sharing-1.0",
|
||||||
|
"CECILL-1.0",
|
||||||
|
"CECILL-1.1",
|
||||||
|
"CECILL-2.0",
|
||||||
|
"CECILL-2.1",
|
||||||
|
"CECILL-B",
|
||||||
|
"CECILL-C",
|
||||||
|
"CERN-OHL-1.1",
|
||||||
|
"CERN-OHL-1.2",
|
||||||
|
"CERN-OHL-P-2.0",
|
||||||
|
"CERN-OHL-S-2.0",
|
||||||
|
"CERN-OHL-W-2.0",
|
||||||
|
"CLISP-exception-2.0",
|
||||||
|
"CNRI-Jython",
|
||||||
|
"CNRI-Python",
|
||||||
|
"CNRI-Python-GPL-Compatible",
|
||||||
|
"CPAL-1.0",
|
||||||
|
"CPL-1.0",
|
||||||
|
"CPOL-1.02",
|
||||||
|
"CUA-OPL-1.0",
|
||||||
|
"Caldera",
|
||||||
|
"ClArtistic",
|
||||||
|
"Classpath-exception-2.0",
|
||||||
|
"Condor-1.1",
|
||||||
|
"Crossword",
|
||||||
|
"CrystalStacker",
|
||||||
|
"Cube",
|
||||||
|
"D-FSL-1.0",
|
||||||
|
"DOC",
|
||||||
|
"DRL-1.0",
|
||||||
|
"DSDP",
|
||||||
|
"DigiRule-FOSS-exception",
|
||||||
|
"Dotseqn",
|
||||||
|
"ECL-1.0",
|
||||||
|
"ECL-2.0",
|
||||||
|
"EFL-1.0",
|
||||||
|
"EFL-2.0",
|
||||||
|
"EPICS",
|
||||||
|
"EPL-1.0",
|
||||||
|
"EPL-2.0",
|
||||||
|
"EUDatagrid",
|
||||||
|
"EUPL-1.0",
|
||||||
|
"EUPL-1.1",
|
||||||
|
"EUPL-1.2",
|
||||||
|
"Entessa",
|
||||||
|
"ErlPL-1.1",
|
||||||
|
"Eurosym",
|
||||||
|
"FLTK-exception",
|
||||||
|
"FSFAP",
|
||||||
|
"FSFUL",
|
||||||
|
"FSFULLR",
|
||||||
|
"FTL",
|
||||||
|
"Fair",
|
||||||
|
"Fawkes-Runtime-exception",
|
||||||
|
"Font-exception-2.0",
|
||||||
|
"Frameworx-1.0",
|
||||||
|
"FreeBSD-DOC",
|
||||||
|
"FreeImage",
|
||||||
|
"GCC-exception-2.0",
|
||||||
|
"GCC-exception-3.1",
|
||||||
|
"GD",
|
||||||
|
"GFDL-1.1",
|
||||||
|
"GFDL-1.1-invariants-only",
|
||||||
|
"GFDL-1.1-invariants-or-later",
|
||||||
|
"GFDL-1.1-no-invariants-only",
|
||||||
|
"GFDL-1.1-no-invariants-or-later",
|
||||||
|
"GFDL-1.1-only",
|
||||||
|
"GFDL-1.1-or-later",
|
||||||
|
"GFDL-1.2",
|
||||||
|
"GFDL-1.2-invariants-only",
|
||||||
|
"GFDL-1.2-invariants-or-later",
|
||||||
|
"GFDL-1.2-no-invariants-only",
|
||||||
|
"GFDL-1.2-no-invariants-or-later",
|
||||||
|
"GFDL-1.2-only",
|
||||||
|
"GFDL-1.2-or-later",
|
||||||
|
"GFDL-1.3",
|
||||||
|
"GFDL-1.3-invariants-only",
|
||||||
|
"GFDL-1.3-invariants-or-later",
|
||||||
|
"GFDL-1.3-no-invariants-only",
|
||||||
|
"GFDL-1.3-no-invariants-or-later",
|
||||||
|
"GFDL-1.3-only",
|
||||||
|
"GFDL-1.3-or-later",
|
||||||
|
"GL2PS",
|
||||||
|
"GLWTPL",
|
||||||
|
"GPL-1.0",
|
||||||
|
"GPL-1.0+",
|
||||||
|
"GPL-1.0-only",
|
||||||
|
"GPL-1.0-or-later",
|
||||||
|
"GPL-2.0",
|
||||||
|
"GPL-2.0+",
|
||||||
|
"GPL-2.0-only",
|
||||||
|
"GPL-2.0-or-later",
|
||||||
|
"GPL-2.0-with-GCC-exception",
|
||||||
|
"GPL-2.0-with-autoconf-exception",
|
||||||
|
"GPL-2.0-with-bison-exception",
|
||||||
|
"GPL-2.0-with-classpath-exception",
|
||||||
|
"GPL-2.0-with-font-exception",
|
||||||
|
"GPL-3.0",
|
||||||
|
"GPL-3.0+",
|
||||||
|
"GPL-3.0-linking-exception",
|
||||||
|
"GPL-3.0-linking-source-exception",
|
||||||
|
"GPL-3.0-only",
|
||||||
|
"GPL-3.0-or-later",
|
||||||
|
"GPL-3.0-with-GCC-exception",
|
||||||
|
"GPL-3.0-with-autoconf-exception",
|
||||||
|
"GPL-CC-1.0",
|
||||||
|
"Giftware",
|
||||||
|
"Glide",
|
||||||
|
"Glulxe",
|
||||||
|
"HPND",
|
||||||
|
"HPND-sell-variant",
|
||||||
|
"HTMLTIDY",
|
||||||
|
"HaskellReport",
|
||||||
|
"Hippocratic-2.1",
|
||||||
|
"IBM-pibs",
|
||||||
|
"ICU",
|
||||||
|
"IJG",
|
||||||
|
"IPA",
|
||||||
|
"IPL-1.0",
|
||||||
|
"ISC",
|
||||||
|
"ImageMagick",
|
||||||
|
"Imlib2",
|
||||||
|
"Info-ZIP",
|
||||||
|
"Intel",
|
||||||
|
"Intel-ACPI",
|
||||||
|
"Interbase-1.0",
|
||||||
|
"JPNIC",
|
||||||
|
"JSON",
|
||||||
|
"JasPer-2.0",
|
||||||
|
"LAL-1.2",
|
||||||
|
"LAL-1.3",
|
||||||
|
"LGPL-2.0",
|
||||||
|
"LGPL-2.0+",
|
||||||
|
"LGPL-2.0-only",
|
||||||
|
"LGPL-2.0-or-later",
|
||||||
|
"LGPL-2.1",
|
||||||
|
"LGPL-2.1+",
|
||||||
|
"LGPL-2.1-only",
|
||||||
|
"LGPL-2.1-or-later",
|
||||||
|
"LGPL-3.0",
|
||||||
|
"LGPL-3.0+",
|
||||||
|
"LGPL-3.0-linking-exception",
|
||||||
|
"LGPL-3.0-only",
|
||||||
|
"LGPL-3.0-or-later",
|
||||||
|
"LGPLLR",
|
||||||
|
"LLVM-exception",
|
||||||
|
"LPL-1.0",
|
||||||
|
"LPL-1.02",
|
||||||
|
"LPPL-1.0",
|
||||||
|
"LPPL-1.1",
|
||||||
|
"LPPL-1.2",
|
||||||
|
"LPPL-1.3a",
|
||||||
|
"LPPL-1.3c",
|
||||||
|
"LZMA-exception",
|
||||||
|
"Latex2e",
|
||||||
|
"Leptonica",
|
||||||
|
"LiLiQ-P-1.1",
|
||||||
|
"LiLiQ-R-1.1",
|
||||||
|
"LiLiQ-Rplus-1.1",
|
||||||
|
"Libpng",
|
||||||
|
"Libtool-exception",
|
||||||
|
"Linux-OpenIB",
|
||||||
|
"Linux-syscall-note",
|
||||||
|
"MIT",
|
||||||
|
"MIT-0",
|
||||||
|
"MIT-CMU",
|
||||||
|
"MIT-Modern-Variant",
|
||||||
|
"MIT-advertising",
|
||||||
|
"MIT-enna",
|
||||||
|
"MIT-feh",
|
||||||
|
"MIT-open-group",
|
||||||
|
"MITNFA",
|
||||||
|
"MPL-1.0",
|
||||||
|
"MPL-1.1",
|
||||||
|
"MPL-2.0",
|
||||||
|
"MPL-2.0-no-copyleft-exception",
|
||||||
|
"MS-PL",
|
||||||
|
"MS-RL",
|
||||||
|
"MTLL",
|
||||||
|
"MakeIndex",
|
||||||
|
"MirOS",
|
||||||
|
"Motosoto",
|
||||||
|
"MulanPSL-1.0",
|
||||||
|
"MulanPSL-2.0",
|
||||||
|
"Multics",
|
||||||
|
"Mup",
|
||||||
|
"NAIST-2003",
|
||||||
|
"NASA-1.3",
|
||||||
|
"NBPL-1.0",
|
||||||
|
"NCGL-UK-2.0",
|
||||||
|
"NCSA",
|
||||||
|
"NGPL",
|
||||||
|
"NIST-PD",
|
||||||
|
"NIST-PD-fallback",
|
||||||
|
"NLOD-1.0",
|
||||||
|
"NLPL",
|
||||||
|
"NOSL",
|
||||||
|
"NPL-1.0",
|
||||||
|
"NPL-1.1",
|
||||||
|
"NPOSL-3.0",
|
||||||
|
"NRL",
|
||||||
|
"NTP",
|
||||||
|
"NTP-0",
|
||||||
|
"Naumen",
|
||||||
|
"Net-SNMP",
|
||||||
|
"NetCDF",
|
||||||
|
"Newsletr",
|
||||||
|
"Nokia",
|
||||||
|
"Nokia-Qt-exception-1.1",
|
||||||
|
"Noweb",
|
||||||
|
"Nunit",
|
||||||
|
"O-UDA-1.0",
|
||||||
|
"OCCT-PL",
|
||||||
|
"OCCT-exception-1.0",
|
||||||
|
"OCLC-2.0",
|
||||||
|
"OCaml-LGPL-linking-exception",
|
||||||
|
"ODC-By-1.0",
|
||||||
|
"ODbL-1.0",
|
||||||
|
"OFL-1.0",
|
||||||
|
"OFL-1.0-RFN",
|
||||||
|
"OFL-1.0-no-RFN",
|
||||||
|
"OFL-1.1",
|
||||||
|
"OFL-1.1-RFN",
|
||||||
|
"OFL-1.1-no-RFN",
|
||||||
|
"OGC-1.0",
|
||||||
|
"OGDL-Taiwan-1.0",
|
||||||
|
"OGL-Canada-2.0",
|
||||||
|
"OGL-UK-1.0",
|
||||||
|
"OGL-UK-2.0",
|
||||||
|
"OGL-UK-3.0",
|
||||||
|
"OGTSL",
|
||||||
|
"OLDAP-1.1",
|
||||||
|
"OLDAP-1.2",
|
||||||
|
"OLDAP-1.3",
|
||||||
|
"OLDAP-1.4",
|
||||||
|
"OLDAP-2.0",
|
||||||
|
"OLDAP-2.0.1",
|
||||||
|
"OLDAP-2.1",
|
||||||
|
"OLDAP-2.2",
|
||||||
|
"OLDAP-2.2.1",
|
||||||
|
"OLDAP-2.2.2",
|
||||||
|
"OLDAP-2.3",
|
||||||
|
"OLDAP-2.4",
|
||||||
|
"OLDAP-2.5",
|
||||||
|
"OLDAP-2.6",
|
||||||
|
"OLDAP-2.7",
|
||||||
|
"OLDAP-2.8",
|
||||||
|
"OML",
|
||||||
|
"OPL-1.0",
|
||||||
|
"OSET-PL-2.1",
|
||||||
|
"OSL-1.0",
|
||||||
|
"OSL-1.1",
|
||||||
|
"OSL-2.0",
|
||||||
|
"OSL-2.1",
|
||||||
|
"OSL-3.0",
|
||||||
|
"OpenJDK-assembly-exception-1.0",
|
||||||
|
"OpenSSL",
|
||||||
|
"PDDL-1.0",
|
||||||
|
"PHP-3.0",
|
||||||
|
"PHP-3.01",
|
||||||
|
"PS-or-PDF-font-exception-20170817",
|
||||||
|
"PSF-2.0",
|
||||||
|
"Parity-6.0.0",
|
||||||
|
"Parity-7.0.0",
|
||||||
|
"Plexus",
|
||||||
|
"PolyForm-Noncommercial-1.0.0",
|
||||||
|
"PolyForm-Small-Business-1.0.0",
|
||||||
|
"PostgreSQL",
|
||||||
|
"Python-2.0",
|
||||||
|
"QPL-1.0",
|
||||||
|
"Qhull",
|
||||||
|
"Qt-GPL-exception-1.0",
|
||||||
|
"Qt-LGPL-exception-1.1",
|
||||||
|
"Qwt-exception-1.0",
|
||||||
|
"RHeCos-1.1",
|
||||||
|
"RPL-1.1",
|
||||||
|
"RPL-1.5",
|
||||||
|
"RPSL-1.0",
|
||||||
|
"RSA-MD",
|
||||||
|
"RSCPL",
|
||||||
|
"Rdisc",
|
||||||
|
"Ruby",
|
||||||
|
"SAX-PD",
|
||||||
|
"SCEA",
|
||||||
|
"SGI-B-1.0",
|
||||||
|
"SGI-B-1.1",
|
||||||
|
"SGI-B-2.0",
|
||||||
|
"SHL-0.5",
|
||||||
|
"SHL-0.51",
|
||||||
|
"SHL-2.0",
|
||||||
|
"SHL-2.1",
|
||||||
|
"SISSL",
|
||||||
|
"SISSL-1.2",
|
||||||
|
"SMLNJ",
|
||||||
|
"SMPPL",
|
||||||
|
"SNIA",
|
||||||
|
"SPL-1.0",
|
||||||
|
"SSH-OpenSSH",
|
||||||
|
"SSH-short",
|
||||||
|
"SSPL-1.0",
|
||||||
|
"SWL",
|
||||||
|
"Saxpath",
|
||||||
|
"Sendmail",
|
||||||
|
"Sendmail-8.23",
|
||||||
|
"SimPL-2.0",
|
||||||
|
"Sleepycat",
|
||||||
|
"Spencer-86",
|
||||||
|
"Spencer-94",
|
||||||
|
"Spencer-99",
|
||||||
|
"StandardML-NJ",
|
||||||
|
"SugarCRM-1.1.3",
|
||||||
|
"Swift-exception",
|
||||||
|
"TAPR-OHL-1.0",
|
||||||
|
"TCL",
|
||||||
|
"TCP-wrappers",
|
||||||
|
"TMate",
|
||||||
|
"TORQUE-1.1",
|
||||||
|
"TOSL",
|
||||||
|
"TU-Berlin-1.0",
|
||||||
|
"TU-Berlin-2.0",
|
||||||
|
"UCL-1.0",
|
||||||
|
"UPL-1.0",
|
||||||
|
"Unicode-DFS-2015",
|
||||||
|
"Unicode-DFS-2016",
|
||||||
|
"Unicode-TOU",
|
||||||
|
"Universal-FOSS-exception-1.0",
|
||||||
|
"Unlicense",
|
||||||
|
"VOSTROM",
|
||||||
|
"VSL-1.0",
|
||||||
|
"Vim",
|
||||||
|
"W3C",
|
||||||
|
"W3C-19980720",
|
||||||
|
"W3C-20150513",
|
||||||
|
"WTFPL",
|
||||||
|
"Watcom-1.0",
|
||||||
|
"Wsuipa",
|
||||||
|
"WxWindows-exception-3.1",
|
||||||
|
"X11",
|
||||||
|
"XFree86-1.1",
|
||||||
|
"XSkat",
|
||||||
|
"Xerox",
|
||||||
|
"Xnet",
|
||||||
|
"YPL-1.0",
|
||||||
|
"YPL-1.1",
|
||||||
|
"ZPL-1.1",
|
||||||
|
"ZPL-2.0",
|
||||||
|
"ZPL-2.1",
|
||||||
|
"Zed",
|
||||||
|
"Zend-2.0",
|
||||||
|
"Zimbra-1.3",
|
||||||
|
"Zimbra-1.4",
|
||||||
|
"Zlib",
|
||||||
|
"blessing",
|
||||||
|
"bzip2-1.0.5",
|
||||||
|
"bzip2-1.0.6",
|
||||||
|
"copyleft-next-0.3.0",
|
||||||
|
"copyleft-next-0.3.1",
|
||||||
|
"curl",
|
||||||
|
"diffmark",
|
||||||
|
"dvipdfm",
|
||||||
|
"eCos-2.0",
|
||||||
|
"eCos-exception-2.0",
|
||||||
|
"eGenix",
|
||||||
|
"etalab-2.0",
|
||||||
|
"freertos-exception-2.0",
|
||||||
|
"gSOAP-1.3b",
|
||||||
|
"gnu-javamail-exception",
|
||||||
|
"gnuplot",
|
||||||
|
"i2p-gpl-java-exception",
|
||||||
|
"iMatix",
|
||||||
|
"libpng-2.0",
|
||||||
|
"libselinux-1.0",
|
||||||
|
"libtiff",
|
||||||
|
"mif-exception",
|
||||||
|
"mpich2",
|
||||||
|
"openvpn-openssl-exception",
|
||||||
|
"psfrag",
|
||||||
|
"psutils",
|
||||||
|
"u-boot-exception-2.0",
|
||||||
|
"wxWindows",
|
||||||
|
"xinetd",
|
||||||
|
"xpp",
|
||||||
|
"zlib-acknowledgement",
|
||||||
|
]
|
123
scripts/west_commands/zspdx/sbom.py
Normal file
123
scripts/west_commands/zspdx/sbom.py
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
# 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
|
218
scripts/west_commands/zspdx/scanner.py
Normal file
218
scripts/west_commands/zspdx/scanner.py
Normal file
|
@ -0,0 +1,218 @@
|
||||||
|
# Copyright (c) 2020, 2021 The Linux Foundation
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
from west import log
|
||||||
|
|
||||||
|
from zspdx.licenses import LICENSES
|
||||||
|
from zspdx.util import getHashes
|
||||||
|
|
||||||
|
# ScannerConfig contains settings used to configure how the SPDX
|
||||||
|
# Document scanning should occur.
|
||||||
|
class ScannerConfig:
|
||||||
|
def __init__(self):
|
||||||
|
super(ScannerConfig, self).__init__()
|
||||||
|
|
||||||
|
# when assembling a Package's data, should we auto-conclude the
|
||||||
|
# Package's license, based on the licenses of its Files?
|
||||||
|
self.shouldConcludePackageLicense = True
|
||||||
|
|
||||||
|
# when assembling a Package's Files' data, should we auto-conclude
|
||||||
|
# each File's license, based on its detected license(s)?
|
||||||
|
self.shouldConcludeFileLicenses = True
|
||||||
|
|
||||||
|
# number of lines to scan for SPDX-License-Identifier (0 = all)
|
||||||
|
# defaults to 20
|
||||||
|
self.numLinesScanned = 20
|
||||||
|
|
||||||
|
# should we calculate SHA256 hashes for each Package's Files?
|
||||||
|
# note that SHA1 hashes are mandatory, per SPDX 2.2
|
||||||
|
self.doSHA256 = True
|
||||||
|
|
||||||
|
# should we calculate MD5 hashes for each Package's Files?
|
||||||
|
self.doMD5 = False
|
||||||
|
|
||||||
|
def parseLineForExpression(line):
|
||||||
|
"""Return parsed SPDX expression if tag found in line, or None otherwise."""
|
||||||
|
p = line.partition("SPDX-License-Identifier:")
|
||||||
|
if p[2] == "":
|
||||||
|
return None
|
||||||
|
# strip away trailing comment marks and whitespace, if any
|
||||||
|
expression = p[2].strip()
|
||||||
|
expression = expression.rstrip("/*")
|
||||||
|
expression = expression.strip()
|
||||||
|
return expression
|
||||||
|
|
||||||
|
def getExpressionData(filePath, numLines):
|
||||||
|
"""
|
||||||
|
Scans the specified file for the first SPDX-License-Identifier:
|
||||||
|
tag in the file.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
- filePath: path to file to scan.
|
||||||
|
- numLines: number of lines to scan for an expression before
|
||||||
|
giving up. If 0, will scan the entire file.
|
||||||
|
Returns: parsed expression if found; None if not found.
|
||||||
|
"""
|
||||||
|
log.dbg(f" - getting licenses for {filePath}")
|
||||||
|
|
||||||
|
with open(filePath, "r") as f:
|
||||||
|
try:
|
||||||
|
lineno = 0
|
||||||
|
for line in f:
|
||||||
|
lineno += 1
|
||||||
|
if lineno > numLines > 0:
|
||||||
|
break
|
||||||
|
expression = parseLineForExpression(line)
|
||||||
|
if expression is not None:
|
||||||
|
return expression
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
# invalid UTF-8 content
|
||||||
|
return None
|
||||||
|
|
||||||
|
# if we get here, we didn't find an expression
|
||||||
|
return None
|
||||||
|
|
||||||
|
def splitExpression(expression):
|
||||||
|
"""
|
||||||
|
Parse a license expression into its constituent identifiers.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
- expression: SPDX license expression
|
||||||
|
Returns: array of split identifiers
|
||||||
|
"""
|
||||||
|
# remove parens and plus sign
|
||||||
|
e2 = re.sub(r'\(|\)|\+', "", expression, flags=re.IGNORECASE)
|
||||||
|
|
||||||
|
# remove word operators, ignoring case, leaving a blank space
|
||||||
|
e3 = re.sub(r' AND | OR | WITH ', " ", e2, flags=re.IGNORECASE)
|
||||||
|
|
||||||
|
# and split on space
|
||||||
|
e4 = e3.split(" ")
|
||||||
|
|
||||||
|
return sorted(e4)
|
||||||
|
|
||||||
|
def calculateVerificationCode(pkg):
|
||||||
|
"""
|
||||||
|
Calculate the SPDX Package Verification Code for all files in the package.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
- pkg: Package
|
||||||
|
Returns: verification code as string
|
||||||
|
"""
|
||||||
|
hashes = []
|
||||||
|
for f in pkg.files.values():
|
||||||
|
hashes.append(f.sha1)
|
||||||
|
hashes.sort()
|
||||||
|
filelist = "".join(hashes)
|
||||||
|
|
||||||
|
hSHA1 = hashlib.sha1()
|
||||||
|
hSHA1.update(filelist.encode('utf-8'))
|
||||||
|
return hSHA1.hexdigest()
|
||||||
|
|
||||||
|
def checkLicenseValid(lic, doc):
|
||||||
|
"""
|
||||||
|
Check whether this license ID is a valid SPDX license ID, and add it
|
||||||
|
to the custom license IDs set for this Document if it isn't.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
- lic: detected license ID
|
||||||
|
- doc: Document
|
||||||
|
"""
|
||||||
|
if lic not in LICENSES:
|
||||||
|
doc.customLicenseIDs.add(lic)
|
||||||
|
|
||||||
|
def getPackageLicenses(pkg):
|
||||||
|
"""
|
||||||
|
Extract lists of all concluded and infoInFile licenses seen.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
- pkg: Package
|
||||||
|
Returns: sorted list of concluded license exprs,
|
||||||
|
sorted list of infoInFile ID's
|
||||||
|
"""
|
||||||
|
licsConcluded = set()
|
||||||
|
licsFromFiles = set()
|
||||||
|
for f in pkg.files.values():
|
||||||
|
licsConcluded.add(f.concludedLicense)
|
||||||
|
for licInfo in f.licenseInfoInFile:
|
||||||
|
licsFromFiles.add(licInfo)
|
||||||
|
return sorted(list(licsConcluded)), sorted(list(licsFromFiles))
|
||||||
|
|
||||||
|
def normalizeExpression(licsConcluded):
|
||||||
|
"""
|
||||||
|
Combine array of license expressions into one AND'd expression,
|
||||||
|
adding parens where needed.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
- licsConcluded: array of license expressions
|
||||||
|
Returns: string with single AND'd expression.
|
||||||
|
"""
|
||||||
|
# return appropriate for simple cases
|
||||||
|
if len(licsConcluded) == 0:
|
||||||
|
return "NOASSERTION"
|
||||||
|
if len(licsConcluded) == 1:
|
||||||
|
return licsConcluded[0]
|
||||||
|
|
||||||
|
# more than one, so we'll need to combine them
|
||||||
|
# iff an expression has spaces, it needs parens
|
||||||
|
revised = []
|
||||||
|
for lic in licsConcluded:
|
||||||
|
if lic in ["NONE", "NOASSERTION"]:
|
||||||
|
continue
|
||||||
|
if " " in lic:
|
||||||
|
revised.append(f"({lic})")
|
||||||
|
else:
|
||||||
|
revised.append(lic)
|
||||||
|
return " AND ".join(revised)
|
||||||
|
|
||||||
|
def scanDocument(cfg, doc):
|
||||||
|
"""
|
||||||
|
Scan for licenses and calculate hashes for all Files and Packages
|
||||||
|
in this Document.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
- cfg: ScannerConfig
|
||||||
|
- doc: Document
|
||||||
|
"""
|
||||||
|
for pkg in doc.pkgs.values():
|
||||||
|
log.inf(f"scanning files in package {pkg.cfg.name} in document {doc.cfg.name}")
|
||||||
|
|
||||||
|
# first, gather File data for this package
|
||||||
|
for f in pkg.files.values():
|
||||||
|
# set relpath based on package's relativeBaseDir
|
||||||
|
f.relpath = os.path.relpath(f.abspath, pkg.cfg.relativeBaseDir)
|
||||||
|
|
||||||
|
# get hashes for file
|
||||||
|
hashes = getHashes(f.abspath)
|
||||||
|
if not hashes:
|
||||||
|
log.wrn("unable to get hashes for file {f.abspath}; skipping")
|
||||||
|
continue
|
||||||
|
hSHA1, hSHA256, hMD5 = hashes
|
||||||
|
f.sha1 = hSHA1
|
||||||
|
if cfg.doSHA256:
|
||||||
|
f.sha256 = hSHA256
|
||||||
|
if cfg.doMD5:
|
||||||
|
f.md5 = hMD5
|
||||||
|
|
||||||
|
# get licenses for file
|
||||||
|
expression = getExpressionData(f.abspath, cfg.numLinesScanned)
|
||||||
|
if expression:
|
||||||
|
if cfg.shouldConcludeFileLicenses:
|
||||||
|
f.concludedLicense = expression
|
||||||
|
f.licenseInfoInFile = splitExpression(expression)
|
||||||
|
|
||||||
|
# check if any custom license IDs should be flagged for document
|
||||||
|
for lic in f.licenseInfoInFile:
|
||||||
|
checkLicenseValid(lic, doc)
|
||||||
|
|
||||||
|
# now, assemble the Package data
|
||||||
|
licsConcluded, licsFromFiles = getPackageLicenses(pkg)
|
||||||
|
if cfg.shouldConcludePackageLicense:
|
||||||
|
pkg.concludedLicense = normalizeExpression(licsConcluded)
|
||||||
|
pkg.licenseInfoFromFiles = licsFromFiles
|
||||||
|
pkg.verificationCode = calculateVerificationCode(pkg)
|
61
scripts/west_commands/zspdx/spdxids.py
Normal file
61
scripts/west_commands/zspdx/spdxids.py
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
# Copyright (c) 2020, 2021 The Linux Foundation
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
def getSPDXIDSafeCharacter(c):
|
||||||
|
"""
|
||||||
|
Converts a character to an SPDX-ID-safe character.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
- c: character to test
|
||||||
|
Returns: c if it is SPDX-ID-safe (letter, number, '-' or '.');
|
||||||
|
'-' otherwise
|
||||||
|
"""
|
||||||
|
if c.isalpha() or c.isdigit() or c == "-" or c == ".":
|
||||||
|
return c
|
||||||
|
return "-"
|
||||||
|
|
||||||
|
def convertToSPDXIDSafe(s):
|
||||||
|
"""
|
||||||
|
Converts a filename or other string to only SPDX-ID-safe characters.
|
||||||
|
Note that a separate check (such as in getUniqueID, below) will need
|
||||||
|
to be used to confirm that this is still a unique identifier, after
|
||||||
|
conversion.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
- s: string to be converted.
|
||||||
|
Returns: string with all non-safe characters replaced with dashes.
|
||||||
|
"""
|
||||||
|
return "".join([getSPDXIDSafeCharacter(c) for c in s])
|
||||||
|
|
||||||
|
def getUniqueFileID(filenameOnly, timesSeen):
|
||||||
|
"""
|
||||||
|
Find an SPDX ID that is unique among others seen so far.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
- filenameOnly: filename only (directories omitted) seeking ID.
|
||||||
|
- timesSeen: dict of all filename-only to number of times seen.
|
||||||
|
Returns: unique SPDX ID; updates timesSeen to include it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
converted = convertToSPDXIDSafe(filenameOnly)
|
||||||
|
spdxID = f"SPDXRef-File-{converted}"
|
||||||
|
|
||||||
|
# determine whether spdxID is unique so far, or not
|
||||||
|
filenameTimesSeen = timesSeen.get(converted, 0) + 1
|
||||||
|
if filenameTimesSeen > 1:
|
||||||
|
# we'll append the # of times seen to the end
|
||||||
|
spdxID += f"-{filenameTimesSeen}"
|
||||||
|
else:
|
||||||
|
# first time seeing this filename
|
||||||
|
# edge case: if the filename itself ends in "-{number}", then we
|
||||||
|
# need to add a "-1" to it, so that we don't end up overlapping
|
||||||
|
# with an appended number from a similarly-named file.
|
||||||
|
p = re.compile(r"-\d+$")
|
||||||
|
if p.search(converted):
|
||||||
|
spdxID += "-1"
|
||||||
|
|
||||||
|
timesSeen[converted] = filenameTimesSeen
|
||||||
|
return spdxID
|
33
scripts/west_commands/zspdx/util.py
Normal file
33
scripts/west_commands/zspdx/util.py
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# Copyright (c) 2020, 2021 The Linux Foundation
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from west import log
|
||||||
|
|
||||||
|
def getHashes(filePath):
|
||||||
|
"""
|
||||||
|
Scan for and return hashes.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
- filePath: path to file to scan.
|
||||||
|
Returns: tuple of (SHA1, SHA256, MD5) hashes for filePath, or
|
||||||
|
None if file is not found.
|
||||||
|
"""
|
||||||
|
hSHA1 = hashlib.sha1()
|
||||||
|
hSHA256 = hashlib.sha256()
|
||||||
|
hMD5 = hashlib.md5()
|
||||||
|
|
||||||
|
log.dbg(f" - getting hashes for {filePath}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(filePath, 'rb') as f:
|
||||||
|
buf = f.read()
|
||||||
|
hSHA1.update(buf)
|
||||||
|
hSHA256.update(buf)
|
||||||
|
hMD5.update(buf)
|
||||||
|
except OSError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return (hSHA1.hexdigest(), hSHA256.hexdigest(), hMD5.hexdigest())
|
652
scripts/west_commands/zspdx/walker.py
Normal file
652
scripts/west_commands/zspdx/walker.py
Normal file
|
@ -0,0 +1,652 @@
|
||||||
|
# Copyright (c) 2020-2021 The Linux Foundation
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from west import log
|
||||||
|
from west.util import west_topdir, WestNotFound
|
||||||
|
|
||||||
|
from zspdx.cmakecache import parseCMakeCacheFile
|
||||||
|
from zspdx.cmakefileapijson import parseReply
|
||||||
|
from zspdx.datatypes import DocumentConfig, Document, File, PackageConfig, Package, RelationshipDataElementType, RelationshipData, Relationship
|
||||||
|
from zspdx.getincludes import getCIncludes
|
||||||
|
import zspdx.spdxids
|
||||||
|
|
||||||
|
# WalkerConfig contains configuration data for the Walker.
|
||||||
|
class WalkerConfig:
|
||||||
|
def __init__(self):
|
||||||
|
super(WalkerConfig, self).__init__()
|
||||||
|
|
||||||
|
# prefix for Document namespaces; should not end with "/"
|
||||||
|
self.namespacePrefix = ""
|
||||||
|
|
||||||
|
# location of build directory
|
||||||
|
self.buildDir = ""
|
||||||
|
|
||||||
|
# should also analyze for included header files?
|
||||||
|
self.analyzeIncludes = False
|
||||||
|
|
||||||
|
# should also add an SPDX document for the SDK?
|
||||||
|
self.includeSDK = False
|
||||||
|
|
||||||
|
# Walker is the main analysis class: it walks through the CMake codemodel,
|
||||||
|
# build files, and corresponding source and SDK files, and gathers the
|
||||||
|
# information needed to build the SPDX data classes.
|
||||||
|
class Walker:
|
||||||
|
# initialize with WalkerConfig
|
||||||
|
def __init__(self, cfg):
|
||||||
|
super(Walker, self).__init__()
|
||||||
|
|
||||||
|
# configuration - WalkerConfig
|
||||||
|
self.cfg = cfg
|
||||||
|
|
||||||
|
# the various Documents that we will be building
|
||||||
|
self.docBuild = None
|
||||||
|
self.docZephyr = None
|
||||||
|
self.docApp = None
|
||||||
|
self.docSDK = None
|
||||||
|
|
||||||
|
# dict of absolute file path => the Document that owns that file
|
||||||
|
self.allFileLinks = {}
|
||||||
|
|
||||||
|
# queue of pending source Files to create, process and assign
|
||||||
|
self.pendingSources = []
|
||||||
|
|
||||||
|
# queue of pending relationships to create, process and assign
|
||||||
|
self.pendingRelationships = []
|
||||||
|
|
||||||
|
# parsed CMake codemodel
|
||||||
|
self.cm = None
|
||||||
|
|
||||||
|
# parsed CMake cache dict, once we have the build path
|
||||||
|
self.cmakeCache = {}
|
||||||
|
|
||||||
|
# C compiler path from parsed CMake cache
|
||||||
|
self.compilerPath = ""
|
||||||
|
|
||||||
|
# SDK install path from parsed CMake cache
|
||||||
|
self.sdkPath = ""
|
||||||
|
|
||||||
|
# primary entry point
|
||||||
|
def makeDocuments(self):
|
||||||
|
# parse CMake cache file and get compiler path
|
||||||
|
log.inf("parsing CMake Cache file")
|
||||||
|
self.getCacheFile()
|
||||||
|
|
||||||
|
# parse codemodel from Walker cfg's build dir
|
||||||
|
log.inf("parsing CMake Codemodel files")
|
||||||
|
self.cm = self.getCodemodel()
|
||||||
|
if not self.cm:
|
||||||
|
log.err("could not parse codemodel from CMake API reply; bailing")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# set up Documents
|
||||||
|
log.inf("setting up SPDX documents")
|
||||||
|
retval = self.setupDocuments()
|
||||||
|
if not retval:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# walk through targets in codemodel to gather information
|
||||||
|
log.inf("walking through targets")
|
||||||
|
self.walkTargets()
|
||||||
|
|
||||||
|
# walk through pending sources and create corresponding files
|
||||||
|
log.inf("walking through pending sources files")
|
||||||
|
self.walkPendingSources()
|
||||||
|
|
||||||
|
# walk through pending relationship data and create relationships
|
||||||
|
log.inf("walking through pending relationships")
|
||||||
|
self.walkRelationships()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
# parse cache file and pull out relevant data
|
||||||
|
def getCacheFile(self):
|
||||||
|
cacheFilePath = os.path.join(self.cfg.buildDir, "CMakeCache.txt")
|
||||||
|
self.cmakeCache = parseCMakeCacheFile(cacheFilePath)
|
||||||
|
if self.cmakeCache:
|
||||||
|
self.compilerPath = self.cmakeCache.get("CMAKE_C_COMPILER", "")
|
||||||
|
self.sdkPath = self.cmakeCache.get("ZEPHYR_SDK_INSTALL_DIR", "")
|
||||||
|
|
||||||
|
# determine path from build dir to CMake file-based API index file, then
|
||||||
|
# parse it and return the Codemodel
|
||||||
|
def getCodemodel(self):
|
||||||
|
log.dbg("getting codemodel from CMake API reply files")
|
||||||
|
|
||||||
|
# make sure the reply directory exists
|
||||||
|
cmakeReplyDirPath = os.path.join(self.cfg.buildDir, ".cmake", "api", "v1", "reply")
|
||||||
|
if not os.path.exists(cmakeReplyDirPath):
|
||||||
|
log.err(f'cmake api reply directory {cmakeReplyDirPath} does not exist')
|
||||||
|
log.err('was query directory created before cmake build ran?')
|
||||||
|
return None
|
||||||
|
if not os.path.isdir(cmakeReplyDirPath):
|
||||||
|
log.err(f'cmake api reply directory {cmakeReplyDirPath} exists but is not a directory')
|
||||||
|
return None
|
||||||
|
|
||||||
|
# find file with "index" prefix; there should only be one
|
||||||
|
indexFilePath = ""
|
||||||
|
for f in os.listdir(cmakeReplyDirPath):
|
||||||
|
if f.startswith("index"):
|
||||||
|
indexFilePath = os.path.join(cmakeReplyDirPath, f)
|
||||||
|
break
|
||||||
|
if indexFilePath == "":
|
||||||
|
# didn't find it
|
||||||
|
log.err(f'cmake api reply index file not found in {cmakeReplyDirPath}')
|
||||||
|
return None
|
||||||
|
|
||||||
|
# parse it
|
||||||
|
return parseReply(indexFilePath)
|
||||||
|
|
||||||
|
# set up Documents before beginning
|
||||||
|
def setupDocuments(self):
|
||||||
|
log.dbg("setting up placeholder documents")
|
||||||
|
|
||||||
|
# set up build document
|
||||||
|
cfgBuild = DocumentConfig()
|
||||||
|
cfgBuild.name = "build"
|
||||||
|
cfgBuild.namespace = self.cfg.namespacePrefix + "/build"
|
||||||
|
cfgBuild.docRefID = "DocumentRef-build"
|
||||||
|
self.docBuild = Document(cfgBuild)
|
||||||
|
|
||||||
|
# we'll create the build packages in walkTargets()
|
||||||
|
|
||||||
|
# the DESCRIBES relationship for the build document will be
|
||||||
|
# with the zephyr_final package
|
||||||
|
rd = RelationshipData()
|
||||||
|
rd.ownerType = RelationshipDataElementType.DOCUMENT
|
||||||
|
rd.ownerDocument = self.docBuild
|
||||||
|
rd.otherType = RelationshipDataElementType.TARGETNAME
|
||||||
|
rd.otherTargetName = "zephyr_final"
|
||||||
|
rd.rlnType = "DESCRIBES"
|
||||||
|
|
||||||
|
# add it to pending relationships queue
|
||||||
|
self.pendingRelationships.append(rd)
|
||||||
|
|
||||||
|
# set up zephyr document
|
||||||
|
cfgZephyr = DocumentConfig()
|
||||||
|
cfgZephyr.name = "zephyr-sources"
|
||||||
|
cfgZephyr.namespace = self.cfg.namespacePrefix + "/zephyr"
|
||||||
|
cfgZephyr.docRefID = "DocumentRef-zephyr"
|
||||||
|
self.docZephyr = Document(cfgZephyr)
|
||||||
|
|
||||||
|
# also set up zephyr sources package
|
||||||
|
cfgPackageZephyr = PackageConfig()
|
||||||
|
cfgPackageZephyr.name = "zephyr-sources"
|
||||||
|
cfgPackageZephyr.spdxID = "SPDXRef-zephyr-sources"
|
||||||
|
# relativeBaseDir is Zephyr sources topdir
|
||||||
|
try:
|
||||||
|
cfgPackageZephyr.relativeBaseDir = west_topdir(self.cm.paths_source)
|
||||||
|
except WestNotFound:
|
||||||
|
log.err(f"cannot find west_topdir for CMake Codemodel sources path {self.cm.paths_source}; bailing")
|
||||||
|
return False
|
||||||
|
pkgZephyr = Package(cfgPackageZephyr, self.docZephyr)
|
||||||
|
self.docZephyr.pkgs[pkgZephyr.cfg.spdxID] = pkgZephyr
|
||||||
|
|
||||||
|
# create DESCRIBES relationship data
|
||||||
|
rd = RelationshipData()
|
||||||
|
rd.ownerType = RelationshipDataElementType.DOCUMENT
|
||||||
|
rd.ownerDocument = self.docZephyr
|
||||||
|
rd.otherType = RelationshipDataElementType.PACKAGEID
|
||||||
|
rd.otherPackageID = cfgPackageZephyr.spdxID
|
||||||
|
rd.rlnType = "DESCRIBES"
|
||||||
|
|
||||||
|
# add it to pending relationships queue
|
||||||
|
self.pendingRelationships.append(rd)
|
||||||
|
|
||||||
|
# set up app document
|
||||||
|
cfgApp = DocumentConfig()
|
||||||
|
cfgApp.name = "app-sources"
|
||||||
|
cfgApp.namespace = self.cfg.namespacePrefix + "/app"
|
||||||
|
cfgApp.docRefID = "DocumentRef-app"
|
||||||
|
self.docApp = Document(cfgApp)
|
||||||
|
|
||||||
|
# also set up app sources package
|
||||||
|
cfgPackageApp = PackageConfig()
|
||||||
|
cfgPackageApp.name = "app-sources"
|
||||||
|
cfgPackageApp.spdxID = "SPDXRef-app-sources"
|
||||||
|
# relativeBaseDir is app sources dir
|
||||||
|
cfgPackageApp.relativeBaseDir = self.cm.paths_source
|
||||||
|
pkgApp = Package(cfgPackageApp, self.docApp)
|
||||||
|
self.docApp.pkgs[pkgApp.cfg.spdxID] = pkgApp
|
||||||
|
|
||||||
|
# create DESCRIBES relationship data
|
||||||
|
rd = RelationshipData()
|
||||||
|
rd.ownerType = RelationshipDataElementType.DOCUMENT
|
||||||
|
rd.ownerDocument = self.docApp
|
||||||
|
rd.otherType = RelationshipDataElementType.PACKAGEID
|
||||||
|
rd.otherPackageID = cfgPackageApp.spdxID
|
||||||
|
rd.rlnType = "DESCRIBES"
|
||||||
|
|
||||||
|
# add it to pending relationships queue
|
||||||
|
self.pendingRelationships.append(rd)
|
||||||
|
|
||||||
|
if self.cfg.includeSDK:
|
||||||
|
# set up SDK document
|
||||||
|
cfgSDK = DocumentConfig()
|
||||||
|
cfgSDK.name = "sdk"
|
||||||
|
cfgSDK.namespace = self.cfg.namespacePrefix + "/sdk"
|
||||||
|
cfgSDK.docRefID = "DocumentRef-sdk"
|
||||||
|
self.docSDK = Document(cfgSDK)
|
||||||
|
|
||||||
|
# also set up zephyr sdk package
|
||||||
|
cfgPackageSDK = PackageConfig()
|
||||||
|
cfgPackageSDK.name = "sdk"
|
||||||
|
cfgPackageSDK.spdxID = "SPDXRef-sdk"
|
||||||
|
# relativeBaseDir is SDK dir
|
||||||
|
cfgPackageSDK.relativeBaseDir = self.sdkPath
|
||||||
|
pkgSDK = Package(cfgPackageSDK, self.docSDK)
|
||||||
|
self.docSDK.pkgs[pkgSDK.cfg.spdxID] = pkgSDK
|
||||||
|
|
||||||
|
# create DESCRIBES relationship data
|
||||||
|
rd = RelationshipData()
|
||||||
|
rd.ownerType = RelationshipDataElementType.DOCUMENT
|
||||||
|
rd.ownerDocument = self.docSDK
|
||||||
|
rd.otherType = RelationshipDataElementType.PACKAGEID
|
||||||
|
rd.otherPackageID = cfgPackageSDK.spdxID
|
||||||
|
rd.rlnType = "DESCRIBES"
|
||||||
|
|
||||||
|
# add it to pending relationships queue
|
||||||
|
self.pendingRelationships.append(rd)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
# walk through targets and gather information
|
||||||
|
def walkTargets(self):
|
||||||
|
log.dbg("walking targets from codemodel")
|
||||||
|
|
||||||
|
# assuming just one configuration; consider whether this is incorrect
|
||||||
|
cfgTargets = self.cm.configurations[0].configTargets
|
||||||
|
for cfgTarget in cfgTargets:
|
||||||
|
# build the Package for this target
|
||||||
|
pkg = self.initConfigTargetPackage(cfgTarget)
|
||||||
|
|
||||||
|
# see whether this target has any build artifacts at all
|
||||||
|
if len(cfgTarget.target.artifacts) > 0:
|
||||||
|
# add its build file
|
||||||
|
bf = self.addBuildFile(cfgTarget, pkg)
|
||||||
|
|
||||||
|
# get its source files
|
||||||
|
self.collectPendingSourceFiles(cfgTarget, pkg, bf)
|
||||||
|
else:
|
||||||
|
log.dbg(f" - target {cfgTarget.name} has no build artifacts")
|
||||||
|
|
||||||
|
# get its target dependencies
|
||||||
|
self.collectTargetDependencies(cfgTargets, cfgTarget, pkg)
|
||||||
|
|
||||||
|
# build a Package in the Build doc for the given ConfigTarget
|
||||||
|
def initConfigTargetPackage(self, cfgTarget):
|
||||||
|
log.dbg(f" - initializing Package for target: {cfgTarget.name}")
|
||||||
|
|
||||||
|
# create target Package's config
|
||||||
|
cfg = PackageConfig()
|
||||||
|
cfg.name = cfgTarget.name
|
||||||
|
cfg.spdxID = "SPDXRef-" + zspdx.spdxids.convertToSPDXIDSafe(cfgTarget.name)
|
||||||
|
cfg.relativeBaseDir = self.cm.paths_build
|
||||||
|
|
||||||
|
# build Package
|
||||||
|
pkg = Package(cfg, self.docBuild)
|
||||||
|
|
||||||
|
# add Package to build Document
|
||||||
|
self.docBuild.pkgs[cfg.spdxID] = pkg
|
||||||
|
return pkg
|
||||||
|
|
||||||
|
# create a target's build product File and add it to its Package
|
||||||
|
# call with:
|
||||||
|
# 1) ConfigTarget
|
||||||
|
# 2) Package for that target
|
||||||
|
# returns: File
|
||||||
|
def addBuildFile(self, cfgTarget, pkg):
|
||||||
|
# assumes only one artifact in each target
|
||||||
|
artifactPath = os.path.join(pkg.cfg.relativeBaseDir, cfgTarget.target.artifacts[0])
|
||||||
|
log.dbg(f" - adding File {artifactPath}")
|
||||||
|
log.dbg(f" - relativeBaseDir: {pkg.cfg.relativeBaseDir}")
|
||||||
|
log.dbg(f" - artifacts[0]: {cfgTarget.target.artifacts[0]}")
|
||||||
|
|
||||||
|
# create build File
|
||||||
|
bf = File(self.docBuild, pkg)
|
||||||
|
bf.abspath = artifactPath
|
||||||
|
bf.relpath = cfgTarget.target.artifacts[0]
|
||||||
|
# can use nameOnDisk b/c it is just the filename w/out directory paths
|
||||||
|
bf.spdxID = zspdx.spdxids.getUniqueFileID(cfgTarget.target.nameOnDisk, self.docBuild.timesSeen)
|
||||||
|
# don't fill hashes / licenses / rlns now, we'll do that after walking
|
||||||
|
|
||||||
|
# add File to Package
|
||||||
|
pkg.files[bf.spdxID] = bf
|
||||||
|
|
||||||
|
# add file path link to Document and global links
|
||||||
|
self.docBuild.fileLinks[bf.abspath] = bf
|
||||||
|
self.allFileLinks[bf.abspath] = self.docBuild
|
||||||
|
|
||||||
|
# also set this file as the target package's build product file
|
||||||
|
pkg.targetBuildFile = bf
|
||||||
|
|
||||||
|
return bf
|
||||||
|
|
||||||
|
# collect a target's source files, add to pending sources queue, and
|
||||||
|
# create pending relationship data entry
|
||||||
|
# call with:
|
||||||
|
# 1) ConfigTarget
|
||||||
|
# 2) Package for that target
|
||||||
|
# 3) build File for that target
|
||||||
|
def collectPendingSourceFiles(self, cfgTarget, pkg, bf):
|
||||||
|
log.dbg(f" - collecting source files and adding to pending queue")
|
||||||
|
|
||||||
|
targetIncludesSet = set()
|
||||||
|
|
||||||
|
# walk through target's sources
|
||||||
|
for src in cfgTarget.target.sources:
|
||||||
|
log.dbg(f" - add pending source file and relationship for {src.path}")
|
||||||
|
# get absolute path if we don't have it
|
||||||
|
srcAbspath = src.path
|
||||||
|
if not os.path.isabs(src.path):
|
||||||
|
srcAbspath = os.path.join(self.cm.paths_source, src.path)
|
||||||
|
|
||||||
|
# check whether it even exists
|
||||||
|
if not (os.path.exists(srcAbspath) and os.path.isfile(srcAbspath)):
|
||||||
|
log.dbg(f" - {srcAbspath} does not exist but is referenced in sources for target {pkg.cfg.name}; skipping")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# add it to pending source files queue
|
||||||
|
self.pendingSources.append(srcAbspath)
|
||||||
|
|
||||||
|
# create relationship data
|
||||||
|
rd = RelationshipData()
|
||||||
|
rd.ownerType = RelationshipDataElementType.FILENAME
|
||||||
|
rd.ownerFileAbspath = bf.abspath
|
||||||
|
rd.otherType = RelationshipDataElementType.FILENAME
|
||||||
|
rd.otherFileAbspath = srcAbspath
|
||||||
|
rd.rlnType = "GENERATED_FROM"
|
||||||
|
|
||||||
|
# add it to pending relationships queue
|
||||||
|
self.pendingRelationships.append(rd)
|
||||||
|
|
||||||
|
# collect this source file's includes
|
||||||
|
if self.cfg.analyzeIncludes and self.compilerPath:
|
||||||
|
includes = self.collectIncludes(cfgTarget, pkg, bf, src)
|
||||||
|
for inc in includes:
|
||||||
|
targetIncludesSet.add(inc)
|
||||||
|
|
||||||
|
# make relationships for the overall included files,
|
||||||
|
# avoiding duplicates for multiple source files including
|
||||||
|
# the same headers
|
||||||
|
targetIncludesList = list(targetIncludesSet)
|
||||||
|
targetIncludesList.sort()
|
||||||
|
for inc in targetIncludesList:
|
||||||
|
# add it to pending source files queue
|
||||||
|
self.pendingSources.append(inc)
|
||||||
|
|
||||||
|
# create relationship data
|
||||||
|
rd = RelationshipData()
|
||||||
|
rd.ownerType = RelationshipDataElementType.FILENAME
|
||||||
|
rd.ownerFileAbspath = bf.abspath
|
||||||
|
rd.otherType = RelationshipDataElementType.FILENAME
|
||||||
|
rd.otherFileAbspath = inc
|
||||||
|
rd.rlnType = "GENERATED_FROM"
|
||||||
|
|
||||||
|
# add it to pending relationships queue
|
||||||
|
self.pendingRelationships.append(rd)
|
||||||
|
|
||||||
|
# collect the include files corresponding to this source file
|
||||||
|
# call with:
|
||||||
|
# 1) ConfigTarget
|
||||||
|
# 2) Package for this target
|
||||||
|
# 3) build File for this target
|
||||||
|
# 4) TargetSource entry for this source file
|
||||||
|
# returns: sorted list of include files for this source file
|
||||||
|
def collectIncludes(self, cfgTarget, pkg, bf, src):
|
||||||
|
# get the right compile group for this source file
|
||||||
|
if len(cfgTarget.target.compileGroups) < (src.compileGroupIndex + 1):
|
||||||
|
log.dbg(f" - {cfgTarget.target.name} has compileGroupIndex {src.compileGroupIndex} but only {len(cfgTarget.target.compileGroups)} found; skipping included files search")
|
||||||
|
return []
|
||||||
|
cg = cfgTarget.target.compileGroups[src.compileGroupIndex]
|
||||||
|
|
||||||
|
# currently only doing C includes
|
||||||
|
if cg.language != "C":
|
||||||
|
log.dbg(f" - {cfgTarget.target.name} has compile group language {cg.language} but currently only searching includes for C files; skipping included files search")
|
||||||
|
return []
|
||||||
|
|
||||||
|
srcAbspath = src.path
|
||||||
|
if src.path[0] != "/":
|
||||||
|
srcAbspath = os.path.join(self.cm.paths_source, src.path)
|
||||||
|
return getCIncludes(self.compilerPath, srcAbspath, cg)
|
||||||
|
|
||||||
|
# collect relationships for dependencies of this target Package
|
||||||
|
# call with:
|
||||||
|
# 1) all ConfigTargets from CodeModel
|
||||||
|
# 2) this particular ConfigTarget
|
||||||
|
# 3) Package for this Target
|
||||||
|
def collectTargetDependencies(self, cfgTargets, cfgTarget, pkg):
|
||||||
|
log.dbg(f" - collecting target dependencies for {pkg.cfg.name}")
|
||||||
|
|
||||||
|
# walk through target's dependencies
|
||||||
|
for dep in cfgTarget.target.dependencies:
|
||||||
|
# extract dep name from its id
|
||||||
|
depFragments = dep.id.split(":")
|
||||||
|
depName = depFragments[0]
|
||||||
|
log.dbg(f" - adding pending relationship for {depName}")
|
||||||
|
|
||||||
|
# create relationship data between dependency packages
|
||||||
|
rd = RelationshipData()
|
||||||
|
rd.ownerType = RelationshipDataElementType.TARGETNAME
|
||||||
|
rd.ownerTargetName = pkg.cfg.name
|
||||||
|
rd.otherType = RelationshipDataElementType.TARGETNAME
|
||||||
|
rd.otherTargetName = depName
|
||||||
|
rd.rlnType = "HAS_PREREQUISITE"
|
||||||
|
|
||||||
|
# add it to pending relationships queue
|
||||||
|
self.pendingRelationships.append(rd)
|
||||||
|
|
||||||
|
# if this is a target with any build artifacts (e.g. non-UTILITY),
|
||||||
|
# also create STATIC_LINK relationship for dependency build files,
|
||||||
|
# together with this Package's own target build file
|
||||||
|
if len(cfgTarget.target.artifacts) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# find the filename for the dependency's build product, using the
|
||||||
|
# codemodel (since we might not have created this dependency's
|
||||||
|
# Package or File yet)
|
||||||
|
depAbspath = ""
|
||||||
|
for ct in cfgTargets:
|
||||||
|
if ct.name == depName:
|
||||||
|
# skip utility targets
|
||||||
|
if len(ct.target.artifacts) == 0:
|
||||||
|
continue
|
||||||
|
# all targets use the same relativeBaseDir, so this works
|
||||||
|
# even though pkg is the owner package
|
||||||
|
depAbspath = os.path.join(pkg.cfg.relativeBaseDir, ct.target.artifacts[0])
|
||||||
|
break
|
||||||
|
if depAbspath == "":
|
||||||
|
continue
|
||||||
|
|
||||||
|
# create relationship data between build files
|
||||||
|
rd = RelationshipData()
|
||||||
|
rd.ownerType = RelationshipDataElementType.FILENAME
|
||||||
|
rd.ownerFileAbspath = pkg.targetBuildFile.abspath
|
||||||
|
rd.otherType = RelationshipDataElementType.FILENAME
|
||||||
|
rd.otherFileAbspath = depAbspath
|
||||||
|
rd.rlnType = "STATIC_LINK"
|
||||||
|
|
||||||
|
# add it to pending relationships queue
|
||||||
|
self.pendingRelationships.append(rd)
|
||||||
|
|
||||||
|
# walk through pending sources and create corresponding files,
|
||||||
|
# assigning them to the appropriate Document and Package
|
||||||
|
def walkPendingSources(self):
|
||||||
|
log.dbg(f"walking pending sources")
|
||||||
|
|
||||||
|
# only one package in each doc; get it
|
||||||
|
pkgZephyr = list(self.docZephyr.pkgs.values())[0]
|
||||||
|
pkgApp = list(self.docApp.pkgs.values())[0]
|
||||||
|
if self.cfg.includeSDK:
|
||||||
|
pkgSDK = list(self.docSDK.pkgs.values())[0]
|
||||||
|
|
||||||
|
for srcAbspath in self.pendingSources:
|
||||||
|
# check whether we've already seen it
|
||||||
|
srcDoc = self.allFileLinks.get(srcAbspath, None)
|
||||||
|
srcPkg = None
|
||||||
|
if srcDoc:
|
||||||
|
log.dbg(f" - {srcAbspath}: already seen, assigned to {srcDoc.cfg.name}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# not yet assigned; figure out where it goes
|
||||||
|
pkgBuild = self.findBuildPackage(srcAbspath)
|
||||||
|
|
||||||
|
if pkgBuild:
|
||||||
|
log.dbg(f" - {srcAbspath}: assigning to build document, package {pkgBuild.cfg.name}")
|
||||||
|
srcDoc = self.docBuild
|
||||||
|
srcPkg = pkgBuild
|
||||||
|
elif self.cfg.includeSDK and os.path.commonpath([srcAbspath, pkgSDK.cfg.relativeBaseDir]) == pkgSDK.cfg.relativeBaseDir:
|
||||||
|
log.dbg(f" - {srcAbspath}: assigning to sdk document")
|
||||||
|
srcDoc = self.docSDK
|
||||||
|
srcPkg = pkgSDK
|
||||||
|
elif os.path.commonpath([srcAbspath, pkgApp.cfg.relativeBaseDir]) == pkgApp.cfg.relativeBaseDir:
|
||||||
|
log.dbg(f" - {srcAbspath}: assigning to app document")
|
||||||
|
srcDoc = self.docApp
|
||||||
|
srcPkg = pkgApp
|
||||||
|
elif os.path.commonpath([srcAbspath, pkgZephyr.cfg.relativeBaseDir]) == pkgZephyr.cfg.relativeBaseDir:
|
||||||
|
log.dbg(f" - {srcAbspath}: assigning to zephyr document")
|
||||||
|
srcDoc = self.docZephyr
|
||||||
|
srcPkg = pkgZephyr
|
||||||
|
else:
|
||||||
|
log.dbg(f" - {srcAbspath}: can't determine which document should own; skipping")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# create File and assign it to the Package and Document
|
||||||
|
sf = File(srcDoc, srcPkg)
|
||||||
|
sf.abspath = srcAbspath
|
||||||
|
sf.relpath = os.path.relpath(srcAbspath, srcPkg.cfg.relativeBaseDir)
|
||||||
|
filenameOnly = os.path.split(srcAbspath)[1]
|
||||||
|
sf.spdxID = zspdx.spdxids.getUniqueFileID(filenameOnly, srcDoc.timesSeen)
|
||||||
|
# don't fill hashes / licenses / rlns now, we'll do that after walking
|
||||||
|
|
||||||
|
# add File to Package
|
||||||
|
srcPkg.files[sf.spdxID] = sf
|
||||||
|
|
||||||
|
# add file path link to Document and global links
|
||||||
|
srcDoc.fileLinks[sf.abspath] = sf
|
||||||
|
self.allFileLinks[sf.abspath] = srcDoc
|
||||||
|
|
||||||
|
# figure out which build Package contains the given file, if any
|
||||||
|
# call with:
|
||||||
|
# 1) absolute path for source filename being searched
|
||||||
|
def findBuildPackage(self, srcAbspath):
|
||||||
|
# Multiple target Packages might "contain" the file path, if they
|
||||||
|
# are nested. If so, the one with the longest path would be the
|
||||||
|
# most deeply-nested target directory, so that's the one which
|
||||||
|
# should get the file path.
|
||||||
|
pkgLongestMatch = None
|
||||||
|
for pkg in self.docBuild.pkgs.values():
|
||||||
|
if os.path.commonpath([srcAbspath, pkg.cfg.relativeBaseDir]) == pkg.cfg.relativeBaseDir:
|
||||||
|
# the package does contain this file; is it the deepest?
|
||||||
|
if pkgLongestMatch:
|
||||||
|
if len(pkg.cfg.relativeBaseDir) > len(pkgLongestMatch.cfg.relativeBaseDir):
|
||||||
|
pkgLongestMatch = pkg
|
||||||
|
else:
|
||||||
|
# first package containing it, so assign it
|
||||||
|
pkgLongestMatch = pkg
|
||||||
|
|
||||||
|
return pkgLongestMatch
|
||||||
|
|
||||||
|
# walk through pending RelationshipData entries, create corresponding
|
||||||
|
# Relationships, and assign them to the applicable Files / Packages
|
||||||
|
def walkRelationships(self):
|
||||||
|
for rlnData in self.pendingRelationships:
|
||||||
|
rln = Relationship()
|
||||||
|
# get left side of relationship data
|
||||||
|
docA, spdxIDA, rlnsA = self.getRelationshipLeft(rlnData)
|
||||||
|
if not docA or not spdxIDA:
|
||||||
|
continue
|
||||||
|
rln.refA = spdxIDA
|
||||||
|
# get right side of relationship data
|
||||||
|
spdxIDB = self.getRelationshipRight(rlnData, docA)
|
||||||
|
if not spdxIDB:
|
||||||
|
continue
|
||||||
|
rln.refB = spdxIDB
|
||||||
|
rln.rlnType = rlnData.rlnType
|
||||||
|
rlnsA.append(rln)
|
||||||
|
log.dbg(f" - adding relationship to {docA.cfg.name}: {rln.refA} {rln.rlnType} {rln.refB}")
|
||||||
|
|
||||||
|
# get owner (left side) document and SPDX ID of Relationship for given RelationshipData
|
||||||
|
# returns: doc, spdxID, rlnsArray (for either Document, Package, or File, as applicable)
|
||||||
|
def getRelationshipLeft(self, rlnData):
|
||||||
|
if rlnData.ownerType == RelationshipDataElementType.FILENAME:
|
||||||
|
# find the document for this file abspath, and then the specific file's ID
|
||||||
|
ownerDoc = self.allFileLinks.get(rlnData.ownerFileAbspath, None)
|
||||||
|
if not ownerDoc:
|
||||||
|
log.dbg(f" - searching for relationship, can't find document with file {rlnData.ownerFileAbspath}; skipping")
|
||||||
|
return None, None, None
|
||||||
|
sf = ownerDoc.fileLinks.get(rlnData.ownerFileAbspath, None)
|
||||||
|
if not sf:
|
||||||
|
log.dbg(f" - searching for relationship for file {rlnData.ownerFileAbspath} points to document {ownerDoc.cfg.name} but file not found; skipping")
|
||||||
|
return None, None, None
|
||||||
|
# found it
|
||||||
|
if not sf.spdxID:
|
||||||
|
log.dbg(f" - searching for relationship for file {rlnData.ownerFileAbspath} found file, but empty ID; skipping")
|
||||||
|
return None, None, None
|
||||||
|
return ownerDoc, sf.spdxID, sf.rlns
|
||||||
|
elif rlnData.ownerType == RelationshipDataElementType.TARGETNAME:
|
||||||
|
# find the document for this target name, and then the specific package's ID
|
||||||
|
# for target names, must be docBuild
|
||||||
|
ownerDoc = self.docBuild
|
||||||
|
# walk through target Packages and check names
|
||||||
|
for pkg in ownerDoc.pkgs.values():
|
||||||
|
if pkg.cfg.name == rlnData.ownerTargetName:
|
||||||
|
if not pkg.cfg.spdxID:
|
||||||
|
log.dbg(f" - searching for relationship for target {rlnData.ownerTargetName} found package, but empty ID; skipping")
|
||||||
|
return None, None, None
|
||||||
|
return ownerDoc, pkg.cfg.spdxID, pkg.rlns
|
||||||
|
log.dbg(f" - searching for relationship for target {rlnData.ownerTargetName}, target not found in build document; skipping")
|
||||||
|
return None, None, None
|
||||||
|
elif rlnData.ownerType == RelationshipDataElementType.DOCUMENT:
|
||||||
|
# will always be SPDXRef-DOCUMENT
|
||||||
|
return rlnData.ownerDocument, "SPDXRef-DOCUMENT", rlnData.ownerDocument.relationships
|
||||||
|
else:
|
||||||
|
log.dbg(f" - unknown relationship type {rlnData.ownerType}; skipping")
|
||||||
|
return None, None, None
|
||||||
|
|
||||||
|
# get other (right side) SPDX ID of Relationship for given RelationshipData
|
||||||
|
def getRelationshipRight(self, rlnData, docA):
|
||||||
|
if rlnData.otherType == RelationshipDataElementType.FILENAME:
|
||||||
|
# find the document for this file abspath, and then the specific file's ID
|
||||||
|
otherDoc = self.allFileLinks.get(rlnData.otherFileAbspath, None)
|
||||||
|
if not otherDoc:
|
||||||
|
log.dbg(f" - searching for relationship, can't find document with file {rlnData.otherFileAbspath}; skipping")
|
||||||
|
return None
|
||||||
|
bf = otherDoc.fileLinks.get(rlnData.otherFileAbspath, None)
|
||||||
|
if not bf:
|
||||||
|
log.dbg(f" - searching for relationship for file {rlnData.otherFileAbspath} points to document {otherDoc.cfg.name} but file not found; skipping")
|
||||||
|
return None
|
||||||
|
# found it
|
||||||
|
if not bf.spdxID:
|
||||||
|
log.dbg(f" - searching for relationship for file {rlnData.otherFileAbspath} found file, but empty ID; skipping")
|
||||||
|
return None
|
||||||
|
# figure out whether to append DocumentRef
|
||||||
|
spdxIDB = bf.spdxID
|
||||||
|
if otherDoc != docA:
|
||||||
|
spdxIDB = otherDoc.cfg.docRefID + ":" + spdxIDB
|
||||||
|
docA.externalDocuments.add(otherDoc)
|
||||||
|
return spdxIDB
|
||||||
|
elif rlnData.otherType == RelationshipDataElementType.TARGETNAME:
|
||||||
|
# find the document for this target name, and then the specific package's ID
|
||||||
|
# for target names, must be docBuild
|
||||||
|
otherDoc = self.docBuild
|
||||||
|
# walk through target Packages and check names
|
||||||
|
for pkg in otherDoc.pkgs.values():
|
||||||
|
if pkg.cfg.name == rlnData.otherTargetName:
|
||||||
|
if not pkg.cfg.spdxID:
|
||||||
|
log.dbg(f" - searching for relationship for target {rlnData.otherTargetName} found package, but empty ID; skipping")
|
||||||
|
return None
|
||||||
|
spdxIDB = pkg.cfg.spdxID
|
||||||
|
if otherDoc != docA:
|
||||||
|
spdxIDB = otherDoc.cfg.docRefID + ":" + spdxIDB
|
||||||
|
docA.externalDocuments.add(otherDoc)
|
||||||
|
return spdxIDB
|
||||||
|
log.dbg(f" - searching for relationship for target {rlnData.otherTargetName}, target not found in build document; skipping")
|
||||||
|
return None
|
||||||
|
elif rlnData.otherType == RelationshipDataElementType.PACKAGEID:
|
||||||
|
# will just be the package ID that was passed in
|
||||||
|
return rlnData.otherPackageID
|
||||||
|
else:
|
||||||
|
log.dbg(f" - unknown relationship type {rlnData.otherType}; skipping")
|
||||||
|
return None
|
153
scripts/west_commands/zspdx/writer.py
Normal file
153
scripts/west_commands/zspdx/writer.py
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
# Copyright (c) 2020, 2021 The Linux Foundation
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from west import log
|
||||||
|
|
||||||
|
from zspdx.util import getHashes
|
||||||
|
|
||||||
|
# Output tag-value SPDX 2.2 content for the given Relationship object.
|
||||||
|
# Arguments:
|
||||||
|
# 1) f: file handle for SPDX document
|
||||||
|
# 2) rln: Relationship object being described
|
||||||
|
def writeRelationshipSPDX(f, rln):
|
||||||
|
f.write(f"Relationship: {rln.refA} {rln.rlnType} {rln.refB}\n")
|
||||||
|
|
||||||
|
# Output tag-value SPDX 2.2 content for the given File object.
|
||||||
|
# Arguments:
|
||||||
|
# 1) f: file handle for SPDX document
|
||||||
|
# 2) bf: File object being described
|
||||||
|
def writeFileSPDX(f, bf):
|
||||||
|
f.write(f"""FileName: ./{bf.relpath}
|
||||||
|
SPDXID: {bf.spdxID}
|
||||||
|
FileChecksum: SHA1: {bf.sha1}
|
||||||
|
""")
|
||||||
|
if bf.sha256 != "":
|
||||||
|
f.write(f"FileChecksum: SHA256: {bf.sha256}\n")
|
||||||
|
if bf.md5 != "":
|
||||||
|
f.write(f"FileChecksum: MD5: {bf.md5}\n")
|
||||||
|
f.write(f"LicenseConcluded: {bf.concludedLicense}\n")
|
||||||
|
if len(bf.licenseInfoInFile) == 0:
|
||||||
|
f.write(f"LicenseInfoInFile: NONE\n")
|
||||||
|
else:
|
||||||
|
for licInfoInFile in bf.licenseInfoInFile:
|
||||||
|
f.write(f"LicenseInfoInFile: {licInfoInFile}\n")
|
||||||
|
f.write(f"FileCopyrightText: {bf.copyrightText}\n\n")
|
||||||
|
|
||||||
|
# write file relationships
|
||||||
|
if len(bf.rlns) > 0:
|
||||||
|
for rln in bf.rlns:
|
||||||
|
writeRelationshipSPDX(f, rln)
|
||||||
|
f.write("\n")
|
||||||
|
|
||||||
|
# Output tag-value SPDX 2.2 content for the given Package object.
|
||||||
|
# Arguments:
|
||||||
|
# 1) f: file handle for SPDX document
|
||||||
|
# 2) pkg: Package object being described
|
||||||
|
def writePackageSPDX(f, pkg):
|
||||||
|
f.write(f"""##### Package: {pkg.cfg.name}
|
||||||
|
|
||||||
|
PackageName: {pkg.cfg.name}
|
||||||
|
SPDXID: {pkg.cfg.spdxID}
|
||||||
|
PackageDownloadLocation: NOASSERTION
|
||||||
|
PackageLicenseConcluded: {pkg.concludedLicense}
|
||||||
|
""")
|
||||||
|
for licFromFiles in pkg.licenseInfoFromFiles:
|
||||||
|
f.write(f"PackageLicenseInfoFromFiles: {licFromFiles}\n")
|
||||||
|
f.write(f"""PackageLicenseDeclared: {pkg.cfg.declaredLicense}
|
||||||
|
PackageCopyrightText: {pkg.cfg.copyrightText}
|
||||||
|
""")
|
||||||
|
|
||||||
|
# flag whether files analyzed / any files present
|
||||||
|
if len(pkg.files) > 0:
|
||||||
|
f.write(f"FilesAnalyzed: true\nPackageVerificationCode: {pkg.verificationCode}\n\n")
|
||||||
|
else:
|
||||||
|
f.write(f"FilesAnalyzed: false\nPackageComment: Utility target; no files\n\n")
|
||||||
|
|
||||||
|
# write package relationships
|
||||||
|
if len(pkg.rlns) > 0:
|
||||||
|
for rln in pkg.rlns:
|
||||||
|
writeRelationshipSPDX(f, rln)
|
||||||
|
f.write("\n")
|
||||||
|
|
||||||
|
# write package files, if any
|
||||||
|
if len(pkg.files) > 0:
|
||||||
|
bfs = list(pkg.files.values())
|
||||||
|
bfs.sort(key = lambda x: x.relpath)
|
||||||
|
for bf in bfs:
|
||||||
|
writeFileSPDX(f, bf)
|
||||||
|
|
||||||
|
# Output tag-value SPDX 2.2 content for a custom license.
|
||||||
|
# Arguments:
|
||||||
|
# 1) f: file handle for SPDX document
|
||||||
|
# 2) lic: custom license ID being described
|
||||||
|
def writeOtherLicenseSPDX(f, lic):
|
||||||
|
f.write(f"""LicenseID: {lic}
|
||||||
|
ExtractedText: {lic}
|
||||||
|
LicenseName: {lic}
|
||||||
|
LicenseComment: Corresponds to the license ID `{lic}` detected in an SPDX-License-Identifier: tag.
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Output tag-value SPDX 2.2 content for the given Document object.
|
||||||
|
# Arguments:
|
||||||
|
# 1) f: file handle for SPDX document
|
||||||
|
# 2) doc: Document object being described
|
||||||
|
def writeDocumentSPDX(f, doc):
|
||||||
|
f.write(f"""SPDXVersion: SPDX-2.2
|
||||||
|
DataLicense: CC0-1.0
|
||||||
|
SPDXID: SPDXRef-DOCUMENT
|
||||||
|
DocumentName: {doc.cfg.name}
|
||||||
|
DocumentNamespace: {doc.cfg.namespace}
|
||||||
|
Creator: Tool: Zephyr SPDX builder
|
||||||
|
Created: {datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")}
|
||||||
|
|
||||||
|
""")
|
||||||
|
|
||||||
|
# write any external document references
|
||||||
|
if len(doc.externalDocuments) > 0:
|
||||||
|
extDocs = list(doc.externalDocuments)
|
||||||
|
extDocs.sort(key = lambda x: x.cfg.docRefID)
|
||||||
|
for extDoc in extDocs:
|
||||||
|
f.write(f"ExternalDocumentRef: {extDoc.cfg.docRefID} {extDoc.cfg.namespace} SHA1: {extDoc.myDocSHA1}\n")
|
||||||
|
f.write(f"\n")
|
||||||
|
|
||||||
|
# write relationships owned by this Document (not by its Packages, etc.), if any
|
||||||
|
if len(doc.relationships) > 0:
|
||||||
|
for rln in doc.relationships:
|
||||||
|
writeRelationshipSPDX(f, rln)
|
||||||
|
f.write(f"\n")
|
||||||
|
|
||||||
|
# write packages
|
||||||
|
for pkg in doc.pkgs.values():
|
||||||
|
writePackageSPDX(f, pkg)
|
||||||
|
|
||||||
|
# write other license info, if any
|
||||||
|
if len(doc.customLicenseIDs) > 0:
|
||||||
|
for lic in list(doc.customLicenseIDs).sort():
|
||||||
|
writeOtherLicenseSPDX(f, lic)
|
||||||
|
|
||||||
|
# Open SPDX document file for writing, write the document, and calculate
|
||||||
|
# its hash for other referring documents to use.
|
||||||
|
# Arguments:
|
||||||
|
# 1) spdxPath: path to write SPDX document
|
||||||
|
# 2) doc: SPDX Document object to write
|
||||||
|
def writeSPDX(spdxPath, doc):
|
||||||
|
# create and write document to disk
|
||||||
|
try:
|
||||||
|
log.inf(f"Writing SPDX document {doc.cfg.name} to {spdxPath}")
|
||||||
|
with open(spdxPath, "w") as f:
|
||||||
|
writeDocumentSPDX(f, doc)
|
||||||
|
except OSError as e:
|
||||||
|
log.err(f"Error: Unable to write to {spdxPath}: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# calculate hash of the document we just wrote
|
||||||
|
hashes = getHashes(spdxPath)
|
||||||
|
if not hashes:
|
||||||
|
log.err(f"Error: created document but unable to calculate hash values")
|
||||||
|
return False
|
||||||
|
doc.myDocSHA1 = hashes[0]
|
||||||
|
|
||||||
|
return True
|
Loading…
Add table
Add a link
Reference in a new issue