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