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