1"""Source List Parser
2
3The syntax of a source list file is a very small subset of GNU Make.  These
4features are supported
5
6 operators: =, +=, :=
7 line continuation
8 non-nested variable expansion
9 comment
10
11The goal is to allow Makefile's and SConscript's to share source listing.
12"""
13
14class SourceListParser(object):
15    def __init__(self):
16        self.symbol_table = {}
17        self._reset()
18
19    def _reset(self, filename=None):
20        self.filename = filename
21
22        self.line_no = 1
23        self.line_cont = ''
24
25    def _error(self, msg):
26        raise RuntimeError('%s:%d: %s' % (self.filename, self.line_no, msg))
27
28    def _next_dereference(self, val, cur):
29        """Locate the next $(...) in value."""
30        deref_pos = val.find('$', cur)
31        if deref_pos < 0:
32            return (-1, -1)
33        elif val[deref_pos + 1] != '(':
34            self._error('non-variable dereference')
35
36        deref_end = val.find(')', deref_pos + 2)
37        if deref_end < 0:
38            self._error('unterminated variable dereference')
39
40        return (deref_pos, deref_end + 1)
41
42    def _expand_value(self, val):
43        """Perform variable expansion."""
44        expanded = ''
45        cur = 0
46        while True:
47            deref_pos, deref_end = self._next_dereference(val, cur)
48            if deref_pos < 0:
49                expanded += val[cur:]
50                break
51
52            sym = val[(deref_pos + 2):(deref_end - 1)]
53            expanded += val[cur:deref_pos] + self.symbol_table[sym]
54            cur = deref_end
55
56        return expanded
57
58    def _parse_definition(self, line):
59        """Parse a variable definition line."""
60        op_pos = line.find('=')
61        op_end = op_pos + 1
62        if op_pos < 0:
63            self._error('not a variable definition')
64
65        if op_pos > 0:
66            if line[op_pos - 1] in [':', '+', '?']:
67                op_pos -= 1
68        else:
69            self._error('only =, :=, and += are supported')
70
71        # set op, sym, and val
72        op = line[op_pos:op_end]
73        sym = line[:op_pos].strip()
74        val = self._expand_value(line[op_end:].lstrip())
75
76        if op in ('=', ':='):
77            self.symbol_table[sym] = val
78        elif op == '+=':
79            self.symbol_table[sym] += ' ' + val
80        elif op == '?=':
81            if sym not in self.symbol_table:
82                self.symbol_table[sym] = val
83
84    def _parse_line(self, line):
85        """Parse a source list line."""
86        # more lines to come
87        if line and line[-1] == '\\':
88            # spaces around "\\\n" are replaced by a single space
89            if self.line_cont:
90                self.line_cont += line[:-1].strip() + ' '
91            else:
92                self.line_cont = line[:-1].rstrip() + ' '
93            return 0
94
95        # combine with previous lines
96        if self.line_cont:
97            line = self.line_cont + line.lstrip()
98            self.line_cont = ''
99
100        if line:
101            begins_with_tab = (line[0] == '\t')
102
103            line = line.lstrip()
104            if line[0] != '#':
105                if begins_with_tab:
106                    self._error('recipe line not supported')
107                else:
108                    self._parse_definition(line)
109
110        return 1
111
112    def parse(self, filename):
113        """Parse a source list file."""
114        if self.filename != filename:
115            fp = open(filename)
116            lines = fp.read().splitlines()
117            fp.close()
118
119            try:
120                self._reset(filename)
121                for line in lines:
122                    self.line_no += self._parse_line(line)
123            except:
124                self._reset()
125                raise
126
127        return self.symbol_table
128
129    def add_symbol(self, name, value):
130        self.symbol_table[name] = value
131