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