1#!/usr/bin/env python3
2# vim: set syntax=python ts=4 :
3#
4# Copyright (c) 2018 Intel Corporation
5# SPDX-License-Identifier: Apache-2.0
6
7import re
8from collections import OrderedDict
9
10class CMakeCacheEntry:
11    '''Represents a CMake cache entry.
12
13    This class understands the type system in a CMakeCache.txt, and
14    converts the following cache types to Python types:
15
16    Cache Type    Python type
17    ----------    -------------------------------------------
18    FILEPATH      str
19    PATH          str
20    STRING        str OR list of str (if ';' is in the value)
21    BOOL          bool
22    INTERNAL      str OR list of str (if ';' is in the value)
23    ----------    -------------------------------------------
24    '''
25
26    # Regular expression for a cache entry.
27    #
28    # CMake variable names can include escape characters, allowing a
29    # wider set of names than is easy to match with a regular
30    # expression. To be permissive here, use a non-greedy match up to
31    # the first colon (':'). This breaks if the variable name has a
32    # colon inside, but it's good enough.
33    CACHE_ENTRY = re.compile(
34        r'''(?P<name>.*?)                               # name
35         :(?P<type>FILEPATH|PATH|STRING|BOOL|INTERNAL)  # type
36         =(?P<value>.*)                                 # value
37        ''', re.X)
38
39    @classmethod
40    def _to_bool(cls, val):
41        # Convert a CMake BOOL string into a Python bool.
42        #
43        #   "True if the constant is 1, ON, YES, TRUE, Y, or a
44        #   non-zero number. False if the constant is 0, OFF, NO,
45        #   FALSE, N, IGNORE, NOTFOUND, the empty string, or ends in
46        #   the suffix -NOTFOUND. Named boolean constants are
47        #   case-insensitive. If the argument is not one of these
48        #   constants, it is treated as a variable."
49        #
50        # https://cmake.org/cmake/help/v3.0/command/if.html
51        val = val.upper()
52        if val in ('ON', 'YES', 'TRUE', 'Y'):
53            return 1
54        elif val in ('OFF', 'NO', 'FALSE', 'N', 'IGNORE', 'NOTFOUND', ''):
55            return 0
56        elif val.endswith('-NOTFOUND'):
57            return 0
58        else:
59            try:
60                v = int(val)
61                return v != 0
62            except ValueError as exc:
63                raise ValueError('invalid bool {}'.format(val)) from exc
64
65    @classmethod
66    def from_line(cls, line, line_no):
67        # Comments can only occur at the beginning of a line.
68        # (The value of an entry could contain a comment character).
69        if line.startswith('//') or line.startswith('#'):
70            return None
71
72        # Whitespace-only lines do not contain cache entries.
73        if not line.strip():
74            return None
75
76        m = cls.CACHE_ENTRY.match(line)
77        if not m:
78            return None
79
80        name, type_, value = (m.group(g) for g in ('name', 'type', 'value'))
81        if type_ == 'BOOL':
82            try:
83                value = cls._to_bool(value)
84            except ValueError as exc:
85                args = exc.args + ('on line {}: {}'.format(line_no, line),)
86                raise ValueError(args) from exc
87        elif type_ in ['STRING', 'INTERNAL']:
88            # If the value is a CMake list (i.e. is a string which
89            # contains a ';'), convert to a Python list.
90            if ';' in value:
91                value = value.split(';')
92
93        return CMakeCacheEntry(name, value)
94
95    def __init__(self, name, value):
96        self.name = name
97        self.value = value
98
99    def __str__(self):
100        fmt = 'CMakeCacheEntry(name={}, value={})'
101        return fmt.format(self.name, self.value)
102
103
104class CMakeCache:
105    '''Parses and represents a CMake cache file.'''
106
107    @staticmethod
108    def from_file(cache_file):
109        return CMakeCache(cache_file)
110
111    def __init__(self, cache_file):
112        self.cache_file = cache_file
113        self.load(cache_file)
114
115    def load(self, cache_file):
116        entries = []
117        with open(cache_file, 'r') as cache:
118            for line_no, line in enumerate(cache):
119                entry = CMakeCacheEntry.from_line(line, line_no)
120                if entry:
121                    entries.append(entry)
122        self._entries = OrderedDict((e.name, e) for e in entries)
123
124    def get(self, name, default=None):
125        entry = self._entries.get(name)
126        if entry is not None:
127            return entry.value
128        else:
129            return default
130
131    def get_list(self, name, default=None):
132        if default is None:
133            default = []
134        entry = self._entries.get(name)
135        if entry is not None:
136            value = entry.value
137            if isinstance(value, list):
138                return value
139            elif isinstance(value, str):
140                return [value] if value else []
141            else:
142                msg = 'invalid value {} type {}'
143                raise RuntimeError(msg.format(value, type(value)))
144        else:
145            return default
146
147    def __contains__(self, name):
148        return name in self._entries
149
150    def __getitem__(self, name):
151        return self._entries[name].value
152
153    def __setitem__(self, name, entry):
154        if not isinstance(entry, CMakeCacheEntry):
155            msg = 'improper type {} for value {}, expecting CMakeCacheEntry'
156            raise TypeError(msg.format(type(entry), entry))
157        self._entries[name] = entry
158
159    def __delitem__(self, name):
160        del self._entries[name]
161
162    def __iter__(self):
163        return iter(self._entries.values())
164