runners.py 10.0 KB

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