-
Notifications
You must be signed in to change notification settings - Fork 48
/
setup_helpers.py
360 lines (318 loc) · 13.7 KB
/
setup_helpers.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
''' Distutils / setuptools helpers
'''
import os
import sys
from os.path import join as pjoin, split as psplit, splitext, dirname, exists
import tempfile
import shutil
from distutils.version import LooseVersion
from distutils.command.install_scripts import install_scripts
from distutils.errors import CompileError, LinkError
from distutils import log
BAT_TEMPLATE = \
r"""@echo off
REM wrapper to use shebang first line of {FNAME}
set mypath=%~dp0
set pyscript="%mypath%{FNAME}"
set /p line1=<%pyscript%
if "%line1:~0,2%" == "#!" (goto :goodstart)
echo First line of %pyscript% does not start with "#!"
exit /b 1
:goodstart
set py_exe=%line1:~2%
REM quote exe in case of spaces in path name
set py_exe="%py_exe%"
call %py_exe% %pyscript% %*
"""
# Path of file to which to write C conditional vars from build-time checks
CONFIG_H = pjoin('build', 'config.h')
# File name (no directory) to which to write Python vars from build-time checks
CONFIG_PY = '__config__.py'
# Directory to which to write libraries for building
LIB_DIR_TMP = pjoin('build', 'extra_libs')
class install_scripts_bat(install_scripts):
""" Make scripts executable on Windows
Scripts are bare file names without extension on Unix, fitting (for example)
Debian rules. They identify as python scripts with the usual ``#!`` first
line. Unix recognizes and uses this first "shebang" line, but Windows does
not. So, on Windows only we add a ``.bat`` wrapper of name
``bare_script_name.bat`` to call ``bare_script_name`` using the python
interpreter from the #! first line of the script.
Notes
-----
See discussion at
http://matthew-brett.github.com/pydagogue/installing_scripts.html and
example at git://github.com/matthew-brett/myscripter.git for more
background.
"""
def run(self):
install_scripts.run(self)
if not os.name == "nt":
return
for filepath in self.get_outputs():
# If we can find an executable name in the #! top line of the script
# file, make .bat wrapper for script.
with open(filepath, 'rt') as fobj:
first_line = fobj.readline()
if not (first_line.startswith('#!') and
'python' in first_line.lower()):
log.info("No #!python executable found, skipping .bat "
"wrapper")
continue
pth, fname = psplit(filepath)
froot, ext = splitext(fname)
bat_file = pjoin(pth, froot + '.bat')
bat_contents = BAT_TEMPLATE.replace('{FNAME}', fname)
log.info("Making %s wrapper for %s" % (bat_file, filepath))
if self.dry_run:
continue
with open(bat_file, 'wt') as fobj:
fobj.write(bat_contents)
def add_flag_checking(build_ext_class, flag_defines, top_package_dir=''):
""" Override input `build_ext_class` to check compiler `flag_defines`
Parameters
----------
build_ext_class : class
Class implementing ``distutils.command.build_ext.build_ext`` interface,
with a ``build_extensions`` method.
flag_defines : sequence
A sequence of elements, where the elements are sequences of length 4
consisting of (``compile_flags``, ``link_flags``, ``code``,
``defvar``). ``compile_flags`` is a sequence of compiler flags;
``link_flags`` is a sequence of linker flags. We
check ``compile_flags`` to see whether a C source string ``code`` will
compile, and ``link_flags`` to see whether the resulting object file
will link. If both compile and link works, we add ``compile_flags`` to
``extra_compile_args`` and ``link_flags`` to ``extra_link_args`` of
each extension when we build the extensions. If ``defvar`` is not
None, it is the name of C variable to be defined in ``build/config.h``
with 1 if the combination of (``compile_flags``, ``link_flags``,
``code``) will compile and link, 0 otherwise. If None, do not write
variable.
top_package_dir : str
String giving name of top-level package, for writing Python file
containing configuration variables. If empty, do not write this file.
Variables written are the same as the Cython variables generated via
the `flag_defines` setting.
Returns
-------
checker_class : class
A class with similar interface to
``distutils.command.build_ext.build_ext``, that adds all working
``compile_flags`` values to the ``extra_compile_args`` and working
``link_flags`` to ``extra_link_args`` attributes of extensions, before
compiling.
"""
class Checker(build_ext_class):
flag_defs = tuple(flag_defines)
def can_compile_link(self, compile_flags, link_flags, code):
cc = self.compiler
fname = 'test.c'
cwd = os.getcwd()
tmpdir = tempfile.mkdtemp()
try:
os.chdir(tmpdir)
with open(fname, 'wt') as fobj:
fobj.write(code)
try:
objects = cc.compile([fname],
extra_postargs=compile_flags)
except CompileError:
return False
try:
# Link shared lib rather then executable to avoid
# http://bugs.python.org/issue4431 with MSVC 10+
cc.link_shared_lib(objects, "testlib",
extra_postargs=link_flags)
except (LinkError, TypeError):
return False
finally:
os.chdir(cwd)
shutil.rmtree(tmpdir)
return True
def build_extensions(self):
""" Hook into extension building to check compiler flags """
def_vars = []
good_compile_flags = []
good_link_flags = []
config_dir = dirname(CONFIG_H)
for compile_flags, link_flags, code, def_var in self.flag_defs:
compile_flags = list(compile_flags)
link_flags = list(link_flags)
flags_good = self.can_compile_link(compile_flags,
link_flags,
code)
if def_var:
def_vars.append((def_var, flags_good))
if flags_good:
good_compile_flags += compile_flags
good_link_flags += link_flags
else:
log.warn("Flags {0} omitted because of compile or link "
"error".format(compile_flags + link_flags))
if def_vars: # write config.h file
if not exists(config_dir):
self.mkpath(config_dir)
with open(CONFIG_H, 'wt') as fobj:
fobj.write('/* Automatically generated; do not edit\n')
fobj.write(' C defines from build-time checks */\n')
for v_name, v_value in def_vars:
fobj.write('int {0} = {1};\n'.format(
v_name, 1 if v_value else 0))
if def_vars and top_package_dir: # write __config__.py file
config_py_dir = (top_package_dir if self.inplace else
pjoin(self.build_lib, top_package_dir))
if not exists(config_py_dir):
self.mkpath(config_py_dir)
config_py = pjoin(config_py_dir, CONFIG_PY)
with open(config_py, 'wt') as fobj:
fobj.write('# Automatically generated; do not edit\n')
fobj.write('# Variables from compile checks\n')
for v_name, v_value in def_vars:
fobj.write('{0} = {1}\n'.format(v_name, v_value))
if def_vars or good_compile_flags or good_link_flags:
for ext in self.extensions:
ext.extra_compile_args += good_compile_flags
ext.extra_link_args += good_link_flags
if def_vars:
ext.include_dirs.append(config_dir)
build_ext_class.build_extensions(self)
return Checker
def get_pkg_version(pkg_name):
""" Return package version for `pkg_name` if installed
Returns
-------
pkg_version : str or None
Return None if package not importable. Return 'unknown' if standard
``__version__`` string not present. Otherwise return version string.
"""
try:
pkg = __import__(pkg_name)
except ImportError:
return None
try:
return pkg.__version__
except AttributeError:
return 'unknown'
def version_error_msg(pkg_name, found_ver, min_ver):
""" Return informative error message for version or None
"""
if found_ver is None:
return 'We need package {0}, but not importable'.format(pkg_name)
if found_ver == 'unknown':
return 'We need {0} version {1}, but cannot get version'.format(
pkg_name, min_ver)
if LooseVersion(found_ver) >= LooseVersion(min_ver):
return None
return 'We need {0} version {1}, but found version {2}'.format(
pkg_name, found_ver, min_ver)
class SetupDependency(object):
""" SetupDependency class
Parameters
----------
import_name : str
Name with which required package should be ``import``ed.
min_ver : str
Distutils version string giving minimum version for package.
req_type : {'install_requires', 'setup_requires'}, optional
Setuptools dependency type.
heavy : {False, True}, optional
If True, and package is already installed (importable), then do not add
to the setuptools dependency lists. This prevents setuptools
reinstalling big packages when the package was installed without using
setuptools, or this is an upgrade, and we want to avoid the pip default
behavior of upgrading all dependencies.
install_name : str, optional
Name identifying package to install from pypi etc, if different from
`import_name`.
"""
def __init__(self, import_name,
min_ver,
req_type='install_requires',
heavy=False,
install_name=None):
self.import_name = import_name
self.min_ver = min_ver
self.req_type = req_type
self.heavy = heavy
self.install_name = (import_name if install_name is None
else install_name)
def check_fill(self, setuptools_kwargs):
""" Process this dependency, maybe filling `setuptools_kwargs`
Run checks on this dependency. If not using setuptools, then raise
error for unmet dependencies. If using setuptools, add missing or
not-heavy dependencies to `setuptools_kwargs`.
A heavy dependency is one that is inconvenient to install
automatically, such as numpy or (particularly) scipy, matplotlib.
Parameters
----------
setuptools_kwargs : dict
Dictionary of setuptools keyword arguments that may be modified
in-place while checking dependencies.
"""
found_ver = get_pkg_version(self.import_name)
ver_err_msg = version_error_msg(self.import_name,
found_ver,
self.min_ver)
if not 'setuptools' in sys.modules:
# Not using setuptools; raise error for any unmet dependencies
if ver_err_msg is not None:
raise RuntimeError(ver_err_msg)
return
# Using setuptools; add packages to given section of
# setup/install_requires, unless it's a heavy dependency for which we
# already have an acceptable importable version.
if self.heavy and ver_err_msg is None:
return
new_req = '{0}>={1}'.format(self.import_name, self.min_ver)
old_reqs = setuptools_kwargs.get(self.req_type, [])
setuptools_kwargs[self.req_type] = old_reqs + [new_req]
class Bunch(object):
def __init__(self, vars):
for key, name in vars.items():
if key.startswith('__'):
continue
self.__dict__[key] = name
def read_vars_from(ver_file):
""" Read variables from Python text file
Parameters
----------
ver_file : str
Filename of file to read
Returns
-------
info_vars : Bunch instance
Bunch object where variables read from `ver_file` appear as
attributes
"""
# Use exec for compabibility with Python 3
ns = {}
with open(ver_file, 'rt') as fobj:
exec(fobj.read(), ns)
return Bunch(ns)
def make_np_ext_builder(build_ext_class):
""" Override input `build_ext_class` to add numpy includes to extension
This is useful to delay call of ``np.get_include`` until the extension is
being built.
Parameters
----------
build_ext_class : class
Class implementing ``distutils.command.build_ext.build_ext`` interface,
with a ``build_extensions`` method.
Returns
-------
np_build_ext_class : class
A class with similar interface to
``distutils.command.build_ext.build_ext``, that adds libraries in
``np.get_include()`` to include directories of extension.
"""
class NpExtBuilder(build_ext_class):
def build_extensions(self):
""" Hook into extension building to add np include dirs
"""
# Delay numpy import until last moment
import numpy as np
for ext in self.extensions:
ext.include_dirs.append(np.get_include())
build_ext_class.build_extensions(self)
return NpExtBuilder