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