Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sage
Path: blob/develop/src/sage_setup/command/sage_build_cython.py
4081 views
1
########################################################################
2
##
3
## Customize the Extensions processed by Cython
4
##
5
########################################################################
6
7
import os
8
import sys
9
import time
10
import json
11
12
# Import setuptools before importing distutils, so that setuptools
13
# can replace distutils by its own vendored copy.
14
import setuptools
15
16
from distutils import log
17
from setuptools import Command
18
19
from sage_setup.util import stable_uniq
20
from sage_setup.find import find_extra_files
21
from sage_setup.cython_options import compiler_directives, compile_time_env_variables
22
23
# Do not put all, but only the most common libraries and their headers
24
# (that are likely to change on an upgrade) here:
25
# [At least at the moment. Make sure the headers aren't copied with "-p",
26
# or explicitly touch them in the respective spkg's spkg-install.]
27
lib_headers = dict()
28
29
# Set by build/bin/sage-build-env-config. Empty if the system package is used.
30
gmp_prefix = os.environ.get("SAGE_GMP_PREFIX", "")
31
if gmp_prefix:
32
lib_headers["gmp"] = [os.path.join(gmp_prefix, 'include', 'gmp.h')] # cf. #8664, #9896
33
lib_headers["gmpxx"] = [os.path.join(gmp_prefix, 'include', 'gmpxx.h')]
34
ntl_prefix = os.environ.get("SAGE_NTL_PREFIX", "")
35
if ntl_prefix:
36
lib_headers["ntl"] = [os.path.join(ntl_prefix, 'include', 'NTL', 'config.h')]
37
38
# Manually add -fno-strict-aliasing, which is needed to compile Cython
39
# and disappears from the default flags if the user has set CFLAGS.
40
#
41
# Add -DCYTHON_CLINE_IN_TRACEBACK=1 which causes the .c line number to
42
# always appear in exception tracebacks (by default, this is a runtime
43
# setting in Cython which causes some overhead every time an exception
44
# is raised).
45
extra_compile_args = ["-fno-strict-aliasing", "-DCYTHON_CLINE_IN_TRACEBACK=1"]
46
extra_link_args = [ ]
47
48
DEVEL = False
49
if DEVEL:
50
extra_compile_args.append('-ggdb')
51
52
53
class sage_build_cython(Command):
54
name = 'build_cython'
55
description = "compile Cython extensions into C/C++ extensions"
56
57
user_options = [
58
# TODO: Temporarily disabled since the value for this option is
59
# hard-coded; change as part of work on #21525
60
#('build-dir=', 'd',
61
# "directory for compiled C/C++ sources and header files"),
62
('profile', 'p',
63
"enable Cython profiling support"),
64
('parallel=', 'j',
65
"run cythonize in parallel with N processes"),
66
('force=', 'f',
67
"force files to be cythonized even if the are not changed")
68
]
69
70
boolean_options = ['debug', 'profile', 'force']
71
72
built_distributions = None
73
74
def initialize_options(self):
75
self.extensions = None
76
self.build_base = None
77
self.build_dir = None
78
79
# Always have Cython produce debugging info by default, unless
80
# SAGE_DEBUG=no explicitly
81
self.debug = True
82
self.profile = None
83
self.parallel = None
84
self.force = None
85
86
self.cython_directives = None
87
self.compile_time_env = None
88
89
self.build_lib = None
90
self.cythonized_files = None
91
92
def finalize_options(self):
93
self.extensions = self.distribution.ext_modules
94
95
# Let Cython generate its files in the "cythonized"
96
# subdirectory of the build_base directory.
97
self.set_undefined_options('build', ('build_base', 'build_base'))
98
self.build_dir = os.path.join(self.build_base, "cythonized")
99
100
# Inherit some options from the 'build_ext' command if possible
101
# (this in turn implies inheritance from the 'build' command)
102
inherit_opts = [('build_lib', 'build_lib'),
103
('debug', 'debug'),
104
('force', 'force')]
105
106
# Python 3.5 now has a parallel option as well
107
inherit_opts.append(('parallel', 'parallel'))
108
109
self.set_undefined_options('build_ext', *inherit_opts)
110
111
# Always produce debugging output unless SAGE_DEBUG=no is given
112
# explicitly
113
self.debug = os.environ.get('SAGE_DEBUG', None) != 'no'
114
115
if self.debug:
116
log.info('Enabling Cython debugging support')
117
118
if self.profile is None:
119
self.profile = os.environ.get('SAGE_PROFILE') == 'yes'
120
121
if self.profile:
122
log.info('Enabling Cython profiling support')
123
124
if self.parallel is None:
125
self.parallel = os.environ.get('SAGE_NUM_THREADS', '0')
126
127
try:
128
self.parallel = int(self.parallel)
129
except ValueError:
130
raise ValueError("parallel should be an integer")
131
132
try:
133
import Cython
134
except ImportError:
135
raise ImportError(
136
"Cython must be installed and importable in order to run "
137
"the cythonize command")
138
139
self.cython_directives = compiler_directives(self.profile)
140
self.compile_time_env = compile_time_env_variables()
141
142
# We check the Cython version and some relevant configuration
143
# options from the earlier build to see if we need to force a
144
# recythonization. If the version or options have changed, we
145
# must recythonize all files.
146
self._version_file = os.path.join(self.build_dir, '.cython_version')
147
self._version_stamp = json.dumps({
148
'version': Cython.__version__,
149
'debug': self.debug,
150
'directives': self.cython_directives,
151
'compile_time_env': self.compile_time_env,
152
}, sort_keys=True)
153
154
# Read an already written version file if it exists and compare to the
155
# current version stamp
156
try:
157
if open(self._version_file).read() == self._version_stamp:
158
force = False
159
else:
160
# version_file exists but its contents are not what we
161
# want => recythonize all Cython code.
162
force = True
163
# In case this cythonization is interrupted, we end up
164
# in an inconsistent state with C code generated by
165
# different Cython versions or with different options.
166
# To ensure that this inconsistent state will be fixed,
167
# we remove the version_file now to force a
168
# recythonization the next time we build Sage.
169
os.unlink(self._version_file)
170
except OSError:
171
# Most likely, the version_file does not exist
172
# => (re)cythonize all Cython code.
173
force = True
174
175
# If the --force flag was given at the command line, always force;
176
# otherwise use what we determined from reading the version file
177
if self.force is None:
178
self.force = force
179
180
def get_cythonized_package_files(self):
181
"""
182
Return a list of files found in the Sage sources and/or Cythonize
183
output directory that should be installed with Python packages (a la
184
``package_files``).
185
"""
186
187
if self.cythonized_files is not None:
188
return self.cythonized_files
189
190
self.cythonized_files = list(find_extra_files(
191
".", ["sage"], self.build_dir, [],
192
distributions=self.built_distributions).items())
193
log.debug(f"cythonized_files = {self.cythonized_files}")
194
195
return self.cythonized_files
196
197
def run(self):
198
"""
199
Call ``cythonize()`` to replace the ``ext_modules`` with the
200
extensions containing Cython-generated C code.
201
"""
202
from sage.env import (cython_aliases, sage_include_directories)
203
# Set variables used in self.create_extension
204
from ..library_order import library_order
205
self.library_order = library_order
206
# Search for dependencies in the source tree and add to the list of include directories
207
self.sage_include_dirs = sage_include_directories(use_sources=True)
208
209
from Cython.Build import cythonize
210
import Cython.Compiler.Options
211
212
Cython.Compiler.Options.embed_pos_in_docstring = True
213
214
log.info("Updating Cython code....")
215
t = time.time()
216
217
from sage.misc.package_dir import cython_namespace_package_support
218
219
with cython_namespace_package_support():
220
extensions = cythonize(
221
self.extensions,
222
nthreads=self.parallel,
223
build_dir=self.build_dir,
224
force=self.force,
225
aliases=cython_aliases(),
226
compiler_directives=self.cython_directives,
227
compile_time_env=self.compile_time_env,
228
create_extension=self.create_extension,
229
# Debugging
230
gdb_debug=self.debug,
231
output_dir=os.path.join(self.build_lib, "sage"),
232
# Disable Cython caching, which is currently too broken to
233
# use reliably: https://github.com/sagemath/sage/issues/17851
234
cache=False,
235
)
236
237
# We use [:] to change the list in-place because the same list
238
# object is pointed to from different places.
239
self.extensions[:] = extensions
240
241
log.info("Finished Cythonizing, time: %.2f seconds." % (time.time() - t))
242
243
with open(self._version_file, 'w') as f:
244
f.write(self._version_stamp)
245
246
# Finally, copy relevant cythonized files from build/cythonized
247
# tree into the build-lib tree
248
for (dst_dir, src_files) in self.get_cythonized_package_files():
249
dst = os.path.join(self.build_lib, dst_dir)
250
self.mkpath(dst)
251
for src in src_files:
252
self.copy_file(src, dst, preserve_mode=False)
253
254
def create_extension(self, template, kwds):
255
"""
256
Create a distutils Extension given data from Cython.
257
258
This adjust the ``kwds`` in the following ways:
259
260
- Make everything depend on *this* setup.py file
261
262
- Add dependencies on header files for certain libraries
263
264
- Sort the libraries according to the library order
265
266
- Add some default compile/link args and directories
267
268
- Choose C99 standard for C code and C++11 for C++ code
269
270
- Drop -std=c99 and similar from C++ extensions
271
272
- Ensure that each flag, library, ... is listed at most once
273
"""
274
lang = kwds.get('language', 'c')
275
cplusplus = (lang == "c++")
276
277
# Libraries: sort them
278
libs = kwds.get('libraries', [])
279
kwds['libraries'] = sorted(set(libs),
280
key=lambda lib: self.library_order.get(lib, 0))
281
282
# Dependencies: add setup.py and lib_headers
283
depends = kwds.get('depends', []) + [self.distribution.script_name]
284
for lib, headers in lib_headers.items():
285
if lib in libs:
286
depends += headers
287
kwds['depends'] = depends # These are sorted and uniq'ed by Cython
288
289
# Process extra_compile_args
290
cflags = []
291
for flag in kwds.get('extra_compile_args', []):
292
if flag.startswith("-std="):
293
if cplusplus and "++" not in flag:
294
continue # Skip -std=c99 and similar for C++
295
cflags.append(flag)
296
cflags = extra_compile_args + cflags
297
kwds['extra_compile_args'] = stable_uniq(cflags)
298
299
# Process extra_link_args
300
ldflags = kwds.get('extra_link_args', []) + extra_link_args
301
kwds['extra_link_args'] = stable_uniq(ldflags)
302
303
# Process library_dirs
304
lib_dirs = kwds.get('library_dirs', [])
305
kwds['library_dirs'] = stable_uniq(lib_dirs)
306
307
# Process include_dirs
308
inc_dirs = kwds.get('include_dirs', []) + self.sage_include_dirs + [self.build_dir]
309
kwds['include_dirs'] = stable_uniq(inc_dirs)
310
311
from Cython.Build.Dependencies import default_create_extension
312
313
return default_create_extension(template, kwds)
314
315