setup_helpers.py
Go to the documentation of this file.
1 """
2 This module provides helpers for C++11+ projects using pybind11.
3 
4 LICENSE:
5 
6 Copyright (c) 2016 Wenzel Jakob <wenzel.jakob@epfl.ch>, All rights reserved.
7 
8 Redistribution and use in source and binary forms, with or without
9 modification, are permitted provided that the following conditions are met:
10 
11 1. Redistributions of source code must retain the above copyright notice, this
12  list of conditions and the following disclaimer.
13 
14 2. Redistributions in binary form must reproduce the above copyright notice,
15  this list of conditions and the following disclaimer in the documentation
16  and/or other materials provided with the distribution.
17 
18 3. Neither the name of the copyright holder nor the names of its contributors
19  may be used to endorse or promote products derived from this software
20  without specific prior written permission.
21 
22 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
23 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
24 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
25 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
26 FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
27 DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
28 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
29 CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
30 OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
31 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32 """
33 
34 # IMPORTANT: If you change this file in the pybind11 repo, also review
35 # setup_helpers.pyi for matching changes.
36 #
37 # If you copy this file in, you don't
38 # need the .pyi file; it's just an interface file for static type checkers.
39 
40 import contextlib
41 import os
42 import platform
43 import shlex
44 import shutil
45 import sys
46 import sysconfig
47 import tempfile
48 import threading
49 import warnings
50 from functools import lru_cache
51 from pathlib import Path
52 from typing import (
53  Any,
54  Callable,
55  Dict,
56  Iterable,
57  Iterator,
58  List,
59  Optional,
60  Tuple,
61  TypeVar,
62  Union,
63 )
64 
65 try:
66  from setuptools import Extension as _Extension
67  from setuptools.command.build_ext import build_ext as _build_ext
68 except ImportError:
69  from distutils.command.build_ext import build_ext as _build_ext
70  from distutils.extension import Extension as _Extension
71 
72 import distutils.ccompiler
73 import distutils.errors
74 
75 WIN = sys.platform.startswith("win32") and "mingw" not in sysconfig.get_platform()
76 MACOS = sys.platform.startswith("darwin")
77 STD_TMPL = "/std:c++{}" if WIN else "-std=c++{}"
78 
79 
80 # It is recommended to use PEP 518 builds if using this module. However, this
81 # file explicitly supports being copied into a user's project directory
82 # standalone, and pulling pybind11 with the deprecated setup_requires feature.
83 # If you copy the file, remember to add it to your MANIFEST.in, and add the current
84 # directory into your path if it sits beside your setup.py.
85 
86 
87 class Pybind11Extension(_Extension): # type: ignore[misc]
88  """
89  Build a C++11+ Extension module with pybind11. This automatically adds the
90  recommended flags when you init the extension and assumes C++ sources - you
91  can further modify the options yourself.
92 
93  The customizations are:
94 
95  * ``/EHsc`` and ``/bigobj`` on Windows
96  * ``stdlib=libc++`` on macOS
97  * ``visibility=hidden`` and ``-g0`` on Unix
98 
99  Finally, you can set ``cxx_std`` via constructor or afterwards to enable
100  flags for C++ std, and a few extra helper flags related to the C++ standard
101  level. It is _highly_ recommended you either set this, or use the provided
102  ``build_ext``, which will search for the highest supported extension for
103  you if the ``cxx_std`` property is not set. Do not set the ``cxx_std``
104  property more than once, as flags are added when you set it. Set the
105  property to None to disable the addition of C++ standard flags.
106 
107  If you want to add pybind11 headers manually, for example for an exact
108  git checkout, then set ``include_pybind11=False``.
109  """
110 
111  # flags are prepended, so that they can be further overridden, e.g. by
112  # ``extra_compile_args=["-g"]``.
113 
114  def _add_cflags(self, flags: List[str]) -> None:
115  self.extra_compile_args[:0] = flags
116 
117  def _add_ldflags(self, flags: List[str]) -> None:
118  self.extra_link_args[:0] = flags
119 
120  def __init__(self, *args: Any, **kwargs: Any) -> None:
121 
122  self._cxx_level = 0
123  cxx_std = kwargs.pop("cxx_std", 0)
124 
125  if "language" not in kwargs:
126  kwargs["language"] = "c++"
127 
128  include_pybind11 = kwargs.pop("include_pybind11", True)
129 
130  super().__init__(*args, **kwargs)
131 
132  # Include the installed package pybind11 headers
133  if include_pybind11:
134  # If using setup_requires, this fails the first time - that's okay
135  try:
136  import pybind11
137 
138  pyinc = pybind11.get_include()
139 
140  if pyinc not in self.include_dirs:
141  self.include_dirs.append(pyinc)
142  except ModuleNotFoundError:
143  pass
144 
145  self.cxx_std = cxx_std
146 
147  cflags = []
148  ldflags = []
149  if WIN:
150  cflags += ["/EHsc", "/bigobj"]
151  else:
152  cflags += ["-fvisibility=hidden"]
153  env_cflags = os.environ.get("CFLAGS", "")
154  env_cppflags = os.environ.get("CPPFLAGS", "")
155  c_cpp_flags = shlex.split(env_cflags) + shlex.split(env_cppflags)
156  if not any(opt.startswith("-g") for opt in c_cpp_flags):
157  cflags += ["-g0"]
158  if MACOS:
159  cflags += ["-stdlib=libc++"]
160  ldflags += ["-stdlib=libc++"]
161  self._add_cflags(cflags)
162  self._add_ldflags(ldflags)
163 
164  @property
165  def cxx_std(self) -> int:
166  """
167  The CXX standard level. If set, will add the required flags. If left at
168  0, it will trigger an automatic search when pybind11's build_ext is
169  used. If None, will have no effect. Besides just the flags, this may
170  add a macos-min 10.9 or 10.14 flag if MACOSX_DEPLOYMENT_TARGET is
171  unset.
172  """
173  return self._cxx_level
174 
175  @cxx_std.setter
176  def cxx_std(self, level: int) -> None:
177 
178  if self._cxx_level:
179  warnings.warn("You cannot safely change the cxx_level after setting it!")
180 
181  # MSVC 2015 Update 3 and later only have 14 (and later 17) modes, so
182  # force a valid flag here.
183  if WIN and level == 11:
184  level = 14
185 
186  self._cxx_level = level
187 
188  if not level:
189  return
190 
191  cflags = [STD_TMPL.format(level)]
192  ldflags = []
193 
194  if MACOS and "MACOSX_DEPLOYMENT_TARGET" not in os.environ:
195  # C++17 requires a higher min version of macOS. An earlier version
196  # (10.12 or 10.13) can be set manually via environment variable if
197  # you are careful in your feature usage, but 10.14 is the safest
198  # setting for general use. However, never set higher than the
199  # current macOS version!
200  current_macos = tuple(int(x) for x in platform.mac_ver()[0].split(".")[:2])
201  desired_macos = (10, 9) if level < 17 else (10, 14)
202  macos_string = ".".join(str(x) for x in min(current_macos, desired_macos))
203  macosx_min = f"-mmacosx-version-min={macos_string}"
204  cflags += [macosx_min]
205  ldflags += [macosx_min]
206 
207  self._add_cflags(cflags)
208  self._add_ldflags(ldflags)
209 
210 
211 # Just in case someone clever tries to multithread
212 tmp_chdir_lock = threading.Lock()
213 
214 
215 @contextlib.contextmanager
216 def tmp_chdir() -> Iterator[str]:
217  "Prepare and enter a temporary directory, cleanup when done"
218 
219  # Threadsafe
220  with tmp_chdir_lock:
221  olddir = os.getcwd()
222  try:
223  tmpdir = tempfile.mkdtemp()
224  os.chdir(tmpdir)
225  yield tmpdir
226  finally:
227  os.chdir(olddir)
228  shutil.rmtree(tmpdir)
229 
230 
231 # cf http://bugs.python.org/issue26689
232 def has_flag(compiler: Any, flag: str) -> bool:
233  """
234  Return the flag if a flag name is supported on the
235  specified compiler, otherwise None (can be used as a boolean).
236  If multiple flags are passed, return the first that matches.
237  """
238 
239  with tmp_chdir():
240  fname = Path("flagcheck.cpp")
241  # Don't trigger -Wunused-parameter.
242  fname.write_text("int main (int, char **) { return 0; }", encoding="utf-8")
243 
244  try:
245  compiler.compile([str(fname)], extra_postargs=[flag])
246  except distutils.errors.CompileError:
247  return False
248  return True
249 
250 
251 # Every call will cache the result
252 cpp_flag_cache = None
253 
254 
255 @lru_cache()
256 def auto_cpp_level(compiler: Any) -> Union[str, int]:
257  """
258  Return the max supported C++ std level (17, 14, or 11). Returns latest on Windows.
259  """
260 
261  if WIN:
262  return "latest"
263 
264  levels = [17, 14, 11]
265 
266  for level in levels:
267  if has_flag(compiler, STD_TMPL.format(level)):
268  return level
269 
270  msg = "Unsupported compiler -- at least C++11 support is needed!"
271  raise RuntimeError(msg)
272 
273 
274 class build_ext(_build_ext): # type: ignore[misc] # noqa: N801
275  """
276  Customized build_ext that allows an auto-search for the highest supported
277  C++ level for Pybind11Extension. This is only needed for the auto-search
278  for now, and is completely optional otherwise.
279  """
280 
281  def build_extensions(self) -> None:
282  """
283  Build extensions, injecting C++ std for Pybind11Extension if needed.
284  """
285 
286  for ext in self.extensions:
287  if hasattr(ext, "_cxx_level") and ext._cxx_level == 0:
288  ext.cxx_std = auto_cpp_level(self.compiler)
289 
290  super().build_extensions()
291 
292 
294  paths: Iterable[str], package_dir: Optional[Dict[str, str]] = None
295 ) -> List[Pybind11Extension]:
296  """
297  Generate Pybind11Extensions from source files directly located in a Python
298  source tree.
299 
300  ``package_dir`` behaves as in ``setuptools.setup``. If unset, the Python
301  package root parent is determined as the first parent directory that does
302  not contain an ``__init__.py`` file.
303  """
304  exts = []
305 
306  if package_dir is None:
307  for path in paths:
308  parent, _ = os.path.split(path)
309  while os.path.exists(os.path.join(parent, "__init__.py")):
310  parent, _ = os.path.split(parent)
311  relname, _ = os.path.splitext(os.path.relpath(path, parent))
312  qualified_name = relname.replace(os.path.sep, ".")
313  exts.append(Pybind11Extension(qualified_name, [path]))
314  return exts
315 
316  for path in paths:
317  for prefix, parent in package_dir.items():
318  if path.startswith(parent):
319  relname, _ = os.path.splitext(os.path.relpath(path, parent))
320  qualified_name = relname.replace(os.path.sep, ".")
321  if prefix:
322  qualified_name = prefix + "." + qualified_name
323  exts.append(Pybind11Extension(qualified_name, [path]))
324  break
325  else:
326  msg = (
327  f"path {path} is not a child of any of the directories listed "
328  f"in 'package_dir' ({package_dir})"
329  )
330  raise ValueError(msg)
331 
332  return exts
333 
334 
335 def naive_recompile(obj: str, src: str) -> bool:
336  """
337  This will recompile only if the source file changes. It does not check
338  header files, so a more advanced function or Ccache is better if you have
339  editable header files in your package.
340  """
341  return os.stat(obj).st_mtime < os.stat(src).st_mtime
342 
343 
344 def no_recompile(obg: str, src: str) -> bool: # pylint: disable=unused-argument
345  """
346  This is the safest but slowest choice (and is the default) - will always
347  recompile sources.
348  """
349  return True
350 
351 
352 S = TypeVar("S", bound="ParallelCompile")
353 
354 CCompilerMethod = Callable[
355  [
356  distutils.ccompiler.CCompiler,
357  List[str],
358  Optional[str],
359  Optional[Union[Tuple[str], Tuple[str, Optional[str]]]],
360  Optional[List[str]],
361  bool,
362  Optional[List[str]],
363  Optional[List[str]],
364  Optional[List[str]],
365  ],
366  List[str],
367 ]
368 
369 
370 # Optional parallel compile utility
371 # inspired by: http://stackoverflow.com/questions/11013851/speeding-up-build-process-with-distutils
372 # and: https://github.com/tbenthompson/cppimport/blob/stable/cppimport/build_module.py
373 # and NumPy's parallel distutils module:
374 # https://github.com/numpy/numpy/blob/master/numpy/distutils/ccompiler.py
376  """
377  Make a parallel compile function. Inspired by
378  numpy.distutils.ccompiler.CCompiler.compile and cppimport.
379 
380  This takes several arguments that allow you to customize the compile
381  function created:
382 
383  envvar:
384  Set an environment variable to control the compilation threads, like
385  NPY_NUM_BUILD_JOBS
386  default:
387  0 will automatically multithread, or 1 will only multithread if the
388  envvar is set.
389  max:
390  The limit for automatic multithreading if non-zero
391  needs_recompile:
392  A function of (obj, src) that returns True when recompile is needed. No
393  effect in isolated mode; use ccache instead, see
394  https://github.com/matplotlib/matplotlib/issues/1507/
395 
396  To use::
397 
398  ParallelCompile("NPY_NUM_BUILD_JOBS").install()
399 
400  or::
401 
402  with ParallelCompile("NPY_NUM_BUILD_JOBS"):
403  setup(...)
404 
405  By default, this assumes all files need to be recompiled. A smarter
406  function can be provided via needs_recompile. If the output has not yet
407  been generated, the compile will always run, and this function is not
408  called.
409  """
410 
411  __slots__ = ("envvar", "default", "max", "_old", "needs_recompile")
412 
413  def __init__(
414  self,
415  envvar: Optional[str] = None,
416  default: int = 0,
417  max: int = 0, # pylint: disable=redefined-builtin
418  needs_recompile: Callable[[str, str], bool] = no_recompile,
419  ) -> None:
420  self.envvar = envvar
421  self.default = default
422  self.max = max
423  self.needs_recompile = needs_recompile
424  self._old: List[CCompilerMethod] = []
425 
426  def function(self) -> CCompilerMethod:
427  """
428  Builds a function object usable as distutils.ccompiler.CCompiler.compile.
429  """
430 
431  def compile_function(
432  compiler: distutils.ccompiler.CCompiler,
433  sources: List[str],
434  output_dir: Optional[str] = None,
435  macros: Optional[Union[Tuple[str], Tuple[str, Optional[str]]]] = None,
436  include_dirs: Optional[List[str]] = None,
437  debug: bool = False,
438  extra_preargs: Optional[List[str]] = None,
439  extra_postargs: Optional[List[str]] = None,
440  depends: Optional[List[str]] = None,
441  ) -> Any:
442 
443  # These lines are directly from distutils.ccompiler.CCompiler
444  macros, objects, extra_postargs, pp_opts, build = compiler._setup_compile( # type: ignore[attr-defined]
445  output_dir, macros, include_dirs, sources, depends, extra_postargs
446  )
447  cc_args = compiler._get_cc_args(pp_opts, debug, extra_preargs) # type: ignore[attr-defined]
448 
449  # The number of threads; start with default.
450  threads = self.default
451 
452  # Determine the number of compilation threads, unless set by an environment variable.
453  if self.envvar is not None:
454  threads = int(os.environ.get(self.envvar, self.default))
455 
456  def _single_compile(obj: Any) -> None:
457  try:
458  src, ext = build[obj]
459  except KeyError:
460  return
461 
462  if not os.path.exists(obj) or self.needs_recompile(obj, src):
463  compiler._compile(obj, src, ext, cc_args, extra_postargs, pp_opts) # type: ignore[attr-defined]
464 
465  try:
466  # Importing .synchronize checks for platforms that have some multiprocessing
467  # capabilities but lack semaphores, such as AWS Lambda and Android Termux.
468  import multiprocessing.synchronize
469  from multiprocessing.pool import ThreadPool
470  except ImportError:
471  threads = 1
472 
473  if threads == 0:
474  try:
475  threads = multiprocessing.cpu_count()
476  threads = self.max if self.max and self.max < threads else threads
477  except NotImplementedError:
478  threads = 1
479 
480  if threads > 1:
481  with ThreadPool(threads) as pool:
482  for _ in pool.imap_unordered(_single_compile, objects):
483  pass
484  else:
485  for ob in objects:
486  _single_compile(ob)
487 
488  return objects
489 
490  return compile_function
491 
492  def install(self: S) -> S:
493  """
494  Installs the compile function into distutils.ccompiler.CCompiler.compile.
495  """
496  distutils.ccompiler.CCompiler.compile = self.function() # type: ignore[assignment]
497  return self
498 
499  def __enter__(self: S) -> S:
500  self._old.append(distutils.ccompiler.CCompiler.compile)
501  return self.install()
502 
503  def __exit__(self, *args: Any) -> None:
504  distutils.ccompiler.CCompiler.compile = self._old.pop() # type: ignore[assignment]
ThreadPoolTempl< StlThreadEnvironment > ThreadPool
bool hasattr(handle obj, handle name)
Definition: pytypes.h:728
#define min(a, b)
Definition: datatypes.h:19
void split(const G &g, const PredecessorMap< KEY > &tree, G &Ab1, G &Ab2)
Definition: graph-inl.h:245
Definition: pytypes.h:1403


gtsam
Author(s):
autogenerated on Tue Jul 4 2023 02:35:42