File indexing completed on 2024-05-12 03:53:40

0001 # -*- coding: utf-8 -*-
0002 #
0003 # SPDX-FileCopyrightText: 2014 Aurélien Gâteau <agateau@kde.org>
0004 #
0005 # SPDX-License-Identifier: BSD-2-Clause
0006 
0007 import logging
0008 import fnmatch
0009 import os
0010 import re
0011 
0012 import gv
0013 import yaml
0014 
0015 from kapidox.depdiagram import gvutils
0016 from kapidox.depdiagram.framework import Framework
0017 
0018 
0019 TARGET_SHAPES = [
0020     "polygon", # lib
0021     "house",   # executable
0022     "octagon", # module (aka plugin)
0023     "diamond", # static lib
0024     ]
0025 
0026 DEPS_SHAPE = "ellipse"
0027 
0028 DEPS_BLACKLIST = [
0029     "-l*", "-W*", # link flags
0030     "/*", # absolute dirs
0031     "m", "pthread", "util", "nsl", "resolv", # generic libs
0032     "*example*", "*demo*", "*test*", "*Test*", "*debug*" # helper targets
0033     ]
0034 
0035 
0036 def preprocess(fname):
0037     graph_handle = gv.read(fname)
0038     txt = open(fname).read()
0039     targets = []
0040 
0041     # Replace the generated node names with their label. CMake generates a graph
0042     # like this:
0043     #
0044     # "node0" [ label="KF5DNSSD" shape="polygon"];
0045     # "node1" [ label="Qt5::Network" shape="ellipse"];
0046     # "node0" -> "node1"
0047     #
0048     # And we turn it into this:
0049     #
0050     # "KF5DNSSD" [ label="KF5DNSSD" shape="polygon"];
0051     # "Qt5::Network" [ label="Qt5::Network" shape="ellipse"];
0052     # "KF5DNSSD" -> "Qt5::Network"
0053     #
0054     # Using real framework names as labels makes it possible to merge multiple
0055     # .dot files.
0056     for node_handle in gvutils.get_node_list(graph_handle):
0057         node = gvutils.Node(node_handle)
0058         label = node.label.replace("KF5::", "")
0059         if node.shape in TARGET_SHAPES:
0060             targets.append(label)
0061         txt = txt.replace('"' + node.name + '"', '"' + label + '"')
0062 
0063     # Sometimes cmake will generate an entry for the target alias, something
0064     # like this:
0065     #
0066     # "node9" [ label="KParts" shape="polygon"];
0067     # ...
0068     # "node15" [ label="KF5::KParts" shape="ellipse"];
0069     # ...
0070     #
0071     # After our node renaming, this ends up with a second "KParts" node
0072     # definition, which we need to get rid of.
0073     for target in targets:
0074         rx = r' *"' + target + '".*label="KF5::' + target + '".*shape="ellipse".*;'
0075         txt = re.sub(rx, '', txt)
0076     return txt
0077 
0078 
0079 def _add_extra_dependencies(fw, dct):
0080     lst = dct.get("framework-dependencies")
0081     if lst is None:
0082         return
0083     for dep in lst:
0084         fw.add_extra_framework(dep)
0085 
0086 
0087 class DotFileParser(object):
0088     def __init__(self, with_qt):
0089         self._with_qt = with_qt
0090 
0091     def parse(self, tier, dot_file):
0092         name = os.path.basename(dot_file).replace(".dot", "")
0093         fw = Framework(tier, name)
0094 
0095         # Preprocess dot files so that they can be merged together.
0096         dot_data =  preprocess(dot_file)
0097         self._init_fw_from_dot_data(fw, dot_data, self._with_qt)
0098 
0099         return fw
0100 
0101     def _init_fw_from_dot_data(self, fw, dot_data, with_qt):
0102         def target_from_node(node):
0103             return node.name.replace("KF5", "")
0104 
0105         src_handle = gv.readstring(dot_data)
0106 
0107         targets = set()
0108         for node_handle in gvutils.get_node_list(src_handle):
0109             node = gvutils.Node(node_handle)
0110             if node.shape in TARGET_SHAPES and self._want(node):
0111                 target = target_from_node(node)
0112                 targets.add(target)
0113                 fw.add_target(target)
0114 
0115         for edge_handle in gvutils.get_edge_list(src_handle):
0116             edge = gvutils.Edge(edge_handle)
0117             target = target_from_node(edge.tail)
0118             if target in targets and self._want(edge.head):
0119                 dep_target = target_from_node(edge.head)
0120                 fw.add_target_dependency(target, dep_target)
0121 
0122     def _want(self, node):
0123         if node.shape not in TARGET_SHAPES and node.shape != DEPS_SHAPE:
0124             return False
0125         name = node.name
0126 
0127         for pattern in DEPS_BLACKLIST:
0128             if fnmatch.fnmatchcase(node.name, pattern):
0129                 return False
0130         if not self._with_qt and name.startswith("Qt"):
0131             return False
0132         return True
0133 
0134 
0135 class FrameworkDb(object):
0136     def __init__(self):
0137         self._fw_list = []
0138         self._fw_for_target = {}
0139 
0140     def populate(self, dot_files, with_qt=False):
0141         """
0142         Init db from dot files
0143         """
0144         parser = DotFileParser(with_qt)
0145         for dot_file in dot_files:
0146             yaml_file = dot_file.replace(".dot", ".yaml")
0147             with open(yaml_file) as f:
0148                 dct = yaml.safe_load(f)
0149 
0150             if 'tier' not in dct:
0151                 # This mean it's not a frameworks
0152                 continue
0153 
0154             tier = dct["tier"]
0155             fw = parser.parse(tier, dot_file)
0156 
0157             _add_extra_dependencies(fw, dct)
0158             self._fw_list.append(fw)
0159         self._update_fw_for_target()
0160 
0161     def _update_fw_for_target(self):
0162         self._fw_for_target = {}
0163         for fw in self._fw_list:
0164             for target in fw.get_targets():
0165                 self._fw_for_target[target] = fw
0166 
0167     def find_by_name(self, name):
0168         for fw in self._fw_list:
0169             if fw.name == name:
0170                 return fw
0171         return None
0172 
0173     def remove_unused_frameworks(self, wanted_fw):
0174         def add_to_set(fw_set, wanted_fw):
0175             fw_set.add(wanted_fw)
0176 
0177             for target in wanted_fw.get_all_target_dependencies():
0178                 fw = self._fw_for_target.get(target)
0179                 if fw is not None and not fw in fw_set:
0180                     add_to_set(fw_set, fw)
0181 
0182             for fw_name in wanted_fw.get_extra_frameworks():
0183                 fw = self.find_by_name(fw_name)
0184                 if not fw:
0185                     logging.warning("Framework {} has an extra dependency on {}, but there is no such framework".format(wanted_fw, fw_name))
0186                     continue
0187                 if not fw in fw_set:
0188                     add_to_set(fw_set, fw)
0189 
0190         fw_set = set()
0191         add_to_set(fw_set, wanted_fw)
0192         self._fw_list = list(fw_set)
0193 
0194     def find_external_targets(self):
0195         all_targets = set([])
0196         fw_targets = set([])
0197         for fw in self._fw_list:
0198             fw_targets.update(fw.get_targets())
0199             all_targets.update(fw.get_all_target_dependencies())
0200         return all_targets.difference(fw_targets)
0201 
0202     def get_framework_for_target(self, target):
0203         return self._fw_for_target[target]
0204 
0205     def __iter__(self):
0206         return iter(self._fw_list)