Merge branch 'master' into temp_remove_particles
[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,
33         UTF8_CHECK,
34         SOURCE_DIR,
35         BUILD_DIR,
36         )
37
38
39 import os
40 from os.path import join, dirname, normpath, splitext
41
42 global_h = set()
43 global_c = set()
44 global_refs = {}
45
46
47 def replace_line(f, i, text, keep_indent=True):
48     file_handle = open(f, 'r')
49     data = file_handle.readlines()
50     file_handle.close()
51
52     l = data[i]
53     ws = l[:len(l) - len(l.lstrip())]
54
55     data[i] = "%s%s\n" % (ws, text)
56
57     file_handle = open(f, 'w')
58     file_handle.writelines(data)
59     file_handle.close()
60
61
62 def source_list(path, filename_check=None):
63     for dirpath, dirnames, filenames in os.walk(path):
64
65         # skip '.git'
66         if dirpath.startswith("."):
67             continue
68
69         for filename in filenames:
70             if filename_check is None or filename_check(filename):
71                 yield os.path.join(dirpath, filename)
72
73
74 # extension checking
75 def is_cmake(filename):
76     ext = splitext(filename)[1]
77     return (ext == ".cmake") or (filename == "CMakeLists.txt")
78
79
80 def is_c_header(filename):
81     ext = splitext(filename)[1]
82     return (ext in {".h", ".hpp", ".hxx", ".hh"})
83
84
85 def is_c(filename):
86     ext = splitext(filename)[1]
87     return (ext in {".c", ".cpp", ".cxx", ".m", ".mm", ".rc", ".cc", ".inl"})
88
89
90 def is_c_any(filename):
91     return is_c(filename) or is_c_header(filename)
92
93
94 def cmake_get_src(f):
95
96     sources_h = []
97     sources_c = []
98
99     filen = open(f, "r", encoding="utf8")
100     it = iter(filen)
101     found = False
102     i = 0
103     # print(f)
104
105     def is_definition(l, f, i, name):
106         if l.startswith("unset("):
107             return False
108
109         if ('set(%s' % name) in l or ('set(' in l and l.endswith(name)):
110             if len(l.split()) > 1:
111                 raise Exception("strict formatting not kept 'set(%s*' %s:%d" % (name, f, i))
112             return True
113
114         if ("list(APPEND %s" % name) in l or ('list(APPEND ' in l and l.endswith(name)):
115             if l.endswith(")"):
116                 raise Exception("strict formatting not kept 'list(APPEND %s...)' on 1 line %s:%d" % (name, f, i))
117             return True
118
119     while it is not None:
120         context_name = ""
121         while it is not None:
122             i += 1
123             try:
124                 l = next(it)
125             except StopIteration:
126                 it = None
127                 break
128             l = l.strip()
129             if not l.startswith("#"):
130                 found = is_definition(l, f, i, "SRC")
131                 if found:
132                     context_name = "SRC"
133                     break
134                 found = is_definition(l, f, i, "INC")
135                 if found:
136                     context_name = "INC"
137                     break
138
139         if found:
140             cmake_base = dirname(f)
141             cmake_base_bin = os.path.join(BUILD_DIR, os.path.relpath(cmake_base, SOURCE_DIR))
142
143             while it is not None:
144                 i += 1
145                 try:
146                     l = next(it)
147                 except StopIteration:
148                     it = None
149                     break
150
151                 l = l.strip()
152
153                 if not l.startswith("#"):
154
155                     if ")" in l:
156                         if l.strip() != ")":
157                             raise Exception("strict formatting not kept '*)' %s:%d" % (f, i))
158                         break
159
160                     # replace dirs
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(f, ignore_used):
244     for index, ig in enumerate(IGNORE):
245         if ig in f:
246             ignore_used[index] = True
247             return True
248     return False
249
250
251 def main():
252
253     print("Scanning:", SOURCE_DIR)
254
255     for cmake in source_list(SOURCE_DIR, is_cmake):
256         cmake_get_src(cmake)
257
258     # First do stupid check, do these files exist?
259     print("\nChecking for missing references:")
260     is_err = False
261     errs = []
262     for f in (global_h | global_c):
263         if f.startswith(BUILD_DIR):
264             continue
265
266         if not os.path.exists(f):
267             refs = global_refs[f]
268             if refs:
269                 for cf, i in refs:
270                     errs.append((cf, i))
271             else:
272                 raise Exception("CMake referenecs missing, internal error, aborting!")
273             is_err = True
274
275     errs.sort()
276     errs.reverse()
277     for cf, i in errs:
278         print("%s:%d" % (cf, i))
279         # Write a 'sed' script, useful if we get a lot of these
280         # print("sed '%dd' '%s' > '%s.tmp' ; mv '%s.tmp' '%s'" % (i, cf, cf, cf, cf))
281
282     if is_err:
283         raise Exception("CMake referenecs missing files, aborting!")
284     del is_err
285     del errs
286
287     ignore_used = [False] * len(IGNORE)
288
289     # now check on files not accounted for.
290     print("\nC/C++ Files CMake doesnt know about...")
291     for cf in sorted(source_list(SOURCE_DIR, is_c)):
292         if not is_ignore(cf, ignore_used):
293             if cf not in global_c:
294                 print("missing_c: ", cf)
295
296             # check if automake builds a corrasponding .o file.
297             '''
298             if cf in global_c:
299                 out1 = os.path.splitext(cf)[0] + ".o"
300                 out2 = os.path.splitext(cf)[0] + ".Po"
301                 out2_dir, out2_file = out2 = os.path.split(out2)
302                 out2 = os.path.join(out2_dir, ".deps", out2_file)
303                 if not os.path.exists(out1) and not os.path.exists(out2):
304                     print("bad_c: ", cf)
305             '''
306
307     print("\nC/C++ Headers CMake doesnt know about...")
308     for hf in sorted(source_list(SOURCE_DIR, is_c_header)):
309         if not is_ignore(hf, ignore_used):
310             if hf not in global_h:
311                 print("missing_h: ", hf)
312
313     if UTF8_CHECK:
314         # test encoding
315         import traceback
316         for files in (global_c, global_h):
317             for f in sorted(files):
318                 if os.path.exists(f):
319                     # ignore outside of our source tree
320                     if "extern" not in f:
321                         i = 1
322                         try:
323                             for l in open(f, "r", encoding="utf8"):
324                                 i += 1
325                         except UnicodeDecodeError:
326                             print("Non utf8: %s:%d" % (f, i))
327                             if i > 1:
328                                 traceback.print_exc()
329
330     # Check ignores aren't stale
331     print("\nCheck for unused 'IGNORE' paths...")
332     for index, ig in enumerate(IGNORE):
333         if not ignore_used[index]:
334             print("unused ignore: %r" % ig)
335
336
337 if __name__ == "__main__":
338     main()