unused_compilation.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. #
  2. # Copyright (c) Contributors to the Open 3D Engine Project.
  3. # For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. #
  5. # SPDX-License-Identifier: Apache-2.0 OR MIT
  6. #
  7. #
  8. import argparse
  9. import fnmatch
  10. import os
  11. import shutil
  12. import sys
  13. sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), '../build'))
  14. import ci_build
  15. EXTENSIONS_OF_INTEREST = ('.c', '.cc', '.cpp', '.cxx', '.h', '.hpp', '.hxx', '.inl')
  16. EXCLUSIONS = (
  17. 'AZ_DECLARE_MODULE_CLASS(',
  18. 'REGISTER_QT_CLASS_DESC(',
  19. 'TEST(',
  20. 'TEST_F(',
  21. 'TEST_P(',
  22. 'INSTANTIATE_TEST_CASE_P(',
  23. 'INSTANTIATE_TYPED_TEST_CASE_P(',
  24. 'AZ_UNIT_TEST_HOOK(',
  25. 'IMPLEMENT_TEST_EXECUTABLE_MAIN(',
  26. 'BENCHMARK_REGISTER_F(',
  27. 'DllMain(',
  28. 'wWinMain(',
  29. 'AZ_DLL_EXPORT',
  30. 'CreatePluginInstance('
  31. )
  32. PATH_EXCLUSIONS = (
  33. '*\\Platform\\Android\\*',
  34. '*\\Platform\\Common\\*',
  35. '*\\Platform\\iOS\\*',
  36. '*\\Platform\\Linux\\*',
  37. '*\\Platform\\Mac\\*',
  38. 'Templates\\*',
  39. 'python\\*',
  40. 'build\\*',
  41. 'install\\*',
  42. 'Gems\\ImGui\\External\\ImGui\\*',
  43. '*_Traits_*.h',
  44. )
  45. def create_filelist(path):
  46. filelist = set()
  47. for input_file in path:
  48. if os.path.isdir(input_file):
  49. for dp, dn, filenames in os.walk(input_file):
  50. if 'build\\windows' in dp:
  51. continue
  52. for f in filenames:
  53. extension = os.path.splitext(f)[1]
  54. extension_lower = extension.lower()
  55. if extension_lower in EXTENSIONS_OF_INTEREST:
  56. normalized_file = os.path.normpath(os.path.join(dp, f))
  57. filelist.add(normalized_file)
  58. else:
  59. extension = os.path.splitext(input_file)[1]
  60. extension_lower = extension.lower()
  61. if extension_lower in EXTENSIONS_OF_INTEREST:
  62. normalized_file = os.path.normpath(os.path.join(os.getcwd(), input_file))
  63. filelist.add(normalized_file)
  64. else:
  65. print(f'Error, file {input_file} does not have an extension of interest')
  66. sys.exit(1)
  67. return filelist
  68. def is_excluded(file):
  69. for path_exclusion in PATH_EXCLUSIONS:
  70. if fnmatch.fnmatch(file, path_exclusion):
  71. return True
  72. with open(file, 'r') as file:
  73. contents = file.read()
  74. for exclusion_term in EXCLUSIONS:
  75. if exclusion_term in contents:
  76. return True
  77. return False
  78. def filter_from_processed(filelist, filter_file_path):
  79. filelist = set([f for f in filelist if not is_excluded(f)])
  80. if os.path.exists(filter_file_path):
  81. with open(filter_file_path, 'r') as filter_file:
  82. processed_files = [s.strip() for s in filter_file.readlines()]
  83. filelist -= set(processed_files)
  84. return filelist
  85. def cleanup_unused_compilation(path):
  86. # 1. Create a list of all h/cpp files (consider multiple extensions)
  87. filelist = create_filelist(path)
  88. # 2. Remove files from "processed" list. This is needed because the process is a bruteforce approach and
  89. # can take a while. If something is found in the middle, we want to be able to continue instead of
  90. # starting over. Removing the "unusued_compilation_processed.txt" will start over.
  91. filter_file_path = os.path.join(os.getcwd(), 'unusued_compilation_processed.txt')
  92. filelist = filter_from_processed(filelist, filter_file_path)
  93. sorted_filelist = sorted(filelist)
  94. # 3. For each file
  95. total_files = len(sorted_filelist)
  96. current_files = 1
  97. for file in sorted_filelist:
  98. print(f"[{current_files}/{total_files}] Trying {file}")
  99. # b. create backup
  100. shutil.copy(file, file + '.bak')
  101. # c. set the file contents as empty
  102. with open(file, 'w') as source_file:
  103. source_file.write('')
  104. # d. build
  105. ret = ci_build.build('build_config.json', 'Windows', 'profile')
  106. # e.1 if build succeeds, leave the file empty (leave backup)
  107. # e.2 if build fails, restore backup
  108. if ret != 0:
  109. shutil.copy(file + '.bak', file)
  110. print(f"\t[FAILED] restoring")
  111. else:
  112. print(f"\t[SUCCEED]")
  113. # f. delete backup
  114. os.remove(file + '.bak')
  115. # add file to processed-list
  116. with open(filter_file_path, 'a') as filter_file:
  117. filter_file.write(file)
  118. filter_file.write('\n')
  119. current_files += 1
  120. if __name__ == "__main__":
  121. parser = argparse.ArgumentParser(description='This script finds C++ files that do not affect compilation (and therefore can be potentially removed)',
  122. formatter_class=argparse.RawTextHelpFormatter)
  123. parser.add_argument('file_or_dir', type=str, nargs='+', default=os.getcwd(),
  124. help='list of files or directories to search within for files to consider')
  125. args = parser.parse_args()
  126. ret = cleanup_unused_compilation(args.file_or_dir)
  127. sys.exit(ret)