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