1 20
import errno
2 20
import os
3 20
import shutil
4

5 20
from distutils.core import Extension
6 20
from distutils.ccompiler import get_default_compiler
7 20
from distutils.command.build_ext import build_ext as DistutilsBuildExt
8

9 20
from ..distutils_helpers import get_main_package_directory
10 20
from ..utils import get_numpy_include_path, import_file
11

12 20
__all__ = ['AstropyHelpersBuildExt']
13

14

15 20
def should_build_with_cython(previous_cython_version, is_release):
16
    """
17
    Returns the previously used Cython version (or 'unknown' if not
18
    previously built) if Cython should be used to build extension modules from
19
    pyx files.
20
    """
21

22
    # Only build with Cython if, of course, Cython is installed, we're in a
23
    # development version (i.e. not release) or the Cython-generated source
24
    # files haven't been created yet (cython_version == 'unknown'). The latter
25
    # case can happen even when release is True if checking out a release tag
26
    # from the repository
27 20
    have_cython = False
28 20
    try:
29 20
        from Cython import __version__ as cython_version  # noqa
30 20
        have_cython = True
31 0
    except ImportError:
32 0
        pass
33

34 20
    if have_cython and (not is_release or previous_cython_version == 'unknown'):
35 20
        return cython_version
36
    else:
37 0
        return False
38

39

40 20
class AstropyHelpersBuildExt(DistutilsBuildExt):
41
    """
42
    A custom 'build_ext' command that allows for manipulating some of the C
43
    extension options at build time.
44
    """
45

46 20
    _uses_cython = False
47 20
    _force_rebuild = False
48

49 20
    def __new__(cls, value, **kwargs):
50

51
        # NOTE: we need to wait until AstropyHelpersBuildExt is initialized to
52
        # import setuptools.command.build_ext because when that package is
53
        # imported, setuptools tries to import Cython - and if it's not found
54
        # it will affect the rest of the build process. This is an issue because
55
        # if we import that module at the top of this one, setup_requires won't
56
        # have been honored yet, so Cython may not yet be available - and if we
57
        # import build_ext too soon, it will think Cython is not available even
58
        # if it is then intalled when setup_requires is processed. To get around
59
        # this we dynamically create a new class that inherits from the
60
        # setuptools build_ext, and by this point setup_requires has been
61
        # processed.
62

63 20
        from setuptools.command.build_ext import build_ext as SetuptoolsBuildExt
64

65 20
        class FinalBuildExt(AstropyHelpersBuildExt, SetuptoolsBuildExt):
66 20
            pass
67

68 20
        new_type = type(cls.__name__, (FinalBuildExt,), dict(cls.__dict__))
69 20
        obj = SetuptoolsBuildExt.__new__(new_type)
70 20
        obj.__init__(value)
71

72 20
        return obj
73

74 20
    def finalize_options(self):
75

76
        # First let's find the package folder, then we can check if the
77
        # version and cython_version are accessible
78 20
        self.package_dir = get_main_package_directory(self.distribution)
79

80 20
        version = import_file(os.path.join(self.package_dir, 'version.py'),
81
                              name='version').version
82 20
        self.is_release = 'dev' not in version
83

84 20
        try:
85 20
            self.previous_cython_version = import_file(os.path.join(self.package_dir,
86
                                                                    'cython_version.py'),
87
                                                       name='cython_version').cython_version
88 20
        except (FileNotFoundError, ImportError):
89 20
            self.previous_cython_version = 'unknown'
90

91 20
        self._uses_cython = should_build_with_cython(self.previous_cython_version, self.is_release)
92

93
        # Add a copy of the _compiler.so module as well, but only if there
94
        # are in fact C modules to compile (otherwise there's no reason to
95
        # include a record of the compiler used). Note that self.extensions
96
        # may not be set yet, but self.distribution.ext_modules is where any
97
        # extension modules passed to setup() can be found
98 20
        extensions = self.distribution.ext_modules
99 20
        if extensions:
100 20
            build_py = self.get_finalized_command('build_py')
101 20
            package_dir = build_py.get_package_dir(self.package_dir)
102 20
            src_path = os.path.relpath(
103
                os.path.join(os.path.dirname(__file__), 'src'))
104 20
            shutil.copy(os.path.join(src_path, 'compiler.c'),
105
                        os.path.join(package_dir, '_compiler.c'))
106 20
            ext = Extension(self.package_dir + '.compiler_version',
107
                            [os.path.join(package_dir, '_compiler.c')])
108 20
            extensions.insert(0, ext)
109

110 20
        super().finalize_options()
111

112
        # If we are using Cython, then make sure we re-build if the version
113
        # of Cython that is installed is different from the version last
114
        # used to generate the C files.
115 20
        if self._uses_cython and self._uses_cython != self.previous_cython_version:
116 20
            self._force_rebuild = True
117

118
        # Regardless of the value of the '--force' option, force a rebuild
119
        # if the debug flag changed from the last build
120 20
        if self._force_rebuild:
121 20
            self.force = True
122

123 20
    def run(self):
124

125
        # For extensions that require 'numpy' in their include dirs,
126
        # replace 'numpy' with the actual paths
127 20
        np_include = None
128 20
        for extension in self.extensions:
129 20
            if 'numpy' in extension.include_dirs:
130 20
                if np_include is None:
131 20
                    np_include = get_numpy_include_path()
132 20
                idx = extension.include_dirs.index('numpy')
133 20
                extension.include_dirs.insert(idx, np_include)
134 20
                extension.include_dirs.remove('numpy')
135

136 20
            self._check_cython_sources(extension)
137

138
        # Note that setuptools automatically uses Cython to discover and
139
        # build extensions if available, so we don't have to explicitly call
140
        # e.g. cythonize.
141

142 20
        super().run()
143

144
        # Update cython_version.py if building with Cython
145

146 20
        if self._uses_cython and self._uses_cython != self.previous_cython_version:
147 20
            build_py = self.get_finalized_command('build_py')
148 20
            package_dir = build_py.get_package_dir(self.package_dir)
149 20
            cython_py = os.path.join(package_dir, 'cython_version.py')
150 20
            with open(cython_py, 'w') as f:
151 20
                f.write('# Generated file; do not modify\n')
152 20
                f.write('cython_version = {0!r}\n'.format(self._uses_cython))
153

154 20
            if os.path.isdir(self.build_lib):
155
                # The build/lib directory may not exist if the build_py
156
                # command was not previously run, which may sometimes be
157
                # the case
158 20
                self.copy_file(cython_py,
159
                               os.path.join(self.build_lib, cython_py),
160
                               preserve_mode=False)
161

162 20
    def _check_cython_sources(self, extension):
163
        """
164
        Where relevant, make sure that the .c files associated with .pyx
165
        modules are present (if building without Cython installed).
166
        """
167

168
        # Determine the compiler we'll be using
169 20
        if self.compiler is None:
170 20
            compiler = get_default_compiler()
171
        else:
172 0
            compiler = self.compiler
173

174
        # Replace .pyx with C-equivalents, unless c files are missing
175 20
        for jdx, src in enumerate(extension.sources):
176 20
            base, ext = os.path.splitext(src)
177 20
            pyxfn = base + '.pyx'
178 20
            cfn = base + '.c'
179 20
            cppfn = base + '.cpp'
180

181 20
            if not os.path.isfile(pyxfn):
182 20
                continue
183

184 20
            if self._uses_cython:
185 0
                extension.sources[jdx] = pyxfn
186
            else:
187 20
                if os.path.isfile(cfn):
188 0
                    extension.sources[jdx] = cfn
189 20
                elif os.path.isfile(cppfn):
190 0
                    extension.sources[jdx] = cppfn
191
                else:
192 20
                    msg = (
193
                        'Could not find C/C++ file {0}.(c/cpp) for Cython '
194
                        'file {1} when building extension {2}. Cython '
195
                        'must be installed to build from a git '
196
                        'checkout.'.format(base, pyxfn, extension.name))
197 20
                    raise IOError(errno.ENOENT, msg, cfn)
198

199
            # Cython (at least as of 0.29.2) uses deprecated Numpy API features
200
            # the use of which produces a few warnings when compiling.
201
            # These additional flags should squelch those warnings.
202
            # TODO: Feel free to remove this if/when a Cython update
203
            # removes use of the deprecated Numpy API
204 0
            if compiler == 'unix':
205 0
                extension.extra_compile_args.extend([
206
                    '-Wp,-w', '-Wno-unused-function'])

Read our documentation on viewing source code .

Loading