0%

Python输出iOS工程下各目录头文件的依赖关系

需求

iOS工程需要按业务性质划分成不同的模块,模块以单独目录存在,因为模块间以头文件的方式存在耦合关系,需要理出这些引用的头文件好进行依赖关系梳理;

实现

环境:python 2.7.4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
#!/usr/bin/python
# -*- coding: UTF-8 -*-

# Heller Chan
# 2018-08-15
# heller.chan@vipshop.com

"""
输出各模块下所有头文件的依赖关系

Input: Xcode工程路径

Output: 以Json格式输出模块头文件依赖列表

Typical usage: $ python mod_deps.py /Users/heller/Development/VIP/vip -i Pods Carthage -e > deps.json
"""

import sys
import os
from sets import Set, ImmutableSet
import re
from os.path import basename
import argparse
import json

local_regex_import = re.compile("^\s*#(?:import|include)\s+\"(?P<filename>\S*)(?P<extension>\.(?:h|hpp|hh))?\"")
system_regex_import = re.compile("^\s*#(?:import|include)\s+[\"<](?P<filename>\S*)(?P<extension>\.(?:h|hpp|hh))?[\">]")
objc_extensions = ['.pch', '.h', '.hh', '.hpp', '.m', '.mm', '.c', '.cc', '.cpp']
modulars = ['Spec', 'Module1', 'Module2', 'Module3']

class AdvancedJSONEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, Set):
return list(obj)
if isinstance(obj, ImmutableSet):
return list(obj)
return json.JSONEncoder.default(self, obj)

def gen_filenames_imported_in_file(path, regex_exclude, system, extensions):
for line in open(path):
results = re.search(system_regex_import, line) if system else re.search(local_regex_import, line)
if results:
filename = results.group('filename')
extension = results.group('extension') if results.group('extension') else ""
if regex_exclude is not None and regex_exclude.search(filename + extension):
continue
yield (filename + extension) if extension else filename

def get_module_name(path):
for modular in modulars:
if "{}/{}".format(modular, modular) in path:
return modular
return None

def get_all_files_with_exts(files, exts):
result = list()
for ext in exts:
objc_files = (f for f in files if f.endswith(ext))
result.extend(objc_files)
return result

def dependencies_in_project(path, exts, exclude, ignore, system, extensions):
modules = {}

regex_exclude = None
if exclude:
regex_exclude = re.compile(exclude)

for root, dirs, files in os.walk(path):

if ignore:
for subfolder in ignore:
if subfolder in dirs:
dirs.remove(subfolder)

objc_files = get_all_files_with_exts(files, exts)

module = get_module_name(root)
if module is not None:
if module not in modules:
modules[module] = dict()

for f in objc_files:

filename = f if extensions else os.path.splitext(f)[0]

if regex_exclude is not None and regex_exclude.search(filename):
continue

if module is not None and filename not in modules[module]:
modules[module][filename] = Set()

path = os.path.join(root, f)

for imported_filename in gen_filenames_imported_in_file(path, regex_exclude, system, extensions):
if imported_filename != filename: # and '+' not in imported_filename and '+' not in filename:
imported_filename = imported_filename if extensions else os.path.splitext(imported_filename)[0]
if module is not None:
modules[module][filename].add(imported_filename)

return modules

def dependencies_in_json_format(path, exclude, ignore, system, extensions):

d = dependencies_in_project(path, objc_extensions, exclude, ignore, system, extensions)

modules = {}
for (k, v) in d.iteritems():
headers = (f for f in v.keys() if f.endswith('.h'))
modules[k] = list()
for header in headers:
modules[k].append(header)
pass
pass

dependencies = {}
for (module_name, dependences) in d.iteritems():

dependencies[module_name] = {}
for (header, dependence_headers) in dependences.iteritems():

for dependence_header in dependence_headers:
for (k, v) in modules.iteritems():
if dependence_header in v and k != module_name:
if k not in dependencies[module_name]:
dependencies[module_name][k] = Set()

dependencies[module_name][k].add("{} -> {}".format(header, dependence_header))
pass
pass
pass
pass
pass

return json.dumps(dependencies, cls=AdvancedJSONEncoder)

def main():
parser = argparse.ArgumentParser()
parser.add_argument("-x", "--exclude", nargs='?', default='' ,help="regular expression of substrings to exclude from module names")
parser.add_argument("-i", "--ignore", nargs='*', help="list of subfolder names to ignore")
parser.add_argument("-s", "--system", action='store_true', default=False, help="include system dependencies")
parser.add_argument("-e", "--extensions", action='store_true', default=False, help="print file extensions")
parser.add_argument("project_path", help="path to folder hierarchy containing Objective-C files")
args= parser.parse_args()

result = dependencies_in_json_format(args.project_path, args.exclude, args.ignore, args.system, args.extensions)
print result

if __name__=='__main__':
main()