1# Copyright (c) 2020 The Linux Foundation
2#
3# SPDX-License-Identifier: Apache-2.0
4
5import json
6import os
7
8from west import log
9
10import zspdx.cmakefileapi
11
12
13def parseReply(replyIndexPath):
14    replyDir, _ = os.path.split(replyIndexPath)
15
16    # first we need to find the codemodel reply file
17    try:
18        with open(replyIndexPath) as indexFile:
19            js = json.load(indexFile)
20
21            # get reply object
22            reply_dict = js.get("reply", {})
23            if reply_dict == {}:
24                log.err('no "reply" field found in index file')
25                return None
26            # get codemodel object
27            cm_dict = reply_dict.get("codemodel-v2", {})
28            if cm_dict == {}:
29                log.err('no "codemodel-v2" field found in "reply" object in index file')
30                return None
31            # and get codemodel filename
32            jsonFile = cm_dict.get("jsonFile", "")
33            if jsonFile == "":
34                log.err('no "jsonFile" field found in "codemodel-v2" object in index file')
35                return None
36
37            return parseCodemodel(replyDir, jsonFile)
38
39    except OSError as e:
40        log.err(f"Error loading {replyIndexPath}: {str(e)}")
41        return None
42    except json.decoder.JSONDecodeError as e:
43        log.err(f"Error parsing JSON in {replyIndexPath}: {str(e)}")
44        return None
45
46def parseCodemodel(replyDir, codemodelFile):
47    codemodelPath = os.path.join(replyDir, codemodelFile)
48
49    try:
50        with open(codemodelPath) as cmFile:
51            js = json.load(cmFile)
52
53            cm = zspdx.cmakefileapi.Codemodel()
54
55            # for correctness, check kind and version
56            kind = js.get("kind", "")
57            if kind != "codemodel":
58                log.err('Error loading CMake API reply: expected "kind":"codemodel" '
59                        f'in {codemodelPath}, got {kind}')
60                return None
61            version = js.get("version", {})
62            versionMajor = version.get("major", -1)
63            if versionMajor != 2:
64                if versionMajor == -1:
65                    log.err("Error loading CMake API reply: expected major version 2 "
66                            f"in {codemodelPath}, no version found")
67                    return None
68                log.err("Error loading CMake API reply: expected major version 2 "
69                        f"in {codemodelPath}, got {versionMajor}")
70                return None
71
72            # get paths
73            paths_dict = js.get("paths", {})
74            cm.paths_source = paths_dict.get("source", "")
75            cm.paths_build = paths_dict.get("build", "")
76
77            # get configurations
78            configs_arr = js.get("configurations", [])
79            for cfg_dict in configs_arr:
80                cfg = parseConfig(cfg_dict, replyDir)
81                if cfg:
82                    cm.configurations.append(cfg)
83
84            # and after parsing is done, link all the indices
85            linkCodemodel(cm)
86
87            return cm
88
89    except OSError as e:
90        log.err(f"Error loading {codemodelPath}: {str(e)}")
91        return None
92    except json.decoder.JSONDecodeError as e:
93        log.err(f"Error parsing JSON in {codemodelPath}: {str(e)}")
94        return None
95
96def parseConfig(cfg_dict, replyDir):
97    cfg = zspdx.cmakefileapi.Config()
98    cfg.name = cfg_dict.get("name", "")
99
100    # parse and add each directory
101    dirs_arr = cfg_dict.get("directories", [])
102    for dir_dict in dirs_arr:
103        if dir_dict != {}:
104            cfgdir = zspdx.cmakefileapi.ConfigDir()
105            cfgdir.source = dir_dict.get("source", "")
106            cfgdir.build = dir_dict.get("build", "")
107            cfgdir.parentIndex = dir_dict.get("parentIndex", -1)
108            cfgdir.childIndexes = dir_dict.get("childIndexes", [])
109            cfgdir.projectIndex = dir_dict.get("projectIndex", -1)
110            cfgdir.targetIndexes = dir_dict.get("targetIndexes", [])
111            minCMakeVer_dict = dir_dict.get("minimumCMakeVersion", {})
112            cfgdir.minimumCMakeVersion = minCMakeVer_dict.get("string", "")
113            cfgdir.hasInstallRule = dir_dict.get("hasInstallRule", False)
114            cfg.directories.append(cfgdir)
115
116    # parse and add each project
117    projects_arr = cfg_dict.get("projects", [])
118    for prj_dict in projects_arr:
119        if prj_dict != {}:
120            prj = zspdx.cmakefileapi.ConfigProject()
121            prj.name = prj_dict.get("name", "")
122            prj.parentIndex = prj_dict.get("parentIndex", -1)
123            prj.childIndexes = prj_dict.get("childIndexes", [])
124            prj.directoryIndexes = prj_dict.get("directoryIndexes", [])
125            prj.targetIndexes = prj_dict.get("targetIndexes", [])
126            cfg.projects.append(prj)
127
128    # parse and add each target
129    cfgTargets_arr = cfg_dict.get("targets", [])
130    for cfgTarget_dict in cfgTargets_arr:
131        if cfgTarget_dict != {}:
132            cfgTarget = zspdx.cmakefileapi.ConfigTarget()
133            cfgTarget.name = cfgTarget_dict.get("name", "")
134            cfgTarget.id = cfgTarget_dict.get("id", "")
135            cfgTarget.directoryIndex = cfgTarget_dict.get("directoryIndex", -1)
136            cfgTarget.projectIndex = cfgTarget_dict.get("projectIndex", -1)
137            cfgTarget.jsonFile = cfgTarget_dict.get("jsonFile", "")
138
139            if cfgTarget.jsonFile != "":
140                cfgTarget.target = parseTarget(os.path.join(replyDir, cfgTarget.jsonFile))
141            else:
142                cfgTarget.target = None
143
144            cfg.configTargets.append(cfgTarget)
145
146    return cfg
147
148def parseTarget(targetPath):
149    try:
150        with open(targetPath) as targetFile:
151            js = json.load(targetFile)
152
153            target = zspdx.cmakefileapi.Target()
154
155            target.name = js.get("name", "")
156            target.id = js.get("id", "")
157            target.type = parseTargetType(js.get("type", "UNKNOWN"))
158            target.backtrace = js.get("backtrace", -1)
159            target.folder = js.get("folder", "")
160
161            # get paths
162            paths_dict = js.get("paths", {})
163            target.paths_source = paths_dict.get("source", "")
164            target.paths_build = paths_dict.get("build", "")
165
166            target.nameOnDisk = js.get("nameOnDisk", "")
167
168            # parse artifacts if present
169            artifacts_arr = js.get("artifacts", [])
170            target.artifacts = []
171            for artifact_dict in artifacts_arr:
172                artifact_path = artifact_dict.get("path", "")
173                if artifact_path != "":
174                    target.artifacts.append(artifact_path)
175
176            target.isGeneratorProvided = js.get("isGeneratorProvided", False)
177
178            # call separate functions to parse subsections
179            parseTargetInstall(target, js)
180            parseTargetLink(target, js)
181            parseTargetArchive(target, js)
182            parseTargetDependencies(target, js)
183            parseTargetSources(target, js)
184            parseTargetSourceGroups(target, js)
185            parseTargetCompileGroups(target, js)
186            parseTargetBacktraceGraph(target, js)
187
188            return target
189
190    except OSError as e:
191        log.err(f"Error loading {targetPath}: {str(e)}")
192        return None
193    except json.decoder.JSONDecodeError as e:
194        log.err(f"Error parsing JSON in {targetPath}: {str(e)}")
195        return None
196
197def parseTargetType(targetType):
198    return {
199        "EXECUTABLE": zspdx.cmakefileapi.TargetType.EXECUTABLE,
200        "STATIC_LIBRARY": zspdx.cmakefileapi.TargetType.STATIC_LIBRARY,
201        "SHARED_LIBRARY": zspdx.cmakefileapi.TargetType.SHARED_LIBRARY,
202        "MODULE_LIBRARY": zspdx.cmakefileapi.TargetType.MODULE_LIBRARY,
203        "OBJECT_LIBRARY": zspdx.cmakefileapi.TargetType.OBJECT_LIBRARY,
204        "UTILITY": zspdx.cmakefileapi.TargetType.UTILITY,
205    }.get(targetType, zspdx.cmakefileapi.TargetType.UNKNOWN)
206
207def parseTargetInstall(target, js):
208    install_dict = js.get("install", {})
209    if install_dict == {}:
210        return
211    prefix_dict = install_dict.get("prefix", {})
212    target.install_prefix = prefix_dict.get("path", "")
213
214    destinations_arr = install_dict.get("destinations", [])
215    for destination_dict in destinations_arr:
216        dest = zspdx.cmakefileapi.TargetInstallDestination()
217        dest.path = destination_dict.get("path", "")
218        dest.backtrace = destination_dict.get("backtrace", -1)
219        target.install_destinations.append(dest)
220
221def parseTargetLink(target, js):
222    link_dict = js.get("link", {})
223    if link_dict == {}:
224        return
225    target.link_language = link_dict.get("language", {})
226    target.link_lto = link_dict.get("lto", False)
227    sysroot_dict = link_dict.get("sysroot", {})
228    target.link_sysroot = sysroot_dict.get("path", "")
229
230    fragments_arr = link_dict.get("commandFragments", [])
231    for fragment_dict in fragments_arr:
232        fragment = zspdx.cmakefileapi.TargetCommandFragment()
233        fragment.fragment = fragment_dict.get("fragment", "")
234        fragment.role = fragment_dict.get("role", "")
235        target.link_commandFragments.append(fragment)
236
237def parseTargetArchive(target, js):
238    archive_dict = js.get("archive", {})
239    if archive_dict == {}:
240        return
241    target.archive_lto = archive_dict.get("lto", False)
242
243    fragments_arr = archive_dict.get("commandFragments", [])
244    for fragment_dict in fragments_arr:
245        fragment = zspdx.cmakefileapi.TargetCommandFragment()
246        fragment.fragment = fragment_dict.get("fragment", "")
247        fragment.role = fragment_dict.get("role", "")
248        target.archive_commandFragments.append(fragment)
249
250def parseTargetDependencies(target, js):
251    dependencies_arr = js.get("dependencies", [])
252    for dependency_dict in dependencies_arr:
253        dep = zspdx.cmakefileapi.TargetDependency()
254        dep.id = dependency_dict.get("id", "")
255        dep.backtrace = dependency_dict.get("backtrace", -1)
256        target.dependencies.append(dep)
257
258def parseTargetSources(target, js):
259    sources_arr = js.get("sources", [])
260    for source_dict in sources_arr:
261        src = zspdx.cmakefileapi.TargetSource()
262        src.path = source_dict.get("path", "")
263        src.compileGroupIndex = source_dict.get("compileGroupIndex", -1)
264        src.sourceGroupIndex = source_dict.get("sourceGroupIndex", -1)
265        src.isGenerated = source_dict.get("isGenerated", False)
266        src.backtrace = source_dict.get("backtrace", -1)
267        target.sources.append(src)
268
269def parseTargetSourceGroups(target, js):
270    sourceGroups_arr = js.get("sourceGroups", [])
271    for sourceGroup_dict in sourceGroups_arr:
272        srcgrp = zspdx.cmakefileapi.TargetSourceGroup()
273        srcgrp.name = sourceGroup_dict.get("name", "")
274        srcgrp.sourceIndexes = sourceGroup_dict.get("sourceIndexes", [])
275        target.sourceGroups.append(srcgrp)
276
277def parseTargetCompileGroups(target, js):
278    compileGroups_arr = js.get("compileGroups", [])
279    for compileGroup_dict in compileGroups_arr:
280        cmpgrp = zspdx.cmakefileapi.TargetCompileGroup()
281        cmpgrp.sourceIndexes = compileGroup_dict.get("sourceIndexes", [])
282        cmpgrp.language = compileGroup_dict.get("language", "")
283        cmpgrp.sysroot = compileGroup_dict.get("sysroot", "")
284
285        commandFragments_arr = compileGroup_dict.get("compileCommandFragments", [])
286        for commandFragment_dict in commandFragments_arr:
287            fragment = commandFragment_dict.get("fragment", "")
288            if fragment != "":
289                cmpgrp.compileCommandFragments.append(fragment)
290
291        includes_arr = compileGroup_dict.get("includes", [])
292        for include_dict in includes_arr:
293            grpInclude = zspdx.cmakefileapi.TargetCompileGroupInclude()
294            grpInclude.path = include_dict.get("path", "")
295            grpInclude.isSystem = include_dict.get("isSystem", False)
296            grpInclude.backtrace = include_dict.get("backtrace", -1)
297            cmpgrp.includes.append(grpInclude)
298
299        precompileHeaders_arr = compileGroup_dict.get("precompileHeaders", [])
300        for precompileHeader_dict in precompileHeaders_arr:
301            grpHeader = zspdx.cmakefileapi.TargetCompileGroupPrecompileHeader()
302            grpHeader.header = precompileHeader_dict.get("header", "")
303            grpHeader.backtrace = precompileHeader_dict.get("backtrace", -1)
304            cmpgrp.precompileHeaders.append(grpHeader)
305
306        defines_arr = compileGroup_dict.get("defines", [])
307        for define_dict in defines_arr:
308            grpDefine = zspdx.cmakefileapi.TargetCompileGroupDefine()
309            grpDefine.define = define_dict.get("define", "")
310            grpDefine.backtrace = define_dict.get("backtrace", -1)
311            cmpgrp.defines.append(grpDefine)
312
313        target.compileGroups.append(cmpgrp)
314
315def parseTargetBacktraceGraph(target, js):
316    backtraceGraph_dict = js.get("backtraceGraph", {})
317    if backtraceGraph_dict == {}:
318        return
319    target.backtraceGraph_commands = backtraceGraph_dict.get("commands", [])
320    target.backtraceGraph_files = backtraceGraph_dict.get("files", [])
321
322    nodes_arr = backtraceGraph_dict.get("nodes", [])
323    for node_dict in nodes_arr:
324        node = zspdx.cmakefileapi.TargetBacktraceGraphNode()
325        node.file = node_dict.get("file", -1)
326        node.line = node_dict.get("line", -1)
327        node.command = node_dict.get("command", -1)
328        node.parent = node_dict.get("parent", -1)
329        target.backtraceGraph_nodes.append(node)
330
331# Create direct pointers for all Configs in Codemodel
332# takes: Codemodel
333def linkCodemodel(cm):
334    for cfg in cm.configurations:
335        linkConfig(cfg)
336
337# Create direct pointers for all contents of Config
338# takes: Config
339def linkConfig(cfg):
340    for cfgDir in cfg.directories:
341        linkConfigDir(cfg, cfgDir)
342    for cfgPrj in cfg.projects:
343        linkConfigProject(cfg, cfgPrj)
344    for cfgTarget in cfg.configTargets:
345        linkConfigTarget(cfg, cfgTarget)
346
347# Create direct pointers for ConfigDir indices
348# takes: Config and ConfigDir
349def linkConfigDir(cfg, cfgDir):
350    if cfgDir.parentIndex == -1:
351        cfgDir.parent = None
352    else:
353        cfgDir.parent = cfg.directories[cfgDir.parentIndex]
354
355    if cfgDir.projectIndex == -1:
356        cfgDir.project = None
357    else:
358        cfgDir.project = cfg.projects[cfgDir.projectIndex]
359
360    cfgDir.children = []
361    for childIndex in cfgDir.childIndexes:
362        cfgDir.children.append(cfg.directories[childIndex])
363
364    cfgDir.targets = []
365    for targetIndex in cfgDir.targetIndexes:
366        cfgDir.targets.append(cfg.configTargets[targetIndex])
367
368# Create direct pointers for ConfigProject indices
369# takes: Config and ConfigProject
370def linkConfigProject(cfg, cfgPrj):
371    if cfgPrj.parentIndex == -1:
372        cfgPrj.parent = None
373    else:
374        cfgPrj.parent = cfg.projects[cfgPrj.parentIndex]
375
376    cfgPrj.children = []
377    for childIndex in cfgPrj.childIndexes:
378        cfgPrj.children.append(cfg.projects[childIndex])
379
380    cfgPrj.directories = []
381    for dirIndex in cfgPrj.directoryIndexes:
382        cfgPrj.directories.append(cfg.directories[dirIndex])
383
384    cfgPrj.targets = []
385    for targetIndex in cfgPrj.targetIndexes:
386        cfgPrj.targets.append(cfg.configTargets[targetIndex])
387
388# Create direct pointers for ConfigTarget indices
389# takes: Config and ConfigTarget
390def linkConfigTarget(cfg, cfgTarget):
391    if cfgTarget.directoryIndex == -1:
392        cfgTarget.directory = None
393    else:
394        cfgTarget.directory = cfg.directories[cfgTarget.directoryIndex]
395
396    if cfgTarget.projectIndex == -1:
397        cfgTarget.project = None
398    else:
399        cfgTarget.project = cfg.projects[cfgTarget.projectIndex]
400
401    # and link target's sources and source groups
402    for ts in cfgTarget.target.sources:
403        linkTargetSource(cfgTarget.target, ts)
404    for tsg in cfgTarget.target.sourceGroups:
405        linkTargetSourceGroup(cfgTarget.target, tsg)
406    for tcg in cfgTarget.target.compileGroups:
407        linkTargetCompileGroup(cfgTarget.target, tcg)
408
409# Create direct pointers for TargetSource indices
410# takes: Target and TargetSource
411def linkTargetSource(target, targetSrc):
412    if targetSrc.compileGroupIndex == -1:
413        targetSrc.compileGroup = None
414    else:
415        targetSrc.compileGroup = target.compileGroups[targetSrc.compileGroupIndex]
416
417    if targetSrc.sourceGroupIndex == -1:
418        targetSrc.sourceGroup = None
419    else:
420        targetSrc.sourceGroup = target.sourceGroups[targetSrc.sourceGroupIndex]
421
422# Create direct pointers for TargetSourceGroup indices
423# takes: Target and TargetSourceGroup
424def linkTargetSourceGroup(target, targetSrcGrp):
425    targetSrcGrp.sources = []
426    for srcIndex in targetSrcGrp.sourceIndexes:
427        targetSrcGrp.sources.append(target.sources[srcIndex])
428
429# Create direct pointers for TargetCompileGroup indices
430# takes: Target and TargetCompileGroup
431def linkTargetCompileGroup(target, targetCmpGrp):
432    targetCmpGrp.sources = []
433    for srcIndex in targetCmpGrp.sourceIndexes:
434        targetCmpGrp.sources.append(target.sources[srcIndex])
435