Merge branch 'master' into blender2.8
[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         # skip '.git'
65         dirnames[:] = [d for d in dirnames if not d.startswith(".")]
66
67         for filename in filenames:
68             if filename_check is None or filename_check(filename):
69                 yield os.path.join(dirpath, filename)
70
71
72 # extension checking
73 def is_cmake(filename):
74     ext = splitext(filename)[1]
75     return (ext == ".cmake") or (filename == "CMakeLists.txt")
76
77
78 def is_c_header(filename):
79     ext = splitext(filename)[1]
80     return (ext in {".h", ".hpp", ".hxx", ".hh"})
81
82
83 def is_c(filename):
84     ext = splitext(filename)[1]
85     return (ext in {".c", ".cpp", ".cxx", ".m", ".mm", ".rc", ".cc", ".inl"})
86
87
88 def is_c_any(filename):
89     return is_c(filename) or is_c_header(filename)
90
91
92 def cmake_get_src(f):
93
94     sources_h = []
95     sources_c = []
96
97     filen = open(f, "r", encoding="utf8")
98     it = iter(filen)
99     found = False
100     i = 0
101     # print(f)
102
103     def is_definition(l, f, i, name):
104         if l.startswith("unset("):
105             return False
106
107         if ('set(%s' % name) in l or ('set(' in l and l.endswith(name)):
108             if len(l.split()) > 1:
109                 raise Exception("strict formatting not kept 'set(%s*' %s:%d" % (name, f, i))
110             return True
111
112         if ("list(APPEND %s" % name) in l or ('list(APPEND ' in l and l.endswith(name)):
113             if l.endswith(")"):
114                 raise Exception("strict formatting not kept 'list(APPEND %s...)' on 1 line %s:%d" % (name, f, i))
115             return True
116
117     while it is not None:
118         context_name = ""
119         while it is not None:
120             i += 1
121             try:
122                 l = next(it)
123             except StopIteration:
124                 it = None
125                 break
126             l = l.strip()
127             if not l.startswith("#"):
128                 found = is_definition(l, f, i, "SRC")
129                 if found:
130                     context_name = "SRC"
131                     break
132                 found = is_definition(l, f, i, "INC")
133                 if found:
134                     context_name = "INC"
135                     break
136
137         if found:
138             cmake_base = dirname(f)
139             cmake_base_bin = os.path.join(BUILD_DIR, os.path.relpath(cmake_base, SOURCE_DIR))
140
141             while it is not None:
142                 i += 1
143                 try:
144                     l = next(it)
145                 except StopIteration:
146                     it = None
147                     break
148
149                 l = l.strip()
150
151                 if not l.startswith("#"):
152
153                     if ")" in l:
154                         if l.strip() != ")":
155                             raise Exception("strict formatting not kept '*)' %s:%d" % (f, i))
156                         break
157
158                     # replace dirs
159                     l = l.replace("${CMAKE_CURRENT_SOURCE_DIR}", cmake_base)
160                     l = l.replace("${CMAKE_CURRENT_BINARY_DIR}", cmake_base_bin)
161                     l = l.strip('"')
162
163                     if not l:
164                         pass
165                     elif l.startswith("$"):
166                         if context_name == "SRC":
167                             # assume if it ends with context_name we know about it
168                             if not l.split("}")[0].endswith(context_name):
169                                 print("Can't use var '%s' %s:%d" % (l, f, i))
170                     elif len(l.split()) > 1:
171                         raise Exception("Multi-line define '%s' %s:%d" % (l, f, i))
172                     else:
173                         new_file = normpath(join(cmake_base, l))
174
175                         if context_name == "SRC":
176                             if is_c_header(new_file):
177                                 sources_h.append(new_file)
178                                 global_refs.setdefault(new_file, []).append((f, i))
179                             elif is_c(new_file):
180                                 sources_c.append(new_file)
181                                 global_refs.setdefault(new_file, []).append((f, i))
182                             elif l in {"PARENT_SCOPE", }:
183                                 # cmake var, ignore
184                                 pass
185                             elif new_file.endswith(".list"):
186                                 pass
187                             elif new_file.endswith(".def"):
188                                 pass
189                             elif new_file.endswith(".cl"):  # opencl
190                                 pass
191                             elif new_file.endswith(".cu"):  # cuda
192                                 pass
193                             elif new_file.endswith(".osl"):  # open shading language
194                                 pass
195                             elif new_file.endswith(".glsl"):
196                                 pass
197                             else:
198                                 raise Exception("unknown file type - not c or h %s -> %s" % (f, new_file))
199
200                         elif context_name == "INC":
201                             if new_file.startswith(BUILD_DIR):
202                                 # assume generated path
203                                 pass
204                             elif os.path.isdir(new_file):
205                                 new_path_rel = os.path.relpath(new_file, cmake_base)
206
207                                 if new_path_rel != l:
208                                     print("overly relative path:\n  %s:%d\n  %s\n  %s" % (f, i, l, new_path_rel))
209
210                                     # # Save time. just replace the line
211                                     # replace_line(f, i - 1, new_path_rel)
212
213                             else:
214                                 raise Exception("non existent include %s:%d -> %s" % (f, i, new_file))
215
216                         # print(new_file)
217
218             global_h.update(set(sources_h))
219             global_c.update(set(sources_c))
220             '''
221             if not sources_h and not sources_c:
222                 raise Exception("No sources %s" % f)
223
224             sources_h_fs = list(source_list(cmake_base, is_c_header))
225             sources_c_fs = list(source_list(cmake_base, is_c))
226             '''
227             # find missing C files:
228             '''
229             for ff in sources_c_fs:
230                 if ff not in sources_c:
231                     print("  missing: " + ff)
232             '''
233
234             # reset
235             del sources_h[:]
236             del sources_c[:]
237
238     filen.close()
239
240
241 def is_ignore(f, ignore_used):
242     for index, ig in enumerate(IGNORE):
243         if ig in f:
244             ignore_used[index] = True
245             return True
246     return False
247
248
249 def main():
250
251     print("Scanning:", SOURCE_DIR)
252
253     for cmake in source_list(SOURCE_DIR, is_cmake):
254         cmake_get_src(cmake)
255
256     # First do stupid check, do these files exist?
257     print("\nChecking for missing references:")
258     is_err = False
259     errs = []
260     for f in (global_h | global_c):
261         if f.startswith(BUILD_DIR):
262             continue
263
264         if not os.path.exists(f):
265             refs = global_refs[f]
266             if refs:
267                 for cf, i in refs:
268                     errs.append((cf, i))
269             else:
270                 raise Exception("CMake referenecs missing, internal error, aborting!")
271             is_err = True
272
273     errs.sort()
274     errs.reverse()
275     for cf, i in errs:
276         print("%s:%d" % (cf, i))
277         # Write a 'sed' script, useful if we get a lot of these
278         # print("sed '%dd' '%s' > '%s.tmp' ; mv '%s.tmp' '%s'" % (i, cf, cf, cf, cf))
279
280     if is_err:
281         raise Exception("CMake referenecs missing files, aborting!")
282     del is_err
283     del errs
284
285     ignore_used = [False] * len(IGNORE)
286
287     # now check on files not accounted for.
288     print("\nC/C++ Files CMake doesnt know about...")
289     for cf in sorted(source_list(SOURCE_DIR, is_c)):
290         if not is_ignore(cf, ignore_used):
291             if cf not in global_c:
292                 print("missing_c: ", cf)
293
294             # check if automake builds a corrasponding .o file.
295             '''
296             if cf in global_c:
297                 out1 = os.path.splitext(cf)[0] + ".o"
298                 out2 = os.path.splitext(cf)[0] + ".Po"
299                 out2_dir, out2_file = out2 = os.path.split(out2)
300                 out2 = os.path.join(out2_dir, ".deps", out2_file)
301                 if not os.path.exists(out1) and not os.path.exists(out2):
302                     print("bad_c: ", cf)
303             '''
304
305     print("\nC/C++ Headers CMake doesnt know about...")
306     for hf in sorted(source_list(SOURCE_DIR, is_c_header)):
307         if not is_ignore(hf, ignore_used):
308             if hf not in global_h:
309                 print("missing_h: ", hf)
310
311     if UTF8_CHECK:
312         # test encoding
313         import traceback
314         for files in (global_c, global_h):
315             for f in sorted(files):
316                 if os.path.exists(f):
317                     # ignore outside of our source tree
318                     if "extern" not in f:
319                         i = 1
320                         try:
321                             for l in open(f, "r", encoding="utf8"):
322                                 i += 1
323                         except UnicodeDecodeError:
324                             print("Non utf8: %s:%d" % (f, i))
325                             if i > 1:
326                                 traceback.print_exc()
327
328     # Check ignores aren't stale
329     print("\nCheck for unused 'IGNORE' paths...")
330     for index, ig in enumerate(IGNORE):
331         if not ignore_used[index]:
332             print("unused ignore: %r" % ig)
333
334
335 if __name__ == "__main__":
336     main()