Skip to content

Commit 20b4a60

Browse files
Setup helpers
1 parent 0a5543b commit 20b4a60

File tree

1 file changed

+360
-0
lines changed

1 file changed

+360
-0
lines changed

setup_helpers.py

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

0 commit comments

Comments
 (0)