summaryrefslogtreecommitdiffstats
path: root/scons/source_list.py
blob: 1d5166ba1fefe34266a676ae8bc8abb55936dede (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
"""Source List Parser

The syntax of a source list file is a very small subset of GNU Make.  These
features are supported

 operators: =, +=, :=
 line continuation
 non-nested variable expansion
 comment

The goal is to allow Makefile's and SConscript's to share source listing.
"""

class SourceListParser(object):
    def __init__(self):
        self._reset()

    def _reset(self, filename=None):
        self.filename = filename

        self.line_no = 1
        self.line_cont = ''
        self.symbol_table = {}

    def _error(self, msg):
        raise RuntimeError('%s:%d: %s' % (self.filename, self.line_no, msg))

    def _next_dereference(self, val, cur):
        """Locate the next $(...) in value."""
        deref_pos = val.find('$', cur)
        if deref_pos < 0:
            return (-1, -1)
        elif val[deref_pos + 1] != '(':
            self._error('non-variable dereference')

        deref_end = val.find(')', deref_pos + 2)
        if deref_end < 0:
            self._error('unterminated variable dereference')

        return (deref_pos, deref_end + 1)

    def _expand_value(self, val):
        """Perform variable expansion."""
        expanded = ''
        cur = 0
        while True:
            deref_pos, deref_end = self._next_dereference(val, cur)
            if deref_pos < 0:
                expanded += val[cur:]
                break

            sym = val[(deref_pos + 2):(deref_end - 1)]
            expanded += val[cur:deref_pos] + self.symbol_table[sym]
            cur = deref_end

        return expanded

    def _parse_definition(self, line):
        """Parse a variable definition line."""
        op_pos = line.find('=')
        op_end = op_pos + 1
        if op_pos < 0:
            self._error('not a variable definition')

        if op_pos > 0:
            if line[op_pos - 1] in [':', '+']:
                op_pos -= 1
        else:
            self._error('only =, :=, and += are supported')

        # set op, sym, and val
        op = line[op_pos:op_end]
        sym = line[:op_pos].strip()
        val = self._expand_value(line[op_end:].lstrip())

        if op in ('=', ':='):
            self.symbol_table[sym] = val
        elif op == '+=':
            self.symbol_table[sym] += ' ' + val

    def _parse_line(self, line):
        """Parse a source list line."""
        # more lines to come
        if line and line[-1] == '\\':
            # spaces around "\\\n" are replaced by a single space
            if self.line_cont:
                self.line_cont += line[:-1].strip() + ' '
            else:
                self.line_cont = line[:-1].rstrip() + ' '
            return 0

        # combine with previous lines
        if self.line_cont:
            line = self.line_cont + line.lstrip()
            self.line_cont = ''

        if line:
            begins_with_tab = (line[0] == '\t')

            line = line.lstrip()
            if line[0] != '#':
                if begins_with_tab:
                    self._error('recipe line not supported')
                else:
                    self._parse_definition(line)

        return 1

    def parse(self, filename):
        """Parse a source list file."""
        if self.filename != filename:
            fp = open(filename)
            lines = fp.read().splitlines()
            fp.close()

            try:
                self._reset(filename)
                for line in lines:
                    self.line_no += self._parse_line(line)
            except:
                self._reset()
                raise

        return self.symbol_table