图片解析应用
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

622 lines
20 KiB

  1. import glob
  2. import os
  3. import shutil
  4. import subprocess
  5. import sys
  6. import tempfile
  7. import warnings
  8. from sysconfig import get_config_var, get_config_vars, get_path
  9. from .runners import (
  10. CCompilerRunner,
  11. CppCompilerRunner,
  12. FortranCompilerRunner
  13. )
  14. from .util import (
  15. get_abspath, make_dirs, copy, Glob, ArbitraryDepthGlob,
  16. glob_at_depth, import_module_from_file, pyx_is_cplus,
  17. sha256_of_string, sha256_of_file, CompileError
  18. )
  19. if os.name == 'posix':
  20. objext = '.o'
  21. elif os.name == 'nt':
  22. objext = '.obj'
  23. else:
  24. warnings.warn("Unknown os.name: {}".format(os.name))
  25. objext = '.o'
  26. def compile_sources(files, Runner=None, destdir=None, cwd=None, keep_dir_struct=False,
  27. per_file_kwargs=None, **kwargs):
  28. """ Compile source code files to object files.
  29. Parameters
  30. ==========
  31. files : iterable of str
  32. Paths to source files, if ``cwd`` is given, the paths are taken as relative.
  33. Runner: CompilerRunner subclass (optional)
  34. Could be e.g. ``FortranCompilerRunner``. Will be inferred from filename
  35. extensions if missing.
  36. destdir: str
  37. Output directory, if cwd is given, the path is taken as relative.
  38. cwd: str
  39. Working directory. Specify to have compiler run in other directory.
  40. also used as root of relative paths.
  41. keep_dir_struct: bool
  42. Reproduce directory structure in `destdir`. default: ``False``
  43. per_file_kwargs: dict
  44. Dict mapping instances in ``files`` to keyword arguments.
  45. \\*\\*kwargs: dict
  46. Default keyword arguments to pass to ``Runner``.
  47. """
  48. _per_file_kwargs = {}
  49. if per_file_kwargs is not None:
  50. for k, v in per_file_kwargs.items():
  51. if isinstance(k, Glob):
  52. for path in glob.glob(k.pathname):
  53. _per_file_kwargs[path] = v
  54. elif isinstance(k, ArbitraryDepthGlob):
  55. for path in glob_at_depth(k.filename, cwd):
  56. _per_file_kwargs[path] = v
  57. else:
  58. _per_file_kwargs[k] = v
  59. # Set up destination directory
  60. destdir = destdir or '.'
  61. if not os.path.isdir(destdir):
  62. if os.path.exists(destdir):
  63. raise OSError("{} is not a directory".format(destdir))
  64. else:
  65. make_dirs(destdir)
  66. if cwd is None:
  67. cwd = '.'
  68. for f in files:
  69. copy(f, destdir, only_update=True, dest_is_dir=True)
  70. # Compile files and return list of paths to the objects
  71. dstpaths = []
  72. for f in files:
  73. if keep_dir_struct:
  74. name, ext = os.path.splitext(f)
  75. else:
  76. name, ext = os.path.splitext(os.path.basename(f))
  77. file_kwargs = kwargs.copy()
  78. file_kwargs.update(_per_file_kwargs.get(f, {}))
  79. dstpaths.append(src2obj(f, Runner, cwd=cwd, **file_kwargs))
  80. return dstpaths
  81. def get_mixed_fort_c_linker(vendor=None, cplus=False, cwd=None):
  82. vendor = vendor or os.environ.get('SYMPY_COMPILER_VENDOR', 'gnu')
  83. if vendor.lower() == 'intel':
  84. if cplus:
  85. return (FortranCompilerRunner,
  86. {'flags': ['-nofor_main', '-cxxlib']}, vendor)
  87. else:
  88. return (FortranCompilerRunner,
  89. {'flags': ['-nofor_main']}, vendor)
  90. elif vendor.lower() == 'gnu' or 'llvm':
  91. if cplus:
  92. return (CppCompilerRunner,
  93. {'lib_options': ['fortran']}, vendor)
  94. else:
  95. return (FortranCompilerRunner,
  96. {}, vendor)
  97. else:
  98. raise ValueError("No vendor found.")
  99. def link(obj_files, out_file=None, shared=False, Runner=None,
  100. cwd=None, cplus=False, fort=False, **kwargs):
  101. """ Link object files.
  102. Parameters
  103. ==========
  104. obj_files: iterable of str
  105. Paths to object files.
  106. out_file: str (optional)
  107. Path to executable/shared library, if ``None`` it will be
  108. deduced from the last item in obj_files.
  109. shared: bool
  110. Generate a shared library?
  111. Runner: CompilerRunner subclass (optional)
  112. If not given the ``cplus`` and ``fort`` flags will be inspected
  113. (fallback is the C compiler).
  114. cwd: str
  115. Path to the root of relative paths and working directory for compiler.
  116. cplus: bool
  117. C++ objects? default: ``False``.
  118. fort: bool
  119. Fortran objects? default: ``False``.
  120. \\*\\*kwargs: dict
  121. Keyword arguments passed to ``Runner``.
  122. Returns
  123. =======
  124. The absolute path to the generated shared object / executable.
  125. """
  126. if out_file is None:
  127. out_file, ext = os.path.splitext(os.path.basename(obj_files[-1]))
  128. if shared:
  129. out_file += get_config_var('EXT_SUFFIX')
  130. if not Runner:
  131. if fort:
  132. Runner, extra_kwargs, vendor = \
  133. get_mixed_fort_c_linker(
  134. vendor=kwargs.get('vendor', None),
  135. cplus=cplus,
  136. cwd=cwd,
  137. )
  138. for k, v in extra_kwargs.items():
  139. if k in kwargs:
  140. kwargs[k].expand(v)
  141. else:
  142. kwargs[k] = v
  143. else:
  144. if cplus:
  145. Runner = CppCompilerRunner
  146. else:
  147. Runner = CCompilerRunner
  148. flags = kwargs.pop('flags', [])
  149. if shared:
  150. if '-shared' not in flags:
  151. flags.append('-shared')
  152. run_linker = kwargs.pop('run_linker', True)
  153. if not run_linker:
  154. raise ValueError("run_linker was set to False (nonsensical).")
  155. out_file = get_abspath(out_file, cwd=cwd)
  156. runner = Runner(obj_files, out_file, flags, cwd=cwd, **kwargs)
  157. runner.run()
  158. return out_file
  159. def link_py_so(obj_files, so_file=None, cwd=None, libraries=None,
  160. cplus=False, fort=False, **kwargs):
  161. """ Link Python extension module (shared object) for importing
  162. Parameters
  163. ==========
  164. obj_files: iterable of str
  165. Paths to object files to be linked.
  166. so_file: str
  167. Name (path) of shared object file to create. If not specified it will
  168. have the basname of the last object file in `obj_files` but with the
  169. extension '.so' (Unix).
  170. cwd: path string
  171. Root of relative paths and working directory of linker.
  172. libraries: iterable of strings
  173. Libraries to link against, e.g. ['m'].
  174. cplus: bool
  175. Any C++ objects? default: ``False``.
  176. fort: bool
  177. Any Fortran objects? default: ``False``.
  178. kwargs**: dict
  179. Keyword arguments passed to ``link(...)``.
  180. Returns
  181. =======
  182. Absolute path to the generate shared object.
  183. """
  184. libraries = libraries or []
  185. include_dirs = kwargs.pop('include_dirs', [])
  186. library_dirs = kwargs.pop('library_dirs', [])
  187. # from distutils/command/build_ext.py:
  188. if sys.platform == "win32":
  189. warnings.warn("Windows not yet supported.")
  190. elif sys.platform == 'darwin':
  191. # Don't use the default code below
  192. pass
  193. elif sys.platform[:3] == 'aix':
  194. # Don't use the default code below
  195. pass
  196. else:
  197. if get_config_var('Py_ENABLE_SHARED'):
  198. cfgDict = get_config_vars()
  199. kwargs['linkline'] = kwargs.get('linkline', []) + [cfgDict['PY_LDFLAGS']] # PY_LDFLAGS or just LDFLAGS?
  200. library_dirs += [cfgDict['LIBDIR']]
  201. for opt in cfgDict['BLDLIBRARY'].split():
  202. if opt.startswith('-l'):
  203. libraries += [opt[2:]]
  204. else:
  205. pass
  206. flags = kwargs.pop('flags', [])
  207. needed_flags = ('-pthread',)
  208. for flag in needed_flags:
  209. if flag not in flags:
  210. flags.append(flag)
  211. return link(obj_files, shared=True, flags=flags, cwd=cwd,
  212. cplus=cplus, fort=fort, include_dirs=include_dirs,
  213. libraries=libraries, library_dirs=library_dirs, **kwargs)
  214. def simple_cythonize(src, destdir=None, cwd=None, **cy_kwargs):
  215. """ Generates a C file from a Cython source file.
  216. Parameters
  217. ==========
  218. src: str
  219. Path to Cython source.
  220. destdir: str (optional)
  221. Path to output directory (default: '.').
  222. cwd: path string (optional)
  223. Root of relative paths (default: '.').
  224. **cy_kwargs:
  225. Second argument passed to cy_compile. Generates a .cpp file if ``cplus=True`` in ``cy_kwargs``,
  226. else a .c file.
  227. """
  228. from Cython.Compiler.Main import (
  229. default_options, CompilationOptions
  230. )
  231. from Cython.Compiler.Main import compile as cy_compile
  232. assert src.lower().endswith('.pyx') or src.lower().endswith('.py')
  233. cwd = cwd or '.'
  234. destdir = destdir or '.'
  235. ext = '.cpp' if cy_kwargs.get('cplus', False) else '.c'
  236. c_name = os.path.splitext(os.path.basename(src))[0] + ext
  237. dstfile = os.path.join(destdir, c_name)
  238. if cwd:
  239. ori_dir = os.getcwd()
  240. else:
  241. ori_dir = '.'
  242. os.chdir(cwd)
  243. try:
  244. cy_options = CompilationOptions(default_options)
  245. cy_options.__dict__.update(cy_kwargs)
  246. # Set language_level if not set by cy_kwargs
  247. # as not setting it is deprecated
  248. if 'language_level' not in cy_kwargs:
  249. cy_options.__dict__['language_level'] = 3
  250. cy_result = cy_compile([src], cy_options)
  251. if cy_result.num_errors > 0:
  252. raise ValueError("Cython compilation failed.")
  253. if os.path.realpath(os.path.dirname(src)) != os.path.realpath(destdir):
  254. if os.path.exists(dstfile):
  255. os.unlink(dstfile)
  256. shutil.move(os.path.join(os.path.dirname(src), c_name), destdir)
  257. finally:
  258. os.chdir(ori_dir)
  259. return dstfile
  260. extension_mapping = {
  261. '.c': (CCompilerRunner, None),
  262. '.cpp': (CppCompilerRunner, None),
  263. '.cxx': (CppCompilerRunner, None),
  264. '.f': (FortranCompilerRunner, None),
  265. '.for': (FortranCompilerRunner, None),
  266. '.ftn': (FortranCompilerRunner, None),
  267. '.f90': (FortranCompilerRunner, None), # ifort only knows about .f90
  268. '.f95': (FortranCompilerRunner, 'f95'),
  269. '.f03': (FortranCompilerRunner, 'f2003'),
  270. '.f08': (FortranCompilerRunner, 'f2008'),
  271. }
  272. def src2obj(srcpath, Runner=None, objpath=None, cwd=None, inc_py=False, **kwargs):
  273. """ Compiles a source code file to an object file.
  274. Files ending with '.pyx' assumed to be cython files and
  275. are dispatched to pyx2obj.
  276. Parameters
  277. ==========
  278. srcpath: str
  279. Path to source file.
  280. Runner: CompilerRunner subclass (optional)
  281. If ``None``: deduced from extension of srcpath.
  282. objpath : str (optional)
  283. Path to generated object. If ``None``: deduced from ``srcpath``.
  284. cwd: str (optional)
  285. Working directory and root of relative paths. If ``None``: current dir.
  286. inc_py: bool
  287. Add Python include path to kwarg "include_dirs". Default: False
  288. \\*\\*kwargs: dict
  289. keyword arguments passed to Runner or pyx2obj
  290. """
  291. name, ext = os.path.splitext(os.path.basename(srcpath))
  292. if objpath is None:
  293. if os.path.isabs(srcpath):
  294. objpath = '.'
  295. else:
  296. objpath = os.path.dirname(srcpath)
  297. objpath = objpath or '.' # avoid objpath == ''
  298. if os.path.isdir(objpath):
  299. objpath = os.path.join(objpath, name + objext)
  300. include_dirs = kwargs.pop('include_dirs', [])
  301. if inc_py:
  302. py_inc_dir = get_path('include')
  303. if py_inc_dir not in include_dirs:
  304. include_dirs.append(py_inc_dir)
  305. if ext.lower() == '.pyx':
  306. return pyx2obj(srcpath, objpath=objpath, include_dirs=include_dirs, cwd=cwd,
  307. **kwargs)
  308. if Runner is None:
  309. Runner, std = extension_mapping[ext.lower()]
  310. if 'std' not in kwargs:
  311. kwargs['std'] = std
  312. flags = kwargs.pop('flags', [])
  313. needed_flags = ('-fPIC',)
  314. for flag in needed_flags:
  315. if flag not in flags:
  316. flags.append(flag)
  317. # src2obj implies not running the linker...
  318. run_linker = kwargs.pop('run_linker', False)
  319. if run_linker:
  320. raise CompileError("src2obj called with run_linker=True")
  321. runner = Runner([srcpath], objpath, include_dirs=include_dirs,
  322. run_linker=run_linker, cwd=cwd, flags=flags, **kwargs)
  323. runner.run()
  324. return objpath
  325. def pyx2obj(pyxpath, objpath=None, destdir=None, cwd=None,
  326. include_dirs=None, cy_kwargs=None, cplus=None, **kwargs):
  327. """
  328. Convenience function
  329. If cwd is specified, pyxpath and dst are taken to be relative
  330. If only_update is set to `True` the modification time is checked
  331. and compilation is only run if the source is newer than the
  332. destination
  333. Parameters
  334. ==========
  335. pyxpath: str
  336. Path to Cython source file.
  337. objpath: str (optional)
  338. Path to object file to generate.
  339. destdir: str (optional)
  340. Directory to put generated C file. When ``None``: directory of ``objpath``.
  341. cwd: str (optional)
  342. Working directory and root of relative paths.
  343. include_dirs: iterable of path strings (optional)
  344. Passed onto src2obj and via cy_kwargs['include_path']
  345. to simple_cythonize.
  346. cy_kwargs: dict (optional)
  347. Keyword arguments passed onto `simple_cythonize`
  348. cplus: bool (optional)
  349. Indicate whether C++ is used. default: auto-detect using ``.util.pyx_is_cplus``.
  350. compile_kwargs: dict
  351. keyword arguments passed onto src2obj
  352. Returns
  353. =======
  354. Absolute path of generated object file.
  355. """
  356. assert pyxpath.endswith('.pyx')
  357. cwd = cwd or '.'
  358. objpath = objpath or '.'
  359. destdir = destdir or os.path.dirname(objpath)
  360. abs_objpath = get_abspath(objpath, cwd=cwd)
  361. if os.path.isdir(abs_objpath):
  362. pyx_fname = os.path.basename(pyxpath)
  363. name, ext = os.path.splitext(pyx_fname)
  364. objpath = os.path.join(objpath, name + objext)
  365. cy_kwargs = cy_kwargs or {}
  366. cy_kwargs['output_dir'] = cwd
  367. if cplus is None:
  368. cplus = pyx_is_cplus(pyxpath)
  369. cy_kwargs['cplus'] = cplus
  370. interm_c_file = simple_cythonize(pyxpath, destdir=destdir, cwd=cwd, **cy_kwargs)
  371. include_dirs = include_dirs or []
  372. flags = kwargs.pop('flags', [])
  373. needed_flags = ('-fwrapv', '-pthread', '-fPIC')
  374. for flag in needed_flags:
  375. if flag not in flags:
  376. flags.append(flag)
  377. options = kwargs.pop('options', [])
  378. if kwargs.pop('strict_aliasing', False):
  379. raise CompileError("Cython requires strict aliasing to be disabled.")
  380. # Let's be explicit about standard
  381. if cplus:
  382. std = kwargs.pop('std', 'c++98')
  383. else:
  384. std = kwargs.pop('std', 'c99')
  385. return src2obj(interm_c_file, objpath=objpath, cwd=cwd,
  386. include_dirs=include_dirs, flags=flags, std=std,
  387. options=options, inc_py=True, strict_aliasing=False,
  388. **kwargs)
  389. def _any_X(srcs, cls):
  390. for src in srcs:
  391. name, ext = os.path.splitext(src)
  392. key = ext.lower()
  393. if key in extension_mapping:
  394. if extension_mapping[key][0] == cls:
  395. return True
  396. return False
  397. def any_fortran_src(srcs):
  398. return _any_X(srcs, FortranCompilerRunner)
  399. def any_cplus_src(srcs):
  400. return _any_X(srcs, CppCompilerRunner)
  401. def compile_link_import_py_ext(sources, extname=None, build_dir='.', compile_kwargs=None,
  402. link_kwargs=None):
  403. """ Compiles sources to a shared object (Python extension) and imports it
  404. Sources in ``sources`` which is imported. If shared object is newer than the sources, they
  405. are not recompiled but instead it is imported.
  406. Parameters
  407. ==========
  408. sources : string
  409. List of paths to sources.
  410. extname : string
  411. Name of extension (default: ``None``).
  412. If ``None``: taken from the last file in ``sources`` without extension.
  413. build_dir: str
  414. Path to directory in which objects files etc. are generated.
  415. compile_kwargs: dict
  416. keyword arguments passed to ``compile_sources``
  417. link_kwargs: dict
  418. keyword arguments passed to ``link_py_so``
  419. Returns
  420. =======
  421. The imported module from of the Python extension.
  422. """
  423. if extname is None:
  424. extname = os.path.splitext(os.path.basename(sources[-1]))[0]
  425. compile_kwargs = compile_kwargs or {}
  426. link_kwargs = link_kwargs or {}
  427. try:
  428. mod = import_module_from_file(os.path.join(build_dir, extname), sources)
  429. except ImportError:
  430. objs = compile_sources(list(map(get_abspath, sources)), destdir=build_dir,
  431. cwd=build_dir, **compile_kwargs)
  432. so = link_py_so(objs, cwd=build_dir, fort=any_fortran_src(sources),
  433. cplus=any_cplus_src(sources), **link_kwargs)
  434. mod = import_module_from_file(so)
  435. return mod
  436. def _write_sources_to_build_dir(sources, build_dir):
  437. build_dir = build_dir or tempfile.mkdtemp()
  438. if not os.path.isdir(build_dir):
  439. raise OSError("Non-existent directory: ", build_dir)
  440. source_files = []
  441. for name, src in sources:
  442. dest = os.path.join(build_dir, name)
  443. differs = True
  444. sha256_in_mem = sha256_of_string(src.encode('utf-8')).hexdigest()
  445. if os.path.exists(dest):
  446. if os.path.exists(dest + '.sha256'):
  447. sha256_on_disk = open(dest + '.sha256').read()
  448. else:
  449. sha256_on_disk = sha256_of_file(dest).hexdigest()
  450. differs = sha256_on_disk != sha256_in_mem
  451. if differs:
  452. with open(dest, 'wt') as fh:
  453. fh.write(src)
  454. open(dest + '.sha256', 'wt').write(sha256_in_mem)
  455. source_files.append(dest)
  456. return source_files, build_dir
  457. def compile_link_import_strings(sources, build_dir=None, **kwargs):
  458. """ Compiles, links and imports extension module from source.
  459. Parameters
  460. ==========
  461. sources : iterable of name/source pair tuples
  462. build_dir : string (default: None)
  463. Path. ``None`` implies use a temporary directory.
  464. **kwargs:
  465. Keyword arguments passed onto `compile_link_import_py_ext`.
  466. Returns
  467. =======
  468. mod : module
  469. The compiled and imported extension module.
  470. info : dict
  471. Containing ``build_dir`` as 'build_dir'.
  472. """
  473. source_files, build_dir = _write_sources_to_build_dir(sources, build_dir)
  474. mod = compile_link_import_py_ext(source_files, build_dir=build_dir, **kwargs)
  475. info = dict(build_dir=build_dir)
  476. return mod, info
  477. def compile_run_strings(sources, build_dir=None, clean=False, compile_kwargs=None, link_kwargs=None):
  478. """ Compiles, links and runs a program built from sources.
  479. Parameters
  480. ==========
  481. sources : iterable of name/source pair tuples
  482. build_dir : string (default: None)
  483. Path. ``None`` implies use a temporary directory.
  484. clean : bool
  485. Whether to remove build_dir after use. This will only have an
  486. effect if ``build_dir`` is ``None`` (which creates a temporary directory).
  487. Passing ``clean == True`` and ``build_dir != None`` raises a ``ValueError``.
  488. This will also set ``build_dir`` in returned info dictionary to ``None``.
  489. compile_kwargs: dict
  490. Keyword arguments passed onto ``compile_sources``
  491. link_kwargs: dict
  492. Keyword arguments passed onto ``link``
  493. Returns
  494. =======
  495. (stdout, stderr): pair of strings
  496. info: dict
  497. Containing exit status as 'exit_status' and ``build_dir`` as 'build_dir'
  498. """
  499. if clean and build_dir is not None:
  500. raise ValueError("Automatic removal of build_dir is only available for temporary directory.")
  501. try:
  502. source_files, build_dir = _write_sources_to_build_dir(sources, build_dir)
  503. objs = compile_sources(list(map(get_abspath, source_files)), destdir=build_dir,
  504. cwd=build_dir, **(compile_kwargs or {}))
  505. prog = link(objs, cwd=build_dir,
  506. fort=any_fortran_src(source_files),
  507. cplus=any_cplus_src(source_files), **(link_kwargs or {}))
  508. p = subprocess.Popen([prog], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  509. exit_status = p.wait()
  510. stdout, stderr = [txt.decode('utf-8') for txt in p.communicate()]
  511. finally:
  512. if clean and os.path.isdir(build_dir):
  513. shutil.rmtree(build_dir)
  514. build_dir = None
  515. info = dict(exit_status=exit_status, build_dir=build_dir)
  516. return (stdout, stderr), info