1# -*- coding: utf-8 -*- 2# Copyright (c) 2013 Matthias Vogelgesang <matthias.vogelgesang@gmail.com> 3 4# Permission is hereby granted, free of charge, to any person obtaining a copy 5# of this software and associated documentation files (the "Software"), to deal 6# in the Software without restriction, including without limitation the rights 7# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8# copies of the Software, and to permit persons to whom the Software is 9# furnished to do so, subject to the following conditions: 10 11# The above copyright notice and this permission notice shall be included in 12# all copies or substantial portions of the Software. 13 14# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20# SOFTWARE. 21 22"""pkgconfig is a Python module to interface with the pkg-config command line 23tool.""" 24 25import subprocess 26import re 27import collections 28 29 30def _compare_versions(v1, v2): 31 """ 32 Compare two version strings and return -1, 0 or 1 depending on the equality 33 of the subset of matching version numbers. 34 35 The implementation is taken from the top answer at 36 http://stackoverflow.com/a/1714190/997768. 37 """ 38 def normalize(v): 39 return [int(x) for x in re.sub(r'(\.0+)*$', '', v).split(".")] 40 41 n1 = normalize(v1) 42 n2 = normalize(v2) 43 44 return (n1 > n2) - (n1 < n2) 45 46 47def _split_version_specifier(spec): 48 """Splits version specifiers in the form ">= 0.1.2" into ('0.1.2', '>=')""" 49 m = re.search(r'([<>=]?=?)?\s*((\d*\.)*\d*)', spec) 50 return m.group(2), m.group(1) 51 52 53def _convert_error(func): 54 def _wrapper(*args, **kwargs): 55 try: 56 return func(*args, **kwargs) 57 except OSError: 58 raise EnvironmentError("pkg-config is not installed") 59 60 return _wrapper 61 62 63@_convert_error 64def _query(package, option): 65 cmd = 'pkg-config {0} {1}'.format(option, package).split() 66 proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, 67 stderr=subprocess.PIPE) 68 out, err = proc.communicate() 69 70 return out.rstrip().decode('utf-8') 71 72 73@_convert_error 74def exists(package): 75 """Return True if package information is available.""" 76 cmd = 'pkg-config --exists {0}'.format(package).split() 77 return subprocess.call(cmd) == 0 78 79 80@_convert_error 81def requires(package): 82 """Return a list of package names that is required by the package""" 83 return _query(package, '--print-requires').split('\n') 84 85 86def cflags(package): 87 """Return the CFLAGS string returned by pkg-config.""" 88 return _query(package, '--cflags') 89 90 91def libs(package): 92 """Return the LDFLAGS string returned by pkg-config.""" 93 return _query(package, '--libs') 94 95 96def installed(package, version): 97 """ 98 Check if the package meets the required version. 99 100 The version specifier consists of an optional comparator (one of =, ==, >, 101 <, >=, <=) and an arbitrarily long version number separated by dots. The 102 should be as you would expect, e.g. for an installed version '0.1.2' of 103 package 'foo': 104 105 >>> installed('foo', '==0.1.2') 106 True 107 >>> installed('foo', '<0.1') 108 False 109 >>> installed('foo', '>= 0.0.4') 110 True 111 """ 112 if not exists(package): 113 return False 114 115 number, comparator = _split_version_specifier(version) 116 modversion = _query(package, '--modversion') 117 118 try: 119 result = _compare_versions(modversion, number) 120 except ValueError: 121 msg = "{0} is not a correct version specifier".format(version) 122 raise ValueError(msg) 123 124 if comparator in ('', '=', '=='): 125 return result == 0 126 127 if comparator == '>': 128 return result > 0 129 130 if comparator == '>=': 131 return result >= 0 132 133 if comparator == '<': 134 return result < 0 135 136 if comparator == '<=': 137 return result <= 0 138 139 140_PARSE_MAP = { 141 '-D': 'define_macros', 142 '-I': 'include_dirs', 143 '-L': 'library_dirs', 144 '-l': 'libraries' 145} 146 147 148def parse(packages): 149 """ 150 Parse the output from pkg-config about the passed package or packages. 151 152 Builds a dictionary containing the 'libraries', the 'library_dirs', 153 the 'include_dirs', and the 'define_macros' that are presented by 154 pkg-config. *package* is a string with space-delimited package names. 155 """ 156 def parse_package(package): 157 result = collections.defaultdict(set) 158 159 # Execute the query to pkg-config and clean the result. 160 out = _query(package, '--cflags --libs') 161 out = out.replace('\\"', '') 162 163 # Iterate through each token in the output. 164 for token in out.split(): 165 key = _PARSE_MAP.get(token[:2]) 166 if key: 167 result[key].add(token[2:].strip()) 168 169 # Iterate and clean define macros. 170 macros = set() 171 for declaration in result['define_macros']: 172 macro = tuple(declaration.split('=')) 173 if len(macro) == 1: 174 macro += '', 175 176 macros.add(macro) 177 178 result['define_macros'] = macros 179 180 # Return parsed configuration. 181 return result 182 183 # Go through all package names and update the result dict accordingly. 184 result = collections.defaultdict(set) 185 186 for package in packages.split(): 187 for k, v in parse_package(package).items(): 188 result[k].update(v) 189 190 return result 191