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