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