μHAL (v2.8.17)
Part of the IPbus software repository
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Modules Pages
setup_helpers.py
Go to the documentation of this file.
1"""
2This module provides helpers for C++11+ projects using pybind11.
3
4LICENSE:
5
6Copyright (c) 2016 Wenzel Jakob <wenzel.jakob@epfl.ch>, All rights reserved.
7
8Redistribution and use in source and binary forms, with or without
9modification, are permitted provided that the following conditions are met:
10
111. Redistributions of source code must retain the above copyright notice, this
12 list of conditions and the following disclaimer.
13
142. 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
183. 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
22THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
23ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
24WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
25DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
26FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
27DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
28SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
29CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
30OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
31OF 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
40import contextlib
41import os
42import platform
43import shlex
44import shutil
45import sys
46import sysconfig
47import tempfile
48import threading
49import warnings
50from functools import lru_cache
51from pathlib import Path
52from typing import (
53 Any,
54 Callable,
55 Dict,
56 Iterable,
57 Iterator,
58 List,
59 Optional,
60 Tuple,
61 TypeVar,
62 Union,
63)
64
65try:
66 from setuptools import Extension as _Extension
67 from setuptools.command.build_ext import build_ext as _build_ext
68except ImportError:
69 from distutils.command.build_ext import build_ext as _build_ext
70 from distutils.extension import Extension as _Extension
71
72import distutils.ccompiler
73import distutils.errors
74
75WIN = sys.platform.startswith("win32") and "mingw" not in sysconfig.get_platform()
76MACOS = sys.platform.startswith("darwin")
77STD_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
87class 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 self._cxx_level = 0
122 cxx_std = kwargs.pop("cxx_std", 0)
123
124 if "language" not in kwargs:
125 kwargs["language"] = "c++"
126
127 include_pybind11 = kwargs.pop("include_pybind11", True)
128
129 super().__init__(*args, **kwargs)
130
131 # Include the installed package pybind11 headers
132 if include_pybind11:
133 # If using setup_requires, this fails the first time - that's okay
134 try:
135 import pybind11
136
137 pyinc = pybind11.get_include()
138
139 if pyinc not in self.include_dirs:
140 self.include_dirs.append(pyinc)
141 except ModuleNotFoundError:
142 pass
143
145
146 cflags = []
147 ldflags = []
148 if WIN:
149 cflags += ["/EHsc", "/bigobj"]
150 else:
151 cflags += ["-fvisibility=hidden"]
152 env_cflags = os.environ.get("CFLAGS", "")
153 env_cppflags = os.environ.get("CPPFLAGS", "")
154 c_cpp_flags = shlex.split(env_cflags) + shlex.split(env_cppflags)
155 if not any(opt.startswith("-g") for opt in c_cpp_flags):
156 cflags += ["-g0"]
157 if MACOS:
158 cflags += ["-stdlib=libc++"]
159 ldflags += ["-stdlib=libc++"]
160 self._add_cflags_add_cflags(cflags)
161 self._add_ldflags_add_ldflags(ldflags)
162
163 @property
164 def cxx_std(self) -> int:
165 """
166 The CXX standard level. If set, will add the required flags. If left at
167 0, it will trigger an automatic search when pybind11's build_ext is
168 used. If None, will have no effect. Besides just the flags, this may
169 add a macos-min 10.9 or 10.14 flag if MACOSX_DEPLOYMENT_TARGET is
170 unset.
171 """
172 return self._cxx_level
173
174 @cxx_std.setter
175 def cxx_std(self, level: int) -> None:
176 if self._cxx_level:
177 warnings.warn(
178 "You cannot safely change the cxx_level after setting it!", stacklevel=2
179 )
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_add_cflags(cflags)
208 self._add_ldflags_add_ldflags(ldflags)
209
210
211# Just in case someone clever tries to multithread
212tmp_chdir_lock = threading.Lock()
213
214
215@contextlib.contextmanager
216def 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
232def 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
252cpp_flag_cache = None
253
254
255@lru_cache()
256def 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
274class 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
335def 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
344def 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
352S = TypeVar("S", bound="ParallelCompile")
353
354CCompilerMethod = 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
375class ParallelCompile:
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
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 # These lines are directly from distutils.ccompiler.CCompiler
443 macros, objects, extra_postargs, pp_opts, build = compiler._setup_compile( # type: ignore[attr-defined]
444 output_dir, macros, include_dirs, sources, depends, extra_postargs
445 )
446 cc_args = compiler._get_cc_args(pp_opts, debug, extra_preargs) # type: ignore[attr-defined]
447
448 # The number of threads; start with default.
449 threads = self.default
450
451 # Determine the number of compilation threads, unless set by an environment variable.
452 if self.envvar is not None:
453 threads = int(os.environ.get(self.envvar, self.default))
454
455 def _single_compile(obj: Any) -> None:
456 try:
457 src, ext = build[obj]
458 except KeyError:
459 return
460
461 if not os.path.exists(obj) or self.needs_recompile(obj, src):
462 compiler._compile(obj, src, ext, cc_args, extra_postargs, pp_opts) # type: ignore[attr-defined]
463
464 try:
465 # Importing .synchronize checks for platforms that have some multiprocessing
466 # capabilities but lack semaphores, such as AWS Lambda and Android Termux.
467 import multiprocessing.synchronize
468 from multiprocessing.pool import ThreadPool
469 except ImportError:
470 threads = 1
471
472 if threads == 0:
473 try:
474 threads = multiprocessing.cpu_count()
475 threads = self.max if self.max and self.max < threads else threads
476 except NotImplementedError:
477 threads = 1
478
479 if threads > 1:
480 with ThreadPool(threads) as pool:
481 for _ in pool.imap_unordered(_single_compile, objects):
482 pass
483 else:
484 for ob in objects:
485 _single_compile(ob)
486
487 return objects
488
489 return compile_function
490
491 def install(self: S) -> S:
492 """
493 Installs the compile function into distutils.ccompiler.CCompiler.compile.
494 """
495 distutils.ccompiler.CCompiler.compile = self.functionfunction() # type: ignore[assignment]
496 return self
497
498 def __enter__(self: S) -> S:
499 self._old.append(distutils.ccompiler.CCompiler.compile)
500 return self.installinstall()
501
502 def __exit__(self, *args: Any) -> None:
503 distutils.ccompiler.CCompiler.compile = self._old.pop() # type: ignore[assignment]
def __init__(self, envvar=None, default=0, max=0, needs_recompile=no_recompile)
def __init__(self, *args, **kwargs)
None _add_cflags(self, List[str] flags)
None _add_ldflags(self, List[str] flags)
Definition: pytypes.h:1200
bool hasattr(handle obj, handle name)
Definition: pytypes.h:517
def no_recompile(obg, src)
def intree_extensions(paths, package_dir=None)
def naive_recompile(obj, src)
def auto_cpp_level(compiler)
def has_flag(compiler, flag)
Definition: setup.py:1