blob: bdbf7859c3585b5f89fa6d3afd4cc572fbd3bbbe [file] [log] [blame]
# -----------------------------------------------------------------------------
# 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:])