runners.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. from __future__ import annotations
  2. from typing import Callable, Optional
  3. from collections import OrderedDict
  4. import os
  5. import re
  6. import subprocess
  7. from .util import (
  8. find_binary_of_command, unique_list, CompileError
  9. )
  10. class CompilerRunner:
  11. """ CompilerRunner base class.
  12. Parameters
  13. ==========
  14. sources : list of str
  15. Paths to sources.
  16. out : str
  17. flags : iterable of str
  18. Compiler flags.
  19. run_linker : bool
  20. compiler_name_exe : (str, str) tuple
  21. Tuple of compiler name & command to call.
  22. cwd : str
  23. Path of root of relative paths.
  24. include_dirs : list of str
  25. Include directories.
  26. libraries : list of str
  27. Libraries to link against.
  28. library_dirs : list of str
  29. Paths to search for shared libraries.
  30. std : str
  31. Standard string, e.g. ``'c++11'``, ``'c99'``, ``'f2003'``.
  32. define: iterable of strings
  33. macros to define
  34. undef : iterable of strings
  35. macros to undefine
  36. preferred_vendor : string
  37. name of preferred vendor e.g. 'gnu' or 'intel'
  38. Methods
  39. =======
  40. run():
  41. Invoke compilation as a subprocess.
  42. """
  43. # Subclass to vendor/binary dict
  44. compiler_dict: dict[str, str]
  45. # Standards should be a tuple of supported standards
  46. # (first one will be the default)
  47. standards: tuple[None | str, ...]
  48. # Subclass to dict of binary/formater-callback
  49. std_formater: dict[str, Callable[[Optional[str]], str]]
  50. # subclass to be e.g. {'gcc': 'gnu', ...}
  51. compiler_name_vendor_mapping: dict[str, str]
  52. def __init__(self, sources, out, flags=None, run_linker=True, compiler=None, cwd='.',
  53. include_dirs=None, libraries=None, library_dirs=None, std=None, define=None,
  54. undef=None, strict_aliasing=None, preferred_vendor=None, linkline=None, **kwargs):
  55. if isinstance(sources, str):
  56. raise ValueError("Expected argument sources to be a list of strings.")
  57. self.sources = list(sources)
  58. self.out = out
  59. self.flags = flags or []
  60. self.cwd = cwd
  61. if compiler:
  62. self.compiler_name, self.compiler_binary = compiler
  63. else:
  64. # Find a compiler
  65. if preferred_vendor is None:
  66. preferred_vendor = os.environ.get('SYMPY_COMPILER_VENDOR', None)
  67. self.compiler_name, self.compiler_binary, self.compiler_vendor = self.find_compiler(preferred_vendor)
  68. if self.compiler_binary is None:
  69. raise ValueError("No compiler found (searched: {})".format(', '.join(self.compiler_dict.values())))
  70. self.define = define or []
  71. self.undef = undef or []
  72. self.include_dirs = include_dirs or []
  73. self.libraries = libraries or []
  74. self.library_dirs = library_dirs or []
  75. self.std = std or self.standards[0]
  76. self.run_linker = run_linker
  77. if self.run_linker:
  78. # both gnu and intel compilers use '-c' for disabling linker
  79. self.flags = list(filter(lambda x: x != '-c', self.flags))
  80. else:
  81. if '-c' not in self.flags:
  82. self.flags.append('-c')
  83. if self.std:
  84. self.flags.append(self.std_formater[
  85. self.compiler_name](self.std))
  86. self.linkline = linkline or []
  87. if strict_aliasing is not None:
  88. nsa_re = re.compile("no-strict-aliasing$")
  89. sa_re = re.compile("strict-aliasing$")
  90. if strict_aliasing is True:
  91. if any(map(nsa_re.match, flags)):
  92. raise CompileError("Strict aliasing cannot be both enforced and disabled")
  93. elif any(map(sa_re.match, flags)):
  94. pass # already enforced
  95. else:
  96. flags.append('-fstrict-aliasing')
  97. elif strict_aliasing is False:
  98. if any(map(nsa_re.match, flags)):
  99. pass # already disabled
  100. else:
  101. if any(map(sa_re.match, flags)):
  102. raise CompileError("Strict aliasing cannot be both enforced and disabled")
  103. else:
  104. flags.append('-fno-strict-aliasing')
  105. else:
  106. msg = "Expected argument strict_aliasing to be True/False, got {}"
  107. raise ValueError(msg.format(strict_aliasing))
  108. @classmethod
  109. def find_compiler(cls, preferred_vendor=None):
  110. """ Identify a suitable C/fortran/other compiler. """
  111. candidates = list(cls.compiler_dict.keys())
  112. if preferred_vendor:
  113. if preferred_vendor in candidates:
  114. candidates = [preferred_vendor]+candidates
  115. else:
  116. raise ValueError("Unknown vendor {}".format(preferred_vendor))
  117. name, path = find_binary_of_command([cls.compiler_dict[x] for x in candidates])
  118. return name, path, cls.compiler_name_vendor_mapping[name]
  119. def cmd(self):
  120. """ List of arguments (str) to be passed to e.g. ``subprocess.Popen``. """
  121. cmd = (
  122. [self.compiler_binary] +
  123. self.flags +
  124. ['-U'+x for x in self.undef] +
  125. ['-D'+x for x in self.define] +
  126. ['-I'+x for x in self.include_dirs] +
  127. self.sources
  128. )
  129. if self.run_linker:
  130. cmd += (['-L'+x for x in self.library_dirs] +
  131. ['-l'+x for x in self.libraries] +
  132. self.linkline)
  133. counted = []
  134. for envvar in re.findall(r'\$\{(\w+)\}', ' '.join(cmd)):
  135. if os.getenv(envvar) is None:
  136. if envvar not in counted:
  137. counted.append(envvar)
  138. msg = "Environment variable '{}' undefined.".format(envvar)
  139. raise CompileError(msg)
  140. return cmd
  141. def run(self):
  142. self.flags = unique_list(self.flags)
  143. # Append output flag and name to tail of flags
  144. self.flags.extend(['-o', self.out])
  145. env = os.environ.copy()
  146. env['PWD'] = self.cwd
  147. # NOTE: intel compilers seems to need shell=True
  148. p = subprocess.Popen(' '.join(self.cmd()),
  149. shell=True,
  150. cwd=self.cwd,
  151. stdin=subprocess.PIPE,
  152. stdout=subprocess.PIPE,
  153. stderr=subprocess.STDOUT,
  154. env=env)
  155. comm = p.communicate()
  156. try:
  157. self.cmd_outerr = comm[0].decode('utf-8')
  158. except UnicodeDecodeError:
  159. self.cmd_outerr = comm[0].decode('iso-8859-1') # win32
  160. self.cmd_returncode = p.returncode
  161. # Error handling
  162. if self.cmd_returncode != 0:
  163. msg = "Error executing '{}' in {} (exited status {}):\n {}\n".format(
  164. ' '.join(self.cmd()), self.cwd, str(self.cmd_returncode), self.cmd_outerr
  165. )
  166. raise CompileError(msg)
  167. return self.cmd_outerr, self.cmd_returncode
  168. class CCompilerRunner(CompilerRunner):
  169. compiler_dict = OrderedDict([
  170. ('gnu', 'gcc'),
  171. ('intel', 'icc'),
  172. ('llvm', 'clang'),
  173. ])
  174. standards = ('c89', 'c90', 'c99', 'c11') # First is default
  175. std_formater = {
  176. 'gcc': '-std={}'.format,
  177. 'icc': '-std={}'.format,
  178. 'clang': '-std={}'.format,
  179. }
  180. compiler_name_vendor_mapping = {
  181. 'gcc': 'gnu',
  182. 'icc': 'intel',
  183. 'clang': 'llvm'
  184. }
  185. def _mk_flag_filter(cmplr_name): # helper for class initialization
  186. not_welcome = {'g++': ("Wimplicit-interface",)} # "Wstrict-prototypes",)}
  187. if cmplr_name in not_welcome:
  188. def fltr(x):
  189. for nw in not_welcome[cmplr_name]:
  190. if nw in x:
  191. return False
  192. return True
  193. else:
  194. def fltr(x):
  195. return True
  196. return fltr
  197. class CppCompilerRunner(CompilerRunner):
  198. compiler_dict = OrderedDict([
  199. ('gnu', 'g++'),
  200. ('intel', 'icpc'),
  201. ('llvm', 'clang++'),
  202. ])
  203. # First is the default, c++0x == c++11
  204. standards = ('c++98', 'c++0x')
  205. std_formater = {
  206. 'g++': '-std={}'.format,
  207. 'icpc': '-std={}'.format,
  208. 'clang++': '-std={}'.format,
  209. }
  210. compiler_name_vendor_mapping = {
  211. 'g++': 'gnu',
  212. 'icpc': 'intel',
  213. 'clang++': 'llvm'
  214. }
  215. class FortranCompilerRunner(CompilerRunner):
  216. standards = (None, 'f77', 'f95', 'f2003', 'f2008')
  217. std_formater = {
  218. 'gfortran': lambda x: '-std=gnu' if x is None else '-std=legacy' if x == 'f77' else '-std={}'.format(x),
  219. 'ifort': lambda x: '-stand f08' if x is None else '-stand f{}'.format(x[-2:]), # f2008 => f08
  220. }
  221. compiler_dict = OrderedDict([
  222. ('gnu', 'gfortran'),
  223. ('intel', 'ifort'),
  224. ])
  225. compiler_name_vendor_mapping = {
  226. 'gfortran': 'gnu',
  227. 'ifort': 'intel',
  228. }