• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# -*- coding: utf-8 -*-
2
3"""
4This module provides helpers for C++11+ projects using pybind11.
5
6LICENSE:
7
8Copyright (c) 2016 Wenzel Jakob <wenzel.jakob@epfl.ch>, All rights reserved.
9
10Redistribution and use in source and binary forms, with or without
11modification, are permitted provided that the following conditions are met:
12
131. Redistributions of source code must retain the above copyright notice, this
14   list of conditions and the following disclaimer.
15
162. Redistributions in binary form must reproduce the above copyright notice,
17   this list of conditions and the following disclaimer in the documentation
18   and/or other materials provided with the distribution.
19
203. Neither the name of the copyright holder nor the names of its contributors
21   may be used to endorse or promote products derived from this software
22   without specific prior written permission.
23
24THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
25ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
26WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
27DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
28FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
29DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
30SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
31CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
32OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
33OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
34"""
35
36# IMPORTANT: If you change this file in the pybind11 repo, also review
37# setup_helpers.pyi for matching changes.
38#
39# If you copy this file in, you don't
40# need the .pyi file; it's just an interface file for static type checkers.
41
42import contextlib
43import os
44import shutil
45import sys
46import tempfile
47import threading
48import platform
49import warnings
50
51try:
52    from setuptools.command.build_ext import build_ext as _build_ext
53    from setuptools import Extension as _Extension
54except ImportError:
55    from distutils.command.build_ext import build_ext as _build_ext
56    from distutils.extension import Extension as _Extension
57
58import distutils.errors
59import distutils.ccompiler
60
61
62WIN = sys.platform.startswith("win32")
63PY2 = sys.version_info[0] < 3
64MACOS = sys.platform.startswith("darwin")
65STD_TMPL = "/std:c++{}" if WIN else "-std=c++{}"
66
67
68# It is recommended to use PEP 518 builds if using this module. However, this
69# file explicitly supports being copied into a user's project directory
70# standalone, and pulling pybind11 with the deprecated setup_requires feature.
71# If you copy the file, remember to add it to your MANIFEST.in, and add the current
72# directory into your path if it sits beside your setup.py.
73
74
75class Pybind11Extension(_Extension):
76    """
77    Build a C++11+ Extension module with pybind11. This automatically adds the
78    recommended flags when you init the extension and assumes C++ sources - you
79    can further modify the options yourself.
80
81    The customizations are:
82
83    * ``/EHsc`` and ``/bigobj`` on Windows
84    * ``stdlib=libc++`` on macOS
85    * ``visibility=hidden`` and ``-g0`` on Unix
86
87    Finally, you can set ``cxx_std`` via constructor or afterwords to enable
88    flags for C++ std, and a few extra helper flags related to the C++ standard
89    level. It is _highly_ recommended you either set this, or use the provided
90    ``build_ext``, which will search for the highest supported extension for
91    you if the ``cxx_std`` property is not set. Do not set the ``cxx_std``
92    property more than once, as flags are added when you set it. Set the
93    property to None to disable the addition of C++ standard flags.
94
95    If you want to add pybind11 headers manually, for example for an exact
96    git checkout, then set ``include_pybind11=False``.
97
98    Warning: do not use property-based access to the instance on Python 2 -
99    this is an ugly old-style class due to Distutils.
100    """
101
102    # flags are prepended, so that they can be further overridden, e.g. by
103    # ``extra_compile_args=["-g"]``.
104
105    def _add_cflags(self, flags):
106        self.extra_compile_args[:0] = flags
107
108    def _add_ldflags(self, flags):
109        self.extra_link_args[:0] = flags
110
111    def __init__(self, *args, **kwargs):
112
113        self._cxx_level = 0
114        cxx_std = kwargs.pop("cxx_std", 0)
115
116        if "language" not in kwargs:
117            kwargs["language"] = "c++"
118
119        include_pybind11 = kwargs.pop("include_pybind11", True)
120
121        # Can't use super here because distutils has old-style classes in
122        # Python 2!
123        _Extension.__init__(self, *args, **kwargs)
124
125        # Include the installed package pybind11 headers
126        if include_pybind11:
127            # If using setup_requires, this fails the first time - that's okay
128            try:
129                import pybind11
130
131                pyinc = pybind11.get_include()
132
133                if pyinc not in self.include_dirs:
134                    self.include_dirs.append(pyinc)
135            except ImportError:
136                pass
137
138        # Have to use the accessor manually to support Python 2 distutils
139        Pybind11Extension.cxx_std.__set__(self, cxx_std)
140
141        cflags = []
142        ldflags = []
143        if WIN:
144            cflags += ["/EHsc", "/bigobj"]
145        else:
146            cflags += ["-fvisibility=hidden", "-g0"]
147            if MACOS:
148                cflags += ["-stdlib=libc++"]
149                ldflags += ["-stdlib=libc++"]
150        self._add_cflags(cflags)
151        self._add_ldflags(ldflags)
152
153    @property
154    def cxx_std(self):
155        """
156        The CXX standard level. If set, will add the required flags. If left
157        at 0, it will trigger an automatic search when pybind11's build_ext
158        is used. If None, will have no effect.  Besides just the flags, this
159        may add a register warning/error fix for Python 2 or macos-min 10.9
160        or 10.14.
161        """
162        return self._cxx_level
163
164    @cxx_std.setter
165    def cxx_std(self, level):
166
167        if self._cxx_level:
168            warnings.warn("You cannot safely change the cxx_level after setting it!")
169
170        # MSVC 2015 Update 3 and later only have 14 (and later 17) modes, so
171        # force a valid flag here.
172        if WIN and level == 11:
173            level = 14
174
175        self._cxx_level = level
176
177        if not level:
178            return
179
180        cflags = [STD_TMPL.format(level)]
181        ldflags = []
182
183        if MACOS and "MACOSX_DEPLOYMENT_TARGET" not in os.environ:
184            # C++17 requires a higher min version of macOS. An earlier version
185            # (10.12 or 10.13) can be set manually via environment variable if
186            # you are careful in your feature usage, but 10.14 is the safest
187            # setting for general use. However, never set higher than the
188            # current macOS version!
189            current_macos = tuple(int(x) for x in platform.mac_ver()[0].split(".")[:2])
190            desired_macos = (10, 9) if level < 17 else (10, 14)
191            macos_string = ".".join(str(x) for x in min(current_macos, desired_macos))
192            macosx_min = "-mmacosx-version-min=" + macos_string
193            cflags += [macosx_min]
194            ldflags += [macosx_min]
195
196        if PY2:
197            if WIN:
198                # Will be ignored on MSVC 2015, where C++17 is not supported so
199                # this flag is not valid.
200                cflags += ["/wd5033"]
201            elif level >= 17:
202                cflags += ["-Wno-register"]
203            elif level >= 14:
204                cflags += ["-Wno-deprecated-register"]
205
206        self._add_cflags(cflags)
207        self._add_ldflags(ldflags)
208
209
210# Just in case someone clever tries to multithread
211tmp_chdir_lock = threading.Lock()
212cpp_cache_lock = threading.Lock()
213
214
215@contextlib.contextmanager
216def tmp_chdir():
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, flag):
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 = "flagcheck.cpp"
241        with open(fname, "w") as f:
242            # Don't trigger -Wunused-parameter.
243            f.write("int main (int, char **) { return 0; }")
244
245        try:
246            compiler.compile([fname], extra_postargs=[flag])
247        except distutils.errors.CompileError:
248            return False
249        return True
250
251
252# Every call will cache the result
253cpp_flag_cache = None
254
255
256def auto_cpp_level(compiler):
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    global cpp_flag_cache
265
266    # If this has been previously calculated with the same args, return that
267    with cpp_cache_lock:
268        if cpp_flag_cache:
269            return cpp_flag_cache
270
271    levels = [17, 14, 11]
272
273    for level in levels:
274        if has_flag(compiler, STD_TMPL.format(level)):
275            with cpp_cache_lock:
276                cpp_flag_cache = level
277            return level
278
279    msg = "Unsupported compiler -- at least C++11 support is needed!"
280    raise RuntimeError(msg)
281
282
283class build_ext(_build_ext):  # noqa: N801
284    """
285    Customized build_ext that allows an auto-search for the highest supported
286    C++ level for Pybind11Extension. This is only needed for the auto-search
287    for now, and is completely optional otherwise.
288    """
289
290    def build_extensions(self):
291        """
292        Build extensions, injecting C++ std for Pybind11Extension if needed.
293        """
294
295        for ext in self.extensions:
296            if hasattr(ext, "_cxx_level") and ext._cxx_level == 0:
297                # Python 2 syntax - old-style distutils class
298                ext.__class__.cxx_std.__set__(ext, auto_cpp_level(self.compiler))
299
300        # Python 2 doesn't allow super here, since distutils uses old-style
301        # classes!
302        _build_ext.build_extensions(self)
303
304
305def naive_recompile(obj, src):
306    """
307    This will recompile only if the source file changes. It does not check
308    header files, so a more advanced function or Ccache is better if you have
309    editable header files in your package.
310    """
311    return os.stat(obj).st_mtime < os.stat(src).st_mtime
312
313
314def no_recompile(obg, src):
315    """
316    This is the safest but slowest choice (and is the default) - will always
317    recompile sources.
318    """
319    return True
320
321
322# Optional parallel compile utility
323# inspired by: http://stackoverflow.com/questions/11013851/speeding-up-build-process-with-distutils
324# and: https://github.com/tbenthompson/cppimport/blob/stable/cppimport/build_module.py
325# and NumPy's parallel distutils module:
326#              https://github.com/numpy/numpy/blob/master/numpy/distutils/ccompiler.py
327class ParallelCompile(object):
328    """
329    Make a parallel compile function. Inspired by
330    numpy.distutils.ccompiler.CCompiler_compile and cppimport.
331
332    This takes several arguments that allow you to customize the compile
333    function created:
334
335    envvar:
336        Set an environment variable to control the compilation threads, like
337        NPY_NUM_BUILD_JOBS
338    default:
339        0 will automatically multithread, or 1 will only multithread if the
340        envvar is set.
341    max:
342        The limit for automatic multithreading if non-zero
343    needs_recompile:
344        A function of (obj, src) that returns True when recompile is needed.  No
345        effect in isolated mode; use ccache instead, see
346        https://github.com/matplotlib/matplotlib/issues/1507/
347
348    To use::
349
350        ParallelCompile("NPY_NUM_BUILD_JOBS").install()
351
352    or::
353
354        with ParallelCompile("NPY_NUM_BUILD_JOBS"):
355            setup(...)
356
357    By default, this assumes all files need to be recompiled. A smarter
358    function can be provided via needs_recompile.  If the output has not yet
359    been generated, the compile will always run, and this function is not
360    called.
361    """
362
363    __slots__ = ("envvar", "default", "max", "_old", "needs_recompile")
364
365    def __init__(self, envvar=None, default=0, max=0, needs_recompile=no_recompile):
366        self.envvar = envvar
367        self.default = default
368        self.max = max
369        self.needs_recompile = needs_recompile
370        self._old = []
371
372    def function(self):
373        """
374        Builds a function object usable as distutils.ccompiler.CCompiler.compile.
375        """
376
377        def compile_function(
378            compiler,
379            sources,
380            output_dir=None,
381            macros=None,
382            include_dirs=None,
383            debug=0,
384            extra_preargs=None,
385            extra_postargs=None,
386            depends=None,
387        ):
388
389            # These lines are directly from distutils.ccompiler.CCompiler
390            macros, objects, extra_postargs, pp_opts, build = compiler._setup_compile(
391                output_dir, macros, include_dirs, sources, depends, extra_postargs
392            )
393            cc_args = compiler._get_cc_args(pp_opts, debug, extra_preargs)
394
395            # The number of threads; start with default.
396            threads = self.default
397
398            # Determine the number of compilation threads, unless set by an environment variable.
399            if self.envvar is not None:
400                threads = int(os.environ.get(self.envvar, self.default))
401
402            def _single_compile(obj):
403                try:
404                    src, ext = build[obj]
405                except KeyError:
406                    return
407
408                if not os.path.exists(obj) or self.needs_recompile(obj, src):
409                    compiler._compile(obj, src, ext, cc_args, extra_postargs, pp_opts)
410
411            try:
412                import multiprocessing
413                from multiprocessing.pool import ThreadPool
414            except ImportError:
415                threads = 1
416
417            if threads == 0:
418                try:
419                    threads = multiprocessing.cpu_count()
420                    threads = self.max if self.max and self.max < threads else threads
421                except NotImplementedError:
422                    threads = 1
423
424            if threads > 1:
425                for _ in ThreadPool(threads).imap_unordered(_single_compile, objects):
426                    pass
427            else:
428                for ob in objects:
429                    _single_compile(ob)
430
431            return objects
432
433        return compile_function
434
435    def install(self):
436        distutils.ccompiler.CCompiler.compile = self.function()
437        return self
438
439    def __enter__(self):
440        self._old.append(distutils.ccompiler.CCompiler.compile)
441        return self.install()
442
443    def __exit__(self, *args):
444        distutils.ccompiler.CCompiler.compile = self._old.pop()
445