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