292 lines
8.8 KiB
Python
292 lines
8.8 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright (c) 2013 Matthias Vogelgesang <matthias.vogelgesang@gmail.com>
|
|
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
|
|
# The above copyright notice and this permission notice shall be included in
|
|
# all copies or substantial portions of the Software.
|
|
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
# SOFTWARE.
|
|
|
|
"""pkgconfig is a Python module to interface with the pkg-config command line
|
|
tool."""
|
|
|
|
import os
|
|
import shlex
|
|
import re
|
|
import collections
|
|
from functools import wraps
|
|
from subprocess import call, PIPE, Popen
|
|
|
|
|
|
class PackageNotFoundError(Exception):
|
|
"""
|
|
Raised if a package was not found.
|
|
"""
|
|
def __init__(self, package):
|
|
message = '%s not found' % package
|
|
super(PackageNotFoundError, self).__init__(message)
|
|
|
|
|
|
def _compare_versions(v1, v2):
|
|
"""
|
|
Compare two version strings and return -1, 0 or 1 depending on the equality
|
|
of the subset of matching version numbers.
|
|
|
|
The implementation is inspired by the top answer at
|
|
http://stackoverflow.com/a/1714190/997768.
|
|
"""
|
|
def normalize(v):
|
|
# strip trailing .0 or .00 or .0.0 or ...
|
|
v = re.sub(r'(\.0+)*$', '', v)
|
|
result = []
|
|
for part in v.split('.'):
|
|
# just digits
|
|
m = re.match(r'^(\d+)$', part)
|
|
if m:
|
|
result.append(int(m.group(1)))
|
|
continue
|
|
# digits letters
|
|
m = re.match(r'^(\d+)([a-zA-Z]+)$', part)
|
|
if m:
|
|
result.append(int(m.group(1)))
|
|
result.append(m.group(2))
|
|
continue
|
|
# digits letters digits
|
|
m = re.match(r'^(\d+)([a-zA-Z]+)(\d+)$', part)
|
|
if m:
|
|
result.append(int(m.group(1)))
|
|
result.append(m.group(2))
|
|
result.append(int(m.group(3)))
|
|
continue
|
|
return tuple(result)
|
|
|
|
n1 = normalize(v1)
|
|
n2 = normalize(v2)
|
|
|
|
return (n1 > n2) - (n1 < n2)
|
|
|
|
|
|
def _split_version_specifier(spec):
|
|
"""Splits version specifiers in the form ">= 0.1.2" into ('0.1.2', '>=')"""
|
|
m = re.search(r'([<>=]?=?)?\s*([0-9.a-zA-Z]+)', spec)
|
|
return m.group(2), m.group(1)
|
|
|
|
|
|
def _convert_error(func):
|
|
@wraps(func)
|
|
def _wrapper(*args, **kwargs):
|
|
try:
|
|
return func(*args, **kwargs)
|
|
except OSError as e:
|
|
raise EnvironmentError("pkg-config probably not installed: %r" % e)
|
|
return _wrapper
|
|
|
|
|
|
def _build_options(option, static=False):
|
|
return (option, '--static') if static else (option,)
|
|
|
|
|
|
def _raise_if_not_exists(package):
|
|
if not exists(package):
|
|
raise PackageNotFoundError(package)
|
|
|
|
|
|
@_convert_error
|
|
def _query(package, *options):
|
|
pkg_config_exe = os.environ.get('PKG_CONFIG', None) or 'pkg-config'
|
|
cmd = '{0} {1} {2}'.format(pkg_config_exe, ' '.join(options), package)
|
|
proc = Popen(shlex.split(cmd), stdout=PIPE, stderr=PIPE)
|
|
out, err = proc.communicate()
|
|
|
|
return out.rstrip().decode('utf-8')
|
|
|
|
|
|
@_convert_error
|
|
def exists(package):
|
|
"""
|
|
Return True if package information is available.
|
|
|
|
If ``pkg-config`` not on path, raises ``EnvironmentError``.
|
|
"""
|
|
pkg_config_exe = os.environ.get('PKG_CONFIG', None) or 'pkg-config'
|
|
cmd = '{0} --exists {1}'.format(pkg_config_exe, package).split()
|
|
return call(cmd) == 0
|
|
|
|
|
|
@_convert_error
|
|
def requires(package):
|
|
"""
|
|
Return a list of package names that is required by the package.
|
|
|
|
If ``pkg-config`` not on path, raises ``EnvironmentError``.
|
|
"""
|
|
return _query(package, '--print-requires').split('\n')
|
|
|
|
|
|
def cflags(package):
|
|
"""
|
|
Return the CFLAGS string returned by pkg-config.
|
|
|
|
If ``pkg-config`` is not on path, raises ``EnvironmentError``.
|
|
"""
|
|
_raise_if_not_exists(package)
|
|
return _query(package, '--cflags')
|
|
|
|
|
|
def modversion(package):
|
|
"""
|
|
Return the version returned by pkg-config.
|
|
|
|
If `pkg-config` is not in the path, raises ``EnvironmentError``.
|
|
"""
|
|
_raise_if_not_exists(package)
|
|
return _query(package, '--modversion')
|
|
|
|
|
|
def libs(package, static=False):
|
|
"""
|
|
Return the LDFLAGS string returned by pkg-config.
|
|
|
|
The static specifier will also include libraries for static linking (i.e.,
|
|
includes any private libraries).
|
|
"""
|
|
_raise_if_not_exists(package)
|
|
return _query(package, *_build_options('--libs', static=static))
|
|
|
|
|
|
def variables(package):
|
|
"""
|
|
Return a dictionary of all the variables defined in the .pc pkg-config file
|
|
of 'package'.
|
|
"""
|
|
_raise_if_not_exists(package)
|
|
result = _query(package, '--print-variables')
|
|
names = (x.strip() for x in result.split('\n') if x != '')
|
|
return dict(((x, _query(package, '--variable={0}'.format(x)).strip()) for x in names))
|
|
|
|
|
|
def installed(package, version):
|
|
"""
|
|
Check if the package meets the required version.
|
|
|
|
The version specifier consists of an optional comparator (one of =, ==, >,
|
|
<, >=, <=) and an arbitrarily long version number separated by dots. The
|
|
should be as you would expect, e.g. for an installed version '0.1.2' of
|
|
package 'foo':
|
|
|
|
>>> installed('foo', '==0.1.2')
|
|
True
|
|
>>> installed('foo', '<0.1')
|
|
False
|
|
>>> installed('foo', '>= 0.0.4')
|
|
True
|
|
|
|
If ``pkg-config`` not on path, raises ``EnvironmentError``.
|
|
"""
|
|
if not exists(package):
|
|
return False
|
|
|
|
number, comparator = _split_version_specifier(version)
|
|
modversion = _query(package, '--modversion')
|
|
|
|
try:
|
|
result = _compare_versions(modversion, number)
|
|
except ValueError:
|
|
msg = "{0} is not a correct version specifier".format(version)
|
|
raise ValueError(msg)
|
|
|
|
if comparator in ('', '=', '=='):
|
|
return result == 0
|
|
|
|
if comparator == '>':
|
|
return result > 0
|
|
|
|
if comparator == '>=':
|
|
return result >= 0
|
|
|
|
if comparator == '<':
|
|
return result < 0
|
|
|
|
if comparator == '<=':
|
|
return result <= 0
|
|
|
|
|
|
_PARSE_MAP = {
|
|
'-D': 'define_macros',
|
|
'-I': 'include_dirs',
|
|
'-L': 'library_dirs',
|
|
'-l': 'libraries'
|
|
}
|
|
|
|
|
|
def parse(packages, static=False):
|
|
"""
|
|
Parse the output from pkg-config about the passed package or packages.
|
|
|
|
Builds a dictionary containing the 'libraries', the 'library_dirs', the
|
|
'include_dirs', and the 'define_macros' that are presented by pkg-config.
|
|
*package* is a string with space-delimited package names.
|
|
|
|
The static specifier will also include libraries for static linking (i.e.,
|
|
includes any private libraries).
|
|
|
|
If ``pkg-config`` is not on path, raises ``EnvironmentError``.
|
|
"""
|
|
for package in packages.split():
|
|
_raise_if_not_exists(package)
|
|
|
|
out = _query(packages, *_build_options('--cflags --libs', static=static))
|
|
out = out.replace('\\"', '')
|
|
result = collections.defaultdict(list)
|
|
|
|
for token in re.split(r'(?<!\\) ', out):
|
|
key = _PARSE_MAP.get(token[:2])
|
|
if key:
|
|
result[key].append(token[2:].strip())
|
|
|
|
def split(m):
|
|
t = tuple(m.split('='))
|
|
return t if len(t) > 1 else (t[0], None)
|
|
|
|
result['define_macros'] = [split(m) for m in result['define_macros']]
|
|
|
|
# only have members with values not being the empty list (which is default
|
|
# anyway):
|
|
return collections.defaultdict(list, ((k, v) for k, v in result.items() if v))
|
|
|
|
|
|
def configure_extension(ext, packages, static=False):
|
|
"""
|
|
Append the ``--cflags`` and ``--libs`` of a space-separated list of
|
|
*packages* to the ``extra_compile_args`` and ``extra_link_args`` of a
|
|
distutils/setuptools ``Extension``.
|
|
"""
|
|
for package in packages.split():
|
|
_raise_if_not_exists(package)
|
|
|
|
def query_and_extend(option, target):
|
|
os_opts = ['--msvc-syntax'] if os.name == 'nt' else []
|
|
flags = _query(packages, *os_opts, *_build_options(option, static=static))
|
|
target.extend(re.split(r'(?<!\\) ', flags.replace('\\"', '')))
|
|
|
|
query_and_extend('--cflags', ext.extra_compile_args)
|
|
query_and_extend('--libs', ext.extra_link_args)
|
|
|
|
|
|
def list_all():
|
|
"""Return a list of all packages found by pkg-config."""
|
|
packages = [line.split()[0] for line in _query('', '--list-all').split('\n')]
|
|
return packages
|