You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
438 lines
13 KiB
438 lines
13 KiB
1 year ago
|
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(OSError):
|
||
|
"""
|
||
|
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(OSError):
|
||
|
"""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)
|
||
|
<class 'numpy.distutils.npy_pkg_config.LibraryInfo'>
|
||
|
>>> 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)
|