1import sys 2import re 3import os 4 5from configparser import RawConfigParser 6 7__all__ = ['FormatError', 'PkgNotFound', 'LibraryInfo', 'VariableSet', 8 'read_config', 'parse_flags'] 9 10_VAR = re.compile(r'\$\{([a-zA-Z0-9_-]+)\}') 11 12class FormatError(IOError): 13 """ 14 Exception thrown when there is a problem parsing a configuration file. 15 16 """ 17 def __init__(self, msg): 18 self.msg = msg 19 20 def __str__(self): 21 return self.msg 22 23class PkgNotFound(IOError): 24 """Exception raised when a package can not be located.""" 25 def __init__(self, msg): 26 self.msg = msg 27 28 def __str__(self): 29 return self.msg 30 31def parse_flags(line): 32 """ 33 Parse a line from a config file containing compile flags. 34 35 Parameters 36 ---------- 37 line : str 38 A single line containing one or more compile flags. 39 40 Returns 41 ------- 42 d : dict 43 Dictionary of parsed flags, split into relevant categories. 44 These categories are the keys of `d`: 45 46 * 'include_dirs' 47 * 'library_dirs' 48 * 'libraries' 49 * 'macros' 50 * 'ignored' 51 52 """ 53 d = {'include_dirs': [], 'library_dirs': [], 'libraries': [], 54 'macros': [], 'ignored': []} 55 56 flags = (' ' + line).split(' -') 57 for flag in flags: 58 flag = '-' + flag 59 if len(flag) > 0: 60 if flag.startswith('-I'): 61 d['include_dirs'].append(flag[2:].strip()) 62 elif flag.startswith('-L'): 63 d['library_dirs'].append(flag[2:].strip()) 64 elif flag.startswith('-l'): 65 d['libraries'].append(flag[2:].strip()) 66 elif flag.startswith('-D'): 67 d['macros'].append(flag[2:].strip()) 68 else: 69 d['ignored'].append(flag) 70 71 return d 72 73def _escape_backslash(val): 74 return val.replace('\\', '\\\\') 75 76class LibraryInfo: 77 """ 78 Object containing build information about a library. 79 80 Parameters 81 ---------- 82 name : str 83 The library name. 84 description : str 85 Description of the library. 86 version : str 87 Version string. 88 sections : dict 89 The sections of the configuration file for the library. The keys are 90 the section headers, the values the text under each header. 91 vars : class instance 92 A `VariableSet` instance, which contains ``(name, value)`` pairs for 93 variables defined in the configuration file for the library. 94 requires : sequence, optional 95 The required libraries for the library to be installed. 96 97 Notes 98 ----- 99 All input parameters (except "sections" which is a method) are available as 100 attributes of the same name. 101 102 """ 103 def __init__(self, name, description, version, sections, vars, requires=None): 104 self.name = name 105 self.description = description 106 if requires: 107 self.requires = requires 108 else: 109 self.requires = [] 110 self.version = version 111 self._sections = sections 112 self.vars = vars 113 114 def sections(self): 115 """ 116 Return the section headers of the config file. 117 118 Parameters 119 ---------- 120 None 121 122 Returns 123 ------- 124 keys : list of str 125 The list of section headers. 126 127 """ 128 return list(self._sections.keys()) 129 130 def cflags(self, section="default"): 131 val = self.vars.interpolate(self._sections[section]['cflags']) 132 return _escape_backslash(val) 133 134 def libs(self, section="default"): 135 val = self.vars.interpolate(self._sections[section]['libs']) 136 return _escape_backslash(val) 137 138 def __str__(self): 139 m = ['Name: %s' % self.name, 'Description: %s' % self.description] 140 if self.requires: 141 m.append('Requires:') 142 else: 143 m.append('Requires: %s' % ",".join(self.requires)) 144 m.append('Version: %s' % self.version) 145 146 return "\n".join(m) 147 148class VariableSet: 149 """ 150 Container object for the variables defined in a config file. 151 152 `VariableSet` can be used as a plain dictionary, with the variable names 153 as keys. 154 155 Parameters 156 ---------- 157 d : dict 158 Dict of items in the "variables" section of the configuration file. 159 160 """ 161 def __init__(self, d): 162 self._raw_data = dict([(k, v) for k, v in d.items()]) 163 164 self._re = {} 165 self._re_sub = {} 166 167 self._init_parse() 168 169 def _init_parse(self): 170 for k, v in self._raw_data.items(): 171 self._init_parse_var(k, v) 172 173 def _init_parse_var(self, name, value): 174 self._re[name] = re.compile(r'\$\{%s\}' % name) 175 self._re_sub[name] = value 176 177 def interpolate(self, value): 178 # Brute force: we keep interpolating until there is no '${var}' anymore 179 # or until interpolated string is equal to input string 180 def _interpolate(value): 181 for k in self._re.keys(): 182 value = self._re[k].sub(self._re_sub[k], value) 183 return value 184 while _VAR.search(value): 185 nvalue = _interpolate(value) 186 if nvalue == value: 187 break 188 value = nvalue 189 190 return value 191 192 def variables(self): 193 """ 194 Return the list of variable names. 195 196 Parameters 197 ---------- 198 None 199 200 Returns 201 ------- 202 names : list of str 203 The names of all variables in the `VariableSet` instance. 204 205 """ 206 return list(self._raw_data.keys()) 207 208 # Emulate a dict to set/get variables values 209 def __getitem__(self, name): 210 return self._raw_data[name] 211 212 def __setitem__(self, name, value): 213 self._raw_data[name] = value 214 self._init_parse_var(name, value) 215 216def parse_meta(config): 217 if not config.has_section('meta'): 218 raise FormatError("No meta section found !") 219 220 d = dict(config.items('meta')) 221 222 for k in ['name', 'description', 'version']: 223 if not k in d: 224 raise FormatError("Option %s (section [meta]) is mandatory, " 225 "but not found" % k) 226 227 if not 'requires' in d: 228 d['requires'] = [] 229 230 return d 231 232def parse_variables(config): 233 if not config.has_section('variables'): 234 raise FormatError("No variables section found !") 235 236 d = {} 237 238 for name, value in config.items("variables"): 239 d[name] = value 240 241 return VariableSet(d) 242 243def parse_sections(config): 244 return meta_d, r 245 246def pkg_to_filename(pkg_name): 247 return "%s.ini" % pkg_name 248 249def parse_config(filename, dirs=None): 250 if dirs: 251 filenames = [os.path.join(d, filename) for d in dirs] 252 else: 253 filenames = [filename] 254 255 config = RawConfigParser() 256 257 n = config.read(filenames) 258 if not len(n) >= 1: 259 raise PkgNotFound("Could not find file(s) %s" % str(filenames)) 260 261 # Parse meta and variables sections 262 meta = parse_meta(config) 263 264 vars = {} 265 if config.has_section('variables'): 266 for name, value in config.items("variables"): 267 vars[name] = _escape_backslash(value) 268 269 # Parse "normal" sections 270 secs = [s for s in config.sections() if not s in ['meta', 'variables']] 271 sections = {} 272 273 requires = {} 274 for s in secs: 275 d = {} 276 if config.has_option(s, "requires"): 277 requires[s] = config.get(s, 'requires') 278 279 for name, value in config.items(s): 280 d[name] = value 281 sections[s] = d 282 283 return meta, vars, sections, requires 284 285def _read_config_imp(filenames, dirs=None): 286 def _read_config(f): 287 meta, vars, sections, reqs = parse_config(f, dirs) 288 # recursively add sections and variables of required libraries 289 for rname, rvalue in reqs.items(): 290 nmeta, nvars, nsections, nreqs = _read_config(pkg_to_filename(rvalue)) 291 292 # Update var dict for variables not in 'top' config file 293 for k, v in nvars.items(): 294 if not k in vars: 295 vars[k] = v 296 297 # Update sec dict 298 for oname, ovalue in nsections[rname].items(): 299 if ovalue: 300 sections[rname][oname] += ' %s' % ovalue 301 302 return meta, vars, sections, reqs 303 304 meta, vars, sections, reqs = _read_config(filenames) 305 306 # FIXME: document this. If pkgname is defined in the variables section, and 307 # there is no pkgdir variable defined, pkgdir is automatically defined to 308 # the path of pkgname. This requires the package to be imported to work 309 if not 'pkgdir' in vars and "pkgname" in vars: 310 pkgname = vars["pkgname"] 311 if not pkgname in sys.modules: 312 raise ValueError("You should import %s to get information on %s" % 313 (pkgname, meta["name"])) 314 315 mod = sys.modules[pkgname] 316 vars["pkgdir"] = _escape_backslash(os.path.dirname(mod.__file__)) 317 318 return LibraryInfo(name=meta["name"], description=meta["description"], 319 version=meta["version"], sections=sections, vars=VariableSet(vars)) 320 321# Trivial cache to cache LibraryInfo instances creation. To be really 322# efficient, the cache should be handled in read_config, since a same file can 323# be parsed many time outside LibraryInfo creation, but I doubt this will be a 324# problem in practice 325_CACHE = {} 326def read_config(pkgname, dirs=None): 327 """ 328 Return library info for a package from its configuration file. 329 330 Parameters 331 ---------- 332 pkgname : str 333 Name of the package (should match the name of the .ini file, without 334 the extension, e.g. foo for the file foo.ini). 335 dirs : sequence, optional 336 If given, should be a sequence of directories - usually including 337 the NumPy base directory - where to look for npy-pkg-config files. 338 339 Returns 340 ------- 341 pkginfo : class instance 342 The `LibraryInfo` instance containing the build information. 343 344 Raises 345 ------ 346 PkgNotFound 347 If the package is not found. 348 349 See Also 350 -------- 351 misc_util.get_info, misc_util.get_pkg_info 352 353 Examples 354 -------- 355 >>> npymath_info = np.distutils.npy_pkg_config.read_config('npymath') 356 >>> type(npymath_info) 357 <class 'numpy.distutils.npy_pkg_config.LibraryInfo'> 358 >>> print(npymath_info) 359 Name: npymath 360 Description: Portable, core math library implementing C99 standard 361 Requires: 362 Version: 0.1 #random 363 364 """ 365 try: 366 return _CACHE[pkgname] 367 except KeyError: 368 v = _read_config_imp(pkg_to_filename(pkgname), dirs) 369 _CACHE[pkgname] = v 370 return v 371 372# TODO: 373# - implements version comparison (modversion + atleast) 374 375# pkg-config simple emulator - useful for debugging, and maybe later to query 376# the system 377if __name__ == '__main__': 378 from optparse import OptionParser 379 import glob 380 381 parser = OptionParser() 382 parser.add_option("--cflags", dest="cflags", action="store_true", 383 help="output all preprocessor and compiler flags") 384 parser.add_option("--libs", dest="libs", action="store_true", 385 help="output all linker flags") 386 parser.add_option("--use-section", dest="section", 387 help="use this section instead of default for options") 388 parser.add_option("--version", dest="version", action="store_true", 389 help="output version") 390 parser.add_option("--atleast-version", dest="min_version", 391 help="Minimal version") 392 parser.add_option("--list-all", dest="list_all", action="store_true", 393 help="Minimal version") 394 parser.add_option("--define-variable", dest="define_variable", 395 help="Replace variable with the given value") 396 397 (options, args) = parser.parse_args(sys.argv) 398 399 if len(args) < 2: 400 raise ValueError("Expect package name on the command line:") 401 402 if options.list_all: 403 files = glob.glob("*.ini") 404 for f in files: 405 info = read_config(f) 406 print("%s\t%s - %s" % (info.name, info.name, info.description)) 407 408 pkg_name = args[1] 409 d = os.environ.get('NPY_PKG_CONFIG_PATH') 410 if d: 411 info = read_config(pkg_name, ['numpy/core/lib/npy-pkg-config', '.', d]) 412 else: 413 info = read_config(pkg_name, ['numpy/core/lib/npy-pkg-config', '.']) 414 415 if options.section: 416 section = options.section 417 else: 418 section = "default" 419 420 if options.define_variable: 421 m = re.search(r'([\S]+)=([\S]+)', options.define_variable) 422 if not m: 423 raise ValueError("--define-variable option should be of " 424 "the form --define-variable=foo=bar") 425 else: 426 name = m.group(1) 427 value = m.group(2) 428 info.vars[name] = value 429 430 if options.cflags: 431 print(info.cflags(section)) 432 if options.libs: 433 print(info.libs(section)) 434 if options.version: 435 print(info.version) 436 if options.min_version: 437 print(info.version >= options.min_version) 438