forked from sfepy/sfepy
-
Notifications
You must be signed in to change notification settings - Fork 0
/
build_helpers.py
347 lines (281 loc) · 10.4 KB
/
build_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
"""
Build helpers for setup.py.
Includes package dependency checks and monkey-patch to numpy.distutils
to work with Cython.
Notes
-----
The original version of this file was adapted from NiPy project [1].
[1] http://nipy.sourceforge.net/
"""
# Standard library imports
import os
import shutil
import glob
import fnmatch
from distutils.cmd import Command
from os.path import join as pjoin, dirname
from distutils.command.clean import clean
from distutils.version import LooseVersion
from distutils.dep_util import newer_group
from distutils.errors import DistutilsError
from numpy.distutils.misc_util import appendpath
from numpy.distutils import log
import sfepy.version as INFO
CYTHON_MIN_VERSION = INFO.CYTHON_MIN_VERSION
class NoOptionsDocs(Command):
user_options = [('None', None, 'this command has no options'),]
def initialize_options(self):
pass
def finalize_options(self):
pass
def get_sphinx_make_command():
if os.name in ['posix']:
return 'make'
elif os.name in ['nt']:
return 'make.bat'
else:
raise ValueError('unsupported system! (%s)' % os.name)
class SphinxHTMLDocs(NoOptionsDocs):
description = """generate html docs by Sphinx"""
def run(self):
os.chdir('doc')
try:
cmd = get_sphinx_make_command()
os.system(cmd + ' html')
finally:
os.chdir('..')
class SphinxPDFDocs(NoOptionsDocs):
description = """generate pdf docs by Sphinx"""
def run(self):
cwd = os.getcwd()
os.chdir('doc')
try:
cmd = get_sphinx_make_command()
os.system(cmd + ' latex')
os.chdir('_build/latex')
os.system(cmd + ' all-pdf')
os.chdir(cwd)
try:
os.remove('doc/sfepy_manual.pdf')
except:
pass
os.rename('doc/_build/latex/SfePy.pdf', 'doc/sfepy_manual.pdf')
finally:
os.chdir(cwd)
class DoxygenDocs(NoOptionsDocs):
description = """generate docs by Doxygen"""
def run(self):
try:
shutil.rmtree('doc/html')
except OSError:
pass
fd_in = open('doc/doxygen.config', 'r')
fd_out = open('doc/doxygenrc', 'w')
for line in fd_in:
aux = line.split('=')
if len(aux) and (aux[0] == 'PROJECT_NUMBER'):
line = '='.join([aux[0], INFO.__version__])
fd_out.write(line)
fd_out.close()
fd_in.close()
os.system('doxygen doc/doxygenrc')
def recursive_glob(top_dir, pattern):
"""
Utility function working like `glob.glob()`, but working recursively
and returning generator.
Parameters
----------
topdir : str
The top-level directory.
pattern : str or list of str
The pattern or list of patterns to match.
"""
if isinstance(pattern, list):
for pat in pattern:
for fn in recursive_glob(top_dir, pat):
yield fn
else:
for dirpath, dirnames, filenames in os.walk(top_dir):
for fn in [fn for fn in filenames
if fnmatch.fnmatchcase(fn, pattern)]:
yield os.path.join(dirpath, fn)
class Clean(clean):
"""
Distutils Command class to clean, enhanced to clean also files
generated during `python setup.py build_ext --inplace`.
"""
def run(self):
clean.run(self)
print('extra clean:')
suffixes = ['*.pyc', '*.o', '*.so', '*.pyd',
'*_wrap.c', '*.bak', '*~', '*%']
for filename in recursive_glob('sfepy', suffixes):
print(filename)
os.remove(filename)
for filename in recursive_glob('examples', suffixes):
print(filename)
os.remove(filename)
for filename in recursive_glob('script', suffixes):
print(filename)
os.remove(filename)
for filename in recursive_glob('tests', suffixes):
print(filename)
os.remove(filename)
for filename in glob.glob('*.pyc'):
print(filename)
os.remove(filename)
for _filename in recursive_glob('sfepy', ['*.pyx']):
filename = _filename.replace('.pyx', '.c')
print(filename)
try:
os.remove(filename)
except OSError:
pass
filename = _filename.replace('.pyx', '.html')
print(filename)
try:
os.remove(filename)
except OSError:
pass
# The command classes for distutils, used by setup.py.
cmdclass = {
'htmldocs' : SphinxHTMLDocs,
'pdfdocs' : SphinxPDFDocs,
'doxygendocs' : DoxygenDocs,
'clean': Clean,
}
def have_good_cython():
try:
from Cython.Compiler.Version import version
except ImportError:
return False
return LooseVersion(version) >= LooseVersion(CYTHON_MIN_VERSION)
def generate_a_pyrex_source(self, base, ext_name, source, extension):
'''
Monkey patch for numpy build_src.build_src method
Uses Cython instead of Pyrex.
'''
good_cython = have_good_cython()
if self.inplace or not good_cython:
target_dir = dirname(base)
else:
target_dir = appendpath(self.build_src, dirname(base))
target_file = pjoin(target_dir, ext_name + '.c')
depends = [source] + extension.depends
sources_changed = newer_group(depends, target_file, 'newer')
if self.force or sources_changed:
if good_cython:
# add distribution (package-wide) include directories, in order
# to pick up needed .pxd files for cython compilation
incl_dirs = extension.include_dirs[:]
dist_incl_dirs = self.distribution.include_dirs
if not dist_incl_dirs is None:
incl_dirs += dist_incl_dirs
import Cython.Compiler.Main
log.info("cythonc:> %s" % (target_file))
self.mkpath(target_dir)
options = Cython.Compiler.Main.CompilationOptions(
defaults=Cython.Compiler.Main.default_options,
include_path=incl_dirs,
output_file=target_file)
cython_result = Cython.Compiler.Main.compile(source,
options=options)
if cython_result.num_errors != 0:
raise DistutilsError("%d errors while compiling "
"%r with Cython"
% (cython_result.num_errors, source))
elif sources_changed and os.path.isfile(target_file):
raise DistutilsError("Cython >=%s required for compiling %r"
" because sources (%s) have changed" %
(CYTHON_MIN_VERSION, source,
','.join(depends)))
else:
raise DistutilsError("Cython >=%s required for compiling %r"
" but not available" %
(CYTHON_MIN_VERSION, source))
return target_file
def package_check(pkg_name, version=None, optional=False, checker=LooseVersion,
version_getter=None, messages=None, show_only=False):
"""
Check if package `pkg_name` is present, and in correct version.
Parameters
----------
pkg_name : str or sequence of str
The name of the package as imported into python. Alternative names
(e.g. for different versions) may be given in a list.
version : str, optional
The minimum version of the package that is required. If not given, the
version is not checked.
optional : bool, optional
If False, raise error for absent package or wrong version;
otherwise warn
checker : callable, optional
If given, the callable with which to return a comparable thing from a
version string. The default is ``distutils.version.LooseVersion``.
version_getter : callable, optional:
If given, the callable that takes `pkg_name` as argument, and returns
the package version string - as in::
``version = version_getter(pkg_name)``
The default is equivalent to::
mod = __import__(pkg_name); version = mod.__version__``
messages : dict, optional
If given, the dictionary providing (some of) output messages.
show_only : bool
If True, do not raise exceptions, only show the package name and version
information.
"""
if version_getter is None:
def version_getter(pkg_name):
mod = __import__(pkg_name)
return mod.__version__
if messages is None:
messages = {}
msgs = {
'available' : '%s is available',
'missing' : '%s is missing',
'opt suffix' : '; you may get run-time errors',
'version' : '%s is available in version %s',
'version old' : '%s is available in version %s, but >= %s is needed',
'no version' : '%s is available, cannot determine version',
}
msgs.update(messages)
if isinstance(pkg_name, str):
names = [pkg_name]
else:
names = pkg_name
import_ok = False
for pkg_name in names:
try:
__import__(pkg_name)
except ImportError:
pass
else:
import_ok = True
pkg_info = pkg_name + (' (optional)' if optional else '')
if not import_ok:
if not (optional or show_only):
raise RuntimeError(msgs['missing'] % pkg_name)
log.warn(msgs['missing'] % pkg_info + msgs['opt suffix'])
return
if not version:
if show_only:
log.info(msgs['available'] % pkg_info)
return
try:
have_version = version_getter(pkg_name)
except AttributeError:
raise RuntimeError(msgs['no version'] % pkg_info)
if not have_version:
if optional or show_only:
log.warn(msgs['no version'] % pkg_info)
else:
raise RuntimeError(msgs['no version'] % pkg_info)
elif checker(have_version) < checker(version):
if optional or show_only:
log.warn(msgs['version old'] % (pkg_info, have_version, version)
+ msgs['opt suffix'])
else:
raise RuntimeError(msgs['version old'] % (pkg_info, have_version,
version))
elif show_only:
log.info(msgs['version'] % (pkg_info, have_version))