aboutsummaryrefslogtreecommitdiff
path: root/extras/msvcdeps.py
blob: fc1ecd4d08c20f6b73854b3b36fb34cbfb85e959 (plain)
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
#!/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()
nodes = {} # Cache the path -> Node lookup

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)

	# Figure out what casing conventions the user's shell used when
	# launching Waf
	(drive, _) = os.path.splitdrive(taskgen.bld.srcnode.abspath())
	taskgen.msvcdeps_drive_lowercase = drive == drive.lower()

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...
	if getattr(path, '__hash__'):
		node_lookup_key = (base_node, path)
	else:
		# Not hashable, assume it is a list and join into a string
		node_lookup_key = (base_node, os.path.sep.join(path))
	try:
		lock.acquire()
		node = cached_nodes[node_lookup_key]
	except KeyError:
		node = base_node.find_resource(path)
		cached_nodes[node_lookup_key] = node
	finally:
		lock.release()
	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)

	bld = self.generator.bld
	unresolved_names = []
	resolved_nodes = []

	lowercase = self.generator.msvcdeps_drive_lowercase
	correct_case_path = bld.path.abspath()
	correct_case_path_len = len(correct_case_path)
	correct_case_path_norm = os.path.normcase(correct_case_path)

	# 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):
			# Force drive letter to match conventions of main source tree
			drive, tail = os.path.splitdrive(path)

			if os.path.normcase(path[:correct_case_path_len]) == correct_case_path_norm:
				# Path is in the sandbox, force it to be correct.  MSVC sometimes returns a lowercase path.
				path = correct_case_path + path[correct_case_path_len:]
			else:
				# Check the drive letter
				if lowercase and (drive != drive.lower()):
					path = drive.lower() + tail
				elif (not lowercase) and (drive != drive.upper()):
					path = drive.upper() + tail
			node = path_to_node(bld.root, path, cached_nodes)
		else:
			base_node = bld.bldnode
			# when calling find_resource, make sure the path does not begin by '..'
			path = [k for k in Utils.split_path(path) if k and k != '.']
			while path[0] == '..':
				path = path[1:]
				base_node = base_node.parent

			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]):
				# Self-dependency
				continue

			resolved_nodes.append(node)

	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()

	try:
		return Task.Task.sig_implicit_deps(self)
	except Errors.WafError:
		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:
			raw_out = e.stdout
			ret = e.returncode

		for line in raw_out.splitlines():
			if line.startswith(INCLUDE_PATTERN):
				inc_path = line[len(INCLUDE_PATTERN):].strip()
				Logs.debug('msvcdeps: Regex matched %s', inc_path)
				self.msvcdeps_paths.append(inc_path)
			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')