diff --git a/README.rst b/README.rst index 75336cabdac7fd98eacb214cfa14b8802587026b..d054bdcc5a02c3eb23531d746fc06beff2e8244b 100644 --- a/README.rst +++ b/README.rst @@ -2,9 +2,9 @@ Python/C++ Bob Extension Building Support =========================================== -This package provides a ``pkg-config`` bridge for Python/C++ extensions for -`Bob <http://www.idiap.ch/software/bob/>`_. You use this package by including it -in the ``setup_requires`` field of your ``setup.py`` file. +This package provides a simple mechanims for building Python/C++ extensions for +`Bob <http://www.idiap.ch/software/bob/>`_. You use this package by including +it in your ``setup.py`` file. Building with ``zc.buildout`` is possible using the ``develop`` recipe in `xbob.buildout <http://pypi.python.org/pypi/xbob.buildout>`_. Follow the @@ -16,7 +16,10 @@ Preparing for C++ Compilation Creating C++/Python bindings should be trivial. Firstly, edit your ``setup.py`` so that you include the following:: + from setuptools import dist + dist.Distribution(dict(setup_requires=['xbob.extension'])) from xbob.extension import Extension + ... setup( @@ -37,9 +40,9 @@ so that you include the following:: "xbob/myext/ext/file2.cpp", "xbob/myext/ext/main.cpp", ], - pkgconfig = [ #bob modules you depend on - 'bob-math', - 'bob-sp', + packages = [ #pkg-config modules to append + 'blitz>=0.10', + 'bob-core', ], include_dirs = [ #optionally, include directories "xbob/myext/ext/headers/", @@ -52,17 +55,16 @@ so that you include the following:: ) These modifications will allow you to compile extensions that are linked -against the named ``pkg-config`` modules. You can specify the modules of -Bob you want to link against. You **don't** have to specify ``bob-python``, -which is automatically added. Furthermore, you can specify any ``pkg-config`` -module and that will be linked in (for example, ``opencv``). Other modules and -options can be set manually using `the standard options for python extensions +against the named ``pkg-config`` modules. Other modules and options can be set +manually using `the standard options for python extensions <http://docs.python.org/2/extending/building.html>`_. To hook-in the building on the package through ``zc.buildout``, add the following section to your ``buildout.cfg``:: [xbob.myext] recipe = xbob.buildout:develop + verbose = true ;enables command-line verbosity + debug = true ;compiles the module in debug mode If you need to build multiple eggs, you will need **one entry per project** on your ``buildout.cfg``. This includes, possibly, dependent projects. Currently, diff --git a/setup.py b/setup.py index 8aaffb4fc7e52b75c82d335ab15604d8bb6cdef1..4c99cce6babc4fdbd06aa89ae50c14b650218486 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ from setuptools import setup, find_packages setup( name="xbob.extension", - version="0.2.1", + version="0.3.0a0", description="Helps projects building Python/C++ extensions for Bob", license="GPLv3", author='Andre Anjos', @@ -28,6 +28,7 @@ setup( install_requires=[ 'setuptools', + 'pypkg', ], classifiers = [ diff --git a/xbob/extension/__init__.py b/xbob/extension/__init__.py index 851652769a6443a3e3d4dd21acf968d9c8acaade..a5e343d2d8c977f34ec0e47bc441dea2412675b0 100644 --- a/xbob/extension/__init__.py +++ b/xbob/extension/__init__.py @@ -6,111 +6,9 @@ """A custom build class for Bob/Python extensions """ -import os -import string -import subprocess -from distutils.extension import Extension as ExtensionBase -from setuptools.command.build_ext import build_ext as build_ext_base - -def pkgconfig(package): - - def uniq(seq, idfun=None): - # order preserving - if idfun is None: - def idfun(x): return x - seen = {} - result = [] - for item in seq: - marker = idfun(item) - # in old Python versions: - # if seen.has_key(marker) - # but in new ones: - if marker in seen: continue - seen[marker] = 1 - result.append(item) - return result - - cmd = [ - 'pkg-config', - '--modversion', - package, - ] - - proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) - - output = proc.communicate()[0] - if isinstance(output, bytes) and not isinstance(output, str): - output = output.decode('utf8') - - if proc.returncode != 0: - raise RuntimeError("PkgConfig did not find package %s. Output:\n%s" % \ - (package, output.strip())) - - version = output.strip() - - flag_map = { - '-I': 'include_dirs', - '-L': 'library_dirs', - '-l': 'libraries', - } - - cmd = [ - 'pkg-config', - '--libs', - '--cflags', - package, - ] - - proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) - - output = proc.communicate()[0] - if isinstance(output, bytes) and not isinstance(output, str): - output = output.decode('utf8') - - if proc.returncode != 0: - raise RuntimeError("PkgConfig did not find package %s. Output:\n%s" % \ - (package, output.strip())) - - kw = {} - - for token in output.split(): - if token[:2] in flag_map: - kw.setdefault(flag_map.get(token[:2]), []).append(token[2:]) - - elif token[0] == '-': # throw others to extra_link_args - kw.setdefault('extra_compile_args', []).append(token) - - else: # these are maybe libraries - import os - if os.path.exists(token): - dirname = os.path.dirname(token) - if dirname not in kw.get('library_dirs', []): - kw.setdefault('library_dirs', []).append(dirname) - - bname = os.path.splitext(os.path.basename(token))[0][3:] - if bname not in kw.get('libraries', []): - kw.setdefault('libraries', []).append(bname) - - for k, v in kw.items(): # remove duplicated - kw[k] = uniq(v) - - try: - # Python 3 style - maketrans = ''.maketrans - except AttributeError: - # fallback for Python 2 - from string import maketrans - - # adds version and HAVE flags - PACKAGE = package.upper().translate(maketrans(" -", "__")) - kw['define_macros'] = [ - ('HAVE_%s' % PACKAGE, '1'), - ('%s_VERSION' % PACKAGE, '"%s"' % version), - ] - - return kw +import platform +from pypkg import pkgconfig +from distutils.extension import Extension as DistutilsExtension def uniq(seq): """Uniqu-fy preserving order""" @@ -119,7 +17,63 @@ def uniq(seq): seen_add = seen.add return [ x for x in seq if x not in seen and not seen_add(x)] -class Extension(ExtensionBase): +def check_packages(packages): + """Checks if the requirements for the given packages are satisfied. + + Raises a :py:class:`RuntimeError` in case requirements are not satisfied. + This means either not finding a package if no version number is specified or + veryfing that the package version does not match the required version by the + builder. + + Package requirements can be set like this:: + + "pkg > VERSION" + + In this case, the package version should be greater than the given version + number. Comparisons are done using :py:mod:`distutils.version.LooseVersion`. + You can use other comparators such as ``<``, ``<=``, ``>=`` or ``==``. If no + version number is given, then we only require that the package is installed. + """ + + from re import split + + retval = [] + + for requirement in uniq(packages): + + splitreq = split(r'\s*(?P<cmp>[<>=]+)\s*', requirement) + + if len(splitreq) == 1: # just package name + + p = pkgconfig(splitreq[0]) + + elif len(splitreq) == 3: # package + version number + + p = pkgconfig(splitreq[0]) + + if splitreq[1] == '>': + assert p > splitreq[2], "%s version is not > `%s'" % (p, splitreq[2]) + elif splitreq[1] == '>=': + assert p >= splitreq[2], "%s version is not >= `%s'" % (p, splitreq[2]) + elif splitreq[1] == '<': + assert p < splitreq[2], "%s version is not < `%s'" % (p, splitreq[2]) + elif splitreq[1] == '<=': + assert p <= splitreq[2], "%s version is not <= `%s'" % (p, splitreq[2]) + elif splitreq[1] == '==': + assert p <= splitreq[2], "%s version is not == `%s'" % (p, splitreq[2]) + else: + raise RuntimeError("cannot parse requirement `%s'", requirement) + + else: + + raise RuntimeError("cannot parse requirement `%s'", requirement) + + retval.append(p) + + return retval + + +class Extension(DistutilsExtension): """Extension building with Bob/Python bindings. See the documentation for :py:class:`distutils.extension.Extension` for more @@ -143,102 +97,59 @@ class Extension(ExtensionBase): registered packages as a dependencies. """ - modules = ['bob-python'] + packages = [] - if 'pkgconfig' in kwargs and kwargs['pkgconfig']: - if isinstance(kwargs['pkgconfig'], str): - modules.append(kwargs['pkgconfig']) + if 'packages' in kwargs and kwargs['packages']: + if isinstance(kwargs['packages'], str): + packages.append(kwargs['packages']) else: - modules.extend(kwargs['pkgconfig']) + packages.extend(kwargs['packages']) - if 'pkgconfig' in kwargs: del kwargs['pkgconfig'] + if 'packages' in kwargs: del kwargs['packages'] - # Only one instance of each - modules = uniq(modules) + # Check all requirements + pkgs = check_packages(packages) # Mixing parameters = { - 'include_dirs': [], + 'define_macros': [], + 'extra_compile_args': ['-std=c++11'], 'library_dirs': [], + 'runtime_library_dirs': [], 'libraries': [], - 'define_macros': [], } - for m in modules: - config = pkgconfig(m) - for key in parameters.keys(): - if key in config and config[key]: - parameters[key].extend(config[key]) + # Compilation options + if platform.system() == 'Darwin': + parameters['extra_compile_args'] += ['-Wno-#warnings'] - # Reset the include_dirs to use '-isystem' - include_dirs = ['-isystem%s' % k for k in parameters['include_dirs']] - if 'extra_compile_args' in kwargs: - kwargs['extra_compile_args'].extend(include_dirs) - else: - kwargs['extra_compile_args'] = include_dirs - del parameters['include_dirs'] + for pkg in pkgs: + + # Adds parameters for each package, in order + parameters['define_macros'] += pkg.package_macros() + + # Include directories are added with a special path + for k in pkg.include_directories(): + parameters['extra_compile_args'].extend(['-isystem', k]) + + parameters['define_macros'] += pkg.package_macros() + parameters['library_dirs'] += pkg.library_directories() + parameters['runtime_library_dirs'] += pkg.library_directories() + parameters['libraries'] += pkg.libraries() # Filter and make unique for key in parameters.keys(): - parameters[key] = uniq(parameters[key]) # Tune input parameters if they were set if key in kwargs: kwargs[key].extend(parameters[key]) else: kwargs[key] = parameters[key] - # Set the runtime_library_dirs specially - if 'runtime_library_dirs' in kwargs: - kwargs['runtime_library_dirs'].extend(parameters('runtime_library_dirs')) - else: - kwargs['runtime_library_dirs'] = parameters['library_dirs'] + if key in ('extra_compile_args'): continue + + kwargs[key] = uniq(kwargs[key]) # Make sure the language is correctly set to C++ kwargs['language'] = 'c++' # Run the constructor for the base class - ExtensionBase.__init__(self, *args, **kwargs) - -class build_ext(build_ext_base): - '''Customized extension to build bob.python bindings in the expected way''' - - linker_is_smart = None - - def __init__(self, *args, **kwargs): - build_ext_base.__init__(self, *args, **kwargs) - - def build_extension(self, ext): - '''Concretely builds the extension given as input''' - - def linker_can_remove_symbols(linker): - '''Tests if the `ld` linker can remove unused symbols from linked - libraries. In this case, use the --no-as-needed flag during link''' - - import tempfile - f, name = tempfile.mkstemp() - del f - - cmd = linker + ['-Wl,--no-as-needed', '-lm', '-o', name] - proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) - output = proc.communicate()[0] - if os.path.exists(name): os.unlink(name) - return True if proc.returncode == 0 else False - - def ld_ok(opt): - '''Tells if a certain option is a go for the linker''' - - if opt.find('-L') == 0: return False - return True - - # Some clean-up on the linker which is screwed up... - self.compiler.linker_so = [k for k in self.compiler.linker_so if ld_ok(k)] - - if self.linker_is_smart is None: - self.linker_is_smart = linker_can_remove_symbols(self.compiler.linker_so) - if self.linker_is_smart: self.compiler.linker_so += ['-Wl,--no-as-needed'] - - if hasattr(self.compiler, 'dll_libraries') and \ - self.compiler.dll_libraries is None: - self.compiler.dll_libraries = [] - - build_ext_base.build_extension(self, ext) + DistutilsExtension.__init__(self, *args, **kwargs)