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 361 362
|
""" 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)
self.cython_directives = {
'language_level': '3',
}
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, min_ver, found_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 'setuptools' not 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
|