Bug 1445683 - Add Python cmake parser; r=chmanchester draft
authorDan Minor <dminor@mozilla.com>
Thu, 05 Apr 2018 15:07:27 -0400
changeset 811425 a26cc11abcd5ec226b4cdf3f21865fe8c35585db
parent 811302 9c7bb8874337c2d40aef3d9945b10490a5115188
child 811426 05d05ff9bddbc4f6097f1cf32a6056e39d825eaf
push id114310
push userbmo:dminor@mozilla.com
push dateWed, 27 Jun 2018 17:14:11 +0000
reviewerschmanchester
bugs1445683
milestone63.0a1
Bug 1445683 - Add Python cmake parser; r=chmanchester MozReview-Commit-ID: Fy5v5pw4Rgi
media/libaom/cmakeparser.py
media/libaom/test_cmakeparser.py
new file mode 100644
--- /dev/null
+++ b/media/libaom/cmakeparser.py
@@ -0,0 +1,278 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+from pyparsing import (CharsNotIn, Group, Forward, Literal, Suppress, Word,
+                       QuotedString, ZeroOrMore, alphas, alphanums)
+from string import Template
+import re
+
+# Grammar for CMake
+comment = Literal('#') + ZeroOrMore(CharsNotIn('\n'))
+quoted_argument = QuotedString('\"', '\\', multiline=True)
+unquoted_argument = CharsNotIn('\n ()#\"\\')
+argument = quoted_argument | unquoted_argument | Suppress(comment)
+arguments = Forward()
+arguments << (argument | (Literal('(') + ZeroOrMore(arguments) + Literal(')')))
+identifier = Word(alphas, alphanums+'_')
+command = Group(identifier + Literal('(') + ZeroOrMore(arguments) + Literal(')'))
+file_elements = command | Suppress(comment)
+cmake = ZeroOrMore(file_elements)
+
+
+def extract_arguments(parsed):
+    """Extract the command arguments skipping the parentheses"""
+    return parsed[2:len(parsed) - 1]
+
+
+def match_block(command, parsed, start):
+    """Find the end of block starting with the command"""
+    depth = 0
+    end = start + 1
+    endcommand = 'end' + command
+    while parsed[end][0] != endcommand or depth > 0:
+        if parsed[end][0] == command:
+            depth += 1
+        elif parsed[end][0] == endcommand:
+            depth -= 1
+        end = end + 1
+        if end == len(parsed):
+            print('error: eof when trying to match block statement: %s'
+                  % parsed[start])
+    return end
+
+
+def parse_if(parsed, start):
+    """Parse if/elseif/else/endif into a list of conditions and commands"""
+    depth = 0
+    conditions = []
+    condition = [extract_arguments(parsed[start])]
+    start = start + 1
+    end = start
+
+    while parsed[end][0] != 'endif' or depth > 0:
+        command = parsed[end][0]
+        if command == 'if':
+            depth += 1
+        elif command == 'else' and depth == 0:
+            condition.append(parsed[start:end])
+            conditions.append(condition)
+            start = end + 1
+            condition = [['TRUE']]
+        elif command == 'elseif' and depth == 0:
+            condition.append(parsed[start:end])
+            conditions.append(condition)
+            condition = [extract_arguments(parsed[end])]
+            start = end + 1
+        elif command == 'endif':
+            depth -= 1
+        end = end + 1
+        if end == len(parsed):
+            print('error: eof when trying to match if statement: %s'
+                  % parsed[start])
+    condition.append(parsed[start:end])
+    conditions.append(condition)
+    return end, conditions
+
+
+def substs(variables, values):
+    """Substitute variables into values"""
+    new_values = []
+    for value in values:
+        t = Template(value)
+        new_value = t.safe_substitute(variables)
+
+        # Safe substitute leaves unrecognized variables in place.
+        # We replace them with the empty string.
+        new_values.append(re.sub('\$\{\w+\}', '', new_value))
+    return new_values
+
+
+def evaluate(variables, cache_variables, parsed):
+    """Evaluate a list of parsed commands, returning sources to build"""
+    i = 0
+    sources = []
+    while i < len(parsed):
+        command = parsed[i][0]
+        arguments = substs(variables, extract_arguments(parsed[i]))
+
+        if command == 'foreach':
+            end = match_block(command, parsed, i)
+            for argument in arguments[1:]:
+                # ; is also a valid divider, why have one when you can have two?
+                argument = argument.replace(';', ' ')
+                for value in argument.split():
+                    variables[arguments[0]] = value
+                    cont_eval, new_sources = evaluate(variables, cache_variables,
+                                                      parsed[i+1:end])
+                    sources.extend(new_sources)
+                    if not cont_eval:
+                        return cont_eval, sources
+        elif command == 'function':
+            # for now we just execute functions inline at point of declaration
+            # as this is sufficient to build libaom
+            pass
+        elif command == 'if':
+            i, conditions = parse_if(parsed, i)
+            for condition in conditions:
+                if evaluate_boolean(variables, condition[0]):
+                    cont_eval, new_sources = evaluate(variables,
+                                                      cache_variables,
+                                                      condition[1])
+                    sources.extend(new_sources)
+                    if not cont_eval:
+                        return cont_eval, sources
+                    break
+        elif command == 'include':
+            if arguments:
+                try:
+                    print('including: %s' % arguments[0])
+                    sources.extend(parse(variables, cache_variables, arguments[0]))
+                except IOError:
+                    print('warning: could not include: %s' % arguments[0])
+        elif command == 'list':
+            try:
+                action = arguments[0]
+                variable = arguments[1]
+                values = arguments[2:]
+                if action == 'APPEND':
+                    if not variables.has_key(variable):
+                        variables[variable] = ' '.join(values)
+                    else:
+                        variables[variable] += ' ' + ' '.join(values)
+            except (IndexError, KeyError):
+                pass
+        elif command == 'option':
+            variable = arguments[0]
+            value = arguments[2]
+            # Allow options to be override without changing CMake files
+            if not variables.has_key(variable):
+                variables[variable] = value
+        elif command == 'return':
+            return False, sources
+        elif command == 'set':
+            variable = arguments[0]
+            values = arguments[1:]
+            # CACHE variables are not set if already present
+            try:
+                cache = values.index('CACHE')
+                values = values[0:cache]
+                if not variables.has_key(variable):
+                    variables[variable] = ' '.join(values)
+                cache_variables.append(variable)
+            except ValueError:
+                variables[variable] = ' '.join(values)
+        elif command == 'add_asm_library':
+            try:
+                sources.extend(variables[arguments[1]].split(' '))
+            except (IndexError, KeyError):
+                pass
+        elif command == 'add_intrinsics_object_library':
+            try:
+                sources.extend(variables[arguments[3]].split(' '))
+            except (IndexError, KeyError):
+                pass
+        elif command == 'add_library':
+            for source in arguments[1:]:
+                sources.extend(source.split(' '))
+        elif command == 'target_sources':
+            for source in arguments[1:]:
+                sources.extend(source.split(' '))
+        elif command == 'MOZDEBUG':
+            print('>>>> MOZDEBUG: %s' % ' '.join(arguments))
+        i += 1
+    return True, sources
+
+
+def evaluate_boolean(variables, arguments):
+    """Evaluate a boolean expression"""
+    if not arguments:
+        return False
+
+    argument = arguments[0]
+
+    if argument == 'NOT':
+        return not evaluate_boolean(variables, arguments[1:])
+
+    if argument == '(':
+        i = 0
+        depth = 1
+        while depth > 0 and i < len(arguments):
+            i += 1
+            if arguments[i] == '(':
+                depth += 1
+            if arguments[i] == ')':
+                depth -= 1
+        return evaluate_boolean(variables, arguments[1:i])
+
+    def evaluate_constant(argument):
+        try:
+            as_int = int(argument)
+            if as_int != 0:
+                return True
+            else:
+                return False
+        except ValueError:
+            upper = argument.upper()
+            if upper in ['ON', 'YES', 'TRUE', 'Y']:
+                return True
+            elif upper in ['OFF', 'NO', 'FALSE', 'N', 'IGNORE', '', 'NOTFOUND']:
+                return False
+            elif upper.endswith('-NOTFOUND'):
+                return False
+        return None
+
+    def lookup_variable(argument):
+        # If statements can have old-style variables which are not demarcated
+        # like ${VARIABLE}. Attempt to look up the variable both ways.
+        try:
+            if re.search('\$\{\w+\}', argument):
+                try:
+                    t = Template(argument)
+                    value = t.substitute(variables)
+                    try:
+                        # Attempt an old-style variable lookup with the
+                        # substituted value.
+                        return variables[value]
+                    except KeyError:
+                        return value
+                except ValueError:
+                    # TODO: CMake supports nesting, e.g. ${${foo}}
+                    return None
+            else:
+                return variables[argument]
+        except KeyError:
+            return None
+
+    lhs = lookup_variable(argument)
+    if lhs is None:
+        # variable resolution failed, treat as string
+        lhs = argument
+
+    if len(arguments) > 1:
+        op = arguments[1]
+        if op == 'AND':
+            return evaluate_constant(lhs) and evaluate_boolean(variables, arguments[2:])
+        elif op == 'MATCHES':
+            rhs = lookup_variable(arguments[2])
+            if not rhs:
+                rhs = arguments[2]
+            return not re.match(rhs, lhs) is None
+        elif op == 'OR':
+            return evaluate_constant(lhs) or evaluate_boolean(variables, arguments[2:])
+        elif op == 'STREQUAL':
+            rhs = lookup_variable(arguments[2])
+            if not rhs:
+                rhs = arguments[2]
+            return lhs == rhs
+    else:
+        lhs = evaluate_constant(lhs)
+        if lhs is None:
+            lhs = lookup_variable(argument)
+
+    return lhs
+
+
+def parse(variables, cache_variables, filename):
+    parsed = cmake.parseFile(filename)
+    cont_eval, sources = evaluate(variables, cache_variables, parsed)
+    return sources
new file mode 100644
--- /dev/null
+++ b/media/libaom/test_cmakeparser.py
@@ -0,0 +1,190 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+from pyparsing import ParseException
+import unittest
+
+import cmakeparser as cp
+
+class TestCMakeParser(unittest.TestCase):
+    def test_arguments(self):
+        self.assertEqual(cp.arguments.parseString('1').asList(), ['1'])
+        self.assertEqual(cp.arguments.parseString('(1 2)').asList(),
+                         ['(', '1', '2', ')'])
+
+    def test_command(self):
+        self.assertEqual(cp.command.parseString('blah()').asList(),
+                         [['blah', '(', ')']])
+        self.assertEqual(cp.command.parseString('blah(1)').asList(),
+                         [['blah', '(', '1', ')']])
+        self.assertEqual(cp.command.parseString('blah(1 (2 3))').asList(),
+                         [['blah', '(', '1', '(', '2', '3', ')', ')']])
+
+    def test_evaluate_boolean(self):
+        self.assertTrue(cp.evaluate_boolean({}, ['TRUE']))
+        self.assertFalse(cp.evaluate_boolean({}, ['NOT', 'TRUE']))
+        self.assertFalse(cp.evaluate_boolean({}, ['TRUE', 'AND', 'FALSE']))
+        self.assertTrue(cp.evaluate_boolean({}, ['ABC', 'MATCHES', '^AB']))
+        self.assertTrue(cp.evaluate_boolean({}, ['TRUE', 'OR', 'FALSE']))
+        self.assertTrue(cp.evaluate_boolean({}, ['ABC', 'STREQUAL', 'ABC']))
+        self.assertTrue(cp.evaluate_boolean({'ABC': '1'}, ['ABC']))
+        self.assertFalse(cp.evaluate_boolean({'ABC': '0'}, ['${ABC}']))
+        self.assertFalse(cp.evaluate_boolean({}, ['ABC']))
+        self.assertFalse(cp.evaluate_boolean({}, ['${ABC}']))
+        self.assertTrue(cp.evaluate_boolean({'YES_ABC': 1, 'VAL': 'ABC'},
+                                            ['YES_${VAL}']))
+        self.assertTrue(cp.evaluate_boolean({'ABC': 'DEF', 'DEF': 1},
+                                            ['${ABC}']))
+        self.assertTrue(cp.evaluate_boolean({}, ['FALSE', 'OR', '(', 'FALSE', 'OR', 'TRUE', ')']))
+        self.assertFalse(cp.evaluate_boolean({}, ['FALSE', 'OR', '(', 'FALSE', 'AND', 'TRUE', ')']))
+
+    def test_foreach(self):
+        s = """
+            set(STUFF A B C D E F)
+            foreach(item ${STUFF})
+                set(YES_${item} 1)
+            endforeach ()
+            """
+        parsed = cp.cmake.parseString(s)
+        variables = {}
+        cp.evaluate(variables, [], parsed)
+        for k in ['A', 'B', 'C', 'D', 'E', 'F']:
+            self.assertEqual(variables['YES_%s' % k], '1')
+
+        s = """
+            set(STUFF "A;B;C;D;E;F")
+            foreach(item ${STUFF})
+                set(${item} 1)
+            endforeach ()
+            """
+        parsed = cp.cmake.parseString(s)
+        variables = {}
+        cp.evaluate(variables, [], parsed)
+        for k in ['A', 'B', 'C', 'D', 'E', 'F']:
+            self.assertEqual(variables[k], '1')
+
+        s = """
+            set(STUFF D E F)
+            foreach(item A B C ${STUFF})
+                set(${item} 1)
+            endforeach ()
+            """
+        parsed = cp.cmake.parseString(s)
+        variables = {}
+        cp.evaluate(variables, [], parsed)
+        for k in ['A', 'B', 'C', 'D', 'E', 'F']:
+            self.assertEqual(variables[k], '1')
+
+    def test_list(self):
+        s = 'list(APPEND TEST 1 1 2 3 5 8 13)'
+        parsed = cp.cmake.parseString(s)
+        variables = {}
+        cache_variables = []
+        cp.evaluate(variables, cache_variables, parsed)
+        self.assertEqual(variables['TEST'], '1 1 2 3 5 8 13')
+        self.assertEqual(len(cache_variables), 0)
+
+        s = """
+            set(TEST 1)
+            list(APPEND TEST 1 2 3 5 8 13)
+            """
+        parsed = cp.cmake.parseString(s)
+        variables = {}
+        cache_variables = []
+        cp.evaluate(variables, cache_variables, parsed)
+        self.assertEqual(variables['TEST'], '1 1 2 3 5 8 13')
+        self.assertEqual(len(cache_variables), 0)
+
+    def test_malformed_input(self):
+        self.assertEqual(len(cp.cmake.parseString('func ((A)')), 0)
+        self.assertEqual(len(cp.cmake.parseString('cmd"arg"(arg2)')), 0)
+        self.assertRaises(ParseException, cp.command.parseString, 'func ((A)')
+        self.assertRaises(ParseException, cp.identifier.parseString,
+                          '-asdlf')
+        self.assertEqual(cp.identifier.parseString('asd-lf')[0], 'asd')
+        self.assertRaises(ParseException, cp.quoted_argument.parseString,
+                          'blah"')
+        s = """
+            " blah blah
+            blah
+            """
+        self.assertRaises(ParseException, cp.quoted_argument.parseString,
+                          s)
+        self.assertRaises(ParseException, cp.quoted_argument.parseString,
+                          'asdlf')
+        self.assertRaises(ParseException, cp.unquoted_argument.parseString,
+                          '#asdflkj')
+
+    def test_parse_if(self):
+        s = """
+            if (A)
+                B()
+            elseif (C)
+                D()
+            elseif (E)
+                F()
+            else ()
+                H()
+            endif ()
+            I()
+            """
+        parsed = cp.cmake.parseString(s)
+        self.assertEqual(len(parsed), 10)
+        end, conditions = cp.parse_if(parsed, 0)
+        self.assertEqual(parsed[end][0], 'endif')
+        self.assertEqual(len(conditions), 4)
+        self.assertEqual(conditions[0][0], ['A'])
+        self.assertEqual(conditions[0][1][0], parsed[1])
+        self.assertEqual(conditions[1][0], ['C'])
+        self.assertEqual(conditions[1][1][0], parsed[3])
+        self.assertEqual(conditions[2][0], ['E'])
+        self.assertEqual(conditions[2][1][0], parsed[5])
+        self.assertEqual(conditions[3][0], ['TRUE'])
+        self.assertEqual(conditions[3][1][0], parsed[7])
+
+    def test_return(self):
+        s = """
+            set(TEST 2)
+            if (true)
+                return()
+            endif ()
+            set(TEST 3)
+            """
+        parsed = cp.cmake.parseString(s)
+        variables = {}
+        cache_variables = []
+        cp.evaluate(variables, cache_variables, parsed)
+        self.assertEqual(variables['TEST'], '2')
+        self.assertEqual(len(cache_variables), 0)
+
+    def test_set(self):
+        s = """set(TEST 2)"""
+        parsed = cp.cmake.parseString(s)
+        variables = {}
+        cache_variables = []
+        cp.evaluate(variables, cache_variables, parsed)
+        self.assertEqual(variables['TEST'], '2')
+        self.assertEqual(len(cache_variables), 0)
+
+        s = """set(TEST 3 CACHE "documentation")"""
+        parsed = cp.cmake.parseString(s)
+        variables = {}
+        cache_variables = []
+        cp.evaluate(variables, cache_variables, parsed)
+        self.assertEqual(variables['TEST'], '3')
+        self.assertTrue('TEST' in cache_variables)
+
+        s = """set(TEST A B C D)"""
+        parsed = cp.cmake.parseString(s)
+        variables = {}
+        cp.evaluate(variables, [], parsed)
+        self.assertEqual(variables['TEST'], 'A B C D')
+
+        s = """set(TEST ${TEST} E F G H)"""
+        parsed = cp.cmake.parseString(s)
+        cp.evaluate(variables, [], parsed)
+        self.assertEqual(variables['TEST'], 'A B C D E F G H')
+
+
+if __name__ == '__main__':
+    unittest.main()