modeling_simple / .venv /lib /python3.14 /site-packages /sympy /utilities /_compilation /compilation.py
| import glob | |
| import os | |
| import shutil | |
| import subprocess | |
| import sys | |
| import tempfile | |
| import warnings | |
| from pathlib import Path | |
| from sysconfig import get_config_var, get_config_vars, get_path | |
| from .runners import ( | |
| CCompilerRunner, | |
| CppCompilerRunner, | |
| FortranCompilerRunner | |
| ) | |
| from .util import ( | |
| get_abspath, make_dirs, copy, Glob, ArbitraryDepthGlob, | |
| glob_at_depth, import_module_from_file, pyx_is_cplus, | |
| sha256_of_string, sha256_of_file, CompileError | |
| ) | |
| if os.name == 'posix': | |
| objext = '.o' | |
| elif os.name == 'nt': | |
| objext = '.obj' | |
| else: | |
| warnings.warn("Unknown os.name: {}".format(os.name)) | |
| objext = '.o' | |
| def compile_sources(files, Runner=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 str | |
| Paths to source files, if ``cwd`` is given, the paths are taken as relative. | |
| Runner: CompilerRunner subclass (optional) | |
| Could be e.g. ``FortranCompilerRunner``. Will be inferred from filename | |
| extensions if missing. | |
| destdir: str | |
| Output directory, if cwd is given, the path is taken as relative. | |
| cwd: str | |
| 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 ``Runner``. | |
| Returns | |
| ======= | |
| List of strings (paths of object files). | |
| """ | |
| _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 OSError("{} 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, Runner, cwd=cwd, **file_kwargs)) | |
| return dstpaths | |
| def get_mixed_fort_c_linker(vendor=None, cplus=False, cwd=None): | |
| vendor = vendor or os.environ.get('SYMPY_COMPILER_VENDOR', 'gnu') | |
| 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.") | |
| def link(obj_files, out_file=None, shared=False, Runner=None, | |
| cwd=None, cplus=False, fort=False, extra_objs=None, **kwargs): | |
| """ Link object files. | |
| Parameters | |
| ========== | |
| obj_files: iterable of str | |
| Paths to object files. | |
| out_file: str (optional) | |
| Path to executable/shared library, if ``None`` it will be | |
| deduced from the last item in obj_files. | |
| shared: bool | |
| Generate a shared library? | |
| Runner: CompilerRunner subclass (optional) | |
| If not given the ``cplus`` and ``fort`` flags will be inspected | |
| (fallback is the C compiler). | |
| cwd: str | |
| Path to the root of relative paths and working directory for compiler. | |
| cplus: bool | |
| C++ objects? default: ``False``. | |
| fort: bool | |
| Fortran objects? default: ``False``. | |
| extra_objs: list | |
| List of paths to extra object files / static libraries. | |
| \\*\\*kwargs: dict | |
| Keyword arguments passed to ``Runner``. | |
| Returns | |
| ======= | |
| The absolute path to the generated shared object / executable. | |
| """ | |
| if out_file is None: | |
| out_file, ext = os.path.splitext(os.path.basename(obj_files[-1])) | |
| if shared: | |
| out_file += get_config_var('EXT_SUFFIX') | |
| if not Runner: | |
| if fort: | |
| Runner, extra_kwargs, vendor = \ | |
| get_mixed_fort_c_linker( | |
| vendor=kwargs.get('vendor', None), | |
| cplus=cplus, | |
| cwd=cwd, | |
| ) | |
| for k, v in extra_kwargs.items(): | |
| if k in kwargs: | |
| kwargs[k].expand(v) | |
| else: | |
| kwargs[k] = v | |
| else: | |
| if cplus: | |
| Runner = CppCompilerRunner | |
| else: | |
| Runner = CCompilerRunner | |
| flags = kwargs.pop('flags', []) | |
| if shared: | |
| if '-shared' not in flags: | |
| flags.append('-shared') | |
| run_linker = kwargs.pop('run_linker', True) | |
| if not run_linker: | |
| raise ValueError("run_linker was set to False (nonsensical).") | |
| out_file = get_abspath(out_file, cwd=cwd) | |
| runner = Runner(obj_files+(extra_objs or []), out_file, flags, cwd=cwd, **kwargs) | |
| runner.run() | |
| return out_file | |
| def link_py_so(obj_files, so_file=None, cwd=None, libraries=None, | |
| cplus=False, fort=False, extra_objs=None, **kwargs): | |
| """ Link Python extension module (shared object) for importing | |
| Parameters | |
| ========== | |
| obj_files: iterable of str | |
| Paths to object files to be linked. | |
| so_file: str | |
| Name (path) of shared object file to create. If not specified it will | |
| have the basname of the last object file in `obj_files` but with the | |
| extension '.so' (Unix). | |
| cwd: path string | |
| Root of relative paths and working directory of linker. | |
| libraries: iterable of strings | |
| Libraries to link against, e.g. ['m']. | |
| cplus: bool | |
| Any C++ objects? default: ``False``. | |
| fort: bool | |
| Any Fortran objects? default: ``False``. | |
| extra_objs: list | |
| List of paths of extra object files / static libraries to link against. | |
| kwargs**: dict | |
| Keyword arguments passed to ``link(...)``. | |
| Returns | |
| ======= | |
| Absolute path to the generate shared object. | |
| """ | |
| libraries = libraries or [] | |
| include_dirs = kwargs.pop('include_dirs', []) | |
| library_dirs = kwargs.pop('library_dirs', []) | |
| # Add Python include and library directories | |
| # PY_LDFLAGS does not available on all python implementations | |
| # e.g. when with pypy, so it's LDFLAGS we need to use | |
| if sys.platform == "win32": | |
| warnings.warn("Windows not yet supported.") | |
| elif sys.platform == 'darwin': | |
| cfgDict = get_config_vars() | |
| kwargs['linkline'] = kwargs.get('linkline', []) + [cfgDict['LDFLAGS']] | |
| library_dirs += [cfgDict['LIBDIR']] | |
| # In macOS, linker needs to compile frameworks | |
| # e.g. "-framework CoreFoundation" | |
| is_framework = False | |
| for opt in cfgDict['LIBS'].split(): | |
| if is_framework: | |
| kwargs['linkline'] = kwargs.get('linkline', []) + ['-framework', opt] | |
| is_framework = False | |
| elif opt.startswith('-l'): | |
| libraries.append(opt[2:]) | |
| elif opt.startswith('-framework'): | |
| is_framework = True | |
| # The python library is not included in LIBS | |
| libfile = cfgDict['LIBRARY'] | |
| libname = ".".join(libfile.split('.')[:-1])[3:] | |
| libraries.append(libname) | |
| elif sys.platform[:3] == 'aix': | |
| # Don't use the default code below | |
| pass | |
| else: | |
| if get_config_var('Py_ENABLE_SHARED'): | |
| cfgDict = get_config_vars() | |
| kwargs['linkline'] = kwargs.get('linkline', []) + [cfgDict['LDFLAGS']] | |
| library_dirs += [cfgDict['LIBDIR']] | |
| for opt in cfgDict['BLDLIBRARY'].split(): | |
| if opt.startswith('-l'): | |
| libraries += [opt[2:]] | |
| else: | |
| pass | |
| flags = kwargs.pop('flags', []) | |
| needed_flags = ('-pthread',) | |
| for flag in needed_flags: | |
| if flag not in flags: | |
| flags.append(flag) | |
| return link(obj_files, shared=True, flags=flags, cwd=cwd, cplus=cplus, fort=fort, | |
| include_dirs=include_dirs, libraries=libraries, | |
| library_dirs=library_dirs, extra_objs=extra_objs, **kwargs) | |
| def simple_cythonize(src, destdir=None, cwd=None, **cy_kwargs): | |
| """ Generates a C file from a Cython source file. | |
| Parameters | |
| ========== | |
| src: str | |
| Path to Cython source. | |
| destdir: str (optional) | |
| Path to output directory (default: '.'). | |
| cwd: path string (optional) | |
| Root of relative paths (default: '.'). | |
| **cy_kwargs: | |
| Second argument passed to cy_compile. Generates a .cpp file if ``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 cwd: | |
| ori_dir = os.getcwd() | |
| else: | |
| ori_dir = '.' | |
| os.chdir(cwd) | |
| try: | |
| cy_options = CompilationOptions(default_options) | |
| cy_options.__dict__.update(cy_kwargs) | |
| # Set language_level if not set by cy_kwargs | |
| # as not setting it is deprecated | |
| if 'language_level' not in cy_kwargs: | |
| cy_options.__dict__['language_level'] = 3 | |
| cy_result = cy_compile([src], cy_options) | |
| if cy_result.num_errors > 0: | |
| raise ValueError("Cython compilation failed.") | |
| # Move generated C file to destination | |
| # In macOS, the generated C file is in the same directory as the source | |
| # but the /var is a symlink to /private/var, so we need to use realpath | |
| if os.path.realpath(os.path.dirname(src)) != os.path.realpath(destdir): | |
| if os.path.exists(dstfile): | |
| os.unlink(dstfile) | |
| shutil.move(os.path.join(os.path.dirname(src), c_name), destdir) | |
| finally: | |
| 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, None), # ifort only knows about .f90 | |
| '.f95': (FortranCompilerRunner, 'f95'), | |
| '.f03': (FortranCompilerRunner, 'f2003'), | |
| '.f08': (FortranCompilerRunner, 'f2008'), | |
| } | |
| def src2obj(srcpath, Runner=None, objpath=None, cwd=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: str | |
| Path to source file. | |
| Runner: CompilerRunner subclass (optional) | |
| If ``None``: deduced from extension of srcpath. | |
| objpath : str (optional) | |
| Path to generated object. If ``None``: deduced from ``srcpath``. | |
| cwd: str (optional) | |
| Working directory and root of relative paths. If ``None``: current dir. | |
| inc_py: bool | |
| Add Python include path to kwarg "include_dirs". Default: False | |
| \\*\\*kwargs: dict | |
| keyword arguments passed to Runner 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 == '' | |
| if os.path.isdir(objpath): | |
| objpath = os.path.join(objpath, name + objext) | |
| include_dirs = kwargs.pop('include_dirs', []) | |
| if inc_py: | |
| py_inc_dir = get_path('include') | |
| 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, | |
| **kwargs) | |
| if Runner is None: | |
| Runner, std = extension_mapping[ext.lower()] | |
| if 'std' not in kwargs: | |
| kwargs['std'] = std | |
| flags = kwargs.pop('flags', []) | |
| needed_flags = ('-fPIC',) | |
| for flag in needed_flags: | |
| if flag not in flags: | |
| flags.append(flag) | |
| # src2obj implies not running the linker... | |
| run_linker = kwargs.pop('run_linker', False) | |
| if run_linker: | |
| raise CompileError("src2obj called with run_linker=True") | |
| runner = Runner([srcpath], objpath, include_dirs=include_dirs, | |
| run_linker=run_linker, cwd=cwd, flags=flags, **kwargs) | |
| runner.run() | |
| return objpath | |
| def pyx2obj(pyxpath, objpath=None, destdir=None, cwd=None, | |
| include_dirs=None, cy_kwargs=None, 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: str | |
| Path to Cython source file. | |
| objpath: str (optional) | |
| Path to object file to generate. | |
| destdir: str (optional) | |
| Directory to put generated C file. When ``None``: directory of ``objpath``. | |
| cwd: str (optional) | |
| Working directory and root of relative paths. | |
| 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` | |
| cplus: bool (optional) | |
| Indicate whether C++ is used. default: auto-detect using ``.util.pyx_is_cplus``. | |
| compile_kwargs: dict | |
| keyword arguments passed onto src2obj | |
| Returns | |
| ======= | |
| Absolute path of generated object file. | |
| """ | |
| assert pyxpath.endswith('.pyx') | |
| cwd = cwd or '.' | |
| objpath = objpath or '.' | |
| destdir = destdir or os.path.dirname(objpath) | |
| abs_objpath = get_abspath(objpath, cwd=cwd) | |
| if os.path.isdir(abs_objpath): | |
| pyx_fname = os.path.basename(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(pyxpath) | |
| cy_kwargs['cplus'] = cplus | |
| interm_c_file = simple_cythonize(pyxpath, destdir=destdir, cwd=cwd, **cy_kwargs) | |
| include_dirs = include_dirs or [] | |
| flags = kwargs.pop('flags', []) | |
| needed_flags = ('-fwrapv', '-pthread', '-fPIC') | |
| for flag in needed_flags: | |
| if flag not in flags: | |
| flags.append(flag) | |
| options = kwargs.pop('options', []) | |
| if kwargs.pop('strict_aliasing', False): | |
| raise CompileError("Cython requires strict aliasing to be disabled.") | |
| # 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, | |
| include_dirs=include_dirs, flags=flags, std=std, | |
| options=options, 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 | |
| def any_fortran_src(srcs): | |
| return _any_X(srcs, FortranCompilerRunner) | |
| def any_cplus_src(srcs): | |
| return _any_X(srcs, CppCompilerRunner) | |
| def compile_link_import_py_ext(sources, extname=None, build_dir='.', compile_kwargs=None, | |
| link_kwargs=None, extra_objs=None): | |
| """ Compiles sources to a shared object (Python extension) and imports it | |
| Sources in ``sources`` which is imported. If shared object is newer than the sources, they | |
| are not recompiled but instead it is imported. | |
| Parameters | |
| ========== | |
| sources : list of strings | |
| List of paths to sources. | |
| extname : string | |
| Name of extension (default: ``None``). | |
| If ``None``: taken from the last file in ``sources`` without extension. | |
| build_dir: str | |
| Path to directory in which objects files etc. are generated. | |
| compile_kwargs: dict | |
| keyword arguments passed to ``compile_sources`` | |
| link_kwargs: dict | |
| keyword arguments passed to ``link_py_so`` | |
| extra_objs: list | |
| List of paths to (prebuilt) object files / static libraries to link against. | |
| Returns | |
| ======= | |
| The imported module from of the Python extension. | |
| """ | |
| if extname is None: | |
| extname = os.path.splitext(os.path.basename(sources[-1]))[0] | |
| compile_kwargs = compile_kwargs or {} | |
| link_kwargs = link_kwargs or {} | |
| try: | |
| mod = import_module_from_file(os.path.join(build_dir, extname), sources) | |
| except ImportError: | |
| objs = compile_sources(list(map(get_abspath, sources)), destdir=build_dir, | |
| cwd=build_dir, **compile_kwargs) | |
| so = link_py_so(objs, cwd=build_dir, fort=any_fortran_src(sources), | |
| cplus=any_cplus_src(sources), extra_objs=extra_objs, **link_kwargs) | |
| mod = import_module_from_file(so) | |
| return mod | |
| def _write_sources_to_build_dir(sources, build_dir): | |
| build_dir = build_dir or tempfile.mkdtemp() | |
| if not os.path.isdir(build_dir): | |
| raise OSError("Non-existent directory: ", build_dir) | |
| source_files = [] | |
| for name, src in sources: | |
| dest = os.path.join(build_dir, name) | |
| differs = True | |
| sha256_in_mem = sha256_of_string(src.encode('utf-8')).hexdigest() | |
| if os.path.exists(dest): | |
| if os.path.exists(dest + '.sha256'): | |
| sha256_on_disk = Path(dest + '.sha256').read_text() | |
| else: | |
| sha256_on_disk = sha256_of_file(dest).hexdigest() | |
| differs = sha256_on_disk != sha256_in_mem | |
| if differs: | |
| with open(dest, 'wt') as fh: | |
| fh.write(src) | |
| with open(dest + '.sha256', 'wt') as fh: | |
| fh.write(sha256_in_mem) | |
| source_files.append(dest) | |
| return source_files, build_dir | |
| def compile_link_import_strings(sources, build_dir=None, **kwargs): | |
| """ Compiles, links and imports extension module from source. | |
| Parameters | |
| ========== | |
| sources : iterable of name/source pair tuples | |
| build_dir : string (default: None) | |
| Path. ``None`` implies use a temporary directory. | |
| **kwargs: | |
| Keyword arguments passed onto `compile_link_import_py_ext`. | |
| Returns | |
| ======= | |
| mod : module | |
| The compiled and imported extension module. | |
| info : dict | |
| Containing ``build_dir`` as 'build_dir'. | |
| """ | |
| source_files, build_dir = _write_sources_to_build_dir(sources, build_dir) | |
| mod = compile_link_import_py_ext(source_files, build_dir=build_dir, **kwargs) | |
| info = {"build_dir": build_dir} | |
| return mod, info | |
| def compile_run_strings(sources, build_dir=None, clean=False, compile_kwargs=None, link_kwargs=None): | |
| """ Compiles, links and runs a program built from sources. | |
| Parameters | |
| ========== | |
| sources : iterable of name/source pair tuples | |
| build_dir : string (default: None) | |
| Path. ``None`` implies use a temporary directory. | |
| clean : bool | |
| Whether to remove build_dir after use. This will only have an | |
| effect if ``build_dir`` is ``None`` (which creates a temporary directory). | |
| Passing ``clean == True`` and ``build_dir != None`` raises a ``ValueError``. | |
| This will also set ``build_dir`` in returned info dictionary to ``None``. | |
| compile_kwargs: dict | |
| Keyword arguments passed onto ``compile_sources`` | |
| link_kwargs: dict | |
| Keyword arguments passed onto ``link`` | |
| Returns | |
| ======= | |
| (stdout, stderr): pair of strings | |
| info: dict | |
| Containing exit status as 'exit_status' and ``build_dir`` as 'build_dir' | |
| """ | |
| if clean and build_dir is not None: | |
| raise ValueError("Automatic removal of build_dir is only available for temporary directory.") | |
| try: | |
| source_files, build_dir = _write_sources_to_build_dir(sources, build_dir) | |
| objs = compile_sources(list(map(get_abspath, source_files)), destdir=build_dir, | |
| cwd=build_dir, **(compile_kwargs or {})) | |
| prog = link(objs, cwd=build_dir, | |
| fort=any_fortran_src(source_files), | |
| cplus=any_cplus_src(source_files), **(link_kwargs or {})) | |
| p = subprocess.Popen([prog], stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
| exit_status = p.wait() | |
| stdout, stderr = [txt.decode('utf-8') for txt in p.communicate()] | |
| finally: | |
| if clean and os.path.isdir(build_dir): | |
| shutil.rmtree(build_dir) | |
| build_dir = None | |
| info = {"exit_status": exit_status, "build_dir": build_dir} | |
| return (stdout, stderr), info | |