| """ |
| This module provides helpers for C++11+ projects using pybind11. |
| |
| LICENSE: |
| |
| Copyright (c) 2016 Wenzel Jakob <wenzel.jakob@epfl.ch>, All rights reserved. |
| |
| Redistribution and use in source and binary forms, with or without |
| modification, are permitted provided that the following conditions are met: |
| |
| 1. Redistributions of source code must retain the above copyright notice, this |
| list of conditions and the following disclaimer. |
| |
| 2. Redistributions in binary form must reproduce the above copyright notice, |
| this list of conditions and the following disclaimer in the documentation |
| and/or other materials provided with the distribution. |
| |
| 3. Neither the name of the copyright holder nor the names of its contributors |
| may be used to endorse or promote products derived from this software |
| without specific prior written permission. |
| |
| THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND |
| ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE |
| FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL |
| DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
| SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER |
| CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, |
| OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| """ |
|
|
| |
| |
| |
| |
| |
| from __future__ import annotations |
|
|
| import contextlib |
| import os |
| import platform |
| import shlex |
| import shutil |
| import sys |
| import sysconfig |
| import tempfile |
| import threading |
| import warnings |
| from functools import lru_cache |
| from pathlib import Path |
| from typing import ( |
| Any, |
| Callable, |
| Iterable, |
| Iterator, |
| List, |
| Optional, |
| Tuple, |
| TypeVar, |
| Union, |
| ) |
|
|
| try: |
| from setuptools import Extension as _Extension |
| from setuptools.command.build_ext import build_ext as _build_ext |
| except ImportError: |
| from distutils.command.build_ext import ( |
| build_ext as _build_ext, |
| ) |
| from distutils.extension import Extension as _Extension |
|
|
| import distutils.ccompiler |
| import distutils.errors |
|
|
| WIN = sys.platform.startswith("win32") and "mingw" not in sysconfig.get_platform() |
| MACOS = sys.platform.startswith("darwin") |
| STD_TMPL = "/std:c++{}" if WIN else "-std=c++{}" |
|
|
|
|
| |
| |
| |
| |
| |
|
|
|
|
| class Pybind11Extension(_Extension): |
| """ |
| Build a C++11+ Extension module with pybind11. This automatically adds the |
| recommended flags when you init the extension and assumes C++ sources - you |
| can further modify the options yourself. |
| |
| The customizations are: |
| |
| * ``/EHsc`` and ``/bigobj`` on Windows |
| * ``stdlib=libc++`` on macOS |
| * ``visibility=hidden`` and ``-g0`` on Unix |
| |
| Finally, you can set ``cxx_std`` via constructor or afterwards to enable |
| flags for C++ std, and a few extra helper flags related to the C++ standard |
| level. It is _highly_ recommended you either set this, or use the provided |
| ``build_ext``, which will search for the highest supported extension for |
| you if the ``cxx_std`` property is not set. Do not set the ``cxx_std`` |
| property more than once, as flags are added when you set it. Set the |
| property to None to disable the addition of C++ standard flags. |
| |
| If you want to add pybind11 headers manually, for example for an exact |
| git checkout, then set ``include_pybind11=False``. |
| """ |
|
|
| |
| |
|
|
| def _add_cflags(self, flags: list[str]) -> None: |
| self.extra_compile_args[:0] = flags |
|
|
| def _add_ldflags(self, flags: list[str]) -> None: |
| self.extra_link_args[:0] = flags |
|
|
| def __init__(self, *args: Any, **kwargs: Any) -> None: |
| self._cxx_level = 0 |
| cxx_std = kwargs.pop("cxx_std", 0) |
|
|
| if "language" not in kwargs: |
| kwargs["language"] = "c++" |
|
|
| include_pybind11 = kwargs.pop("include_pybind11", True) |
|
|
| super().__init__(*args, **kwargs) |
|
|
| |
| if include_pybind11: |
| |
| try: |
| import pybind11 |
|
|
| pyinc = pybind11.get_include() |
|
|
| if pyinc not in self.include_dirs: |
| self.include_dirs.append(pyinc) |
| except ModuleNotFoundError: |
| pass |
|
|
| self.cxx_std = cxx_std |
|
|
| cflags = [] |
| if WIN: |
| cflags += ["/EHsc", "/bigobj"] |
| else: |
| cflags += ["-fvisibility=hidden"] |
| env_cflags = os.environ.get("CFLAGS", "") |
| env_cppflags = os.environ.get("CPPFLAGS", "") |
| c_cpp_flags = shlex.split(env_cflags) + shlex.split(env_cppflags) |
| if not any(opt.startswith("-g") for opt in c_cpp_flags): |
| cflags += ["-g0"] |
| self._add_cflags(cflags) |
|
|
| @property |
| def cxx_std(self) -> int: |
| """ |
| The CXX standard level. If set, will add the required flags. If left at |
| 0, it will trigger an automatic search when pybind11's build_ext is |
| used. If None, will have no effect. Besides just the flags, this may |
| add a macos-min 10.9 or 10.14 flag if MACOSX_DEPLOYMENT_TARGET is |
| unset. |
| """ |
| return self._cxx_level |
|
|
| @cxx_std.setter |
| def cxx_std(self, level: int) -> None: |
| if self._cxx_level: |
| warnings.warn( |
| "You cannot safely change the cxx_level after setting it!", stacklevel=2 |
| ) |
|
|
| |
| |
| if WIN and level == 11: |
| level = 14 |
|
|
| self._cxx_level = level |
|
|
| if not level: |
| return |
|
|
| cflags = [STD_TMPL.format(level)] |
| ldflags = [] |
|
|
| if MACOS and "MACOSX_DEPLOYMENT_TARGET" not in os.environ: |
| |
| |
| |
| |
| |
| current_macos = tuple(int(x) for x in platform.mac_ver()[0].split(".")[:2]) |
| desired_macos = (10, 9) if level < 17 else (10, 14) |
| macos_string = ".".join(str(x) for x in min(current_macos, desired_macos)) |
| macosx_min = f"-mmacosx-version-min={macos_string}" |
| cflags += [macosx_min] |
| ldflags += [macosx_min] |
|
|
| self._add_cflags(cflags) |
| self._add_ldflags(ldflags) |
|
|
|
|
| |
| tmp_chdir_lock = threading.Lock() |
|
|
|
|
| @contextlib.contextmanager |
| def tmp_chdir() -> Iterator[str]: |
| "Prepare and enter a temporary directory, cleanup when done" |
|
|
| |
| with tmp_chdir_lock: |
| olddir = os.getcwd() |
| try: |
| tmpdir = tempfile.mkdtemp() |
| os.chdir(tmpdir) |
| yield tmpdir |
| finally: |
| os.chdir(olddir) |
| shutil.rmtree(tmpdir) |
|
|
|
|
| |
| def has_flag(compiler: Any, flag: str) -> bool: |
| """ |
| Return the flag if a flag name is supported on the |
| specified compiler, otherwise None (can be used as a boolean). |
| If multiple flags are passed, return the first that matches. |
| """ |
|
|
| with tmp_chdir(): |
| fname = Path("flagcheck.cpp") |
| |
| fname.write_text("int main (int, char **) { return 0; }", encoding="utf-8") |
|
|
| try: |
| compiler.compile([str(fname)], extra_postargs=[flag]) |
| except distutils.errors.CompileError: |
| return False |
| return True |
|
|
|
|
| |
| cpp_flag_cache = None |
|
|
|
|
| @lru_cache() |
| def auto_cpp_level(compiler: Any) -> str | int: |
| """ |
| Return the max supported C++ std level (17, 14, or 11). Returns latest on Windows. |
| """ |
|
|
| if WIN: |
| return "latest" |
|
|
| levels = [17, 14, 11] |
|
|
| for level in levels: |
| if has_flag(compiler, STD_TMPL.format(level)): |
| return level |
|
|
| msg = "Unsupported compiler -- at least C++11 support is needed!" |
| raise RuntimeError(msg) |
|
|
|
|
| class build_ext(_build_ext): |
| """ |
| Customized build_ext that allows an auto-search for the highest supported |
| C++ level for Pybind11Extension. This is only needed for the auto-search |
| for now, and is completely optional otherwise. |
| """ |
|
|
| def build_extensions(self) -> None: |
| """ |
| Build extensions, injecting C++ std for Pybind11Extension if needed. |
| """ |
|
|
| for ext in self.extensions: |
| if hasattr(ext, "_cxx_level") and ext._cxx_level == 0: |
| ext.cxx_std = auto_cpp_level(self.compiler) |
|
|
| super().build_extensions() |
|
|
|
|
| def intree_extensions( |
| paths: Iterable[str], package_dir: dict[str, str] | None = None |
| ) -> list[Pybind11Extension]: |
| """ |
| Generate Pybind11Extensions from source files directly located in a Python |
| source tree. |
| |
| ``package_dir`` behaves as in ``setuptools.setup``. If unset, the Python |
| package root parent is determined as the first parent directory that does |
| not contain an ``__init__.py`` file. |
| """ |
| exts = [] |
|
|
| if package_dir is None: |
| for path in paths: |
| parent, _ = os.path.split(path) |
| while os.path.exists(os.path.join(parent, "__init__.py")): |
| parent, _ = os.path.split(parent) |
| relname, _ = os.path.splitext(os.path.relpath(path, parent)) |
| qualified_name = relname.replace(os.path.sep, ".") |
| exts.append(Pybind11Extension(qualified_name, [path])) |
| return exts |
|
|
| for path in paths: |
| for prefix, parent in package_dir.items(): |
| if path.startswith(parent): |
| relname, _ = os.path.splitext(os.path.relpath(path, parent)) |
| qualified_name = relname.replace(os.path.sep, ".") |
| if prefix: |
| qualified_name = prefix + "." + qualified_name |
| exts.append(Pybind11Extension(qualified_name, [path])) |
| break |
| else: |
| msg = ( |
| f"path {path} is not a child of any of the directories listed " |
| f"in 'package_dir' ({package_dir})" |
| ) |
| raise ValueError(msg) |
|
|
| return exts |
|
|
|
|
| def naive_recompile(obj: str, src: str) -> bool: |
| """ |
| This will recompile only if the source file changes. It does not check |
| header files, so a more advanced function or Ccache is better if you have |
| editable header files in your package. |
| """ |
| return os.stat(obj).st_mtime < os.stat(src).st_mtime |
|
|
|
|
| def no_recompile(obg: str, src: str) -> bool: |
| """ |
| This is the safest but slowest choice (and is the default) - will always |
| recompile sources. |
| """ |
| return True |
|
|
|
|
| S = TypeVar("S", bound="ParallelCompile") |
|
|
| CCompilerMethod = Callable[ |
| [ |
| distutils.ccompiler.CCompiler, |
| List[str], |
| Optional[str], |
| Optional[List[Union[Tuple[str], Tuple[str, Optional[str]]]]], |
| Optional[List[str]], |
| bool, |
| Optional[List[str]], |
| Optional[List[str]], |
| Optional[List[str]], |
| ], |
| List[str], |
| ] |
|
|
|
|
| |
| |
| |
| |
| |
| class ParallelCompile: |
| """ |
| Make a parallel compile function. Inspired by |
| numpy.distutils.ccompiler.CCompiler.compile and cppimport. |
| |
| This takes several arguments that allow you to customize the compile |
| function created: |
| |
| envvar: |
| Set an environment variable to control the compilation threads, like |
| NPY_NUM_BUILD_JOBS |
| default: |
| 0 will automatically multithread, or 1 will only multithread if the |
| envvar is set. |
| max: |
| The limit for automatic multithreading if non-zero |
| needs_recompile: |
| A function of (obj, src) that returns True when recompile is needed. No |
| effect in isolated mode; use ccache instead, see |
| https://github.com/matplotlib/matplotlib/issues/1507/ |
| |
| To use:: |
| |
| ParallelCompile("NPY_NUM_BUILD_JOBS").install() |
| |
| or:: |
| |
| with ParallelCompile("NPY_NUM_BUILD_JOBS"): |
| setup(...) |
| |
| By default, this assumes all files need to be recompiled. A smarter |
| function can be provided via needs_recompile. If the output has not yet |
| been generated, the compile will always run, and this function is not |
| called. |
| """ |
|
|
| __slots__ = ("envvar", "default", "max", "_old", "needs_recompile") |
|
|
| def __init__( |
| self, |
| envvar: str | None = None, |
| default: int = 0, |
| max: int = 0, |
| needs_recompile: Callable[[str, str], bool] = no_recompile, |
| ) -> None: |
| self.envvar = envvar |
| self.default = default |
| self.max = max |
| self.needs_recompile = needs_recompile |
| self._old: list[CCompilerMethod] = [] |
|
|
| def function(self) -> CCompilerMethod: |
| """ |
| Builds a function object usable as distutils.ccompiler.CCompiler.compile. |
| """ |
|
|
| def compile_function( |
| compiler: distutils.ccompiler.CCompiler, |
| sources: list[str], |
| output_dir: str | None = None, |
| macros: list[tuple[str] | tuple[str, str | None]] | None = None, |
| include_dirs: list[str] | None = None, |
| debug: bool = False, |
| extra_preargs: list[str] | None = None, |
| extra_postargs: list[str] | None = None, |
| depends: list[str] | None = None, |
| ) -> Any: |
| |
| macros, objects, extra_postargs, pp_opts, build = compiler._setup_compile( |
| output_dir, macros, include_dirs, sources, depends, extra_postargs |
| ) |
| cc_args = compiler._get_cc_args(pp_opts, debug, extra_preargs) |
|
|
| |
| threads = self.default |
|
|
| |
| if self.envvar is not None: |
| threads = int(os.environ.get(self.envvar, self.default)) |
|
|
| def _single_compile(obj: Any) -> None: |
| try: |
| src, ext = build[obj] |
| except KeyError: |
| return |
|
|
| if not os.path.exists(obj) or self.needs_recompile(obj, src): |
| compiler._compile(obj, src, ext, cc_args, extra_postargs, pp_opts) |
|
|
| try: |
| |
| |
| import multiprocessing.synchronize |
| from multiprocessing.pool import ThreadPool |
| except ImportError: |
| threads = 1 |
|
|
| if threads == 0: |
| try: |
| threads = multiprocessing.cpu_count() |
| threads = self.max if self.max and self.max < threads else threads |
| except NotImplementedError: |
| threads = 1 |
|
|
| if threads > 1: |
| with ThreadPool(threads) as pool: |
| for _ in pool.imap_unordered(_single_compile, objects): |
| pass |
| else: |
| for ob in objects: |
| _single_compile(ob) |
|
|
| return objects |
|
|
| return compile_function |
|
|
| def install(self: S) -> S: |
| """ |
| Installs the compile function into distutils.ccompiler.CCompiler.compile. |
| """ |
| distutils.ccompiler.CCompiler.compile = self.function() |
| return self |
|
|
| def __enter__(self: S) -> S: |
| self._old.append(distutils.ccompiler.CCompiler.compile) |
| return self.install() |
|
|
| def __exit__(self, *args: Any) -> None: |
| distutils.ccompiler.CCompiler.compile = self._old.pop() |
|
|