1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
|
#!/usr/bin/env python
# encoding: utf-8
# Copyright Garmin International or its subsidiaries, 2012-2013
'''
Off-load dependency scanning from Python code to MSVC compiler
This tool is safe to load in any environment; it will only activate the
MSVC exploits when it finds that a particular taskgen uses MSVC to
compile.
Empirical testing shows about a 10% execution time savings from using
this tool as compared to c_preproc.
The technique of gutting scan() and pushing the dependency calculation
down to post_run() is cribbed from gccdeps.py.
This affects the cxx class, so make sure to load Qt5 after this tool.
Usage::
def options(opt):
opt.load('compiler_cxx')
def configure(conf):
conf.load('compiler_cxx msvcdeps')
'''
import os, sys, tempfile, threading
from waflib import Context, Errors, Logs, Task, Utils
from waflib.Tools import c_preproc, c, cxx, msvc
from waflib.TaskGen import feature, before_method
lock = threading.Lock()
PREPROCESSOR_FLAG = '/showIncludes'
INCLUDE_PATTERN = 'Note: including file:'
# Extensible by outside tools
supported_compilers = ['msvc']
@feature('c', 'cxx')
@before_method('process_source')
def apply_msvcdeps_flags(taskgen):
if taskgen.env.CC_NAME not in supported_compilers:
return
for flag in ('CFLAGS', 'CXXFLAGS'):
if taskgen.env.get_flat(flag).find(PREPROCESSOR_FLAG) < 0:
taskgen.env.append_value(flag, PREPROCESSOR_FLAG)
def get_correct_path_case(base_path, path):
'''
Return a case-corrected version of ``path`` by searching the filesystem for
``path``, relative to ``base_path``, using the case returned by the filesystem.
'''
components = Utils.split_path(path)
corrected_path = ''
if os.path.isabs(path):
corrected_path = components.pop(0).upper() + os.sep
for part in components:
part = part.lower()
search_path = os.path.join(base_path, corrected_path)
if part == '..':
corrected_path = os.path.join(corrected_path, part)
search_path = os.path.normpath(search_path)
continue
for item in sorted(os.listdir(search_path)):
if item.lower() == part:
corrected_path = os.path.join(corrected_path, item)
break
else:
raise ValueError("Can't find %r in %r" % (part, search_path))
return corrected_path
def path_to_node(base_node, path, cached_nodes):
'''
Take the base node and the path and return a node
Results are cached because searching the node tree is expensive
The following code is executed by threads, it is not safe, so a lock is needed...
'''
# normalize the path to remove parent path components (..)
path = os.path.normpath(path)
# normalize the path case to increase likelihood of a cache hit
node_lookup_key = (base_node, os.path.normcase(path))
try:
node = cached_nodes[node_lookup_key]
except KeyError:
# retry with lock on cache miss
with lock:
try:
node = cached_nodes[node_lookup_key]
except KeyError:
path = get_correct_path_case(base_node.abspath(), path)
node = cached_nodes[node_lookup_key] = base_node.find_node(path)
return node
def post_run(self):
if self.env.CC_NAME not in supported_compilers:
return super(self.derived_msvcdeps, self).post_run()
# TODO this is unlikely to work with netcache
if getattr(self, 'cached', None):
return Task.Task.post_run(self)
resolved_nodes = []
unresolved_names = []
bld = self.generator.bld
# Dynamically bind to the cache
try:
cached_nodes = bld.cached_nodes
except AttributeError:
cached_nodes = bld.cached_nodes = {}
for path in self.msvcdeps_paths:
node = None
if os.path.isabs(path):
node = path_to_node(bld.root, path, cached_nodes)
else:
# when calling find_resource, make sure the path does not begin with '..'
base_node = bld.bldnode
path = [k for k in Utils.split_path(path) if k and k != '.']
while path[0] == '..':
path.pop(0)
base_node = base_node.parent
path = os.sep.join(path)
node = path_to_node(base_node, path, cached_nodes)
if not node:
raise ValueError('could not find %r for %r' % (path, self))
else:
if not c_preproc.go_absolute:
if not (node.is_child_of(bld.srcnode) or node.is_child_of(bld.bldnode)):
# System library
Logs.debug('msvcdeps: Ignoring system include %r', node)
continue
if id(node) == id(self.inputs[0]):
# ignore the source file, it is already in the dependencies
# this way, successful config tests may be retrieved from the cache
continue
resolved_nodes.append(node)
Logs.debug('deps: msvcdeps for %s returned %s', self, resolved_nodes)
bld.node_deps[self.uid()] = resolved_nodes
bld.raw_deps[self.uid()] = unresolved_names
try:
del self.cache_sig
except AttributeError:
pass
Task.Task.post_run(self)
def scan(self):
if self.env.CC_NAME not in supported_compilers:
return super(self.derived_msvcdeps, self).scan()
resolved_nodes = self.generator.bld.node_deps.get(self.uid(), [])
unresolved_names = []
return (resolved_nodes, unresolved_names)
def sig_implicit_deps(self):
if self.env.CC_NAME not in supported_compilers:
return super(self.derived_msvcdeps, self).sig_implicit_deps()
bld = self.generator.bld
try:
return self.compute_sig_implicit_deps()
except Errors.TaskNotReady:
raise ValueError("Please specify the build order precisely with msvcdeps (c/c++ tasks)")
except EnvironmentError:
# If a file is renamed, assume the dependencies are stale and must be recalculated
for x in bld.node_deps.get(self.uid(), []):
if not x.is_bld() and not x.exists():
try:
del x.parent.children[x.name]
except KeyError:
pass
key = self.uid()
bld.node_deps[key] = []
bld.raw_deps[key] = []
return Utils.SIG_NIL
def exec_command(self, cmd, **kw):
if self.env.CC_NAME not in supported_compilers:
return super(self.derived_msvcdeps, self).exec_command(cmd, **kw)
if not 'cwd' in kw:
kw['cwd'] = self.get_cwd()
if self.env.PATH:
env = kw['env'] = dict(kw.get('env') or self.env.env or os.environ)
env['PATH'] = self.env.PATH if isinstance(self.env.PATH, str) else os.pathsep.join(self.env.PATH)
# The Visual Studio IDE adds an environment variable that causes
# the MS compiler to send its textual output directly to the
# debugging window rather than normal stdout/stderr.
#
# This is unrecoverably bad for this tool because it will cause
# all the dependency scanning to see an empty stdout stream and
# assume that the file being compiled uses no headers.
#
# See http://blogs.msdn.com/b/freik/archive/2006/04/05/569025.aspx
#
# Attempting to repair the situation by deleting the offending
# envvar at this point in tool execution will not be good enough--
# its presence poisons the 'waf configure' step earlier. We just
# want to put a sanity check here in order to help developers
# quickly diagnose the issue if an otherwise-good Waf tree
# is then executed inside the MSVS IDE.
assert 'VS_UNICODE_OUTPUT' not in kw['env']
cmd, args = self.split_argfile(cmd)
try:
(fd, tmp) = tempfile.mkstemp()
os.write(fd, '\r\n'.join(args).encode())
os.close(fd)
self.msvcdeps_paths = []
kw['env'] = kw.get('env', os.environ.copy())
kw['cwd'] = kw.get('cwd', os.getcwd())
kw['quiet'] = Context.STDOUT
kw['output'] = Context.STDOUT
out = []
if Logs.verbose:
Logs.debug('argfile: @%r -> %r', tmp, args)
try:
raw_out = self.generator.bld.cmd_and_log(cmd + ['@' + tmp], **kw)
ret = 0
except Errors.WafError as e:
# Use e.msg if e.stdout is not set
raw_out = getattr(e, 'stdout', e.msg)
# Return non-zero error code even if we didn't
# get one from the exception object
ret = getattr(e, 'returncode', 1)
Logs.debug('msvcdeps: Running for: %s' % self.inputs[0])
for line in raw_out.splitlines():
if line.startswith(INCLUDE_PATTERN):
# Only strip whitespace after log to preserve
# dependency structure in debug output
inc_path = line[len(INCLUDE_PATTERN):]
Logs.debug('msvcdeps: Regex matched %s', inc_path)
self.msvcdeps_paths.append(inc_path.strip())
else:
out.append(line)
# Pipe through the remaining stdout content (not related to /showIncludes)
if self.generator.bld.logger:
self.generator.bld.logger.debug('out: %s' % os.linesep.join(out))
else:
sys.stdout.write(os.linesep.join(out) + os.linesep)
return ret
finally:
try:
os.remove(tmp)
except OSError:
# anti-virus and indexers can keep files open -_-
pass
def wrap_compiled_task(classname):
derived_class = type(classname, (Task.classes[classname],), {})
derived_class.derived_msvcdeps = derived_class
derived_class.post_run = post_run
derived_class.scan = scan
derived_class.sig_implicit_deps = sig_implicit_deps
derived_class.exec_command = exec_command
for k in ('c', 'cxx'):
if k in Task.classes:
wrap_compiled_task(k)
def options(opt):
raise ValueError('Do not load msvcdeps options')
|