PageRenderTime 71ms CodeModel.GetById 40ms RepoModel.GetById 0ms app.codeStats 0ms

/scipy/_build_utils/_fortran.py

https://github.com/matthew-brett/scipy
Python | 444 lines | 334 code | 47 blank | 63 comment | 32 complexity | b885eb58ecd3c6d57d6aca1b7c0005a3 MD5 | raw file
  1. import re
  2. import os
  3. import sys
  4. from distutils.util import get_platform
  5. import numpy as np
  6. from .system_info import combine_dict
  7. __all__ = ['needs_g77_abi_wrapper', 'get_g77_abi_wrappers',
  8. 'gfortran_legacy_flag_hook', 'blas_ilp64_pre_build_hook',
  9. 'get_f2py_int64_options', 'generic_pre_build_hook',
  10. 'write_file_content', 'ilp64_pre_build_hook']
  11. def get_fcompiler_ilp64_flags():
  12. """
  13. Dictionary of compiler flags for switching to 8-byte default integer
  14. size.
  15. """
  16. flags = {
  17. 'absoft': ['-i8'], # Absoft
  18. 'compaq': ['-i8'], # Compaq Fortran
  19. 'compaqv': ['/integer_size:64'], # Compaq Visual Fortran
  20. 'g95': ['-i8'], # g95
  21. 'gnu95': ['-fdefault-integer-8'], # GNU gfortran
  22. 'ibm': ['-qintsize=8'], # IBM XL Fortran
  23. 'intel': ['-i8'], # Intel Fortran Compiler for 32-bit
  24. 'intele': ['-i8'], # Intel Fortran Compiler for Itanium
  25. 'intelem': ['-i8'], # Intel Fortran Compiler for 64-bit
  26. 'intelv': ['-i8'], # Intel Visual Fortran Compiler for 32-bit
  27. 'intelev': ['-i8'], # Intel Visual Fortran Compiler for Itanium
  28. 'intelvem': ['-i8'], # Intel Visual Fortran Compiler for 64-bit
  29. 'lahey': ['--long'], # Lahey/Fujitsu Fortran 95 Compiler
  30. 'mips': ['-i8'], # MIPSpro Fortran Compiler
  31. 'nag': ['-i8'], # NAGWare Fortran 95 compiler
  32. 'nagfor': ['-i8'], # NAG Fortran compiler
  33. 'pathf95': ['-i8'], # PathScale Fortran compiler
  34. 'pg': ['-i8'], # Portland Group Fortran Compiler
  35. 'flang': ['-i8'], # Portland Group Fortran LLVM Compiler
  36. 'sun': ['-i8'], # Sun or Forte Fortran 95 Compiler
  37. }
  38. # No support for this:
  39. # - g77
  40. # - hpux
  41. # Unknown:
  42. # - vast
  43. return flags
  44. def get_fcompiler_macro_include_flags(path):
  45. """
  46. Dictionary of compiler flags for cpp-style preprocessing, with
  47. an #include search path, and safety options necessary for macro
  48. expansion.
  49. """
  50. intel_opts = ['-fpp', '-I' + path]
  51. nag_opts = ['-fpp', '-I' + path]
  52. flags = {
  53. 'absoft': ['-W132', '-cpp', '-I' + path],
  54. 'gnu95': ['-cpp', '-ffree-line-length-none',
  55. '-ffixed-line-length-none', '-I' + path],
  56. 'intel': intel_opts,
  57. 'intele': intel_opts,
  58. 'intelem': intel_opts,
  59. 'intelv': intel_opts,
  60. 'intelev': intel_opts,
  61. 'intelvem': intel_opts,
  62. 'lahey': ['-Cpp', '--wide', '-I' + path],
  63. 'mips': ['-col120', '-I' + path],
  64. 'nag': nag_opts,
  65. 'nagfor': nag_opts,
  66. 'pathf95': ['-ftpp', '-macro-expand', '-I' + path],
  67. 'flang': ['-Mpreprocess', '-Mextend', '-I' + path],
  68. 'sun': ['-fpp', '-I' + path],
  69. }
  70. # No support for this:
  71. # - ibm (line length option turns on fixed format)
  72. # TODO:
  73. # - pg
  74. return flags
  75. def uses_mkl(info):
  76. r_mkl = re.compile("mkl")
  77. libraries = info.get('libraries', '')
  78. for library in libraries:
  79. if r_mkl.search(library):
  80. return True
  81. return False
  82. def needs_g77_abi_wrapper(info):
  83. """Returns True if g77 ABI wrapper must be used."""
  84. try:
  85. needs_wrapper = int(os.environ["SCIPY_USE_G77_ABI_WRAPPER"]) != 0
  86. except KeyError:
  87. needs_wrapper = uses_mkl(info)
  88. return needs_wrapper
  89. def get_g77_abi_wrappers(info):
  90. """
  91. Returns file names of source files containing Fortran ABI wrapper
  92. routines.
  93. """
  94. wrapper_sources = []
  95. path = os.path.abspath(os.path.dirname(__file__))
  96. if needs_g77_abi_wrapper(info):
  97. wrapper_sources += [
  98. os.path.join(path, 'src', 'wrap_g77_abi_f.f'),
  99. os.path.join(path, 'src', 'wrap_g77_abi_c.c'),
  100. ]
  101. else:
  102. wrapper_sources += [
  103. os.path.join(path, 'src', 'wrap_dummy_g77_abi.f'),
  104. ]
  105. return wrapper_sources
  106. def gfortran_legacy_flag_hook(cmd, ext):
  107. """
  108. Pre-build hook to add dd gfortran legacy flag -fallow-argument-mismatch
  109. """
  110. from .compiler_helper import try_add_flag
  111. from distutils.version import LooseVersion
  112. if isinstance(ext, dict):
  113. # build_clib
  114. compilers = ((cmd._f_compiler, ext.setdefault('extra_f77_compile_args', [])),
  115. (cmd._f_compiler, ext.setdefault('extra_f90_compile_args', [])))
  116. else:
  117. # build_ext
  118. compilers = ((cmd._f77_compiler, ext.extra_f77_compile_args),
  119. (cmd._f90_compiler, ext.extra_f90_compile_args))
  120. for compiler, args in compilers:
  121. if compiler is None:
  122. continue
  123. if compiler.compiler_type == "gnu95" and compiler.version >= LooseVersion("10"):
  124. try_add_flag(args, compiler, "-fallow-argument-mismatch")
  125. def _get_build_src_dir():
  126. plat_specifier = ".{}-{}.{}".format(get_platform(), *sys.version_info[:2])
  127. return os.path.join('build', 'src' + plat_specifier)
  128. def get_f2py_int64_options():
  129. if np.dtype('i') == np.dtype(np.int64):
  130. int64_name = 'int'
  131. elif np.dtype('l') == np.dtype(np.int64):
  132. int64_name = 'long'
  133. elif np.dtype('q') == np.dtype(np.int64):
  134. int64_name = 'long_long'
  135. else:
  136. raise RuntimeError("No 64-bit integer type available in f2py!")
  137. f2cmap_fn = os.path.join(_get_build_src_dir(), 'int64.f2cmap')
  138. text = "{'integer': {'': '%s'}, 'logical': {'': '%s'}}\n" % (
  139. int64_name, int64_name)
  140. write_file_content(f2cmap_fn, text)
  141. return ['--f2cmap', f2cmap_fn]
  142. def ilp64_pre_build_hook(cmd, ext):
  143. """
  144. Pre-build hook for adding Fortran compiler flags that change
  145. default integer size to 64-bit.
  146. """
  147. fcompiler_flags = get_fcompiler_ilp64_flags()
  148. return generic_pre_build_hook(cmd, ext, fcompiler_flags=fcompiler_flags)
  149. def blas_ilp64_pre_build_hook(blas_info):
  150. """
  151. Pre-build hook for adding ILP64 BLAS compilation flags, and
  152. mangling Fortran source files to rename BLAS/LAPACK symbols when
  153. there are symbol suffixes.
  154. Examples
  155. --------
  156. ::
  157. from scipy._build_utils import blas_ilp64_pre_build_hook
  158. ext = config.add_extension(...)
  159. ext._pre_build_hook = blas_ilp64_pre_build_hook(blas_info)
  160. """
  161. return lambda cmd, ext: _blas_ilp64_pre_build_hook(cmd, ext, blas_info)
  162. def _blas_ilp64_pre_build_hook(cmd, ext, blas_info):
  163. # Determine BLAS symbol suffix/prefix, if any
  164. macros = dict(blas_info.get('define_macros', []))
  165. prefix = macros.get('BLAS_SYMBOL_PREFIX', '')
  166. suffix = macros.get('BLAS_SYMBOL_SUFFIX', '')
  167. if suffix:
  168. if not suffix.endswith('_'):
  169. # Symbol suffix has to end with '_' to be Fortran-compatible
  170. raise RuntimeError("BLAS/LAPACK has incompatible symbol suffix: "
  171. "{!r}".format(suffix))
  172. suffix = suffix[:-1]
  173. # When symbol prefix/suffix is present, we have to patch sources
  174. if prefix or suffix:
  175. include_dir = os.path.join(_get_build_src_dir(), 'blas64-include')
  176. fcompiler_flags = combine_dict(get_fcompiler_ilp64_flags(),
  177. get_fcompiler_macro_include_flags(include_dir))
  178. # Add the include dir for C code
  179. if isinstance(ext, dict):
  180. ext.setdefault('include_dirs', [])
  181. ext['include_dirs'].append(include_dir)
  182. else:
  183. ext.include_dirs.append(include_dir)
  184. # Create name-mapping include files
  185. include_name_f = 'blas64-prefix-defines.inc'
  186. include_name_c = 'blas64-prefix-defines.h'
  187. include_fn_f = os.path.join(include_dir, include_name_f)
  188. include_fn_c = os.path.join(include_dir, include_name_c)
  189. text = ""
  190. for symbol in get_blas_lapack_symbols():
  191. text += '#define {} {}{}_{}\n'.format(symbol, prefix, symbol, suffix)
  192. text += '#define {} {}{}_{}\n'.format(symbol.upper(), prefix, symbol, suffix)
  193. # Code generation may give source codes with mixed-case names
  194. for j in (1, 2):
  195. s = symbol[:j].lower() + symbol[j:].upper()
  196. text += '#define {} {}{}_{}\n'.format(s, prefix, symbol, suffix)
  197. s = symbol[:j].upper() + symbol[j:].lower()
  198. text += '#define {} {}{}_{}\n'.format(s, prefix, symbol, suffix)
  199. write_file_content(include_fn_f, text)
  200. ctext = re.sub(r'^#define (.*) (.*)$', r'#define \1_ \2_', text, flags=re.M)
  201. write_file_content(include_fn_c, text + "\n" + ctext)
  202. # Patch sources to include it
  203. def patch_source(filename, old_text):
  204. text = '#include "{}"\n'.format(include_name_f)
  205. text += old_text
  206. return text
  207. else:
  208. fcompiler_flags = get_fcompiler_ilp64_flags()
  209. patch_source = None
  210. return generic_pre_build_hook(cmd, ext,
  211. fcompiler_flags=fcompiler_flags,
  212. patch_source_func=patch_source,
  213. source_fnpart="_blas64")
  214. def generic_pre_build_hook(cmd, ext, fcompiler_flags, patch_source_func=None,
  215. source_fnpart=None):
  216. """
  217. Pre-build hook for adding compiler flags and patching sources.
  218. Parameters
  219. ----------
  220. cmd : distutils.core.Command
  221. Hook input. Current distutils command (build_clib or build_ext).
  222. ext : dict or numpy.distutils.extension.Extension
  223. Hook input. Configuration information for library (dict, build_clib)
  224. or extension (numpy.distutils.extension.Extension, build_ext).
  225. fcompiler_flags : dict
  226. Dictionary of ``{'compiler_name': ['-flag1', ...]}`` containing
  227. compiler flags to set.
  228. patch_source_func : callable, optional
  229. Function patching sources, see `_generic_patch_sources` below.
  230. source_fnpart : str, optional
  231. String to append to the modified file basename before extension.
  232. """
  233. is_clib = isinstance(ext, dict)
  234. if is_clib:
  235. build_info = ext
  236. del ext
  237. # build_clib doesn't have separate f77/f90 compilers
  238. f77 = cmd._f_compiler
  239. f90 = cmd._f_compiler
  240. else:
  241. f77 = cmd._f77_compiler
  242. f90 = cmd._f90_compiler
  243. # Add compiler flags
  244. if is_clib:
  245. f77_args = build_info.setdefault('extra_f77_compile_args', [])
  246. f90_args = build_info.setdefault('extra_f90_compile_args', [])
  247. compilers = [(f77, f77_args), (f90, f90_args)]
  248. else:
  249. compilers = [(f77, ext.extra_f77_compile_args),
  250. (f90, ext.extra_f90_compile_args)]
  251. for compiler, args in compilers:
  252. if compiler is None:
  253. continue
  254. try:
  255. flags = fcompiler_flags[compiler.compiler_type]
  256. except KeyError as e:
  257. raise RuntimeError(
  258. "Compiler {!r} is not supported in this "
  259. "configuration.".format(compiler.compiler_type)
  260. ) from e
  261. args.extend(flag for flag in flags if flag not in args)
  262. # Mangle sources
  263. if patch_source_func is not None:
  264. if is_clib:
  265. build_info.setdefault('depends', []).extend(build_info['sources'])
  266. new_sources = _generic_patch_sources(build_info['sources'], patch_source_func,
  267. source_fnpart)
  268. build_info['sources'][:] = new_sources
  269. else:
  270. ext.depends.extend(ext.sources)
  271. new_sources = _generic_patch_sources(ext.sources, patch_source_func,
  272. source_fnpart)
  273. ext.sources[:] = new_sources
  274. def _generic_patch_sources(filenames, patch_source_func, source_fnpart, root_dir=None):
  275. """
  276. Patch Fortran sources, creating new source files.
  277. Parameters
  278. ----------
  279. filenames : list
  280. List of Fortran source files to patch.
  281. Files not ending in ``.f`` or ``.f90`` are left unaltered.
  282. patch_source_func : callable(filename, old_contents) -> new_contents
  283. Function to apply to file contents, returning new file contents
  284. as a string.
  285. source_fnpart : str
  286. String to append to the modified file basename before extension.
  287. root_dir : str, optional
  288. Source root directory. Default: cwd
  289. Returns
  290. -------
  291. new_filenames : list
  292. List of names of the newly created patched sources.
  293. """
  294. new_filenames = []
  295. if root_dir is None:
  296. root_dir = os.getcwd()
  297. root_dir = os.path.abspath(root_dir)
  298. src_dir = os.path.join(root_dir, _get_build_src_dir())
  299. for src in filenames:
  300. base, ext = os.path.splitext(os.path.basename(src))
  301. if ext not in ('.f', '.f90'):
  302. new_filenames.append(src)
  303. continue
  304. with open(src, 'r') as fsrc:
  305. text = patch_source_func(src, fsrc.read())
  306. # Generate useful target directory name under src_dir
  307. src_path = os.path.abspath(os.path.dirname(src))
  308. for basedir in [src_dir, root_dir]:
  309. if os.path.commonpath([src_path, basedir]) == basedir:
  310. rel_path = os.path.relpath(src_path, basedir)
  311. break
  312. else:
  313. raise ValueError(f"{src!r} not under {root_dir!r}")
  314. dst = os.path.join(src_dir, rel_path, base + source_fnpart + ext)
  315. write_file_content(dst, text)
  316. new_filenames.append(dst)
  317. return new_filenames
  318. def write_file_content(filename, content):
  319. """
  320. Write content to file, but only if it differs from the current one.
  321. """
  322. if os.path.isfile(filename):
  323. with open(filename, 'r') as f:
  324. old_content = f.read()
  325. if old_content == content:
  326. return
  327. dirname = os.path.dirname(filename)
  328. if not os.path.isdir(dirname):
  329. os.makedirs(dirname)
  330. with open(filename, 'w') as f:
  331. f.write(content)
  332. def get_blas_lapack_symbols():
  333. cached = getattr(get_blas_lapack_symbols, 'cached', None)
  334. if cached is not None:
  335. return cached
  336. # Obtain symbol list from Cython Blas/Lapack interface
  337. srcdir = os.path.join(os.path.dirname(__file__), os.pardir, 'linalg')
  338. symbols = []
  339. # Get symbols from the generated files
  340. for fn in ['cython_blas_signatures.txt', 'cython_lapack_signatures.txt']:
  341. with open(os.path.join(srcdir, fn), 'r') as f:
  342. for line in f:
  343. m = re.match(r"^\s*[a-z]+\s+([a-z0-9]+)\(", line)
  344. if m:
  345. symbols.append(m.group(1))
  346. # Get the rest from the generator script
  347. # (we cannot import it directly here, so use exec)
  348. sig_fn = os.path.join(srcdir, '_cython_signature_generator.py')
  349. with open(sig_fn, 'r') as f:
  350. code = f.read()
  351. ns = {'__name__': '<module>'}
  352. exec(code, ns)
  353. symbols.extend(ns['blas_exclusions'])
  354. symbols.extend(ns['lapack_exclusions'])
  355. get_blas_lapack_symbols.cached = tuple(sorted(set(symbols)))
  356. return get_blas_lapack_symbols.cached