Source code for pycompilation.compilation

# -*- coding: utf-8 -*-

"""
Motivation
==========

Distutils does not allow to use object files in compilation
(see http://bugs.python.org/issue5372)
hence the compilation of source files cannot be cached
unless doing something like what compile_sources / src2obj do.

Distutils does not support fortran out of the box (motivation of
numpy distutils), furthermore:
linking mixed C++/Fortran use either Fortran (Intel) or
C++ (GNU) compiler.
"""

from __future__ import (
    print_function, division, absolute_import, unicode_literals
)

import glob
import os
import re
import shutil
import sys
import tempfile

from .util import (
    MetaReaderWriter, missing_or_other_newer, get_abspath,
    expand_collection_in_dict, make_dirs, copy, Glob, ArbitraryDepthGlob,
    glob_at_depth, CompilationError, FileNotFoundError,
    import_module_from_file, pyx_is_cplus,
    md5_of_string, md5_of_file
)

from .runners import (
    CCompilerRunner,
    CppCompilerRunner,
    FortranCompilerRunner
)

from distutils.sysconfig import get_config_var, get_config_vars

sharedext = get_config_var('SO')

if os.name == 'posix':  # Future improvement to make cross-platform
    # flagprefix = '-'
    objext = '.o'
elif os.name == 'nt':
    # flagprefix = '/' <-- let's assume mingw compilers...
    objext = '.obj'
else:
    raise ImportError("Unknown os.name: {}".format(os.name))


[docs]def get_mixed_fort_c_linker(vendor=None, metadir=None, cplus=False, cwd=None): vendor = vendor or os.environ.get('COMPILER_VENDOR', None) if not vendor: metadir = get_abspath(metadir or '.', cwd=cwd) reader = MetaReaderWriter('.metadata_CompilerRunner') try: vendor = reader.get_from_metadata_file(metadir, 'vendor') except FileNotFoundError: vendor = None if vendor.lower() == 'intel': if cplus: return (FortranCompilerRunner, {'flags': ['-nofor_main', '-cxxlib']}, vendor) else: return (FortranCompilerRunner, {'flags': ['-nofor_main']}, vendor) elif vendor.lower() == 'gnu' or 'llvm': if cplus: return (CppCompilerRunner, {'lib_options': ['fortran']}, vendor) else: return (FortranCompilerRunner, {}, vendor) else: raise ValueError("No vendor found.")
[docs]def compile_sources(files, CompilerRunner_=None, destdir=None, cwd=None, keep_dir_struct=False, per_file_kwargs=None, **kwargs): """ Compile source code files to object files. Parameters ---------- files: iterable of path strings source files, if cwd is given, the paths are taken as relative. CompilerRunner_: CompilerRunner instance (optional) could be e.g. pycompilation.FortranCompilerRunner Will be inferred from filename extensions if missing. destdir: path string output directory, if cwd is given, the path is taken as relative cwd: path string working directory. Specify to have compiler run in other directory. also used as root of relative paths. keep_dir_struct: bool Reproduce directory structure in `destdir`. default: False per_file_kwargs: dict dict mapping instances in `files` to keyword arguments **kwargs: dict default keyword arguments to pass to CompilerRunner_ """ _per_file_kwargs = {} if per_file_kwargs is not None: for k, v in per_file_kwargs.items(): if isinstance(k, Glob): for path in glob.glob(k.pathname): _per_file_kwargs[path] = v elif isinstance(k, ArbitraryDepthGlob): for path in glob_at_depth(k.filename, cwd): _per_file_kwargs[path] = v else: _per_file_kwargs[k] = v # Set up destination directory destdir = destdir or '.' if not os.path.isdir(destdir): if os.path.exists(destdir): raise IOError("{} is not a directory".format(destdir)) else: make_dirs(destdir) if cwd is None: cwd = '.' for f in files: copy(f, destdir, only_update=True, dest_is_dir=True) # Compile files and return list of paths to the objects dstpaths = [] for f in files: if keep_dir_struct: name, ext = os.path.splitext(f) else: name, ext = os.path.splitext(os.path.basename(f)) file_kwargs = kwargs.copy() file_kwargs.update(_per_file_kwargs.get(f, {})) dstpaths.append(src2obj( f, CompilerRunner_, cwd=cwd, **file_kwargs )) return dstpaths
[docs]def simple_cythonize(src, destdir=None, cwd=None, logger=None, full_module_name=None, only_update=False, **cy_kwargs): """ Generates a C file from a Cython source file. Parameters ---------- src: path string path to Cython source destdir: path string (optional) Path to output directory (default: '.') cwd: path string (optional) Root of relative paths (default: '.') logger: logging.Logger info level used. full_module_name: string passed to cy_compile (default: None) only_update: bool Only cythonize if source is newer. default: False **cy_kwargs: second argument passed to cy_compile. Generates a .cpp file is cplus=True in cy_kwargs, else a .c file. """ from Cython.Compiler.Main import ( default_options, CompilationOptions ) from Cython.Compiler.Main import compile as cy_compile assert src.lower().endswith('.pyx') or src.lower().endswith('.py') cwd = cwd or '.' destdir = destdir or '.' ext = '.cpp' if cy_kwargs.get('cplus', False) else '.c' c_name = os.path.splitext(os.path.basename(src))[0] + ext dstfile = os.path.join(destdir, c_name) if only_update: if not missing_or_other_newer(dstfile, src, cwd=cwd): msg = '{0} newer than {1}, did not re-cythonize.'.format( dstfile, src) if logger: logger.info(msg) else: print(msg) return dstfile if cwd: ori_dir = os.getcwd() else: ori_dir = '.' os.chdir(cwd) cy_options = CompilationOptions(default_options) cy_options.__dict__.update(cy_kwargs) if logger: logger.info("Cythonizing {0} to {1}".format( src, dstfile)) cy_compile([src], cy_options, full_module_name=full_module_name) if os.path.abspath(os.path.dirname( src)) != os.path.abspath(destdir): if os.path.exists(dstfile): os.unlink(dstfile) shutil.move(os.path.join(os.path.dirname(src), c_name), destdir) os.chdir(ori_dir) return dstfile
extension_mapping = { '.c': (CCompilerRunner, None), '.cpp': (CppCompilerRunner, None), '.cxx': (CppCompilerRunner, None), '.f': (FortranCompilerRunner, None), '.for': (FortranCompilerRunner, None), '.ftn': (FortranCompilerRunner, None), '.f90': (FortranCompilerRunner, 'f2008'), # ifort only knows about .f90 '.f95': (FortranCompilerRunner, 'f95'), '.f03': (FortranCompilerRunner, 'f2003'), '.f08': (FortranCompilerRunner, 'f2008'), }
[docs]def src2obj(srcpath, CompilerRunner_=None, objpath=None, only_update=False, cwd=None, out_ext=None, inc_py=False, **kwargs): """ Compiles a source code file to an object file. Files ending with '.pyx' assumed to be cython files and are dispatched to pyx2obj. Parameters ---------- srcpath: path string path to source file CompilerRunner_: pycompilation.CompilerRunner subclass (optional) Default: deduced from extension of srcpath objpath: path string (optional) path to generated object. defualt: deduced from srcpath only_update: bool only compile if source is newer than objpath. default: False cwd: path string (optional) working directory and root of relative paths. default: current dir. out_ext: string set when objpath is a dir and you want to override defaults ('.o'/'.obj' for Unix/Windows). inc_py: bool add Python include path to include_dirs. default: False **kwargs: dict keyword arguments passed onto CompilerRunner_ or pyx2obj """ name, ext = os.path.splitext(os.path.basename(srcpath)) if objpath is None: if os.path.isabs(srcpath): objpath = '.' else: objpath = os.path.dirname(srcpath) objpath = objpath or '.' # avoid objpath == '' out_ext = out_ext or objext if os.path.isdir(objpath): objpath = os.path.join(objpath, name+out_ext) include_dirs = kwargs.pop('include_dirs', []) if inc_py: from distutils.sysconfig import get_python_inc py_inc_dir = get_python_inc() if py_inc_dir not in include_dirs: include_dirs.append(py_inc_dir) if ext.lower() == '.pyx': return pyx2obj(srcpath, objpath=objpath, include_dirs=include_dirs, cwd=cwd, only_update=only_update, **kwargs) if CompilerRunner_ is None: CompilerRunner_, std = extension_mapping[ext.lower()] if 'std' not in kwargs: kwargs['std'] = std # src2obj implies not running the linker... run_linker = kwargs.pop('run_linker', False) if run_linker: raise CompilationError("src2obj called with run_linker=True") if only_update: if not missing_or_other_newer(objpath, srcpath, cwd=cwd): msg = "Found {0}, did not recompile.".format(objpath) if kwargs.get('logger', None): kwargs['logger'].info(msg) else: print(msg) return objpath runner = CompilerRunner_( [srcpath], objpath, include_dirs=include_dirs, run_linker=run_linker, cwd=cwd, **kwargs) runner.run() return objpath
[docs]def pyx2obj(pyxpath, objpath=None, interm_c_dir=None, cwd=None, logger=None, full_module_name=None, only_update=False, metadir=None, include_numpy=False, include_dirs=None, cy_kwargs=None, gdb=False, cplus=None, **kwargs): """ Convenience function If cwd is specified, pyxpath and dst are taken to be relative If only_update is set to `True` the modification time is checked and compilation is only run if the source is newer than the destination Parameters ---------- pyxpath: path string path to Cython source file objpath: path string (optional) path to object file to generate interm_c_dir: path string (optional) directory to put generated C file. cwd: path string (optional) working directory and root of relative paths logger: logging.Logger (optional) passed onto `simple_cythonize` and `src2obj` full_module_name: string (optional) passed onto `simple_cythonize` only_update: bool (optional) passed onto `simple_cythonize` and `src2obj` metadir: path string (optional) passed onto src2obj include_numpy: bool (optional) Add numpy include directory to include_dirs. default: False include_dirs: iterable of path strings (optional) Passed onto src2obj and via cy_kwargs['include_path'] to simple_cythonize. cy_kwargs: dict (optional) keyword arguments passed onto `simple_cythonize` gdb: bool (optional) convenience: cy_kwargs['gdb_debug'] is set True if gdb=True, default: False cplus: bool (optional) Indicate whether C++ is used. default: auto-detect using `pyx_is_cplus` **kwargs: dict keyword arguments passed onto src2obj Returns ------- Absolute path of generated object file. """ assert pyxpath.endswith('.pyx') cwd = cwd or '.' objpath = objpath or '.' interm_c_dir = interm_c_dir or os.path.dirname(objpath) abs_objpath = get_abspath(objpath, cwd=cwd) abs_pyxpath = get_abspath(pyxpath, cwd=cwd) if os.path.isdir(abs_objpath): pyx_fname = os.path.basename(abs_pyxpath) name, ext = os.path.splitext(pyx_fname) objpath = os.path.join(objpath, name+objext) cy_kwargs = cy_kwargs or {} cy_kwargs['output_dir'] = cwd if cplus is None: cplus = pyx_is_cplus(abs_pyxpath) cy_kwargs['cplus'] = cplus if gdb: cy_kwargs['gdb_debug'] = True if include_dirs: cy_kwargs['include_path'] = include_dirs interm_c_file = simple_cythonize( abs_pyxpath, destdir=interm_c_dir, cwd=cwd, logger=logger, full_module_name=full_module_name, only_update=only_update, **cy_kwargs) include_dirs = include_dirs or [] if include_numpy: import numpy numpy_inc_dir = numpy.get_include() if numpy_inc_dir not in include_dirs: include_dirs.append(numpy_inc_dir) flags = kwargs.pop('flags', []) needed_flags = ('-fwrapv', '-pthread') if not cplus: needed_flags += ('-Wstrict-prototypes',) # not really needed.. for flag in needed_flags: if flag not in flags: flags.append(flag) options = kwargs.pop('options', []) if kwargs.pop('strict_aliasing', False): raise CompilationError("Cython req. strict aliasing to be disabled.") if 'pic' not in options: options.append('pic') if 'warn' not in options: options.append('warn') # Let's be explicit about standard if cplus: std = kwargs.pop('std', 'c++98') else: std = kwargs.pop('std', 'c99') return src2obj( interm_c_file, objpath=objpath, cwd=cwd, only_update=only_update, metadir=metadir, include_dirs=include_dirs, flags=flags, std=std, options=options, logger=logger, inc_py=True, strict_aliasing=False, **kwargs)
def _any_X(srcs, cls): for src in srcs: name, ext = os.path.splitext(src) key = ext.lower() if key in extension_mapping: if extension_mapping[key][0] == cls: return True return False
[docs]def any_fort(srcs): return _any_X(srcs, FortranCompilerRunner)
[docs]def any_cplus(srcs): return _any_X(srcs, CppCompilerRunner)