diff --git a/check_source/check_cmake_consistency.py b/check_source/check_cmake_consistency.py new file mode 100755 index 0000000000000000000000000000000000000000..eff67a3da2add09698c2868939707740fcd43d1f --- /dev/null +++ b/check_source/check_cmake_consistency.py @@ -0,0 +1,424 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0-or-later + +# <pep8 compliant> + +# Note: this code should be cleaned up / refactored. + +import sys +if sys.version_info.major < 3: + print("\nPython3.x needed, found %s.\nAborting!\n" % + sys.version.partition(" ")[0]) + sys.exit(1) + +import os +from os.path import ( + dirname, + join, + normpath, + splitext, +) + +from check_cmake_consistency_config import ( + IGNORE_SOURCE, + IGNORE_SOURCE_MISSING, + IGNORE_CMAKE, + UTF8_CHECK, + SOURCE_DIR, + BUILD_DIR, +) + +from typing import ( + Callable, + Dict, + Generator, + Iterator, + List, + Optional, + Tuple, +) + + +global_h = set() +global_c = set() +global_refs: Dict[str, List[Tuple[str, int]]] = {} + +# Flatten `IGNORE_SOURCE_MISSING` to avoid nested looping. +IGNORE_SOURCE_MISSING_FLAT = [ + (k, ignore_path) for k, ig_list in IGNORE_SOURCE_MISSING + for ignore_path in ig_list +] + +# Ignore cmake file, path pairs. +global_ignore_source_missing: Dict[str, List[str]] = {} +for k, v in IGNORE_SOURCE_MISSING_FLAT: + global_ignore_source_missing.setdefault(k, []).append(v) +del IGNORE_SOURCE_MISSING_FLAT + + +def replace_line(f: str, i: int, text: str, keep_indent: bool = True) -> None: + file_handle = open(f, 'r') + data = file_handle.readlines() + file_handle.close() + + l = data[i] + ws = l[:len(l) - len(l.lstrip())] + + data[i] = "%s%s\n" % (ws, text) + + file_handle = open(f, 'w') + file_handle.writelines(data) + file_handle.close() + + +def source_list( + path: str, + filename_check: Optional[Callable[[str], bool]] = None, +) -> Generator[str, None, None]: + for dirpath, dirnames, filenames in os.walk(path): + # skip '.git' + dirnames[:] = [d for d in dirnames if not d.startswith(".")] + + for filename in filenames: + if filename_check is None or filename_check(filename): + yield os.path.join(dirpath, filename) + + +# extension checking +def is_cmake(filename: str) -> bool: + ext = splitext(filename)[1] + return (ext == ".cmake") or (filename == "CMakeLists.txt") + + +def is_c_header(filename: str) -> bool: + ext = splitext(filename)[1] + return (ext in {".h", ".hpp", ".hxx", ".hh"}) + + +def is_c(filename: str) -> bool: + ext = splitext(filename)[1] + return (ext in {".c", ".cpp", ".cxx", ".m", ".mm", ".rc", ".cc", ".inl", ".metal"}) + + +def is_c_any(filename: str) -> bool: + return is_c(filename) or is_c_header(filename) + + +def cmake_get_src(f: str) -> None: + + sources_h = [] + sources_c = [] + + filen = open(f, "r", encoding="utf8") + it: Optional[Iterator[str]] = iter(filen) + found = False + i = 0 + # print(f) + + def is_definition(l: str, f: str, i: int, name: str) -> Tuple[bool, int]: + """ + Return (is_definition, single_line_offset). + """ + if l.startswith("unset("): + return False, -1 + + single_line_offset = -1 + name_test = 'set(%s' % name + single_line_offset = l.find(name_test) + if (single_line_offset != -1) or ('set(' in l and l.endswith(name)): + if single_line_offset != -1: + single_line_offset += len(name_test) + # if len(l.split()) > 1: + # raise Exception("strict formatting not kept 'set(%s*' %s:%d" % (name, f, i)) + if l.endswith(")"): + pass + while single_line_offset < len(l) and l[single_line_offset] != " ": + single_line_offset += 1 + else: + single_line_offset = -1 + return True, single_line_offset + + name_test = "list(APPEND %s" % name + single_line_offset = l.find(name_test) + if (single_line_offset != -1) or ('list(APPEND ' in l and l.endswith(name)): + if single_line_offset != -1: + single_line_offset += len(name_test) + if l.endswith(")"): + # raise Exception("strict formatting not kept 'list(APPEND %s...)' on 1 line %s:%d" % (name, f, i)) + pass + while single_line_offset < len(l) and l[single_line_offset] != " ": + single_line_offset += 1 + else: + single_line_offset = -1 + return True, single_line_offset + return False, -1 + + while it is not None: + context_name = "" + while it is not None: + i += 1 + try: + l = next(it) + except StopIteration: + it = None + break + l = l.strip() + if not l.startswith("#"): + for var in ("SRC", "INC"): + found, single_line_offset = is_definition(l, f, i, var) + if found: + context_name = var + break + if found: + break + + if found: + tokens = [] + if single_line_offset != -1: + end = False + for w in l[single_line_offset:].split(): + if w.startswith("#"): + break + if w.endswith(")"): + w = w[:-1].rstrip() + end = True + tokens.append((w, i)) + if end: + break + del end + if len(tokens) > 1: + print("Expect multi-variable to be split across multiple lines! '%s' %s:%d" % (l, f, i)) + else: + while it is not None: + i += 1 + try: + l = next(it) + except StopIteration: + it = None + break + l = l.strip() + if not l.startswith("#"): + # Remove in-line comments. + l = l.split(" # ")[0].rstrip() + if ")" in l: + if l.strip() != ")": + raise Exception("strict formatting not kept '*)' %s:%d" % (f, i)) + break + tokens.append((l, i)) + + cmake_base = dirname(f) + cmake_base_bin = os.path.join(BUILD_DIR, os.path.relpath(cmake_base, SOURCE_DIR)) + + # Find known missing sources list (if we have one). + f_rel = os.path.relpath(f, SOURCE_DIR) + f_rel_key = f_rel + if os.sep != "/": + f_rel_key = f_rel_key.replace(os.sep, "/") + local_ignore_source_missing = global_ignore_source_missing.get(f_rel_key, []) + + + for l, line_number in tokens: + # replace dirs + l = l.replace("${CMAKE_SOURCE_DIR}", SOURCE_DIR) + l = l.replace("${CMAKE_CURRENT_SOURCE_DIR}", cmake_base) + l = l.replace("${CMAKE_CURRENT_BINARY_DIR}", cmake_base_bin) + l = l.strip('"') + + if not l: + pass + elif l in local_ignore_source_missing: + local_ignore_source_missing.remove(l) + elif l.startswith("$"): + if context_name == "SRC": + # assume if it ends with context_name we know about it + if not l.split("}")[0].endswith(context_name): + print("Can't use var '%s' %s:%d" % (l, f, line_number)) + elif len(l.split()) > 1: + raise Exception("Multi-line define '%s' %s:%d" % (l, f, line_number)) + else: + new_file = normpath(join(cmake_base, l)) + + if context_name == "SRC": + if is_c_header(new_file): + sources_h.append(new_file) + global_refs.setdefault(new_file, []).append((f, line_number)) + elif is_c(new_file): + sources_c.append(new_file) + global_refs.setdefault(new_file, []).append((f, line_number)) + elif l in {"PARENT_SCOPE", }: + # cmake var, ignore + pass + elif new_file.endswith(".list"): + pass + elif new_file.endswith(".def"): + pass + elif new_file.endswith(".cl"): # opencl + pass + elif new_file.endswith(".cu"): # cuda + pass + elif new_file.endswith(".osl"): # open shading language + pass + elif new_file.endswith(".glsl"): + pass + else: + raise Exception("unknown file type - not c or h %s -> %s" % (f, new_file)) + + elif context_name == "INC": + if new_file.startswith(BUILD_DIR): + # assume generated path + pass + elif os.path.isdir(new_file): + new_path_rel = os.path.relpath(new_file, cmake_base) + + if new_path_rel != l: + print("overly relative path:\n %s:%d\n %s\n %s" % (f, line_number, l, new_path_rel)) + + # # Save time. just replace the line + # replace_line(f, line_number - 1, new_path_rel) + + else: + raise Exception("non existent include %s:%d -> %s" % (f, line_number, new_file)) + + # print(new_file) + + global_h.update(set(sources_h)) + global_c.update(set(sources_c)) + ''' + if not sources_h and not sources_c: + raise Exception("No sources %s" % f) + + sources_h_fs = list(source_list(cmake_base, is_c_header)) + sources_c_fs = list(source_list(cmake_base, is_c)) + ''' + # find missing C files: + ''' + for ff in sources_c_fs: + if ff not in sources_c: + print(" missing: " + ff) + ''' + + # reset + del sources_h[:] + del sources_c[:] + + filen.close() + + +def is_ignore_source(f: str, ignore_used: List[bool]) -> bool: + for index, ignore_path in enumerate(IGNORE_SOURCE): + if ignore_path in f: + ignore_used[index] = True + return True + return False + + +def is_ignore_cmake(f: str, ignore_used: List[bool]) -> bool: + for index, ignore_path in enumerate(IGNORE_CMAKE): + if ignore_path in f: + ignore_used[index] = True + return True + return False + + +def main() -> None: + + print("Scanning:", SOURCE_DIR) + + ignore_used_source = [False] * len(IGNORE_SOURCE) + ignore_used_cmake = [False] * len(IGNORE_CMAKE) + + for cmake in source_list(SOURCE_DIR, is_cmake): + if not is_ignore_cmake(cmake, ignore_used_cmake): + cmake_get_src(cmake) + + # First do stupid check, do these files exist? + print("\nChecking for missing references:") + is_err = False + errs = [] + for f in (global_h | global_c): + if f.startswith(BUILD_DIR): + continue + + if not os.path.exists(f): + refs = global_refs[f] + if refs: + for cf, i in refs: + errs.append((cf, i)) + else: + raise Exception("CMake references missing, internal error, aborting!") + is_err = True + + errs.sort() + errs.reverse() + for cf, i in errs: + print("%s:%d" % (cf, i)) + # Write a 'sed' script, useful if we get a lot of these + # print("sed '%dd' '%s' > '%s.tmp' ; mv '%s.tmp' '%s'" % (i, cf, cf, cf, cf)) + + if is_err: + raise Exception("CMake references missing files, aborting!") + del is_err + del errs + + # now check on files not accounted for. + print("\nC/C++ Files CMake does not know about...") + for cf in sorted(source_list(SOURCE_DIR, is_c)): + if not is_ignore_source(cf, ignore_used_source): + if cf not in global_c: + print("missing_c: ", cf) + + # Check if automake builds a corresponding .o file. + ''' + if cf in global_c: + out1 = os.path.splitext(cf)[0] + ".o" + out2 = os.path.splitext(cf)[0] + ".Po" + out2_dir, out2_file = out2 = os.path.split(out2) + out2 = os.path.join(out2_dir, ".deps", out2_file) + if not os.path.exists(out1) and not os.path.exists(out2): + print("bad_c: ", cf) + ''' + + print("\nC/C++ Headers CMake does not know about...") + for hf in sorted(source_list(SOURCE_DIR, is_c_header)): + if not is_ignore_source(hf, ignore_used_source): + if hf not in global_h: + print("missing_h: ", hf) + + if UTF8_CHECK: + # test encoding + import traceback + for files in (global_c, global_h): + for f in sorted(files): + if os.path.exists(f): + # ignore outside of our source tree + if "extern" not in f: + i = 1 + try: + for _ in open(f, "r", encoding="utf8"): + i += 1 + except UnicodeDecodeError: + print("Non utf8: %s:%d" % (f, i)) + if i > 1: + traceback.print_exc() + + # Check ignores aren't stale + print("\nCheck for unused 'IGNORE_SOURCE' paths...") + for index, ignore_path in enumerate(IGNORE_SOURCE): + if not ignore_used_source[index]: + print("unused ignore: %r" % ignore_path) + + # Check ignores aren't stale + print("\nCheck for unused 'IGNORE_SOURCE_MISSING' paths...") + for k, v in sorted(global_ignore_source_missing.items()): + for ignore_path in v: + print("unused ignore: %r -> %r" % (ignore_path, k)) + + # Check ignores aren't stale + print("\nCheck for unused 'IGNORE_CMAKE' paths...") + for index, ignore_path in enumerate(IGNORE_CMAKE): + if not ignore_used_cmake[index]: + print("unused ignore: %r" % ignore_path) + + +if __name__ == "__main__": + main() diff --git a/check_source/check_cmake_consistency_config.py b/check_source/check_cmake_consistency_config.py new file mode 100644 index 0000000000000000000000000000000000000000..0d92c188252328b586393f411025aaf415b37c5e --- /dev/null +++ b/check_source/check_cmake_consistency_config.py @@ -0,0 +1,62 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +import os + +IGNORE_SOURCE = ( + "/test/", + "/tests/gtests/", + "/release/", + + # specific source files + "extern/audaspace/", + + # Use for `WIN32` only. + "source/creator/blender_launcher_win32.c", + + # specific source files + "extern/bullet2/src/BulletCollision/CollisionDispatch/btBox2dBox2dCollisionAlgorithm.cpp", + "extern/bullet2/src/BulletCollision/CollisionDispatch/btConvex2dConvex2dAlgorithm.cpp", + "extern/bullet2/src/BulletCollision/CollisionDispatch/btInternalEdgeUtility.cpp", + "extern/bullet2/src/BulletCollision/CollisionShapes/btBox2dShape.cpp", + "extern/bullet2/src/BulletCollision/CollisionShapes/btConvex2dShape.cpp", + "extern/bullet2/src/BulletDynamics/Character/btKinematicCharacterController.cpp", + "extern/bullet2/src/BulletDynamics/ConstraintSolver/btHinge2Constraint.cpp", + "extern/bullet2/src/BulletDynamics/ConstraintSolver/btUniversalConstraint.cpp", + + "doc/doxygen/doxygen.extern.h", + "doc/doxygen/doxygen.intern.h", + "doc/doxygen/doxygen.main.h", + "doc/doxygen/doxygen.source.h", + "extern/bullet2/src/BulletCollision/CollisionDispatch/btBox2dBox2dCollisionAlgorithm.h", + "extern/bullet2/src/BulletCollision/CollisionDispatch/btConvex2dConvex2dAlgorithm.h", + "extern/bullet2/src/BulletCollision/CollisionDispatch/btInternalEdgeUtility.h", + "extern/bullet2/src/BulletCollision/CollisionShapes/btBox2dShape.h", + "extern/bullet2/src/BulletCollision/CollisionShapes/btConvex2dShape.h", + "extern/bullet2/src/BulletDynamics/Character/btKinematicCharacterController.h", + "extern/bullet2/src/BulletDynamics/ConstraintSolver/btHinge2Constraint.h", + "extern/bullet2/src/BulletDynamics/ConstraintSolver/btUniversalConstraint.h", + + "build_files/build_environment/patches/config_gmpxx.h", +) + +# Ignore cmake file, path pairs. +IGNORE_SOURCE_MISSING = ( + ( # Use for `WITH_NANOVDB`. + "intern/cycles/kernel/CMakeLists.txt", ( + "nanovdb/util/CSampleFromVoxels.h", + "nanovdb/util/SampleFromVoxels.h", + "nanovdb/NanoVDB.h", + "nanovdb/CNanoVDB.h", + ), + ), +) + +IGNORE_CMAKE = ( + "extern/audaspace/CMakeLists.txt", +) + +UTF8_CHECK = True + +SOURCE_DIR = os.path.normpath(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))) + +# doesn't have to exist, just use as reference +BUILD_DIR = os.path.normpath(os.path.abspath(os.path.normpath(os.path.join(SOURCE_DIR, "..", "build"))))