1# This Source Code Form is subject to the terms of the Mozilla Public 2# License, v. 2.0. If a copy of the MPL was not distributed with this 3# file, You can obtain one at http://mozilla.org/MPL/2.0/. 4from pyparsing import (CharsNotIn, Group, Forward, Literal, Suppress, Word, 5 QuotedString, ZeroOrMore, alphas, alphanums) 6from string import Template 7import re 8 9# Grammar for CMake 10comment = Literal('#') + ZeroOrMore(CharsNotIn('\n')) 11quoted_argument = QuotedString('\"', '\\', multiline=True) 12unquoted_argument = CharsNotIn('\n ()#\"\\') 13argument = quoted_argument | unquoted_argument | Suppress(comment) 14arguments = Forward() 15arguments << (argument | (Literal('(') + ZeroOrMore(arguments) + Literal(')'))) 16identifier = Word(alphas, alphanums+'_') 17command = Group(identifier + Literal('(') + ZeroOrMore(arguments) + Literal(')')) 18file_elements = command | Suppress(comment) 19cmake = ZeroOrMore(file_elements) 20 21 22def extract_arguments(parsed): 23 """Extract the command arguments skipping the parentheses""" 24 return parsed[2:len(parsed) - 1] 25 26 27def match_block(command, parsed, start): 28 """Find the end of block starting with the command""" 29 depth = 0 30 end = start + 1 31 endcommand = 'end' + command 32 while parsed[end][0] != endcommand or depth > 0: 33 if parsed[end][0] == command: 34 depth += 1 35 elif parsed[end][0] == endcommand: 36 depth -= 1 37 end = end + 1 38 if end == len(parsed): 39 print('error: eof when trying to match block statement: %s' 40 % parsed[start]) 41 return end 42 43 44def parse_if(parsed, start): 45 """Parse if/elseif/else/endif into a list of conditions and commands""" 46 depth = 0 47 conditions = [] 48 condition = [extract_arguments(parsed[start])] 49 start = start + 1 50 end = start 51 52 while parsed[end][0] != 'endif' or depth > 0: 53 command = parsed[end][0] 54 if command == 'if': 55 depth += 1 56 elif command == 'else' and depth == 0: 57 condition.append(parsed[start:end]) 58 conditions.append(condition) 59 start = end + 1 60 condition = [['TRUE']] 61 elif command == 'elseif' and depth == 0: 62 condition.append(parsed[start:end]) 63 conditions.append(condition) 64 condition = [extract_arguments(parsed[end])] 65 start = end + 1 66 elif command == 'endif': 67 depth -= 1 68 end = end + 1 69 if end == len(parsed): 70 print('error: eof when trying to match if statement: %s' 71 % parsed[start]) 72 condition.append(parsed[start:end]) 73 conditions.append(condition) 74 return end, conditions 75 76 77def substs(variables, values): 78 """Substitute variables into values""" 79 new_values = [] 80 for value in values: 81 t = Template(value) 82 new_value = t.safe_substitute(variables) 83 84 # Safe substitute leaves unrecognized variables in place. 85 # We replace them with the empty string. 86 new_values.append(re.sub('\$\{\w+\}', '', new_value)) 87 return new_values 88 89 90def evaluate(variables, cache_variables, parsed): 91 """Evaluate a list of parsed commands, returning sources to build""" 92 i = 0 93 sources = [] 94 while i < len(parsed): 95 command = parsed[i][0] 96 arguments = substs(variables, extract_arguments(parsed[i])) 97 98 if command == 'foreach': 99 end = match_block(command, parsed, i) 100 for argument in arguments[1:]: 101 # ; is also a valid divider, why have one when you can have two? 102 argument = argument.replace(';', ' ') 103 for value in argument.split(): 104 variables[arguments[0]] = value 105 cont_eval, new_sources = evaluate(variables, cache_variables, 106 parsed[i+1:end]) 107 sources.extend(new_sources) 108 if not cont_eval: 109 return cont_eval, sources 110 elif command == 'function': 111 # for now we just execute functions inline at point of declaration 112 # as this is sufficient to build libaom 113 pass 114 elif command == 'if': 115 i, conditions = parse_if(parsed, i) 116 for condition in conditions: 117 if evaluate_boolean(variables, condition[0]): 118 cont_eval, new_sources = evaluate(variables, 119 cache_variables, 120 condition[1]) 121 sources.extend(new_sources) 122 if not cont_eval: 123 return cont_eval, sources 124 break 125 elif command == 'include': 126 if arguments: 127 try: 128 print('including: %s' % arguments[0]) 129 sources.extend(parse(variables, cache_variables, arguments[0])) 130 except IOError: 131 print('warning: could not include: %s' % arguments[0]) 132 elif command == 'list': 133 try: 134 action = arguments[0] 135 variable = arguments[1] 136 values = arguments[2:] 137 if action == 'APPEND': 138 if variable not in variables: 139 variables[variable] = ' '.join(values) 140 else: 141 variables[variable] += ' ' + ' '.join(values) 142 except (IndexError, KeyError): 143 pass 144 elif command == 'option': 145 variable = arguments[0] 146 value = arguments[2] 147 # Allow options to be override without changing CMake files 148 if variable not in variables: 149 variables[variable] = value 150 elif command == 'return': 151 return False, sources 152 elif command == 'set': 153 variable = arguments[0] 154 values = arguments[1:] 155 # CACHE variables are not set if already present 156 try: 157 cache = values.index('CACHE') 158 values = values[0:cache] 159 if variable not in variables: 160 variables[variable] = ' '.join(values) 161 cache_variables.append(variable) 162 except ValueError: 163 variables[variable] = ' '.join(values) 164 # we need to emulate the behavior of these function calls 165 # because we don't support interpreting them directly 166 # see bug 1492292 167 elif command in ['set_aom_config_var', 'set_aom_detect_var']: 168 variable = arguments[0] 169 value = arguments[1] 170 if variable not in variables: 171 variables[variable] = value 172 cache_variables.append(variable) 173 elif command == 'set_aom_option_var': 174 # option vars cannot go into cache_variables 175 variable = arguments[0] 176 value = arguments[2] 177 if variable not in variables: 178 variables[variable] = value 179 elif command == 'add_asm_library': 180 try: 181 sources.extend(variables[arguments[1]].split(' ')) 182 except (IndexError, KeyError): 183 pass 184 elif command == 'add_intrinsics_object_library': 185 try: 186 sources.extend(variables[arguments[3]].split(' ')) 187 except (IndexError, KeyError): 188 pass 189 elif command == 'add_library': 190 for source in arguments[1:]: 191 sources.extend(source.split(' ')) 192 elif command == 'target_sources': 193 for source in arguments[1:]: 194 sources.extend(source.split(' ')) 195 elif command == 'MOZDEBUG': 196 print('>>>> MOZDEBUG: %s' % ' '.join(arguments)) 197 i += 1 198 return True, sources 199 200 201def evaluate_boolean(variables, arguments): 202 """Evaluate a boolean expression""" 203 if not arguments: 204 return False 205 206 argument = arguments[0] 207 208 if argument == 'NOT': 209 return not evaluate_boolean(variables, arguments[1:]) 210 211 if argument == '(': 212 i = 0 213 depth = 1 214 while depth > 0 and i < len(arguments): 215 i += 1 216 if arguments[i] == '(': 217 depth += 1 218 if arguments[i] == ')': 219 depth -= 1 220 return evaluate_boolean(variables, arguments[1:i]) 221 222 def evaluate_constant(argument): 223 try: 224 as_int = int(argument) 225 if as_int != 0: 226 return True 227 else: 228 return False 229 except ValueError: 230 upper = argument.upper() 231 if upper in ['ON', 'YES', 'TRUE', 'Y']: 232 return True 233 elif upper in ['OFF', 'NO', 'FALSE', 'N', 'IGNORE', '', 'NOTFOUND']: 234 return False 235 elif upper.endswith('-NOTFOUND'): 236 return False 237 return None 238 239 def lookup_variable(argument): 240 # If statements can have old-style variables which are not demarcated 241 # like ${VARIABLE}. Attempt to look up the variable both ways. 242 try: 243 if re.search('\$\{\w+\}', argument): 244 try: 245 t = Template(argument) 246 value = t.substitute(variables) 247 try: 248 # Attempt an old-style variable lookup with the 249 # substituted value. 250 return variables[value] 251 except KeyError: 252 return value 253 except ValueError: 254 # TODO: CMake supports nesting, e.g. ${${foo}} 255 return None 256 else: 257 return variables[argument] 258 except KeyError: 259 return None 260 261 lhs = lookup_variable(argument) 262 if lhs is None: 263 # variable resolution failed, treat as string 264 lhs = argument 265 266 if len(arguments) > 1: 267 op = arguments[1] 268 if op == 'AND': 269 return evaluate_constant(lhs) and evaluate_boolean(variables, arguments[2:]) 270 elif op == 'MATCHES': 271 rhs = lookup_variable(arguments[2]) 272 if not rhs: 273 rhs = arguments[2] 274 return not re.match(rhs, lhs) is None 275 elif op == 'OR': 276 return evaluate_constant(lhs) or evaluate_boolean(variables, arguments[2:]) 277 elif op == 'STREQUAL': 278 rhs = lookup_variable(arguments[2]) 279 if not rhs: 280 rhs = arguments[2] 281 return lhs == rhs 282 else: 283 lhs = evaluate_constant(lhs) 284 if lhs is None: 285 lhs = lookup_variable(argument) 286 287 return lhs 288 289 290def parse(variables, cache_variables, filename): 291 parsed = cmake.parseFile(filename) 292 cont_eval, sources = evaluate(variables, cache_variables, parsed) 293 return sources 294