|
11 | 11 | import shutil
|
12 | 12 | import warnings
|
13 | 13 |
|
| 14 | +try: |
| 15 | + BUILD_CACHE_DIR=None |
| 16 | + |
| 17 | + # uncomment to activate the build cache |
| 18 | + #BUILD_CACHE_DIR="/tmp/.pandas_build_cache/" |
| 19 | + |
| 20 | + if os.isdir(BUILD_CACHE_DIR): |
| 21 | + print("--------------------------------------------------------") |
| 22 | + print("BUILD CACHE ACTIVATED. be careful, this is experimental.") |
| 23 | + print("--------------------------------------------------------") |
| 24 | + else: |
| 25 | + BUILD_CACHE_DIR=None |
| 26 | +except : |
| 27 | + pass |
| 28 | + |
14 | 29 | # may need to work around setuptools bug by providing a fake Pyrex
|
15 | 30 | try:
|
16 | 31 | import Cython
|
|
87 | 102 |
|
88 | 103 | from distutils.extension import Extension
|
89 | 104 | from distutils.command.build import build
|
90 |
| -from distutils.command.build_ext import build_ext |
91 | 105 | from distutils.command.sdist import sdist
|
| 106 | +from distutils.command.build_ext import build_ext |
| 107 | + |
| 108 | +try: |
| 109 | + from Cython.Distutils import build_ext |
| 110 | + #from Cython.Distutils import Extension # to get pyrex debugging symbols |
| 111 | + cython=True |
| 112 | +except ImportError: |
| 113 | + cython=False |
92 | 114 |
|
93 | 115 | from os.path import splitext, basename, join as pjoin
|
94 | 116 |
|
@@ -314,38 +336,213 @@ def build_extensions(self):
|
314 | 336 | for ext in self.extensions:
|
315 | 337 | self.build_extension(ext)
|
316 | 338 |
|
| 339 | +class CompilationCacheMixin(object): |
| 340 | + def __init__(self,*args,**kwds): |
| 341 | + cache_dir=kwds.pop("cache_dir",BUILD_CACHE_DIR) |
| 342 | + self.cache_dir=cache_dir |
| 343 | + if not os.path.isdir(cache_dir): |
| 344 | + raise Exception("Error: path to Cache directory [%s] is not a dir"); |
| 345 | + |
| 346 | + def _copy_from_cache(self,hash,target): |
| 347 | + src=os.path.join(self.cache_dir,hash) |
| 348 | + if os.path.exists(src): |
| 349 | + # print("Cache HIT: asked to copy file %s in %s" % (src,os.path.abspath(target))) |
| 350 | + s="." |
| 351 | + for d in target.split(os.path.sep)[:-1]: |
| 352 | + s=os.path.join(s,d) |
| 353 | + if not os.path.exists(s): |
| 354 | + os.mkdir(s) |
| 355 | + shutil.copyfile(src,target) |
| 356 | + |
| 357 | + return True |
| 358 | + |
| 359 | + return False |
| 360 | + |
| 361 | + def _put_to_cache(self,hash,src): |
| 362 | + target=os.path.join(self.cache_dir,hash) |
| 363 | + # print( "Cache miss: asked to copy file from %s to %s" % (src,target)) |
| 364 | + s="." |
| 365 | + for d in target.split(os.path.sep)[:-1]: |
| 366 | + s=os.path.join(s,d) |
| 367 | + if not os.path.exists(s): |
| 368 | + os.mkdir(s) |
| 369 | + shutil.copyfile(src,target) |
| 370 | + |
| 371 | + def _hash_obj(self,obj): |
| 372 | + """ |
| 373 | + you should override this method to provide a sensible |
| 374 | + implementation of hashing functions for your intended objects |
| 375 | + """ |
| 376 | + try: |
| 377 | + return hash(obj) |
| 378 | + except: |
| 379 | + raise NotImplementedError("You must override this method") |
| 380 | + |
| 381 | + # this is missing in 2.5, mro will do the right thing |
| 382 | + def get_ext_fullpath(self, ext_name): |
| 383 | + """Returns the path of the filename for a given extension. |
| 384 | +
|
| 385 | + The file is located in `build_lib` or directly in the package |
| 386 | + (inplace option). |
| 387 | + """ |
| 388 | + import string |
| 389 | + # makes sure the extension name is only using dots |
| 390 | + all_dots = string.maketrans('/'+os.sep, '..') |
| 391 | + ext_name = ext_name.translate(all_dots) |
| 392 | + |
| 393 | + fullname = self.get_ext_fullname(ext_name) |
| 394 | + modpath = fullname.split('.') |
| 395 | + filename = self.get_ext_filename(ext_name) |
| 396 | + filename = os.path.split(filename)[-1] |
| 397 | + |
| 398 | + if not self.inplace: |
| 399 | + # no further work needed |
| 400 | + # returning : |
| 401 | + # build_dir/package/path/filename |
| 402 | + filename = os.path.join(*modpath[:-1]+[filename]) |
| 403 | + return os.path.join(self.build_lib, filename) |
| 404 | + |
| 405 | + # the inplace option requires to find the package directory |
| 406 | + # using the build_py command for that |
| 407 | + package = '.'.join(modpath[0:-1]) |
| 408 | + build_py = self.get_finalized_command('build_py') |
| 409 | + package_dir = os.path.abspath(build_py.get_package_dir(package)) |
| 410 | + |
| 411 | + # returning |
| 412 | + # package_dir/filename |
| 413 | + return os.path.join(package_dir, filename) |
| 414 | + |
| 415 | +class CompilationCacheExtMixin(CompilationCacheMixin): |
| 416 | + def __init__(self,*args,**kwds): |
| 417 | + CompilationCacheMixin.__init__(self,*args,**kwds) |
| 418 | + |
| 419 | + def _hash_file(self,fname): |
| 420 | + from hashlib import sha1 |
| 421 | + try: |
| 422 | + hash=sha1() |
| 423 | + hash.update(self.build_lib.encode('utf-8')) |
| 424 | + try: |
| 425 | + if sys.version_info[0] >= 3: |
| 426 | + import io |
| 427 | + f=io.open(fname,"rb") |
| 428 | + else: |
| 429 | + f=open(fname) |
| 430 | + |
| 431 | + first_line=f.readline() |
| 432 | + # ignore cython generation timestamp header |
| 433 | + if "Generated by Cython" not in first_line.decode('utf-8'): |
| 434 | + hash.update(first_line) |
| 435 | + hash.update(f.read()) |
| 436 | + return hash.hexdigest() |
| 437 | + |
| 438 | + except: |
| 439 | + raise |
| 440 | + return None |
| 441 | + finally: |
| 442 | + f.close() |
| 443 | + |
| 444 | + except IOError: |
| 445 | + return None |
| 446 | + |
| 447 | + def _hash_obj(self,ext): |
| 448 | + from hashlib import sha1 |
| 449 | + |
| 450 | + sources = ext.sources |
| 451 | + if sources is None or \ |
| 452 | + (not hasattr(sources,'__iter__') ) or \ |
| 453 | + isinstance(sources,str) or \ |
| 454 | + sys.version[0]==2 and isinstance(sources,unicode): #argh |
| 455 | + return False |
| 456 | + |
| 457 | + sources = list(sources) + ext.depends |
| 458 | + hash=sha1() |
| 459 | + try: |
| 460 | + for fname in sources: |
| 461 | + fhash=self._hash_file(fname) |
| 462 | + if fhash: |
| 463 | + hash.update(fhash.encode('utf-8')) |
| 464 | + except: |
| 465 | + return None |
| 466 | + |
| 467 | + return hash.hexdigest() |
| 468 | + |
| 469 | +class CachingBuildExt(build_ext,CompilationCacheExtMixin): |
| 470 | + def __init__(self,*args,**kwds): |
| 471 | + CompilationCacheExtMixin.__init__(self,*args,**kwds) |
| 472 | + kwds.pop("cache_dir",None) |
| 473 | + build_ext.__init__(self,*args,**kwds) |
| 474 | + |
| 475 | + def build_extension(self, ext,*args,**kwds): |
| 476 | + ext_path = self.get_ext_fullpath(ext.name) |
| 477 | + build_path = os.path.join(self.build_lib,os.path.basename(ext_path)) |
| 478 | + |
| 479 | + hash=self._hash_obj(ext) |
| 480 | + if hash and self._copy_from_cache(hash,ext_path): |
| 481 | + return |
| 482 | + |
| 483 | + build_ext.build_extension(self,ext,*args,**kwds) |
| 484 | + |
| 485 | + hash=self._hash_obj(ext) |
| 486 | + if os.path.exists(build_path): |
| 487 | + self._put_to_cache(hash,build_path) # build_ext |
| 488 | + if os.path.exists(ext_path): |
| 489 | + self._put_to_cache(hash,ext_path) # develop |
| 490 | + |
| 491 | + |
| 492 | + def cython_sources(self, sources, extension): |
| 493 | + import re |
| 494 | + cplus = self.cython_cplus or getattr(extension, 'cython_cplus', 0) or \ |
| 495 | + (extension.language and extension.language.lower() == 'c++') |
| 496 | + target_ext = '.c' |
| 497 | + if cplus: |
| 498 | + target_ext = '.cpp' |
| 499 | + |
| 500 | + for i,s in enumerate(sources): |
| 501 | + if not re.search("\.(pyx|pxi|pxd)$",s): |
| 502 | + continue |
| 503 | + ext_dir=os.path.dirname(s) |
| 504 | + ext_basename=re.sub("\.[^\.]+$","",os.path.basename(s)) |
| 505 | + ext_basename += target_ext |
| 506 | + target= os.path.join(ext_dir,ext_basename) |
| 507 | + hash=self._hash_file(s) |
| 508 | + sources[i]=target |
| 509 | + if hash and self._copy_from_cache(hash,target): |
| 510 | + continue |
| 511 | + build_ext.cython_sources(self,[s],extension) |
| 512 | + self._put_to_cache(hash,target) |
| 513 | + |
| 514 | + return sources |
| 515 | + |
| 516 | +class CythonCommand(build_ext): |
| 517 | + """Custom distutils command subclassed from Cython.Distutils.build_ext |
| 518 | + to compile pyx->c, and stop there. All this does is override the |
| 519 | + C-compile method build_extension() with a no-op.""" |
| 520 | + def build_extension(self, ext): |
| 521 | + pass |
| 522 | + |
| 523 | +class DummyBuildSrc(Command): |
| 524 | + """ numpy's build_src command interferes with Cython's build_ext. |
| 525 | + """ |
| 526 | + user_options = [] |
| 527 | + def initialize_options(self): |
| 528 | + self.py_modules_dict = {} |
| 529 | + def finalize_options(self): |
| 530 | + pass |
| 531 | + def run(self): |
| 532 | + pass |
| 533 | + |
317 | 534 | cmdclass = {'clean': CleanCommand,
|
318 | 535 | 'build': build}
|
319 |
| - |
320 |
| -try: |
321 |
| - from Cython.Distutils import build_ext |
322 |
| - #from Cython.Distutils import Extension # to get pyrex debugging symbols |
323 |
| - cython=True |
324 |
| -except ImportError: |
325 |
| - cython=False |
326 |
| - suffix = '.c' |
327 |
| - cmdclass['build_ext'] = CheckingBuildExt |
328 |
| -else: |
| 536 | +if cython: |
329 | 537 | suffix = '.pyx'
|
330 |
| - class CythonCommand(build_ext): |
331 |
| - """Custom distutils command subclassed from Cython.Distutils.build_ext |
332 |
| - to compile pyx->c, and stop there. All this does is override the |
333 |
| - C-compile method build_extension() with a no-op.""" |
334 |
| - def build_extension(self, ext): |
335 |
| - pass |
336 |
| - |
337 |
| - class DummyBuildSrc(Command): |
338 |
| - """ numpy's build_src command interferes with Cython's build_ext. |
339 |
| - """ |
340 |
| - user_options = [] |
341 |
| - def initialize_options(self): |
342 |
| - self.py_modules_dict = {} |
343 |
| - def finalize_options(self): |
344 |
| - pass |
345 |
| - def run(self): |
346 |
| - pass |
| 538 | + cmdclass['build_ext'] = build_ext |
| 539 | + if BUILD_CACHE_DIR: # use the cache |
| 540 | + cmdclass['build_ext'] = CachingBuildExt |
| 541 | +else: |
347 | 542 |
|
| 543 | + suffix = '.c' |
348 | 544 | cmdclass['build_src'] = DummyBuildSrc
|
| 545 | + |
349 | 546 | cmdclass['cython'] = CythonCommand
|
350 | 547 | cmdclass['build_ext'] = build_ext
|
351 | 548 | cmdclass['sdist'] = CheckSDist
|
|
0 commit comments