cmake_consistency_check.py 12 KB

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