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
9from zspdx.sbom import SBOMConfig, makeSPDX, setupCmakeQuery
10from zspdx.version import SPDX_VERSION_2_3, SUPPORTED_SPDX_VERSIONS, parse
11
12SPDX_DESCRIPTION = """\
13This command creates an SPDX 2.2 or 2.3 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('--spdx-version', choices=[str(v) for v in SUPPORTED_SPDX_VERSIONS],
45                default=str(SPDX_VERSION_2_3),
46                help="SPDX specification version to use (default: 2.3)")
47        parser.add_argument('--analyze-includes', action="store_true",
48                help="also analyze included header files")
49        parser.add_argument('--include-sdk', action="store_true",
50                help="also generate SPDX document for SDK")
51
52        return parser
53
54    def do_run(self, args, unknown_args):
55        self.dbg("running zephyr SPDX generator")
56
57        self.dbg("  --init is", args.init)
58        self.dbg("  --build-dir is", args.build_dir)
59        self.dbg("  --namespace-prefix is", args.namespace_prefix)
60        self.dbg("  --spdx-dir is", args.spdx_dir)
61        self.dbg("  --spdx-version is", args.spdx_version)
62        self.dbg("  --analyze-includes is", args.analyze_includes)
63        self.dbg("  --include-sdk is", args.include_sdk)
64
65        if args.init:
66            self.do_run_init(args)
67        else:
68            self.do_run_spdx(args)
69
70    def do_run_init(self, args):
71        self.inf("initializing CMake file-based API prior to build")
72
73        if not args.build_dir:
74            self.die("Build directory not specified; call `west spdx --init --build-dir=BUILD_DIR`")
75
76        # initialize CMake file-based API - empty query file
77        query_ready = setupCmakeQuery(args.build_dir)
78        if query_ready:
79            self.inf("initialized; run `west build` then run `west spdx`")
80        else:
81            self.die("Couldn't create CMake file-based API query directory\n"
82                     "You can manually create an empty file at "
83                     "$BUILDDIR/.cmake/api/v1/query/codemodel-v2")
84
85    def do_run_spdx(self, args):
86        if not args.build_dir:
87            self.die("Build directory not specified; call `west spdx --build-dir=BUILD_DIR`")
88
89        # create the SPDX files
90        cfg = SBOMConfig()
91        cfg.buildDir = args.build_dir
92        try:
93            version_obj = parse(args.spdx_version)
94        except Exception:
95            self.die(f"Invalid SPDX version: {args.spdx_version}")
96        cfg.spdxVersion = version_obj
97        if args.namespace_prefix:
98            cfg.namespacePrefix = args.namespace_prefix
99        else:
100            # create default namespace according to SPDX spec
101            # note that this is intentionally _not_ an actual URL where
102            # this document will be stored
103            cfg.namespacePrefix = f"http://spdx.org/spdxdocs/zephyr-{str(uuid.uuid4())}"
104        if args.spdx_dir:
105            cfg.spdxDir = args.spdx_dir
106        else:
107            cfg.spdxDir = os.path.join(args.build_dir, "spdx")
108        if args.analyze_includes:
109            cfg.analyzeIncludes = True
110        if args.include_sdk:
111            cfg.includeSDK = True
112
113        # make sure SPDX directory exists, or create it if it doesn't
114        if os.path.exists(cfg.spdxDir):
115            if not os.path.isdir(cfg.spdxDir):
116                self.err(f'SPDX output directory {cfg.spdxDir} exists but is not a directory')
117                return
118            # directory exists, we're good
119        else:
120            # create the directory
121            os.makedirs(cfg.spdxDir, exist_ok=False)
122
123        if not makeSPDX(cfg):
124            self.die("Failed to create SPDX output")
125