# -*- coding: utf-8 -*- from __future__ import absolute_import, division import argparse import os import platform import re import shutil import subprocess import sys import warnings import setuptools from distutils.core import Command from Cython.Distutils import build_ext as _build_ext __package__ = 'pydriver' # set to True to skip automatic PCL_HELPER compilation (in this case you have to compile it manually before invoking setup.py) SKIP_PCL_HELPER = False if platform.system() == 'Windows': # requires manual compilation on Windows SKIP_PCL_HELPER = True # current working directory (directory of setup.py) cwd = os.path.abspath(os.path.dirname(__file__)) # pcl_helper directories pcl_helper_dir = os.path.join(__package__, 'pcl', 'pcl_helper') pcl_helper_dir_build = os.path.join(pcl_helper_dir, 'build') pcl_helper_dir_lib = os.path.join(pcl_helper_dir, 'lib') # version.py file path version_py_path = os.path.join(cwd, __package__, 'version.py') # source code template for version.py version_py_src = """# this file was created automatically by setup.py __version__ = '{version}' __version_info__ = {{ 'full': __version__, 'short': '.'.join(__version__.split('.')[:2]) }} """ def read(fname): return open(os.path.join(cwd, fname)).read() def update_version_py(): """Update version.py using "git describe" command""" if not os.path.isdir('.git'): print('This does not appear to be a Git repository, leaving version.py unchanged.') return False try: describe_output = subprocess.check_output(['git', 'describe', '--long', '--dirty']).decode('ascii').strip() except: print('Unable to run Git, leaving version.py unchanged.') return False # output looks like <version tag>-<commits since tag>-g<hash> and can end with '-dirty', e.g. v0.1.0-14-gd9f10e2-dirty # our version tags look like 'v0.1.0' or 'v0.1' and optionally additional segments (e.g. v0.1.0rc1), see PEP 0440 describe_parts = re.match('^v([0-9]+\.[0-9]+(?:\.[0-9]+)?\S*)-([0-9]+)-g([0-9a-f]+)(?:-(dirty))?$', describe_output) assert describe_parts is not None, 'Unexpected output from "git describe": {}'.format(describe_output) version_tag, n_commits, commit_hash, dirty_flag = describe_parts.groups() version_parts = re.match('^([0-9]+)\.([0-9]+)(?:\.([0-9]+))?(\S*)$', version_tag) assert version_parts is not None, 'Unexpected version format: {}'.format(version_tag) version_major, version_minor, version_micro, version_segments = version_parts.groups() version_major = int(version_major) version_minor = int(version_minor) version_micro = int(version_micro) if version_micro is not None else 0 n_commits = int(n_commits) if dirty_flag is not None: print('WARNING: Uncommitted changes detected.') if n_commits > 0: # non-exact match, dev version version_micro += 1 version_segments += '.dev{}+{}'.format(n_commits, commit_hash) # final version string if version_micro > 0: version = '{}.{}.{}{}'.format(version_major, version_minor, version_micro, version_segments) else: version = '{}.{}{}'.format(version_major, version_minor, version_segments) with open(version_py_path, 'w') as f: f.write(version_py_src.format(version=version)) print('Set version to: {}'.format(version)) # success return True # update version.py (if we're in a Git repository) update_version_py() # "import" version information without importing the package exec(open(version_py_path).read()) class build_pcl_helper(Command): description = 'build pcl_helper library (inplace)' user_options = [] def initialize_options(self): self.cwd_pcl_helper_dir_build = None def finalize_options(self): # build inplace self.cwd_pcl_helper_dir_build = os.path.join(cwd, pcl_helper_dir_build) def run(self): # create build dir if it doesn't exist if not os.path.exists(self.cwd_pcl_helper_dir_build): os.makedirs(self.cwd_pcl_helper_dir_build) # build pcl_helper if platform.system() == 'Windows': self._build_pcl_helper_windows(self.cwd_pcl_helper_dir_build) else: self._build_pcl_helper_linux(self.cwd_pcl_helper_dir_build) def _build_pcl_helper_linux(self, build_dir): subprocess.check_call(['cmake', '..'], cwd=build_dir) subprocess.check_call('make', cwd=build_dir) def _build_pcl_helper_windows(self, build_dir): raise NotImplementedError class build_ext(_build_ext): user_options = _build_ext.user_options + [ ('skip-pcl-helper', None, 'skip pcl_helper compilation (assume manual compilation)'), ] boolean_options = _build_ext.boolean_options + ['skip-pcl-helper'] def initialize_options(self): _build_ext.initialize_options(self) # don't skip pcl helper by default self.skip_pcl_helper = False # pcl_helper location in source directory self.cwd_pcl_helper_dir_lib = None # pcl_helper location in package build directory self.build_pcl_helper_dir_lib = None def finalize_options(self): _build_ext.finalize_options(self) # prevent numpy from thinking it is still in its setup process: __builtins__.__NUMPY_SETUP__ = False import numpy as np self.include_dirs.append(np.get_include()) # finalize pcl_helper directories self.cwd_pcl_helper_dir_lib = os.path.join(cwd, pcl_helper_dir_lib) self.build_pcl_helper_dir_lib = os.path.join(self.build_lib, pcl_helper_dir_lib) # check global flag SKIP_PCL_HELPER self.skip_pcl_helper = self.skip_pcl_helper or SKIP_PCL_HELPER def build_extensions(self, *args, **kwargs): compiler_type = self.compiler.compiler_type if compiler_type not in extra_args: compiler_type = 'unix' # probably some unix-like compiler # merge compile and link arguments with global arguments for current compiler for e in self.extensions: e.extra_compile_args = list(set(e.extra_compile_args + extra_args[compiler_type]['extra_compile_args'])) e.extra_link_args = list(set(e.extra_link_args + extra_args[compiler_type]['extra_link_args'])) _build_ext.build_extensions(self, *args, **kwargs) def run(self): if not self.skip_pcl_helper: # build pcl_helper first try: self.run_command('build_pcl_helper') except: print('Error: pcl_helper could not be compiled automatically') print('Please compile pcl_helper manually (see %s/pcl/pcl_helper/README.rst for instructions)' % __package__ + \ ' and set SKIP_PCL_HELPER in setup.py to True.') raise # copy pcl_helper library to package build directory self.copy_tree(self.cwd_pcl_helper_dir_lib, self.build_pcl_helper_dir_lib) _build_ext.run(self) def get_outputs(self): # add contents of pcl_helper library directory to outputs (so they can be uninstalled) outputs = [] for dirpath, dirnames, filenames in os.walk(self.build_pcl_helper_dir_lib): outputs.extend([os.path.join(dirpath, f) for f in filenames]) return _build_ext.get_outputs(self) + outputs class CleanCommand(Command): """Custom clean command to tidy up the project root.""" user_options = [] def initialize_options(self): pass def finalize_options(self): pass def run(self): self._remove_dirs('__pycache__') self._remove_dir(cwd, 'build') self._remove_dir(cwd, 'build_c') self._remove_dir(cwd, 'dist') self._remove_dir(cwd, '.eggs') self._remove_dir(cwd, '{}.egg-info'.format(__package__)) self._remove_dir(cwd, pcl_helper_dir_build) self._remove_dir(cwd, pcl_helper_dir_lib) self._remove_files('pyc') self._remove_files('pyo') self._remove_files('pyd') self._remove_files('so') def _remove_dirs(self, dirname, parent_dir=None): if parent_dir is None: full_parent_dir = cwd else: full_parent_dir = os.path.join(cwd, parent_dir) matches = [] for dirpath, dirnames, filenames in os.walk(full_parent_dir): matches.extend([os.path.join(dirpath, d) for d in dirnames if d==dirname]) for d in matches: self._remove_dir(d) def _remove_dir(self, *args): dirpath = os.path.abspath(os.path.join(*args)) # sanity checks if not os.path.exists(dirpath): # nothing to do return if not os.path.isdir(dirpath): print('"{}" is not a directory, aborting...'.format(dirpath)) sys.exit() path_check = True if not dirpath.startswith(cwd): path_check = False if path_check and len(dirpath) > len(cwd): # first character after cwd should be a slash or a backslash if dirpath[len(cwd)] != os.sep: path_check = False if not path_check: print('The directory "{}" appears to be outside of main directory ({}), aborting...'.format(dirpath, cwd)) sys.exit() # all sanity checks ok if not os.path.islink(dirpath): print('Removing directory: ' + dirpath) shutil.rmtree(dirpath, ignore_errors=True) else: print("Can't remove symlink to directory: " + dirpath) def _remove_files(self, ext, parent_dir=None): if parent_dir is None: full_parent_dir = cwd else: full_parent_dir = os.path.join(cwd, parent_dir) matches = [] for dirpath, dirnames, filenames in os.walk(full_parent_dir): matches.extend([os.path.join(dirpath, f) for f in filenames if f.endswith('.'+ext)]) for f in matches: self._remove_file(f) def _remove_file(self, *args): filepath = os.path.abspath(os.path.join(*args)) # sanity checks if not os.path.exists(filepath): # nothing to do return if not os.path.isfile(filepath): print('"{}" is not a file, aborting...'.format(filepath)) sys.exit() filepath_dir = os.path.abspath(os.path.dirname(filepath)) path_check = True if not filepath_dir.startswith(cwd): path_check = False if path_check and len(filepath_dir) > len(cwd): # first character after cwd should be a slash or a backslash if filepath_dir[len(cwd)] != os.sep: path_check = False if not path_check: print('The file "{}" appears to be outside of main directory ({}), aborting...'.format(filepath, cwd)) sys.exit() # all sanity checks ok print('Removing file: ' + filepath) os.remove(filepath) class lazy_cythonize(list): # cythonize only if needed (e.g. not for "clean" command) def __init__(self, extensions, *args, **kwargs): self._list = None self.extensions = extensions self.args = args self.kwargs = kwargs def c_list(self): if self._list is None: self._list = self._cythonize() return self._list def __iter__(self): for e in self.c_list(): yield e def __getitem__(self, ii): return self.c_list()[ii] def __len__(self): return len(self.c_list()) def _cythonize(self): from Cython.Build import cythonize return cythonize(self.extensions, *self.args, **self.kwargs) # setup argument parser parser = argparse.ArgumentParser( description = '%s setup script, basic install: python setup.py install' % __package__, epilog = 'Other arguments will be passed to setuptools, use --help-setup for more information.', ) # add arguments which we will parse and pass to setuptools parser.add_argument('command', nargs = '?', help = 'command to pass to setuptools, use "install" to install package') parser.add_argument('--debug', '-g', action = 'store_true', help = 'compile/link with debugging information') parser.add_argument('--force', '-f', action = 'store_true', help = 'forcibly build everything (ignore file timestamps)') parser.add_argument('--help-setup', action = 'store_true', help = 'show setuptools help and exit') # add own arguments parser.add_argument('--annotate', action = 'store_true', help = 'let Cython generate HTML files with performance information') parser.add_argument('--cython-build-dir', default = 'build_c', help = 'directory for C/C++ sources and HTML files generated by Cython (default: build_c)') parser.add_argument('--inplace', action = 'store_true', help = 'build inplace') parser.add_argument('--no-openmp', dest = 'openmp', action = 'store_false', help = 'compile/link without OpenMP support') parser.add_argument('--profile', action = 'store_true', help = 'enable profiling with cProfile') parser.add_argument('--skip-pcl-helper', action = 'store_true', help = 'skip pcl_helper compilation (assume manual compilation)') # parse command line arguments cmdargs, unknown_args = parser.parse_known_args() if cmdargs.help_setup: # show setuptools help and exit sys.argv = [sys.argv[0], '--help'] setuptools.setup() sys.exit() # construct new command line arguments for setuptools # leave the script name setuptools_argv = [sys.argv[0]] # pass setuptools options which we already have parsed if cmdargs.command: setuptools_argv.append(cmdargs.command) if cmdargs.force: setuptools_argv.append('--force') if cmdargs.debug: setuptools_argv.append('--debug') # add all unknown args setuptools_argv += unknown_args # replace sys.argv by arguments which will be passed to setuptools sys.argv = setuptools_argv # initialize setuptools arguments setup_args = { 'name': __package__, 'version': __version__, 'url': 'http://github.com/lpltk/pydriver', 'license': 'MIT', 'author': 'Leonard Plotkin', 'author_email': 'git@leonard-plotkin.de', 'description': 'A framework for training and evaluating object detectors and classifiers in road traffic environment.', 'long_description': read('README.rst'), 'zip_safe': False, 'package_dir': {__package__: __package__}, 'packages': setuptools.find_packages(), 'package_data': {__package__+'.pcl': ['pcl_helper/lib/*']}, 'include_package_data': True, 'platforms': 'any', 'setup_requires': [ 'numpy>=1.8.1', 'cython>=0.22.1', ], 'install_requires': [ 'numpy>=1.8.1', 'cython>=0.22.1', 'scipy>=0.13.3', 'scikit-image', 'scikit-learn', ], 'classifiers': [ 'Development Status :: 3 - Alpha', 'Environment :: Console', 'Intended Audience :: Science/Research', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Cython', 'Programming Language :: C++', 'Topic :: Scientific/Engineering :: Image Recognition', ], } # common include directories setup_args['include_dirs'] = [ os.path.join(__package__, 'common'), # common constants, structs and typedefs ] extra_args = {} # arguments for unix compilers extra_args['unix'] = {'extra_compile_args': [], 'extra_link_args': []} if cmdargs.openmp: extra_args['unix']['extra_compile_args'].append('-fopenmp') extra_args['unix']['extra_link_args'].append('-fopenmp') if not cmdargs.debug: extra_args['unix']['extra_compile_args'].append('-O3') # maximum optimization extra_args['unix']['extra_compile_args'].append('-w') # suppress warnings # arguments for MSVC # PYDs compiled with MSVC and /openmp could not be embedded in a MSVC project (regardless of the project's /openmp setting). # They crash with the error R6034: An application has made an attempt to load the C runtime library incorrectly. # The problem seems to be that the resulting exe and pyd binaries had incompatible manifests. It could also be caused by using # the Visual Studio Express Edition which has limited support for extended features such as OpenMP. extra_args['msvc'] = {'extra_compile_args': [], 'extra_link_args': []} extra_args['msvc']['extra_compile_args'].append('/EHsc') # exception handling option if cmdargs.openmp: extra_args['msvc']['extra_compile_args'].append('/openmp') if not cmdargs.debug: extra_args['msvc']['extra_compile_args'].append('/O2') # optimize for speed extra_args['msvc']['extra_compile_args'].append('/W0') # suppress warnings # helper function for creating extensions with standard options def create_extension(*args, **kwargs): def add_package_path(kwarg_key): # prepend package directory to every element in list in kwargs[kwarg_key] kwargs[kwarg_key] = [os.path.join(__package__, e) for e in kwargs.get(kwarg_key, [])] # add package name to extension module name and paths args = (__package__ + '.' + args[0],) + args[1:] add_package_path('sources') add_package_path('include_dirs') add_package_path('library_dirs') # generate C++ code (instead of C) by default if 'language' not in kwargs: kwargs['language'] = 'c++' return setuptools.Extension(*args, **kwargs) extensions = [ # common create_extension( 'common.constants', sources = ['common/constants.pyx'], ), create_extension( 'common.functions', sources = ['common/functions.pyx'], ), # geometry create_extension( 'geometry.geometry', sources = ['geometry/geometry.pyx'], ), # stereo create_extension( 'stereo.stereo', sources = ['stereo/stereo.pyx'], ), # pcl create_extension( 'pcl.pcl', sources = ['pcl/pcl.pyx'], language = 'c++', include_dirs = ['pcl/pcl_helper'], libraries = ['pcl_helper'], library_dirs = ['pcl/pcl_helper/lib'], extra_link_args = ['-Wl,-rpath,$ORIGIN/pcl_helper/lib'] if platform.system() != 'Windows' else [], # handle paths in __init__.py on Windows ), # preprocessing create_extension( 'preprocessing.preprocessing', sources = ['preprocessing/preprocessing.pyx'], ), # keypoints create_extension( 'keypoints.base', sources = ['keypoints/base.pyx'], ), create_extension( 'keypoints.harris', sources = ['keypoints/harris.pyx'], ), create_extension( 'keypoints.iss', sources = ['keypoints/iss.pyx'], ), # features create_extension( 'features.shot', sources = ['features/shot.pyx'], ), create_extension( 'features.base', sources = ['features/base.pyx'], ), # detectors create_extension( 'detectors.vocabularies', sources = ['detectors/vocabularies.pyx'], ), create_extension( 'detectors.detectors', sources = ['detectors/detectors.pyx'], ), ] # setup commands setup_args['cmdclass'] = { 'build_pcl_helper': build_pcl_helper, 'build_ext': build_ext, 'clean': CleanCommand, } # keyword arguments for cythonize() cython_kwargs = { 'build_dir': cmdargs.cython_build_dir, # build directory 'compiler_directives': { 'embedsignature': True, # embed signatures for documentation tools }, } if cmdargs.force: cython_kwargs['force'] = True # enforce full recompilation if cmdargs.annotate: cython_kwargs['annotate'] = True # generate HTML reports (in cmdargs.cython_build_dir) if cmdargs.profile: if cmdargs.debug: warnings.warn(UserWarning('You should only profile in release mode with full optimization.')) cython_kwargs['profile'] = True # globally enable profiling with cProfile setup_args['ext_modules'] = lazy_cythonize(extensions, **cython_kwargs) setup_args['options'] = { 'build_ext': { 'inplace': cmdargs.inplace, 'skip_pcl_helper': cmdargs.skip_pcl_helper, }, } try: setuptools.setup(**setup_args) except Exception as e: print('Compilation errors encountered, aborting...') print('Exception information:') print(e) sys.exit(1)