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>
diff --git a/scripts/west_commands/spdx.py b/scripts/west_commands/spdx.py
new file mode 100644
index 0000000..e06ffec
--- /dev/null
+++ b/scripts/west_commands/spdx.py
@@ -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)