Merge branch 'blender2.7'
[blender.git] / build_files / cmake / cmake_consistency_check.py
1 #!/usr/bin/env python3
2
3 # ***** BEGIN GPL LICENSE BLOCK *****
4 #
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software Foundation,
17 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 #
19 # Contributor(s): Campbell Barton
20 #
21 # ***** END GPL LICENSE BLOCK *****
22
23 # <pep8 compliant>
24
25 import sys
26 if not sys.version.startswith("3"):
27     print("\nPython3.x needed, found %s.\nAborting!\n" %
28           sys.version.partition(" ")[0])
29     sys.exit(1)
30
31 from cmake_consistency_check_config import (
32     IGNORE_SOURCE,
33     IGNORE_CMAKE,
34     UTF8_CHECK,
35     SOURCE_DIR,
36     BUILD_DIR,
37 )
38
39
40 import os
41 from os.path import join, dirname, normpath, splitext
42
43 global_h = set()
44 global_c = set()
45 global_refs = {}
46
47
48 def replace_line(f, i, text, keep_indent=True):
49     file_handle = open(f, 'r')
50     data = file_handle.readlines()
51     file_handle.close()
52
53     l = data[i]
54     ws = l[:len(l) - len(l.lstrip())]
55
56     data[i] = "%s%s\n" % (ws, text)
57
58     file_handle = open(f, 'w')
59     file_handle.writelines(data)
60     file_handle.close()
61
62
63 def source_list(path, filename_check=None):
64     for dirpath, dirnames, filenames in os.walk(path):
65         # skip '.git'
66         dirnames[:] = [d for d in dirnames if not d.startswith(".")]
67
68         for filename in filenames:
69             if filename_check is None or filename_check(filename):
70                 yield os.path.join(dirpath, filename)
71
72
73 # extension checking
74 def is_cmake(filename):
75     ext = splitext(filename)[1]
76     return (ext == ".cmake") or (filename == "CMakeLists.txt")
77
78
79 def is_c_header(filename):
80     ext = splitext(filename)[1]
81     return (ext in {".h", ".hpp", ".hxx", ".hh"})
82
83
84 def is_c(filename):
85     ext = splitext(filename)[1]
86     return (ext in {".c", ".cpp", ".cxx", ".m", ".mm", ".rc", ".cc", ".inl"})
87
88
89 def is_c_any(filename):
90     return is_c(filename) or is_c_header(filename)
91
92
93 def cmake_get_src(f):
94
95     sources_h = []
96     sources_c = []
97
98     filen = open(f, "r", encoding="utf8")
99     it = iter(filen)
100     found = False
101     i = 0
102     # print(f)
103
104     def is_definition(l, f, i, name):
105         if l.startswith("unset("):
106             return False
107
108         if ('set(%s' % name) in l or ('set(' in l and l.endswith(name)):
109             if len(l.split()) > 1:
110                 raise Exception("strict formatting not kept 'set(%s*' %s:%d" % (name, f, i))
111             return True
112
113         if ("list(APPEND %s" % name) in l or ('list(APPEND ' in l and l.endswith(name)):
114             if l.endswith(")"):
115                 raise Exception("strict formatting not kept 'list(APPEND %s...)' on 1 line %s:%d" % (name, f, i))
116             return True
117
118     while it is not None:
119         context_name = ""
120         while it is not None:
121             i += 1
122             try:
123                 l = next(it)
124             except StopIteration:
125                 it = None
126                 break
127             l = l.strip()
128             if not l.startswith("#"):
129                 found = is_definition(l, f, i, "SRC")
130                 if found:
131                     context_name = "SRC"
132                     break
133                 found = is_definition(l, f, i, "INC")
134                 if found:
135                     context_name = "INC"
136                     break
137
138         if found:
139             cmake_base = dirname(f)
140             cmake_base_bin = os.path.join(BUILD_DIR, os.path.relpath(cmake_base, SOURCE_DIR))
141
142             while it is not None:
143                 i += 1
144                 try:
145                     l = next(it)
146                 except StopIteration:
147                     it = None
148                     break
149
150                 l = l.strip()
151
152                 if not l.startswith("#"):
153
154                     if ")" in l:
155                         if l.strip() != ")":
156                             raise Exception("strict formatting not kept '*)' %s:%d" % (f, i))
157                         break
158
159                     # replace dirs
160                     l = l.replace("${CMAKE_SOURCE_DIR}", SOURCE_DIR)
161                     l = l.replace("${CMAKE_CURRENT_SOURCE_DIR}", cmake_base)
162                     l = l.replace("${CMAKE_CURRENT_BINARY_DIR}", cmake_base_bin)
163                     l = l.strip('"')
164
165                     if not l:
166                         pass
167                     elif l.startswith("$"):
168                         if context_name == "SRC":
169                             # assume if it ends with context_name we know about it
170                             if not l.split("}")[0].endswith(context_name):
171                                 print("Can't use var '%s' %s:%d" % (l, f, i))
172                     elif len(l.split()) > 1:
173                         raise Exception("Multi-line define '%s' %s:%d" % (l, f, i))
174                     else:
175                         new_file = normpath(join(cmake_base, l))
176
177                         if context_name == "SRC":
178                             if is_c_header(new_file):
179                                 sources_h.append(new_file)
180                                 global_refs.setdefault(new_file, []).append((f, i))
181                             elif is_c(new_file):
182                                 sources_c.append(new_file)
183                                 global_refs.setdefault(new_file, []).append((f, i))
184                             elif l in {"PARENT_SCOPE", }:
185                                 # cmake var, ignore
186                                 pass
187                             elif new_file.endswith(".list"):
188                                 pass
189                             elif new_file.endswith(".def"):
190                                 pass
191                             elif new_file.endswith(".cl"):  # opencl
192                                 pass
193                             elif new_file.endswith(".cu"):  # cuda
194                                 pass
195                             elif new_file.endswith(".osl"):  # open shading language
196                                 pass
197                             elif new_file.endswith(".glsl"):
198                                 pass
199                             else:
200                                 raise Exception("unknown file type - not c or h %s -> %s" % (f, new_file))
201
202                         elif context_name == "INC":
203                             if new_file.startswith(BUILD_DIR):
204                                 # assume generated path
205                                 pass
206                             elif os.path.isdir(new_file):
207                                 new_path_rel = os.path.relpath(new_file, cmake_base)
208
209                                 if new_path_rel != l:
210                                     print("overly relative path:\n  %s:%d\n  %s\n  %s" % (f, i, l, new_path_rel))
211
212                                     # # Save time. just replace the line
213                                     # replace_line(f, i - 1, new_path_rel)
214
215                             else:
216                                 raise Exception("non existent include %s:%d -> %s" % (f, i, new_file))
217
218                         # print(new_file)
219
220             global_h.update(set(sources_h))
221             global_c.update(set(sources_c))
222             '''
223             if not sources_h and not sources_c:
224                 raise Exception("No sources %s" % f)
225
226             sources_h_fs = list(source_list(cmake_base, is_c_header))
227             sources_c_fs = list(source_list(cmake_base, is_c))
228             '''
229             # find missing C files:
230             '''
231             for ff in sources_c_fs:
232                 if ff not in sources_c:
233                     print("  missing: " + ff)
234             '''
235
236             # reset
237             del sources_h[:]
238             del sources_c[:]
239
240     filen.close()
241
242
243 def is_ignore_source(f, ignore_used):
244     for index, ig in enumerate(IGNORE_SOURCE):
245         if ig in f:
246             ignore_used[index] = True
247             return True
248     return False
249
250
251 def is_ignore_cmake(f, ignore_used):
252     for index, ig in enumerate(IGNORE_CMAKE):
253         if ig in f:
254             ignore_used[index] = True
255             return True
256     return False
257
258
259 def main():
260
261     print("Scanning:", SOURCE_DIR)
262
263     ignore_used_source = [False] * len(IGNORE_SOURCE)
264     ignore_used_cmake = [False] * len(IGNORE_CMAKE)
265
266     for cmake in source_list(SOURCE_DIR, is_cmake):
267         if not is_ignore_cmake(cmake, ignore_used_cmake):
268             cmake_get_src(cmake)
269
270     # First do stupid check, do these files exist?
271     print("\nChecking for missing references:")
272     is_err = False
273     errs = []
274     for f in (global_h | global_c):
275         if f.startswith(BUILD_DIR):
276             continue
277
278         if not os.path.exists(f):
279             refs = global_refs[f]
280             if refs:
281                 for cf, i in refs:
282                     errs.append((cf, i))
283             else:
284                 raise Exception("CMake referenecs missing, internal error, aborting!")
285             is_err = True
286
287     errs.sort()
288     errs.reverse()
289     for cf, i in errs:
290         print("%s:%d" % (cf, i))
291         # Write a 'sed' script, useful if we get a lot of these
292         # print("sed '%dd' '%s' > '%s.tmp' ; mv '%s.tmp' '%s'" % (i, cf, cf, cf, cf))
293
294     if is_err:
295         raise Exception("CMake referenecs missing files, aborting!")
296     del is_err
297     del errs
298
299     # now check on files not accounted for.
300     print("\nC/C++ Files CMake does not know about...")
301     for cf in sorted(source_list(SOURCE_DIR, is_c)):
302         if not is_ignore_source(cf, ignore_used_source):
303             if cf not in global_c:
304                 print("missing_c: ", cf)
305
306             # check if automake builds a corrasponding .o file.
307             '''
308             if cf in global_c:
309                 out1 = os.path.splitext(cf)[0] + ".o"
310                 out2 = os.path.splitext(cf)[0] + ".Po"
311                 out2_dir, out2_file = out2 = os.path.split(out2)
312                 out2 = os.path.join(out2_dir, ".deps", out2_file)
313                 if not os.path.exists(out1) and not os.path.exists(out2):
314                     print("bad_c: ", cf)
315             '''
316
317     print("\nC/C++ Headers CMake does not know about...")
318     for hf in sorted(source_list(SOURCE_DIR, is_c_header)):
319         if not is_ignore_source(hf, ignore_used_source):
320             if hf not in global_h:
321                 print("missing_h: ", hf)
322
323     if UTF8_CHECK:
324         # test encoding
325         import traceback
326         for files in (global_c, global_h):
327             for f in sorted(files):
328                 if os.path.exists(f):
329                     # ignore outside of our source tree
330                     if "extern" not in f:
331                         i = 1
332                         try:
333                             for l in open(f, "r", encoding="utf8"):
334                                 i += 1
335                         except UnicodeDecodeError:
336                             print("Non utf8: %s:%d" % (f, i))
337                             if i > 1:
338                                 traceback.print_exc()
339
340     # Check ignores aren't stale
341     print("\nCheck for unused 'IGNORE_SOURCE' paths...")
342     for index, ig in enumerate(IGNORE_SOURCE):
343         if not ignore_used_source[index]:
344             print("unused ignore: %r" % ig)
345
346     # Check ignores aren't stale
347     print("\nCheck for unused 'IGNORE_CMAKE' paths...")
348     for index, ig in enumerate(IGNORE_CMAKE):
349         if not ignore_used_cmake[index]:
350             print("unused ignore: %r" % ig)
351
352
353 if __name__ == "__main__":
354     main()