1# Copyright (c) 2021 The Linux Foundation
2#
3# SPDX-License-Identifier: Apache-2.0
4
5import os
6import uuid
7
8from west.commands import WestCommand
9
10from zspdx.sbom import SBOMConfig, makeSPDX, setupCmakeQuery
11
12SPDX_DESCRIPTION = """\
13This command creates an SPDX 2.2 tag-value bill of materials
14following the completion of a Zephyr build.
15
16Prior to the build, an empty file must be created at
17BUILDDIR/.cmake/api/v1/query/codemodel-v2 in order to enable
18the CMake file-based API, which the SPDX command relies upon.
19This can be done by calling `west spdx --init` prior to
20calling `west build`."""
21
22class ZephyrSpdx(WestCommand):
23    def __init__(self):
24        super().__init__(
25                'spdx',
26                'create SPDX bill of materials',
27                SPDX_DESCRIPTION)
28
29    def do_add_parser(self, parser_adder):
30        parser = parser_adder.add_parser(self.name,
31                help=self.help,
32                description = self.description)
33
34        # If you update these options, make sure to keep the docs in
35        # doc/guides/west/zephyr-cmds.rst up to date.
36        parser.add_argument('-i', '--init', action="store_true",
37                help="initialize CMake file-based API")
38        parser.add_argument('-d', '--build-dir',
39                help="build directory")
40        parser.add_argument('-n', '--namespace-prefix',
41                help="namespace prefix")
42        parser.add_argument('-s', '--spdx-dir',
43                help="SPDX output directory")
44        parser.add_argument('--analyze-includes', action="store_true",
45                help="also analyze included header files")
46        parser.add_argument('--include-sdk', action="store_true",
47                help="also generate SPDX document for SDK")
48
49        return parser
50
51    def do_run(self, args, unknown_args):
52        self.dbg(f"running zephyr SPDX generator")
53
54        self.dbg(f"  --init is", args.init)
55        self.dbg(f"  --build-dir is", args.build_dir)
56        self.dbg(f"  --namespace-prefix is", args.namespace_prefix)
57        self.dbg(f"  --spdx-dir is", args.spdx_dir)
58        self.dbg(f"  --analyze-includes is", args.analyze_includes)
59        self.dbg(f"  --include-sdk is", args.include_sdk)
60
61        if args.init:
62            self.do_run_init(args)
63        else:
64            self.do_run_spdx(args)
65
66    def do_run_init(self, args):
67        self.inf("initializing CMake file-based API prior to build")
68
69        if not args.build_dir:
70            self.die("Build directory not specified; call `west spdx --init --build-dir=BUILD_DIR`")
71
72        # initialize CMake file-based API - empty query file
73        query_ready = setupCmakeQuery(args.build_dir)
74        if query_ready:
75            self.inf("initialized; run `west build` then run `west spdx`")
76        else:
77            self.die("Couldn't create CMake file-based API query directory\n"
78                     "You can manually create an empty file at "
79                     "$BUILDDIR/.cmake/api/v1/query/codemodel-v2")
80
81    def do_run_spdx(self, args):
82        if not args.build_dir:
83            self.die("Build directory not specified; call `west spdx --build-dir=BUILD_DIR`")
84
85        # create the SPDX files
86        cfg = SBOMConfig()
87        cfg.buildDir = args.build_dir
88        if args.namespace_prefix:
89            cfg.namespacePrefix = args.namespace_prefix
90        else:
91            # create default namespace according to SPDX spec
92            # note that this is intentionally _not_ an actual URL where
93            # this document will be stored
94            cfg.namespacePrefix = f"http://spdx.org/spdxdocs/zephyr-{str(uuid.uuid4())}"
95        if args.spdx_dir:
96            cfg.spdxDir = args.spdx_dir
97        else:
98            cfg.spdxDir = os.path.join(args.build_dir, "spdx")
99        if args.analyze_includes:
100            cfg.analyzeIncludes = True
101        if args.include_sdk:
102            cfg.includeSDK = True
103
104        # make sure SPDX directory exists, or create it if it doesn't
105        if os.path.exists(cfg.spdxDir):
106            if not os.path.isdir(cfg.spdxDir):
107                self.err(f'SPDX output directory {cfg.spdxDir} exists but is not a directory')
108                return
109            # directory exists, we're good
110        else:
111            # create the directory
112            os.makedirs(cfg.spdxDir, exist_ok=False)
113
114        if not makeSPDX(cfg):
115            self.die("Failed to create SPDX output")
116