#!/usr/bin/env python
# vim: set fileencoding=utf-8 :
# Andre Anjos <andre.anjos@idiap.ch>
# Wed 16 Oct 10:08:42 2013 CEST
import os
import sys
import subprocess
import logging
from .utils import uniq, uniq_paths, find_executable, construct_search_paths
def call_pkgconfig(cmd, paths=None):
"""Calls pkg-config with a constructed PKG_CONFIG_PATH environment variable.
The PKG_CONFIG_PATH variable is constructed using
:any:`construct_search_paths`.
Parameters
----------
cmd : [str]
A list of commands to be given to pkg-config.
paths : [str]
A list of paths to be added to PKG_CONFIG_PATH.
Returns
-------
returncode : int
The exit status of pkg-config.
stdout : str
The stdout output of pkg-config.
stderr : str
The stderr output of pkg-config.
Raises
------
OSError
If the `pkg-config' executable is not found.
"""
# locates the pkg-config program
pkg_config = find_executable('pkg-config')
if not pkg_config:
raise OSError("Cannot find `pkg-config' - did you install it?")
pkg_path = construct_search_paths(
prefixes=paths, suffix=os.sep + 'lib' + os.sep + 'pkgconfig')
env = os.environ.copy()
var = os.pathsep.join(pkg_path)
old = env.get('PKG_CONFIG_PATH', False)
env['PKG_CONFIG_PATH'] = os.pathsep.join([var, old]) if old else var
# calls the program
cmd = pkg_config[:1] + [str(k) for k in cmd]
subproc = subprocess.Popen(
cmd,
env=env,
stderr=subprocess.PIPE,
stdout=subprocess.PIPE
)
logging.debug("Running `%s'" % (" ".join(cmd),))
stdout, stderr = subproc.communicate()
# handles py3k string conversion, if necessary
if isinstance(stdout, bytes) and not isinstance(stdout, str):
stdout = stdout.decode('utf8')
if isinstance(stderr, bytes) and not isinstance(stderr, str):
stderr = stderr.decode('utf8')
# always print the stdout
logger = logging.getLogger('pkgconfig')
for k in stdout.split('\n'):
if k: logger.debug(k)
return subproc.returncode, stdout, stderr
def version():
"""Determines the version of pkg-config which is installed"""
status, stdout, stderr = call_pkgconfig(['--version'])
if status != 0:
raise RuntimeError("pkg-config is not installed - please do it")
return stdout.strip()
[docs]class pkgconfig:
"""A class for capturing configuration information from pkg-config
Example usage:
.. doctest::
:options: +NORMALIZE_WHITESPACE +ELLIPSIS
>>> from bob.extension import pkgconfig
>>> blitz = pkgconfig('blitz')
>>> blitz.include_directories()
[...]
>>> blitz.library_directories()
[...]
If the package does not exist, a RuntimeError is raised. All calls to any
methods of a ``pkgconfig`` object are translated into a subprocess call that
queries for that specific information. If ``pkg-config`` fails, a
RuntimeError is raised.
"""
def __init__(self, name, paths=None):
"""Constructor
Parameters:
name
The name of the package of interest, as you would pass on the command
line
paths
Search paths to be added to the environment's PKG_CONFIG_PATH to search
for packages.
Equivalent command line version:
.. code-block:: sh
$ PKG_CONFIG_PATH=<paths> pkg-config <name>
"""
status, stdout, stderr = call_pkgconfig(['--modversion', name], paths)
if status != 0:
raise RuntimeError("pkg-config package `%s' was not found" % name)
self.name = name
self.version = stdout.strip()
self.paths = paths
def __xcall__(self, cmd):
"""Calls call_pkgconfig() with self.name and self.paths"""
return call_pkgconfig(cmd + [self.name], self.paths)
def __cmp__(self, other):
"""Compares this package with a version number
We create a new ``distutils.version.LooseVersion`` object out of your input
argument and then, compare it to our own version, returning the result.
Returns an integer smaller than zero if this package's version number is
smaller than the provided value. Returns zero in case of a match and
greater than zero in the other case.
"""
from distutils.version import LooseVersion
return cmp(self.version, LooseVersion(other))
def __ge__(self, other):
from distutils.version import LooseVersion
return LooseVersion(self.version) >= LooseVersion(other)
def __gt__(self, other):
from distutils.version import LooseVersion
return LooseVersion(self.version) > LooseVersion(other)
def __le__(self, other):
from distutils.version import LooseVersion
return LooseVersion(self.version) <= LooseVersion(other)
def __lt__(self, other):
from distutils.version import LooseVersion
return LooseVersion(self.version) < LooseVersion(other)
def __eq__(self, other):
from distutils.version import LooseVersion
return LooseVersion(self.version) == LooseVersion(other)
def __ne__(self, other):
from distutils.version import LooseVersion
return LooseVersion(self.version) != LooseVersion(other)
[docs] def include_directories(self):
"""Returns a pre-processed list containing include directories.
Equivalent command line version:
.. code-block:: sh
$ PKG_CONFIG_PATH=<paths> pkg-config --cflags-only-I <name>
"""
status, stdout, stderr = self.__xcall__(['--cflags-only-I'])
if status != 0:
raise RuntimeError("error querying --cflags-only-I for package `%s': %s" % (self.name, stderr))
retval = []
for token in stdout.split():
retval.append(token[2:])
return uniq(retval)
[docs] def cflags_other(self):
"""Returns a pre-processed dictionary containing compilation options.
Equivalent command line version:
.. code-block:: sh
$ PKG_CONFIG_PATH=<paths> pkg-config --cflags-only-other <name>
The returned dictionary contains two entries ``extra_compile_args`` and
``define_macros``. The ``define_macros`` entries are ready for deployment
in the ``setup()`` function of your package.
"""
status, stdout, stderr = self.__xcall__(['--cflags-only-other'])
if status != 0:
raise RuntimeError("error querying --cflags-only-other for package `%s': %s" % (self.name, stderr))
flag_map = {
'-D': 'define_macros',
}
kw = {}
for token in stdout.split():
if token[:2] in flag_map:
kw.setdefault(flag_map.get(token[:2]), []).append(token[2:])
else: # throw others to extra_link_args
kw.setdefault('extra_compile_args', []).append(token)
# make it uniq
for k, v in kw.items(): kw[k] = uniq(v)
# for macros, separate them so they can be plugged on C/C++ extensions
if 'define_macros' in kw:
for k, string in enumerate(kw['define_macros']):
if string.find('=') != -1:
kw['define_macros'][k] = string.split('=', 2)
else:
kw['define_macros'][k] = (string, None)
return kw
[docs] def libraries(self):
"""Returns a pre-processed list containing libraries to link against
Equivalent command line version:
.. code-block:: sh
$ PKG_CONFIG_PATH=<paths> pkg-config --libs-only-l <name>
"""
status, stdout, stderr = self.__xcall__(['--libs-only-l'])
if status != 0:
raise RuntimeError("error querying --libs-only-l for package `%s': %s" % (self.name, stderr))
retval = []
for token in stdout.split():
retval.append(token[2:])
return uniq(retval)
[docs] def other_libraries(self):
"""Returns a pre-processed list containing libraries to link against
Equivalent command line version:
.. code-block:: sh
$ PKG_CONFIG_PATH=<paths> pkg-config --libs-only-other <name>
"""
status, stdout, stderr = self.__xcall__(['--libs-only-other'])
if status != 0:
raise RuntimeError("error querying --libs-only-other for package `%s': %s" % (self.name, stderr))
return uniq(stdout.split())
[docs] def library_directories(self):
"""Returns a pre-processed list containing library directories.
Equivalent command line version:
.. code-block:: sh
$ PKG_CONFIG_PATH=<paths> pkg-config --libs-only-L <name>
"""
status, stdout, stderr = self.__xcall__(['--libs-only-L'])
if status != 0:
raise RuntimeError("error querying --libs-only-L for package `%s': %s" % (self.name, stderr))
retval = []
for token in stdout.split():
retval.append(token[2:])
return uniq(retval)
[docs] def variable_names(self):
"""Returns a list with all variable names know to this package
Equivalent command line version:
.. code-block:: sh
$ PKG_CONFIG_PATH=<paths> pkg-config --print-variables <name>
"""
status, stdout, stderr = self.__xcall__(['--print-variables'])
if status != 0:
raise RuntimeError("error querying --print-variables for package `%s': %s" % (self.name, stderr))
return stdout.strip().split()
[docs] def variable(self, name):
"""Returns a variable with a specific name (if it exists)
Equivalent command line version:
.. code-block:: sh
$ PKG_CONFIG_PATH=<paths> pkg-config --variable=<variable-name> <name>
.. warning::
If a variable does not exist in a package, pkg-config does not signal an
error. Instead, it returns an empty string. So, do we.
"""
status, stdout, stderr = self.__xcall__(['--variable=%s' % name])
if status != 0:
raise RuntimeError("error querying --variable=%s for package `%s': %s" % (name, self.name, stderr))
return stdout.strip()
[docs] def package_macros(self):
"""Returns package availability and version number macros
This method returns a python list with 1 macro indicating package
availability, using standard GNU compatible names. For
example, if the package is named ``blitz``, this
command would return:
.. doctest::
:options: +NORMALIZE_WHITESPACE +ELLIPSIS
>>> from bob.extension import pkgconfig
>>> blitz = pkgconfig('blitz')
>>> blitz.package_macros()
[('HAVE_BLITZ', '1')]
"""
from re import sub
NAME = sub(r'[\.\-\s]', '_', self.name.upper())
return [('HAVE_' + NAME, '1')]
__all__ = ['pkgconfig']