1import setuptools
2from setuptools.command.build_ext import build_ext
3from setuptools.dist import Distribution
4import numpy as np
5
6import functools
7import os
8import subprocess
9import sys
10from tempfile import mkdtemp
11from contextlib import contextmanager
12from pathlib import Path
13
14# Wire in distutils components from setuptools
15CCompiler = setuptools.distutils.ccompiler.CCompiler
16new_compiler = setuptools.distutils.ccompiler.new_compiler
17customize_compiler = setuptools.distutils.sysconfig.customize_compiler
18log = setuptools.distutils.log
19
20
21_configs = {
22 # DLL suffix, Python C extension suffix
23 'win': ('.dll', '.pyd'),
24 'default': ('.so', '.so'),
25}
26
27
28def get_configs(arg):
29 return _configs.get(sys.platform[:3], _configs['default'])[arg]
30
31
32find_shared_ending = functools.partial(get_configs, 0)
33find_pyext_ending = functools.partial(get_configs, 1)
34
35@contextmanager
36def _gentmpfile(suffix):
37 # windows locks the tempfile so use a tempdir + file, see
38 # https://github.com/numba/numba/issues/3304
39 try:
40 tmpdir = mkdtemp()
41 ntf = open(os.path.join(tmpdir, "temp%s" % suffix), 'wt')
42 yield ntf
43 finally:
44 try:
45 ntf.close()
46 os.remove(ntf)
47 except:
48 pass
49 else:
50 os.rmdir(tmpdir)
51
52
53@functools.lru_cache(maxsize=1)
54def external_compiler_works():
55 """
56 Returns True if the "external compiler" bound in numpy.distutil is present
57 and working, False otherwise.
58 """
59 compiler = new_compiler()
60 customize_compiler(compiler)
61 for suffix in ['.c', '.cxx']:
62 try:
63 with _gentmpfile(suffix) as ntf:
64 simple_c = "int main(void) { return 0; }"
65 ntf.write(simple_c)
66 ntf.flush()
67 ntf.close()
68 # *output_dir* is set to avoid the compiler putting temp files
69 # in the current directory.
70 compiler.compile([ntf.name], output_dir=Path(ntf.name).anchor)
71 except Exception: # likely CompileError or file system issue
72 return False
73 return True
74
75
76class _DummyExtension(object):
77 libraries = []
78
79
80class Toolchain(object):
81
82 def __init__(self):
83 if not external_compiler_works():
84 self._raise_external_compiler_error()
85
86 self._verbose = False
87 self._compiler = new_compiler()
88 customize_compiler(self._compiler)
89 self._build_ext = build_ext(Distribution())
90 self._build_ext.finalize_options()
91 self._py_lib_dirs = self._build_ext.library_dirs
92 self._py_include_dirs = self._build_ext.include_dirs
93 np_compile_args = {'include_dirs': [np.get_include(),],}
94 if sys.platform == 'win32':
95 np_compile_args['libraries'] = []
96 else:
97 np_compile_args['libraries'] = ['m',]
98 self._math_info = np_compile_args
99
100 @property
101 def verbose(self):
102 return self._verbose
103
104 @verbose.setter
105 def verbose(self, value):
106 self._verbose = value
107 # DEBUG will let Numpy spew many messages, so stick to INFO
108 # to print commands executed by distutils
109 log.set_threshold(log.INFO if value else log.WARN)
110
111 def _raise_external_compiler_error(self):
112 basemsg = ("Attempted to compile AOT function without the "
113 "compiler used by `numpy.distutils` present.")
114 conda_msg = "If using conda try:\n\n#> conda install %s"
115 plt = sys.platform
116 if plt.startswith('linux'):
117 if sys.maxsize <= 2 ** 32:
118 compilers = ['gcc_linux-32', 'gxx_linux-32']
119 else:
120 compilers = ['gcc_linux-64', 'gxx_linux-64']
121 msg = "%s %s" % (basemsg, conda_msg % ' '.join(compilers))
122 elif plt.startswith('darwin'):
123 compilers = ['clang_osx-64', 'clangxx_osx-64']
124 msg = "%s %s" % (basemsg, conda_msg % ' '.join(compilers))
125 elif plt.startswith('win32'):
126 winmsg = "Cannot find suitable msvc."
127 msg = "%s %s" % (basemsg, winmsg)
128 else:
129 msg = "Unknown platform %s" % plt
130 raise RuntimeError(msg)
131
132 def compile_objects(self, sources, output_dir,
133 include_dirs=(), depends=(), macros=(),
134 extra_cflags=None):
135 """
136 Compile the given source files into a separate object file each,
137 all beneath the *output_dir*. A list of paths to object files
138 is returned.
139
140 *macros* has the same format as in distutils: a list of 1- or 2-tuples.
141 If a 1-tuple (name,), the given name is considered undefined by
142 the C preprocessor.
143 If a 2-tuple (name, value), the given name is expanded into the
144 given value by the C preprocessor.
145 """
146 objects = self._compiler.compile(sources,
147 output_dir=output_dir,
148 include_dirs=include_dirs,
149 depends=depends,
150 macros=macros or [],
151 extra_preargs=extra_cflags)
152 return objects
153
154 def link_shared(self, output, objects, libraries=(),
155 library_dirs=(), export_symbols=(),
156 extra_ldflags=None):
157 """
158 Create a shared library *output* linking the given *objects*
159 and *libraries* (all strings).
160 """
161 output_dir, output_filename = os.path.split(output)
162 self._compiler.link(CCompiler.SHARED_OBJECT, objects,
163 output_filename, output_dir,
164 libraries, library_dirs,
165 export_symbols=export_symbols,
166 extra_preargs=extra_ldflags)
167
168 def get_python_libraries(self):
169 """
170 Get the library arguments necessary to link with Python.
171 """
172 libs = self._build_ext.get_libraries(_DummyExtension())
173 if sys.platform == 'win32':
174 # Under Windows, need to link explicitly against the CRT,
175 # as the MSVC compiler would implicitly do.
176 # (XXX msvcrtd in pydebug mode?)
177 libs = libs + ['msvcrt']
178 return libs + self._math_info['libraries']
179
180 def get_python_library_dirs(self):
181 """
182 Get the library directories necessary to link with Python.
183 """
184 return list(self._py_lib_dirs)
185
186 def get_python_include_dirs(self):
187 """
188 Get the include directories necessary to compile against the Python
189 and Numpy C APIs.
190 """
191 return list(self._py_include_dirs) + self._math_info['include_dirs']
192
193 def get_ext_filename(self, ext_name):
194 """
195 Given a C extension's module name, return its intended filename.
196 """
197 return self._build_ext.get_ext_filename(ext_name)
198
199
200def _quote_arg(arg):
201 """
202 Quote the argument for safe use in a shell command line.
203 """
204 # If there is a quote in the string, assume relevants parts of the
205 # string are already quoted (e.g. '-I"C:\\Program Files\\..."')
206 if '"' not in arg and ' ' in arg:
207 return '"%s"' % arg
208 return arg
209
210
211def _is_sequence(arg):
212 if isinstance(arg, (str, bytes)):
213 return False
214 try:
215 len(arg)
216 return True
217 except Exception:
218 return False