Tool: A library dependency trace tool
This is a python script to help developers search the specific
library's dependencies.
Signed-off-by: Jianliang Shen <jianliang.shen@arm.com>
Change-Id: I20bd5f3175faf3192fa77a8939fa1049d328e0c5
diff --git a/depend-trace-tool/lib_trace.py b/depend-trace-tool/lib_trace.py
new file mode 100644
index 0000000..bdbf785
--- /dev/null
+++ b/depend-trace-tool/lib_trace.py
@@ -0,0 +1,473 @@
+# -----------------------------------------------------------------------------
+# Copyright (c) 2021, Arm Limited. All rights reserved.
+#
+# SPDX-License-Identifier: BSD-3-Clause
+#
+# -----------------------------------------------------------------------------
+
+import os
+import glob
+import sys
+import getopt
+import json
+from graphviz import Digraph
+
+# Const variables
+Public, Interface, Private = 0, 1, 2
+Static_lib, Interface_lib, Unknown_lib = 'lightgreen', 'lightblue2', 'pink'
+input_flag, output_flag = 0, 1
+library_flag, include_flag, source_flag, compile_definition_flag = 0, 1, 2, 3
+help_msg = 'This is a tool to draw a library\'s link relationship.\
+The usage is:\n\
+python3 lib_trace.py -l <library_name>\n\
+ -p <repo_path_1,repo_path_2,...>\n\
+ -d <max depth>\n\
+ -i # draw src libraries\n\
+ -o # draw dst libraries\n\
+ -h # help message'
+edge_color = {
+ 'PUBLIC': 'green',
+ 'PRIVATE': 'red',
+ 'INTERFACE': 'blue',
+ '': 'black'
+}
+gz_library = Digraph(
+ name="CMake Library Relationship",
+ comment='comment',
+ filename=None,
+ directory=None,
+ format='png',
+ engine=None,
+ encoding="UTF-8",
+ graph_attr={'rankdir': 'LR'},
+ node_attr={
+ 'color': 'lightblue2',
+ 'fontcolor': 'black',
+ 'fontname': 'TimesNewRoman',
+ 'fontsize': '24',
+ 'shape': 'Mrecord',
+ 'style': 'filled',
+ },
+ edge_attr={
+ 'color': '#999999',
+ 'fontcolor': 'black',
+ 'fontsize': '16',
+ 'fontname': 'TimesNewRoman'
+ },
+ body=None,
+ strict=True
+)
+
+def find_file_in_path(path, name_list):
+ """
+ Search file in a list of 'path' which name is in 'name_list' and the content
+ in the file includes certain cmake function key words.
+ """
+ file_list, content = [], ''
+ path_list = path.split(',')
+ for sub_path in path_list:
+ for name in name_list:
+ for root, dirs, files in os.walk(sub_path):
+ pattern = os.path.join(root, name)
+ for sub_file in glob.glob(pattern):
+ content = open(sub_file,
+ encoding="utf8",
+ errors='ignore').read()
+ if 'add_library' in content \
+ or 'target_link_libraries' in content \
+ or 'target_include_directories' in content \
+ or 'target_sources' in content\
+ or 'target_compile_definitions' in content:
+ file_list.append(sub_file)
+ return file_list
+
+def get_library_name_and_property(file_list):
+ """
+ Get library name and property, including static, interface and unknown.
+ """
+ ret = {}
+ for sub_file in file_list:
+ file_content = open(sub_file).read()
+ position_start = 0
+ while position_start >= 0:
+ position_start = file_content.find(
+ 'add_library', position_start + len('add_library'))
+ if position_start > 0:
+ position_end = file_content.find(')', position_start) + 1
+ add_library = file_content[position_start:position_end]
+ lib_name = add_library[add_library.find(
+ '(')+1: add_library.find(' ')]
+ if add_library.find('STATIC') > 0:
+ ret[lib_name] = Static_lib
+ elif add_library.find('INTERFACE') > 0:
+ ret[lib_name] = Interface_lib
+ else:
+ ret[lib_name] = Unknown_lib
+ return ret
+
+def check_input_library():
+ """
+ Check the input library whether exists.
+ """
+ flag = False
+ for s in all_libs.keys():
+ if s == library:
+ flag = True
+ if not flag:
+ print("Error: library %s doesn't exist!"% library)
+ exit(2)
+
+def get_relationship(key_word, relationship_flag):
+ """
+ Get relationship in cmake files between target and source.
+
+ The target is usaually a library name and it will be added into the key_word
+ to search suitable dependencies, including source_libraries, include paths,
+ source files and compiler definitions. These different classes are
+ determined by relationship_flag.
+
+ It will return a list of [source, target, cmake_key_word, condition]
+ """
+ def rename_file(initial_file, current_path, relationship_flag):
+ """
+ Format the source name into ablsolute path if it is a include path or
+ source file.
+ TODO: Add more checks about CMAKE variables
+ """
+ ret = ""
+ if relationship_flag == library_flag or \
+ relationship_flag == compile_definition_flag:
+ ret = initial_file
+ elif relationship_flag == include_flag:
+ if 'CMAKE_CURRENT_SOURCE_DIR' in initial_file:
+ ret = current_path[:-len('CMakeLists.txt')] + \
+ initial_file[len('$\{CMAKE_CURRENT_SOURCE_DIR\}') - 1:]
+ elif '$' in initial_file:
+ ret = initial_file
+ else:
+ if len(initial_file) == 1 and initial_file[0] == '.':
+ ret = current_path[:-
+ len('CMakeLists.txt')] + initial_file[1:]
+ else:
+ ret = current_path[:- len('CMakeLists.txt')] + initial_file
+ elif relationship_flag == source_flag:
+ if 'CMAKE_CURRENT_SOURCE_DIR' in initial_file:
+ ret = current_path[:-len('CMakeLists.txt')] + \
+ initial_file[len('$\{CMAKE_CURRENT_SOURCE_DIR\}') - 1:]
+ elif '$' in initial_file:
+ ret = initial_file
+ else:
+ ret = current_path[:- len('CMakeLists.txt')] + initial_file
+ return ret
+
+ def delete_comment(input):
+ """
+ Sometimes there are comments in cmake key content which will affect the
+ source deal.
+ """
+ left_idx = 0
+ right_idx = 0
+ while left_idx >= 0:
+ left_idx = input.find('#', left_idx + 1)
+ right_idx = input.find('\n', left_idx)
+ input = input.replace(input[left_idx:right_idx], "")
+ return input
+
+ ret, cmake_key_word = [], ""
+ for sub_file in file_list:
+ left_idx = 0
+ file_content = open(sub_file,
+ encoding="utf8",
+ errors='ignore').read()
+ while left_idx >= 0:
+ left_idx = file_content.find(key_word, left_idx + len(key_word))
+ if left_idx > 0:
+ right_idx = file_content.find(')', left_idx) + 1
+
+ # Get the key content without any cmake comment
+ key_content = delete_comment(file_content[left_idx:right_idx])
+
+ # Get source list
+ src_list = key_content.split()
+
+ # Get the target library name
+ target = src_list[0][key_content.find('(') + 1:]
+
+ for src in src_list[1:-1]:
+ if src in ['PUBLIC', 'INTERFACE', 'PRIVATE']:
+ cmake_key_word = src
+ continue
+ else:
+ condition = ""
+ if src.find(':') > 0:
+
+ # Get link condition
+ condition = src[2:-(len(src.split(':')[-1]) + 1)]
+ src = src.split(':')[-1][:-1]
+
+ ret.append([rename_file(src,
+ sub_file,
+ relationship_flag),
+ target,
+ cmake_key_word,
+ condition])
+ return ret
+
+def append_lib():
+ """
+ Append more libraries into all_libs from link_library
+ """
+ tmp = []
+ for s in all_libs:
+ tmp.append(s)
+
+ for s in link_library:
+ if s[0] not in tmp:
+ tmp.append(s[0])
+ all_libs[s[0]] = Unknown_lib
+ if s[1] not in tmp:
+ tmp.append(s[1])
+ all_libs[s[1]] = Unknown_lib
+
+def restruct_relationship(flag, target_library, input_relationship):
+ """
+ The input_relationship is a list of [source, target, cmake_key_word,
+ condition], and it shows like:
+ condition
+ source -----cmake_key_word---> target
+
+ This function will traverse the input_relationship and restruct the
+ relationship into a dictionary.
+ """
+ ret = {'PUBLIC': [], 'PRIVATE': [], 'INTERFACE': []}
+ lib_public, lib_interface, lib_private = [], [], []
+ if flag == input_flag:
+ target_idx, source_idx = 1, 0
+ elif flag == output_flag:
+ """
+ An exception situation is when searching the target's destination
+ library which links the target. So the index will be reversed.
+ """
+ target_idx, source_idx = 0, 1
+
+ for lib in input_relationship:
+
+ # Avoid repeat
+ for s in ret['PUBLIC']:
+ lib_public.append(s['name'])
+ for s in ret['PRIVATE']:
+ lib_interface.append(s['name'])
+ for s in ret['INTERFACE']:
+ lib_private.append(s['name'])
+
+ if target_library == lib[target_idx]:
+ if lib[2] in ['PUBLIC', 'public', 'Public'] and \
+ lib[source_idx] not in lib_public:
+ ret['PUBLIC'].append({'name': lib[source_idx],
+ 'condition': lib[3]})
+ if lib[2] in ['INTERFACE', 'interface', 'Interface'] and \
+ lib[source_idx] not in lib_interface:
+ ret['INTERFACE'].append({'name': lib[source_idx],
+ 'condition': lib[3]})
+ if lib[2] in ['PRIVATE', 'private', 'Private'] and \
+ lib[source_idx] not in lib_private:
+ ret['PRIVATE'].append({'name': lib[source_idx],
+ 'condition': lib[3]})
+ for s in ret.keys():
+
+ # Sort the ret with key 'name'
+ ret[s] = sorted(ret[s], key=lambda i: i['name'])
+ return ret
+
+def draw_graph(flag, target_library, drawed_libs, drawed_edges, deep_size):
+ """
+ Draw the library graph.
+ Parameters:
+ flag: To get whether source or destination libraries
+ target_library: The target library
+ drawed_libs: A list of libraries which already be added as a node
+ drawed_edges: A list of drawed edges in the graph
+ deep_size: Current iteration count
+ """
+ if flag == input_flag:
+ libs = restruct_relationship(flag, target_library, link_library)
+ elif flag == output_flag:
+ libs = restruct_relationship(flag, target_library, link_library)
+ lib_count = 0
+
+ # Get source's count which is the condition of iteration.
+ for s in libs.keys():
+ lib_count += len(libs[s])
+ if lib_count != 0 and deep_size < max_dept:
+ for s in libs.keys():
+ for lib in libs[s]:
+
+ # Draw source node
+ if lib['name'] not in drawed_libs:
+ deep_size += 1
+
+ # Draw iteration
+ draw_graph(flag, lib['name'], drawed_libs, drawed_edges,
+ deep_size)
+ deep_size -= 1
+ gz_library.node(name=lib['name'],
+ color=all_libs[lib['name']])
+ drawed_libs.append(lib['name'])
+
+ # Draw taget node
+ if target_library not in drawed_libs:
+ gz_library.node(name=target_library,
+ color=all_libs[target_library])
+ drawed_libs.append(target_library)
+
+ # Draw edage
+ if [lib['name'], target_library] not in drawed_edges:
+ if flag == input_flag:
+ gz_library.edge(lib['name'],
+ target_library,
+ color=edge_color[s],
+ label=lib['condition'])
+ drawed_edges.append([lib['name'], target_library])
+ elif flag == output_flag:
+ gz_library.edge(target_library,
+ lib['name'],
+ color=edge_color[s],
+ label=lib['condition'])
+ drawed_edges.append([target_library, lib['name']])
+
+def get_library_relationship():
+ """
+ Get a list of all library relationship.
+ """
+ key_word = 'target_link_libraries'
+ ret = get_relationship(key_word, library_flag)
+ return ret
+
+def get_library_include(target_library):
+ """
+ Get a dictionary of library include pathes.
+ """
+ key_word = 'target_include_directories' + '(' + target_library
+ ret = restruct_relationship(input_flag,
+ target_library,
+ get_relationship(key_word,
+ include_flag))
+ return ret
+
+def get_library_source(target_library):
+ """
+ Get a dictionary of library source files.
+ """
+ key_word = 'target_sources' + '(' + library
+ ret = restruct_relationship(input_flag,
+ target_library,
+ get_relationship(key_word,
+ source_flag))
+ return ret
+
+def get_library_compile_definitions(target_library):
+ """
+ Get a dictionary of library compile definitions.
+ """
+ key_word = 'target_compile_definitions' + '(' + library
+ ret = restruct_relationship(input_flag,
+ target_library,
+ get_relationship(key_word,
+ compile_definition_flag))
+ return ret
+
+def main(argv):
+ opts = []
+ global all_libs, library, link_library, PATH, max_dept, file_list
+ src_flag, dst_flag = False, False
+ drawed_edges, drawed_libs = [], []
+
+ try:
+ opts, args = getopt.getopt(
+ argv, "hiol:p:d:", ["ilib=", "ppath=", "ddept="])
+ except getopt.GetoptError:
+ print(help_msg)
+ sys.exit(2)
+ if not opts:
+ print(help_msg)
+ sys.exit(2)
+
+ # Get input options
+ for opt, arg in opts:
+ if opt == '-h':
+ print(help_msg)
+ sys.exit()
+ elif opt in ("-l", "--ilib"):
+ library = arg
+ elif opt in ("-p", "--ppath"):
+ PATH = arg
+ elif opt in ("-d", "--ddept"):
+ max_dept = int(arg)
+ elif opt == '-o':
+ dst_flag = True
+ elif opt == '-i':
+ src_flag = True
+
+ # Get all library relationship and all libraries
+ file_list = find_file_in_path(PATH, ['*.txt', '*.cmake'])
+ all_libs = get_library_name_and_property(file_list)
+ link_library = get_library_relationship()
+ append_lib()
+
+ check_input_library()
+
+ src_lib = restruct_relationship(input_flag, library, link_library)
+ dst_lib = restruct_relationship(output_flag, library, link_library)
+ include = get_library_include(library)
+ source = get_library_source(library)
+ definitions = get_library_compile_definitions(library)
+
+ # Draw graph
+ if src_flag:
+ drawed_libs = []
+ draw_graph(input_flag, library, drawed_libs, drawed_edges, 0)
+ if dst_flag:
+ drawed_libs = []
+ draw_graph(output_flag, library, drawed_libs, drawed_edges, 0)
+
+ # Redraw the target library as a bigger node
+ gz_library.node(name=library,
+ fontsize='36',
+ penwidth='10',
+ shape='oval')
+
+ # Save picture
+ gz_library.render(filename=library)
+
+ # Write into a file with JSON format
+ log = {'library name': library,
+ 'source libraries': src_lib,
+ 'destination libraries': dst_lib,
+ 'include directories': include,
+ 'source files': source,
+ 'compiler definitions': definitions
+ }
+ fo = open(library + ".log", "w")
+ fo.write(json.dumps(log,
+ sort_keys=False, indent=4, separators=(',', ': ')))
+ fo.close()
+
+ fo = open(library + '.txt', "w")
+ for s in log.keys():
+ if s == 'library name':
+ fo.write('library name: ' + s + '\n')
+ continue
+ fo.write('\n' + s + ':\n')
+ for t in log[s].keys():
+ if log[s][t]:
+ fo.write('\t' + t + '\n')
+ for x in log[s][t]:
+ if x['condition']:
+ fo.write('\t\t' + x['name'] + '\t<----\t'\
+ + x['condition'] + '\n')
+ else:
+ fo.write('\t\t' + x['name'] + '\n')
+ fo.close()
+
+if __name__ == "__main__":
+ main(sys.argv[1:])