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