Merge branch 'blender-v2.93-release'
[blender.git] / build_files / cmake / project_source_info.py
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 # ***** END GPL LICENSE BLOCK *****
18
19 # <pep8 compliant>
20
21 __all__ = (
22     "build_info",
23     "SOURCE_DIR",
24 )
25
26
27 import sys
28 if sys.version_info.major < 3:
29     print("\nPython3.x or newer needed, found %s.\nAborting!\n" %
30           sys.version.partition(" ")[0])
31     sys.exit(1)
32
33
34 import os
35 from os.path import join, dirname, normpath, abspath
36
37 import subprocess
38
39 from typing import (
40     Any,
41     Callable,
42     Generator,
43     List,
44     Optional,
45     Sequence,
46     Tuple,
47     Union,
48     cast,
49 )
50
51
52 SOURCE_DIR = join(dirname(__file__), "..", "..")
53 SOURCE_DIR = normpath(SOURCE_DIR)
54 SOURCE_DIR = abspath(SOURCE_DIR)
55
56
57 def is_c_header(filename: str) -> bool:
58     ext = os.path.splitext(filename)[1]
59     return (ext in {".h", ".hpp", ".hxx", ".hh"})
60
61
62 def is_c(filename: str) -> bool:
63     ext = os.path.splitext(filename)[1]
64     return (ext in {".c", ".cpp", ".cxx", ".m", ".mm", ".rc", ".cc", ".inl", ".osl"})
65
66
67 def is_c_any(filename: str) -> bool:
68     return is_c(filename) or is_c_header(filename)
69
70
71 # copied from project_info.py
72 CMAKE_DIR = "."
73
74
75 def cmake_cache_var_iter() -> Generator[Tuple[str, str, str], None, None]:
76     import re
77     re_cache = re.compile(r'([A-Za-z0-9_\-]+)?:?([A-Za-z0-9_\-]+)?=(.*)$')
78     with open(join(CMAKE_DIR, "CMakeCache.txt"), 'r', encoding='utf-8') as cache_file:
79         for l in cache_file:
80             match = re_cache.match(l.strip())
81             if match is not None:
82                 var, type_, val = match.groups()
83                 yield (var, type_ or "", val)
84
85
86 def cmake_cache_var(var: str) -> Optional[str]:
87     for var_iter, type_iter, value_iter in cmake_cache_var_iter():
88         if var == var_iter:
89             return value_iter
90     return None
91
92
93 def cmake_cache_var_or_exit(var: str) -> str:
94     value = cmake_cache_var(var)
95     if value is None:
96         print("Unable to find %r exiting!" % value)
97         sys.exit(1)
98     return value
99
100
101 def do_ignore(filepath: str, ignore_prefix_list: Optional[Sequence[str]]) -> bool:
102     if ignore_prefix_list is None:
103         return False
104
105     relpath = os.path.relpath(filepath, SOURCE_DIR)
106     return any([relpath.startswith(prefix) for prefix in ignore_prefix_list])
107
108
109 def makefile_log() -> List[str]:
110     import subprocess
111     import time
112
113     # support both make and ninja
114     make_exe = cmake_cache_var_or_exit("CMAKE_MAKE_PROGRAM")
115
116     make_exe_basename = os.path.basename(make_exe)
117
118     if make_exe_basename.startswith(("make", "gmake")):
119         print("running 'make' with --dry-run ...")
120         process = subprocess.Popen([make_exe, "--always-make", "--dry-run", "--keep-going", "VERBOSE=1"],
121                                    stdout=subprocess.PIPE,
122                                    )
123     elif make_exe_basename.startswith("ninja"):
124         print("running 'ninja' with -t commands ...")
125         process = subprocess.Popen([make_exe, "-t", "commands"],
126                                    stdout=subprocess.PIPE,
127                                    )
128
129     if process is None:
130         print("Can't execute process")
131         sys.exit(1)
132
133
134     while process.poll():
135         time.sleep(1)
136
137     # We know this is always true based on the input arguments to `Popen`.
138     stdout: IO[bytes] = process.stdout # type: ignore
139
140     out = stdout.read()
141     stdout.close()
142     print("done!", len(out), "bytes")
143     return cast(List[str], out.decode("utf-8", errors="ignore").split("\n"))
144
145
146 def build_info(
147         use_c: bool = True,
148         use_cxx: bool = True,
149         ignore_prefix_list: Optional[List[str]] = None,
150 ) -> List[Tuple[str, List[str], List[str]]]:
151     makelog = makefile_log()
152
153     source = []
154
155     compilers = []
156     if use_c:
157         compilers.append(cmake_cache_var_or_exit("CMAKE_C_COMPILER"))
158     if use_cxx:
159         compilers.append(cmake_cache_var_or_exit("CMAKE_CXX_COMPILER"))
160
161     print("compilers:", " ".join(compilers))
162
163     fake_compiler = "%COMPILER%"
164
165     print("parsing make log ...")
166
167     for line in makelog:
168
169         args: Union[str, List[str]] = line.split()
170
171         if not any([(c in args) for c in compilers]):
172             continue
173
174         # join args incase they are not.
175         args = ' '.join(args)
176         args = args.replace(" -isystem", " -I")
177         args = args.replace(" -D ", " -D")
178         args = args.replace(" -I ", " -I")
179
180         for c in compilers:
181             args = args.replace(c, fake_compiler)
182         args = args.split()
183         # end
184
185         # remove compiler
186         args[:args.index(fake_compiler) + 1] = []
187
188         c_files = [f for f in args if is_c(f)]
189         inc_dirs = [f[2:].strip() for f in args if f.startswith('-I')]
190         defs = [f[2:].strip() for f in args if f.startswith('-D')]
191         for c in sorted(c_files):
192
193             if do_ignore(c, ignore_prefix_list):
194                 continue
195
196             source.append((c, inc_dirs, defs))
197
198         # make relative includes absolute
199         # not totally essential but useful
200         for i, f in enumerate(inc_dirs):
201             if not os.path.isabs(f):
202                 inc_dirs[i] = os.path.abspath(os.path.join(CMAKE_DIR, f))
203
204         # safety check that our includes are ok
205         for f in inc_dirs:
206             if not os.path.exists(f):
207                 raise Exception("%s missing" % f)
208
209     print("done!")
210
211     return source
212
213
214 def build_defines_as_source() -> str:
215     """
216     Returns a string formatted as an include:
217         '#defines A=B\n#define....'
218     """
219     import subprocess
220     # works for both gcc and clang
221     cmd = (cmake_cache_var_or_exit("CMAKE_C_COMPILER"), "-dM", "-E", "-")
222     process = subprocess.Popen(
223         cmd,
224         stdout=subprocess.PIPE,
225         stdin=subprocess.DEVNULL,
226     )
227
228     # We know this is always true based on the input arguments to `Popen`.
229     stdout: IO[bytes] = process.stdout # type: ignore
230
231     return cast(str, stdout.read().strip().decode('ascii'))
232
233
234 def build_defines_as_args() -> List[str]:
235     return [
236         ("-D" + "=".join(l.split(maxsplit=2)[1:]))
237         for l in build_defines_as_source().split("\n")
238         if l.startswith('#define')
239     ]
240
241
242 # could be moved elsewhere!, this just happens to be used by scripts that also
243 # use this module.
244 def queue_processes(
245         process_funcs: Sequence[Tuple[Callable[..., subprocess.Popen[Any]], Tuple[Any, ...]]],
246         job_total: int =-1,
247 ) -> None:
248     """ Takes a list of function arg pairs, each function must return a process
249     """
250
251     if job_total == -1:
252         import multiprocessing
253         job_total = multiprocessing.cpu_count()
254         del multiprocessing
255
256     if job_total == 1:
257         for func, args in process_funcs:
258             sys.stdout.flush()
259             sys.stderr.flush()
260
261             process = func(*args)
262             process.wait()
263     else:
264         import time
265
266         processes: List[subprocess.Popen[Any]] = []
267         for func, args in process_funcs:
268             # wait until a thread is free
269             while 1:
270                 processes[:] = [p for p in processes if p.poll() is None]
271
272                 if len(processes) <= job_total:
273                     break
274                 else:
275                     time.sleep(0.1)
276
277             sys.stdout.flush()
278             sys.stderr.flush()
279
280             processes.append(func(*args))
281
282
283 def main() -> None:
284     if not os.path.exists(join(CMAKE_DIR, "CMakeCache.txt")):
285         print("This script must run from the cmake build dir")
286         return
287
288     for s in build_info():
289         print(s)
290
291
292 if __name__ == "__main__":
293     main()