import sys import re import os from configparser import RawConfigParser __all__ = ['FormatError', 'PkgNotFound', 'LibraryInfo', 'VariableSet', 'read_config', 'parse_flags'] _VAR = re.compile(r'\$\{([a-zA-Z0-9_-]+)\}') class FormatError(IOError): """ Exception thrown when there is a problem parsing a configuration file. """ def __init__(self, msg): self.msg = msg def __str__(self): return self.msg class PkgNotFound(IOError): """Exception raised when a package can not be located.""" def __init__(self, msg): self.msg = msg def __str__(self): return self.msg def parse_flags(line): """ Parse a line from a config file containing compile flags. Parameters ---------- line : str A single line containing one or more compile flags. Returns ------- d : dict Dictionary of parsed flags, split into relevant categories. These categories are the keys of `d`: * 'include_dirs' * 'library_dirs' * 'libraries' * 'macros' * 'ignored' """ d = {'include_dirs': [], 'library_dirs': [], 'libraries': [], 'macros': [], 'ignored': []} flags = (' ' + line).split(' -') for flag in flags: flag = '-' + flag if len(flag) > 0: if flag.startswith('-I'): d['include_dirs'].append(flag[2:].strip()) elif flag.startswith('-L'): d['library_dirs'].append(flag[2:].strip()) elif flag.startswith('-l'): d['libraries'].append(flag[2:].strip()) elif flag.startswith('-D'): d['macros'].append(flag[2:].strip()) else: d['ignored'].append(flag) return d def _escape_backslash(val): return val.replace('\\', '\\\\') class LibraryInfo: """ Object containing build information about a library. Parameters ---------- name : str The library name. description : str Description of the library. version : str Version string. sections : dict The sections of the configuration file for the library. The keys are the section headers, the values the text under each header. vars : class instance A `VariableSet` instance, which contains ``(name, value)`` pairs for variables defined in the configuration file for the library. requires : sequence, optional The required libraries for the library to be installed. Notes ----- All input parameters (except "sections" which is a method) are available as attributes of the same name. """ def __init__(self, name, description, version, sections, vars, requires=None): self.name = name self.description = description if requires: self.requires = requires else: self.requires = [] self.version = version self._sections = sections self.vars = vars def sections(self): """ Return the section headers of the config file. Parameters ---------- None Returns ------- keys : list of str The list of section headers. """ return list(self._sections.keys()) def cflags(self, section="default"): val = self.vars.interpolate(self._sections[section]['cflags']) return _escape_backslash(val) def libs(self, section="default"): val = self.vars.interpolate(self._sections[section]['libs']) return _escape_backslash(val) def __str__(self): m = ['Name: %s' % self.name, 'Description: %s' % self.description] if self.requires: m.append('Requires:') else: m.append('Requires: %s' % ",".join(self.requires)) m.append('Version: %s' % self.version) return "\n".join(m) class VariableSet: """ Container object for the variables defined in a config file. `VariableSet` can be used as a plain dictionary, with the variable names as keys. Parameters ---------- d : dict Dict of items in the "variables" section of the configuration file. """ def __init__(self, d): self._raw_data = dict([(k, v) for k, v in d.items()]) self._re = {} self._re_sub = {} self._init_parse() def _init_parse(self): for k, v in self._raw_data.items(): self._init_parse_var(k, v) def _init_parse_var(self, name, value): self._re[name] = re.compile(r'\$\{%s\}' % name) self._re_sub[name] = value def interpolate(self, value): # Brute force: we keep interpolating until there is no '${var}' anymore # or until interpolated string is equal to input string def _interpolate(value): for k in self._re.keys(): value = self._re[k].sub(self._re_sub[k], value) return value while _VAR.search(value): nvalue = _interpolate(value) if nvalue == value: break value = nvalue return value def variables(self): """ Return the list of variable names. Parameters ---------- None Returns ------- names : list of str The names of all variables in the `VariableSet` instance. """ return list(self._raw_data.keys()) # Emulate a dict to set/get variables values def __getitem__(self, name): return self._raw_data[name] def __setitem__(self, name, value): self._raw_data[name] = value self._init_parse_var(name, value) def parse_meta(config): if not config.has_section('meta'): raise FormatError("No meta section found !") d = dict(config.items('meta')) for k in ['name', 'description', 'version']: if not k in d: raise FormatError("Option %s (section [meta]) is mandatory, " "but not found" % k) if not 'requires' in d: d['requires'] = [] return d def parse_variables(config): if not config.has_section('variables'): raise FormatError("No variables section found !") d = {} for name, value in config.items("variables"): d[name] = value return VariableSet(d) def parse_sections(config): return meta_d, r def pkg_to_filename(pkg_name): return "%s.ini" % pkg_name def parse_config(filename, dirs=None): if dirs: filenames = [os.path.join(d, filename) for d in dirs] else: filenames = [filename] config = RawConfigParser() n = config.read(filenames) if not len(n) >= 1: raise PkgNotFound("Could not find file(s) %s" % str(filenames)) # Parse meta and variables sections meta = parse_meta(config) vars = {} if config.has_section('variables'): for name, value in config.items("variables"): vars[name] = _escape_backslash(value) # Parse "normal" sections secs = [s for s in config.sections() if not s in ['meta', 'variables']] sections = {} requires = {} for s in secs: d = {} if config.has_option(s, "requires"): requires[s] = config.get(s, 'requires') for name, value in config.items(s): d[name] = value sections[s] = d return meta, vars, sections, requires def _read_config_imp(filenames, dirs=None): def _read_config(f): meta, vars, sections, reqs = parse_config(f, dirs) # recursively add sections and variables of required libraries for rname, rvalue in reqs.items(): nmeta, nvars, nsections, nreqs = _read_config(pkg_to_filename(rvalue)) # Update var dict for variables not in 'top' config file for k, v in nvars.items(): if not k in vars: vars[k] = v # Update sec dict for oname, ovalue in nsections[rname].items(): if ovalue: sections[rname][oname] += ' %s' % ovalue return meta, vars, sections, reqs meta, vars, sections, reqs = _read_config(filenames) # FIXME: document this. If pkgname is defined in the variables section, and # there is no pkgdir variable defined, pkgdir is automatically defined to # the path of pkgname. This requires the package to be imported to work if not 'pkgdir' in vars and "pkgname" in vars: pkgname = vars["pkgname"] if not pkgname in sys.modules: raise ValueError("You should import %s to get information on %s" % (pkgname, meta["name"])) mod = sys.modules[pkgname] vars["pkgdir"] = _escape_backslash(os.path.dirname(mod.__file__)) return LibraryInfo(name=meta["name"], description=meta["description"], version=meta["version"], sections=sections, vars=VariableSet(vars)) # Trivial cache to cache LibraryInfo instances creation. To be really # efficient, the cache should be handled in read_config, since a same file can # be parsed many time outside LibraryInfo creation, but I doubt this will be a # problem in practice _CACHE = {} def read_config(pkgname, dirs=None): """ Return library info for a package from its configuration file. Parameters ---------- pkgname : str Name of the package (should match the name of the .ini file, without the extension, e.g. foo for the file foo.ini). dirs : sequence, optional If given, should be a sequence of directories - usually including the NumPy base directory - where to look for npy-pkg-config files. Returns ------- pkginfo : class instance The `LibraryInfo` instance containing the build information. Raises ------ PkgNotFound If the package is not found. See Also -------- misc_util.get_info, misc_util.get_pkg_info Examples -------- >>> npymath_info = np.distutils.npy_pkg_config.read_config('npymath') >>> type(npymath_info) >>> print(npymath_info) Name: npymath Description: Portable, core math library implementing C99 standard Requires: Version: 0.1 #random """ try: return _CACHE[pkgname] except KeyError: v = _read_config_imp(pkg_to_filename(pkgname), dirs) _CACHE[pkgname] = v return v # TODO: # - implements version comparison (modversion + atleast) # pkg-config simple emulator - useful for debugging, and maybe later to query # the system if __name__ == '__main__': from optparse import OptionParser import glob parser = OptionParser() parser.add_option("--cflags", dest="cflags", action="store_true", help="output all preprocessor and compiler flags") parser.add_option("--libs", dest="libs", action="store_true", help="output all linker flags") parser.add_option("--use-section", dest="section", help="use this section instead of default for options") parser.add_option("--version", dest="version", action="store_true", help="output version") parser.add_option("--atleast-version", dest="min_version", help="Minimal version") parser.add_option("--list-all", dest="list_all", action="store_true", help="Minimal version") parser.add_option("--define-variable", dest="define_variable", help="Replace variable with the given value") (options, args) = parser.parse_args(sys.argv) if len(args) < 2: raise ValueError("Expect package name on the command line:") if options.list_all: files = glob.glob("*.ini") for f in files: info = read_config(f) print("%s\t%s - %s" % (info.name, info.name, info.description)) pkg_name = args[1] d = os.environ.get('NPY_PKG_CONFIG_PATH') if d: info = read_config(pkg_name, ['numpy/core/lib/npy-pkg-config', '.', d]) else: info = read_config(pkg_name, ['numpy/core/lib/npy-pkg-config', '.']) if options.section: section = options.section else: section = "default" if options.define_variable: m = re.search(r'([\S]+)=([\S]+)', options.define_variable) if not m: raise ValueError("--define-variable option should be of " "the form --define-variable=foo=bar") else: name = m.group(1) value = m.group(2) info.vars[name] = value if options.cflags: print(info.cflags(section)) if options.libs: print(info.libs(section)) if options.version: print(info.version) if options.min_version: print(info.version >= options.min_version)