project_source_info.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. # ***** BEGIN GPL LICENSE BLOCK *****
  2. #
  3. # This program is free software; you can redistribute it and/or
  4. # modify it under the terms of the GNU General Public License
  5. # as published by the Free Software Foundation; either version 2
  6. # of the License, or (at your option) any later version.
  7. #
  8. # This program is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. # GNU General Public License for more details.
  12. #
  13. # You should have received a copy of the GNU General Public License
  14. # along with this program; if not, write to the Free Software Foundation,
  15. # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  16. #
  17. # Contributor(s): Campbell Barton
  18. #
  19. # ***** END GPL LICENSE BLOCK *****
  20. # <pep8 compliant>
  21. __all__ = (
  22. "build_info",
  23. "SOURCE_DIR",
  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. import os
  31. from os.path import join, dirname, normpath, abspath
  32. SOURCE_DIR = join(dirname(__file__), "..", "..")
  33. SOURCE_DIR = normpath(SOURCE_DIR)
  34. SOURCE_DIR = abspath(SOURCE_DIR)
  35. def is_c_header(filename):
  36. ext = os.path.splitext(filename)[1]
  37. return (ext in {".h", ".hpp", ".hxx", ".hh"})
  38. def is_c(filename):
  39. ext = os.path.splitext(filename)[1]
  40. return (ext in {".c", ".cpp", ".cxx", ".m", ".mm", ".rc", ".cc", ".inl", ".osl"})
  41. def is_c_any(filename):
  42. return os.path.s_c(filename) or is_c_header(filename)
  43. # copied from project_info.py
  44. CMAKE_DIR = "."
  45. def cmake_cache_var_iter():
  46. import re
  47. re_cache = re.compile(r'([A-Za-z0-9_\-]+)?:?([A-Za-z0-9_\-]+)?=(.*)$')
  48. with open(join(CMAKE_DIR, "CMakeCache.txt"), 'r', encoding='utf-8') as cache_file:
  49. for l in cache_file:
  50. match = re_cache.match(l.strip())
  51. if match is not None:
  52. var, type_, val = match.groups()
  53. yield (var, type_ or "", val)
  54. def cmake_cache_var(var):
  55. for var_iter, type_iter, value_iter in cmake_cache_var_iter():
  56. if var == var_iter:
  57. return value_iter
  58. return None
  59. def do_ignore(filepath, ignore_prefix_list):
  60. if ignore_prefix_list is None:
  61. return False
  62. relpath = os.path.relpath(filepath, SOURCE_DIR)
  63. return any([relpath.startswith(prefix) for prefix in ignore_prefix_list])
  64. def makefile_log():
  65. import subprocess
  66. import time
  67. # support both make and ninja
  68. make_exe = cmake_cache_var("CMAKE_MAKE_PROGRAM")
  69. make_exe_basename = os.path.basename(make_exe)
  70. if make_exe_basename.startswith(("make", "gmake")):
  71. print("running 'make' with --dry-run ...")
  72. process = subprocess.Popen([make_exe, "--always-make", "--dry-run", "--keep-going", "VERBOSE=1"],
  73. stdout=subprocess.PIPE,
  74. )
  75. elif make_exe_basename.startswith("ninja"):
  76. print("running 'ninja' with -t commands ...")
  77. process = subprocess.Popen([make_exe, "-t", "commands"],
  78. stdout=subprocess.PIPE,
  79. )
  80. while process.poll():
  81. time.sleep(1)
  82. out = process.stdout.read()
  83. process.stdout.close()
  84. print("done!", len(out), "bytes")
  85. return out.decode("utf-8", errors="ignore").split("\n")
  86. def build_info(use_c=True, use_cxx=True, ignore_prefix_list=None):
  87. makelog = makefile_log()
  88. source = []
  89. compilers = []
  90. if use_c:
  91. compilers.append(cmake_cache_var("CMAKE_C_COMPILER"))
  92. if use_cxx:
  93. compilers.append(cmake_cache_var("CMAKE_CXX_COMPILER"))
  94. print("compilers:", " ".join(compilers))
  95. fake_compiler = "%COMPILER%"
  96. print("parsing make log ...")
  97. for line in makelog:
  98. args = line.split()
  99. if not any([(c in args) for c in compilers]):
  100. continue
  101. # join args incase they are not.
  102. args = ' '.join(args)
  103. args = args.replace(" -isystem", " -I")
  104. args = args.replace(" -D ", " -D")
  105. args = args.replace(" -I ", " -I")
  106. for c in compilers:
  107. args = args.replace(c, fake_compiler)
  108. args = args.split()
  109. # end
  110. # remove compiler
  111. args[:args.index(fake_compiler) + 1] = []
  112. c_files = [f for f in args if is_c(f)]
  113. inc_dirs = [f[2:].strip() for f in args if f.startswith('-I')]
  114. defs = [f[2:].strip() for f in args if f.startswith('-D')]
  115. for c in sorted(c_files):
  116. if do_ignore(c, ignore_prefix_list):
  117. continue
  118. source.append((c, inc_dirs, defs))
  119. # make relative includes absolute
  120. # not totally essential but useful
  121. for i, f in enumerate(inc_dirs):
  122. if not os.path.isabs(f):
  123. inc_dirs[i] = os.path.abspath(os.path.join(CMAKE_DIR, f))
  124. # safety check that our includes are ok
  125. for f in inc_dirs:
  126. if not os.path.exists(f):
  127. raise Exception("%s missing" % f)
  128. print("done!")
  129. return source
  130. def build_defines_as_source():
  131. """
  132. Returns a string formatted as an include:
  133. '#defines A=B\n#define....'
  134. """
  135. import subprocess
  136. # works for both gcc and clang
  137. cmd = (cmake_cache_var("CMAKE_C_COMPILER"), "-dM", "-E", "-")
  138. return subprocess.Popen(cmd,
  139. stdout=subprocess.PIPE,
  140. stdin=subprocess.DEVNULL,
  141. ).stdout.read().strip().decode('ascii')
  142. def build_defines_as_args():
  143. return [("-D" + "=".join(l.split(maxsplit=2)[1:]))
  144. for l in build_defines_as_source().split("\n")
  145. if l.startswith('#define')]
  146. # could be moved elsewhere!, this just happens to be used by scripts that also
  147. # use this module.
  148. def queue_processes(process_funcs, job_total=-1):
  149. """ Takes a list of function arg pairs, each function must return a process
  150. """
  151. if job_total == -1:
  152. import multiprocessing
  153. job_total = multiprocessing.cpu_count()
  154. del multiprocessing
  155. if job_total == 1:
  156. for func, args in process_funcs:
  157. sys.stdout.flush()
  158. sys.stderr.flush()
  159. process = func(*args)
  160. process.wait()
  161. else:
  162. import time
  163. processes = []
  164. for func, args in process_funcs:
  165. # wait until a thread is free
  166. while 1:
  167. processes[:] = [p for p in processes if p.poll() is None]
  168. if len(processes) <= job_total:
  169. break
  170. else:
  171. time.sleep(0.1)
  172. sys.stdout.flush()
  173. sys.stderr.flush()
  174. processes.append(func(*args))
  175. def main():
  176. if not os.path.exists(join(CMAKE_DIR, "CMakeCache.txt")):
  177. print("This script must run from the cmake build dir")
  178. return
  179. for s in build_info():
  180. print(s)
  181. if __name__ == "__main__":
  182. main()