msvcdeps.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. #!/usr/bin/env python
  2. # encoding: utf-8
  3. # Copyright Garmin International or its subsidiaries, 2012-2013
  4. '''
  5. Off-load dependency scanning from Python code to MSVC compiler
  6. This tool is safe to load in any environment; it will only activate the
  7. MSVC exploits when it finds that a particular taskgen uses MSVC to
  8. compile.
  9. Empirical testing shows about a 10% execution time savings from using
  10. this tool as compared to c_preproc.
  11. The technique of gutting scan() and pushing the dependency calculation
  12. down to post_run() is cribbed from gccdeps.py.
  13. This affects the cxx class, so make sure to load Qt5 after this tool.
  14. Usage::
  15. def options(opt):
  16. opt.load('compiler_cxx')
  17. def configure(conf):
  18. conf.load('compiler_cxx msvcdeps')
  19. '''
  20. import os, sys, tempfile, threading
  21. from waflib import Context, Errors, Logs, Task, Utils
  22. from waflib.Tools import c_preproc, c, cxx, msvc
  23. from waflib.TaskGen import feature, before_method
  24. lock = threading.Lock()
  25. nodes = {} # Cache the path -> Node lookup
  26. PREPROCESSOR_FLAG = '/showIncludes'
  27. INCLUDE_PATTERN = 'Note: including file:'
  28. # Extensible by outside tools
  29. supported_compilers = ['msvc']
  30. @feature('c', 'cxx')
  31. @before_method('process_source')
  32. def apply_msvcdeps_flags(taskgen):
  33. if taskgen.env.CC_NAME not in supported_compilers:
  34. return
  35. for flag in ('CFLAGS', 'CXXFLAGS'):
  36. if taskgen.env.get_flat(flag).find(PREPROCESSOR_FLAG) < 0:
  37. taskgen.env.append_value(flag, PREPROCESSOR_FLAG)
  38. # Figure out what casing conventions the user's shell used when
  39. # launching Waf
  40. (drive, _) = os.path.splitdrive(taskgen.bld.srcnode.abspath())
  41. taskgen.msvcdeps_drive_lowercase = drive == drive.lower()
  42. def path_to_node(base_node, path, cached_nodes):
  43. # Take the base node and the path and return a node
  44. # Results are cached because searching the node tree is expensive
  45. # The following code is executed by threads, it is not safe, so a lock is needed...
  46. if getattr(path, '__hash__'):
  47. node_lookup_key = (base_node, path)
  48. else:
  49. # Not hashable, assume it is a list and join into a string
  50. node_lookup_key = (base_node, os.path.sep.join(path))
  51. try:
  52. lock.acquire()
  53. node = cached_nodes[node_lookup_key]
  54. except KeyError:
  55. node = base_node.find_resource(path)
  56. cached_nodes[node_lookup_key] = node
  57. finally:
  58. lock.release()
  59. return node
  60. def post_run(self):
  61. if self.env.CC_NAME not in supported_compilers:
  62. return super(self.derived_msvcdeps, self).post_run()
  63. # TODO this is unlikely to work with netcache
  64. if getattr(self, 'cached', None):
  65. return Task.Task.post_run(self)
  66. bld = self.generator.bld
  67. unresolved_names = []
  68. resolved_nodes = []
  69. lowercase = self.generator.msvcdeps_drive_lowercase
  70. correct_case_path = bld.path.abspath()
  71. correct_case_path_len = len(correct_case_path)
  72. correct_case_path_norm = os.path.normcase(correct_case_path)
  73. # Dynamically bind to the cache
  74. try:
  75. cached_nodes = bld.cached_nodes
  76. except AttributeError:
  77. cached_nodes = bld.cached_nodes = {}
  78. for path in self.msvcdeps_paths:
  79. node = None
  80. if os.path.isabs(path):
  81. # Force drive letter to match conventions of main source tree
  82. drive, tail = os.path.splitdrive(path)
  83. if os.path.normcase(path[:correct_case_path_len]) == correct_case_path_norm:
  84. # Path is in the sandbox, force it to be correct. MSVC sometimes returns a lowercase path.
  85. path = correct_case_path + path[correct_case_path_len:]
  86. else:
  87. # Check the drive letter
  88. if lowercase and (drive != drive.lower()):
  89. path = drive.lower() + tail
  90. elif (not lowercase) and (drive != drive.upper()):
  91. path = drive.upper() + tail
  92. node = path_to_node(bld.root, path, cached_nodes)
  93. else:
  94. base_node = bld.bldnode
  95. # when calling find_resource, make sure the path does not begin by '..'
  96. path = [k for k in Utils.split_path(path) if k and k != '.']
  97. while path[0] == '..':
  98. path = path[1:]
  99. base_node = base_node.parent
  100. node = path_to_node(base_node, path, cached_nodes)
  101. if not node:
  102. raise ValueError('could not find %r for %r' % (path, self))
  103. else:
  104. if not c_preproc.go_absolute:
  105. if not (node.is_child_of(bld.srcnode) or node.is_child_of(bld.bldnode)):
  106. # System library
  107. Logs.debug('msvcdeps: Ignoring system include %r', node)
  108. continue
  109. if id(node) == id(self.inputs[0]):
  110. # Self-dependency
  111. continue
  112. resolved_nodes.append(node)
  113. bld.node_deps[self.uid()] = resolved_nodes
  114. bld.raw_deps[self.uid()] = unresolved_names
  115. try:
  116. del self.cache_sig
  117. except AttributeError:
  118. pass
  119. Task.Task.post_run(self)
  120. def scan(self):
  121. if self.env.CC_NAME not in supported_compilers:
  122. return super(self.derived_msvcdeps, self).scan()
  123. resolved_nodes = self.generator.bld.node_deps.get(self.uid(), [])
  124. unresolved_names = []
  125. return (resolved_nodes, unresolved_names)
  126. def sig_implicit_deps(self):
  127. if self.env.CC_NAME not in supported_compilers:
  128. return super(self.derived_msvcdeps, self).sig_implicit_deps()
  129. try:
  130. return Task.Task.sig_implicit_deps(self)
  131. except Errors.WafError:
  132. return Utils.SIG_NIL
  133. def exec_command(self, cmd, **kw):
  134. if self.env.CC_NAME not in supported_compilers:
  135. return super(self.derived_msvcdeps, self).exec_command(cmd, **kw)
  136. if not 'cwd' in kw:
  137. kw['cwd'] = self.get_cwd()
  138. if self.env.PATH:
  139. env = kw['env'] = dict(kw.get('env') or self.env.env or os.environ)
  140. env['PATH'] = self.env.PATH if isinstance(self.env.PATH, str) else os.pathsep.join(self.env.PATH)
  141. # The Visual Studio IDE adds an environment variable that causes
  142. # the MS compiler to send its textual output directly to the
  143. # debugging window rather than normal stdout/stderr.
  144. #
  145. # This is unrecoverably bad for this tool because it will cause
  146. # all the dependency scanning to see an empty stdout stream and
  147. # assume that the file being compiled uses no headers.
  148. #
  149. # See http://blogs.msdn.com/b/freik/archive/2006/04/05/569025.aspx
  150. #
  151. # Attempting to repair the situation by deleting the offending
  152. # envvar at this point in tool execution will not be good enough--
  153. # its presence poisons the 'waf configure' step earlier. We just
  154. # want to put a sanity check here in order to help developers
  155. # quickly diagnose the issue if an otherwise-good Waf tree
  156. # is then executed inside the MSVS IDE.
  157. assert 'VS_UNICODE_OUTPUT' not in kw['env']
  158. cmd, args = self.split_argfile(cmd)
  159. try:
  160. (fd, tmp) = tempfile.mkstemp()
  161. os.write(fd, '\r\n'.join(args).encode())
  162. os.close(fd)
  163. self.msvcdeps_paths = []
  164. kw['env'] = kw.get('env', os.environ.copy())
  165. kw['cwd'] = kw.get('cwd', os.getcwd())
  166. kw['quiet'] = Context.STDOUT
  167. kw['output'] = Context.STDOUT
  168. out = []
  169. if Logs.verbose:
  170. Logs.debug('argfile: @%r -> %r', tmp, args)
  171. try:
  172. raw_out = self.generator.bld.cmd_and_log(cmd + ['@' + tmp], **kw)
  173. ret = 0
  174. except Errors.WafError as e:
  175. raw_out = e.stdout
  176. ret = e.returncode
  177. for line in raw_out.splitlines():
  178. if line.startswith(INCLUDE_PATTERN):
  179. inc_path = line[len(INCLUDE_PATTERN):].strip()
  180. Logs.debug('msvcdeps: Regex matched %s', inc_path)
  181. self.msvcdeps_paths.append(inc_path)
  182. else:
  183. out.append(line)
  184. # Pipe through the remaining stdout content (not related to /showIncludes)
  185. if self.generator.bld.logger:
  186. self.generator.bld.logger.debug('out: %s' % os.linesep.join(out))
  187. else:
  188. sys.stdout.write(os.linesep.join(out) + os.linesep)
  189. return ret
  190. finally:
  191. try:
  192. os.remove(tmp)
  193. except OSError:
  194. # anti-virus and indexers can keep files open -_-
  195. pass
  196. def wrap_compiled_task(classname):
  197. derived_class = type(classname, (Task.classes[classname],), {})
  198. derived_class.derived_msvcdeps = derived_class
  199. derived_class.post_run = post_run
  200. derived_class.scan = scan
  201. derived_class.sig_implicit_deps = sig_implicit_deps
  202. derived_class.exec_command = exec_command
  203. for k in ('c', 'cxx'):
  204. if k in Task.classes:
  205. wrap_compiled_task(k)
  206. def options(opt):
  207. raise ValueError('Do not load msvcdeps options')