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