blob: bdbf7859c3585b5f89fa6d3afd4cc572fbd3bbbe [file] [log] [blame]
Jianliang Shenf33d7932021-10-13 17:32:10 +08001# -----------------------------------------------------------------------------
2# Copyright (c) 2021, Arm Limited. All rights reserved.
3#
4# SPDX-License-Identifier: BSD-3-Clause
5#
6# -----------------------------------------------------------------------------
7
8import os
9import glob
10import sys
11import getopt
12import json
13from graphviz import Digraph
14
15# Const variables
16Public, Interface, Private = 0, 1, 2
17Static_lib, Interface_lib, Unknown_lib = 'lightgreen', 'lightblue2', 'pink'
18input_flag, output_flag = 0, 1
19library_flag, include_flag, source_flag, compile_definition_flag = 0, 1, 2, 3
20help_msg = 'This is a tool to draw a library\'s link relationship.\
21The usage is:\n\
22python3 lib_trace.py -l <library_name>\n\
23 -p <repo_path_1,repo_path_2,...>\n\
24 -d <max depth>\n\
25 -i # draw src libraries\n\
26 -o # draw dst libraries\n\
27 -h # help message'
28edge_color = {
29 'PUBLIC': 'green',
30 'PRIVATE': 'red',
31 'INTERFACE': 'blue',
32 '': 'black'
33}
34gz_library = Digraph(
35 name="CMake Library Relationship",
36 comment='comment',
37 filename=None,
38 directory=None,
39 format='png',
40 engine=None,
41 encoding="UTF-8",
42 graph_attr={'rankdir': 'LR'},
43 node_attr={
44 'color': 'lightblue2',
45 'fontcolor': 'black',
46 'fontname': 'TimesNewRoman',
47 'fontsize': '24',
48 'shape': 'Mrecord',
49 'style': 'filled',
50 },
51 edge_attr={
52 'color': '#999999',
53 'fontcolor': 'black',
54 'fontsize': '16',
55 'fontname': 'TimesNewRoman'
56 },
57 body=None,
58 strict=True
59)
60
61def find_file_in_path(path, name_list):
62 """
63 Search file in a list of 'path' which name is in 'name_list' and the content
64 in the file includes certain cmake function key words.
65 """
66 file_list, content = [], ''
67 path_list = path.split(',')
68 for sub_path in path_list:
69 for name in name_list:
70 for root, dirs, files in os.walk(sub_path):
71 pattern = os.path.join(root, name)
72 for sub_file in glob.glob(pattern):
73 content = open(sub_file,
74 encoding="utf8",
75 errors='ignore').read()
76 if 'add_library' in content \
77 or 'target_link_libraries' in content \
78 or 'target_include_directories' in content \
79 or 'target_sources' in content\
80 or 'target_compile_definitions' in content:
81 file_list.append(sub_file)
82 return file_list
83
84def get_library_name_and_property(file_list):
85 """
86 Get library name and property, including static, interface and unknown.
87 """
88 ret = {}
89 for sub_file in file_list:
90 file_content = open(sub_file).read()
91 position_start = 0
92 while position_start >= 0:
93 position_start = file_content.find(
94 'add_library', position_start + len('add_library'))
95 if position_start > 0:
96 position_end = file_content.find(')', position_start) + 1
97 add_library = file_content[position_start:position_end]
98 lib_name = add_library[add_library.find(
99 '(')+1: add_library.find(' ')]
100 if add_library.find('STATIC') > 0:
101 ret[lib_name] = Static_lib
102 elif add_library.find('INTERFACE') > 0:
103 ret[lib_name] = Interface_lib
104 else:
105 ret[lib_name] = Unknown_lib
106 return ret
107
108def check_input_library():
109 """
110 Check the input library whether exists.
111 """
112 flag = False
113 for s in all_libs.keys():
114 if s == library:
115 flag = True
116 if not flag:
117 print("Error: library %s doesn't exist!"% library)
118 exit(2)
119
120def get_relationship(key_word, relationship_flag):
121 """
122 Get relationship in cmake files between target and source.
123
124 The target is usaually a library name and it will be added into the key_word
125 to search suitable dependencies, including source_libraries, include paths,
126 source files and compiler definitions. These different classes are
127 determined by relationship_flag.
128
129 It will return a list of [source, target, cmake_key_word, condition]
130 """
131 def rename_file(initial_file, current_path, relationship_flag):
132 """
133 Format the source name into ablsolute path if it is a include path or
134 source file.
135 TODO: Add more checks about CMAKE variables
136 """
137 ret = ""
138 if relationship_flag == library_flag or \
139 relationship_flag == compile_definition_flag:
140 ret = initial_file
141 elif relationship_flag == include_flag:
142 if 'CMAKE_CURRENT_SOURCE_DIR' in initial_file:
143 ret = current_path[:-len('CMakeLists.txt')] + \
144 initial_file[len('$\{CMAKE_CURRENT_SOURCE_DIR\}') - 1:]
145 elif '$' in initial_file:
146 ret = initial_file
147 else:
148 if len(initial_file) == 1 and initial_file[0] == '.':
149 ret = current_path[:-
150 len('CMakeLists.txt')] + initial_file[1:]
151 else:
152 ret = current_path[:- len('CMakeLists.txt')] + initial_file
153 elif relationship_flag == source_flag:
154 if 'CMAKE_CURRENT_SOURCE_DIR' in initial_file:
155 ret = current_path[:-len('CMakeLists.txt')] + \
156 initial_file[len('$\{CMAKE_CURRENT_SOURCE_DIR\}') - 1:]
157 elif '$' in initial_file:
158 ret = initial_file
159 else:
160 ret = current_path[:- len('CMakeLists.txt')] + initial_file
161 return ret
162
163 def delete_comment(input):
164 """
165 Sometimes there are comments in cmake key content which will affect the
166 source deal.
167 """
168 left_idx = 0
169 right_idx = 0
170 while left_idx >= 0:
171 left_idx = input.find('#', left_idx + 1)
172 right_idx = input.find('\n', left_idx)
173 input = input.replace(input[left_idx:right_idx], "")
174 return input
175
176 ret, cmake_key_word = [], ""
177 for sub_file in file_list:
178 left_idx = 0
179 file_content = open(sub_file,
180 encoding="utf8",
181 errors='ignore').read()
182 while left_idx >= 0:
183 left_idx = file_content.find(key_word, left_idx + len(key_word))
184 if left_idx > 0:
185 right_idx = file_content.find(')', left_idx) + 1
186
187 # Get the key content without any cmake comment
188 key_content = delete_comment(file_content[left_idx:right_idx])
189
190 # Get source list
191 src_list = key_content.split()
192
193 # Get the target library name
194 target = src_list[0][key_content.find('(') + 1:]
195
196 for src in src_list[1:-1]:
197 if src in ['PUBLIC', 'INTERFACE', 'PRIVATE']:
198 cmake_key_word = src
199 continue
200 else:
201 condition = ""
202 if src.find(':') > 0:
203
204 # Get link condition
205 condition = src[2:-(len(src.split(':')[-1]) + 1)]
206 src = src.split(':')[-1][:-1]
207
208 ret.append([rename_file(src,
209 sub_file,
210 relationship_flag),
211 target,
212 cmake_key_word,
213 condition])
214 return ret
215
216def append_lib():
217 """
218 Append more libraries into all_libs from link_library
219 """
220 tmp = []
221 for s in all_libs:
222 tmp.append(s)
223
224 for s in link_library:
225 if s[0] not in tmp:
226 tmp.append(s[0])
227 all_libs[s[0]] = Unknown_lib
228 if s[1] not in tmp:
229 tmp.append(s[1])
230 all_libs[s[1]] = Unknown_lib
231
232def restruct_relationship(flag, target_library, input_relationship):
233 """
234 The input_relationship is a list of [source, target, cmake_key_word,
235 condition], and it shows like:
236 condition
237 source -----cmake_key_word---> target
238
239 This function will traverse the input_relationship and restruct the
240 relationship into a dictionary.
241 """
242 ret = {'PUBLIC': [], 'PRIVATE': [], 'INTERFACE': []}
243 lib_public, lib_interface, lib_private = [], [], []
244 if flag == input_flag:
245 target_idx, source_idx = 1, 0
246 elif flag == output_flag:
247 """
248 An exception situation is when searching the target's destination
249 library which links the target. So the index will be reversed.
250 """
251 target_idx, source_idx = 0, 1
252
253 for lib in input_relationship:
254
255 # Avoid repeat
256 for s in ret['PUBLIC']:
257 lib_public.append(s['name'])
258 for s in ret['PRIVATE']:
259 lib_interface.append(s['name'])
260 for s in ret['INTERFACE']:
261 lib_private.append(s['name'])
262
263 if target_library == lib[target_idx]:
264 if lib[2] in ['PUBLIC', 'public', 'Public'] and \
265 lib[source_idx] not in lib_public:
266 ret['PUBLIC'].append({'name': lib[source_idx],
267 'condition': lib[3]})
268 if lib[2] in ['INTERFACE', 'interface', 'Interface'] and \
269 lib[source_idx] not in lib_interface:
270 ret['INTERFACE'].append({'name': lib[source_idx],
271 'condition': lib[3]})
272 if lib[2] in ['PRIVATE', 'private', 'Private'] and \
273 lib[source_idx] not in lib_private:
274 ret['PRIVATE'].append({'name': lib[source_idx],
275 'condition': lib[3]})
276 for s in ret.keys():
277
278 # Sort the ret with key 'name'
279 ret[s] = sorted(ret[s], key=lambda i: i['name'])
280 return ret
281
282def draw_graph(flag, target_library, drawed_libs, drawed_edges, deep_size):
283 """
284 Draw the library graph.
285 Parameters:
286 flag: To get whether source or destination libraries
287 target_library: The target library
288 drawed_libs: A list of libraries which already be added as a node
289 drawed_edges: A list of drawed edges in the graph
290 deep_size: Current iteration count
291 """
292 if flag == input_flag:
293 libs = restruct_relationship(flag, target_library, link_library)
294 elif flag == output_flag:
295 libs = restruct_relationship(flag, target_library, link_library)
296 lib_count = 0
297
298 # Get source's count which is the condition of iteration.
299 for s in libs.keys():
300 lib_count += len(libs[s])
301 if lib_count != 0 and deep_size < max_dept:
302 for s in libs.keys():
303 for lib in libs[s]:
304
305 # Draw source node
306 if lib['name'] not in drawed_libs:
307 deep_size += 1
308
309 # Draw iteration
310 draw_graph(flag, lib['name'], drawed_libs, drawed_edges,
311 deep_size)
312 deep_size -= 1
313 gz_library.node(name=lib['name'],
314 color=all_libs[lib['name']])
315 drawed_libs.append(lib['name'])
316
317 # Draw taget node
318 if target_library not in drawed_libs:
319 gz_library.node(name=target_library,
320 color=all_libs[target_library])
321 drawed_libs.append(target_library)
322
323 # Draw edage
324 if [lib['name'], target_library] not in drawed_edges:
325 if flag == input_flag:
326 gz_library.edge(lib['name'],
327 target_library,
328 color=edge_color[s],
329 label=lib['condition'])
330 drawed_edges.append([lib['name'], target_library])
331 elif flag == output_flag:
332 gz_library.edge(target_library,
333 lib['name'],
334 color=edge_color[s],
335 label=lib['condition'])
336 drawed_edges.append([target_library, lib['name']])
337
338def get_library_relationship():
339 """
340 Get a list of all library relationship.
341 """
342 key_word = 'target_link_libraries'
343 ret = get_relationship(key_word, library_flag)
344 return ret
345
346def get_library_include(target_library):
347 """
348 Get a dictionary of library include pathes.
349 """
350 key_word = 'target_include_directories' + '(' + target_library
351 ret = restruct_relationship(input_flag,
352 target_library,
353 get_relationship(key_word,
354 include_flag))
355 return ret
356
357def get_library_source(target_library):
358 """
359 Get a dictionary of library source files.
360 """
361 key_word = 'target_sources' + '(' + library
362 ret = restruct_relationship(input_flag,
363 target_library,
364 get_relationship(key_word,
365 source_flag))
366 return ret
367
368def get_library_compile_definitions(target_library):
369 """
370 Get a dictionary of library compile definitions.
371 """
372 key_word = 'target_compile_definitions' + '(' + library
373 ret = restruct_relationship(input_flag,
374 target_library,
375 get_relationship(key_word,
376 compile_definition_flag))
377 return ret
378
379def main(argv):
380 opts = []
381 global all_libs, library, link_library, PATH, max_dept, file_list
382 src_flag, dst_flag = False, False
383 drawed_edges, drawed_libs = [], []
384
385 try:
386 opts, args = getopt.getopt(
387 argv, "hiol:p:d:", ["ilib=", "ppath=", "ddept="])
388 except getopt.GetoptError:
389 print(help_msg)
390 sys.exit(2)
391 if not opts:
392 print(help_msg)
393 sys.exit(2)
394
395 # Get input options
396 for opt, arg in opts:
397 if opt == '-h':
398 print(help_msg)
399 sys.exit()
400 elif opt in ("-l", "--ilib"):
401 library = arg
402 elif opt in ("-p", "--ppath"):
403 PATH = arg
404 elif opt in ("-d", "--ddept"):
405 max_dept = int(arg)
406 elif opt == '-o':
407 dst_flag = True
408 elif opt == '-i':
409 src_flag = True
410
411 # Get all library relationship and all libraries
412 file_list = find_file_in_path(PATH, ['*.txt', '*.cmake'])
413 all_libs = get_library_name_and_property(file_list)
414 link_library = get_library_relationship()
415 append_lib()
416
417 check_input_library()
418
419 src_lib = restruct_relationship(input_flag, library, link_library)
420 dst_lib = restruct_relationship(output_flag, library, link_library)
421 include = get_library_include(library)
422 source = get_library_source(library)
423 definitions = get_library_compile_definitions(library)
424
425 # Draw graph
426 if src_flag:
427 drawed_libs = []
428 draw_graph(input_flag, library, drawed_libs, drawed_edges, 0)
429 if dst_flag:
430 drawed_libs = []
431 draw_graph(output_flag, library, drawed_libs, drawed_edges, 0)
432
433 # Redraw the target library as a bigger node
434 gz_library.node(name=library,
435 fontsize='36',
436 penwidth='10',
437 shape='oval')
438
439 # Save picture
440 gz_library.render(filename=library)
441
442 # Write into a file with JSON format
443 log = {'library name': library,
444 'source libraries': src_lib,
445 'destination libraries': dst_lib,
446 'include directories': include,
447 'source files': source,
448 'compiler definitions': definitions
449 }
450 fo = open(library + ".log", "w")
451 fo.write(json.dumps(log,
452 sort_keys=False, indent=4, separators=(',', ': ')))
453 fo.close()
454
455 fo = open(library + '.txt', "w")
456 for s in log.keys():
457 if s == 'library name':
458 fo.write('library name: ' + s + '\n')
459 continue
460 fo.write('\n' + s + ':\n')
461 for t in log[s].keys():
462 if log[s][t]:
463 fo.write('\t' + t + '\n')
464 for x in log[s][t]:
465 if x['condition']:
466 fo.write('\t\t' + x['name'] + '\t<----\t'\
467 + x['condition'] + '\n')
468 else:
469 fo.write('\t\t' + x['name'] + '\n')
470 fo.close()
471
472if __name__ == "__main__":
473 main(sys.argv[1:])