1#!/usr/bin/python
2
3"""Python module for generating .ninja files.
4
5Note that this is emphatically not a required piece of Ninja; it's
6just a helpful utility for build-file-generation systems that already
7use Python.
8"""
9
10import textwrap
11
12def escape_path(word):
13    return word.replace('$ ', '$$ ').replace(' ', '$ ').replace(':', '$:')
14
15class Writer(object):
16    def __init__(self, output, width=78):
17        self.output = output
18        self.width = width
19
20    def newline(self):
21        self.output.write('\n')
22
23    def comment(self, text):
24        for line in textwrap.wrap(text, self.width - 2):
25            self.output.write('# ' + line + '\n')
26
27    def variable(self, key, value, indent=0):
28        if value is None:
29            return
30        if isinstance(value, list):
31            value = ' '.join(filter(None, value))  # Filter out empty strings.
32        self._line('%s = %s' % (key, value), indent)
33
34    def pool(self, name, depth):
35        self._line('pool %s' % name)
36        self.variable('depth', depth, indent=1)
37
38    def rule(self, name, command, description=None, depfile=None,
39             generator=False, pool=None, restat=False, rspfile=None,
40             rspfile_content=None, deps=None):
41        self._line('rule %s' % name)
42        self.variable('command', command, indent=1)
43        if description:
44            self.variable('description', description, indent=1)
45        if depfile:
46            self.variable('depfile', depfile, indent=1)
47        if generator:
48            self.variable('generator', '1', indent=1)
49        if pool:
50            self.variable('pool', pool, indent=1)
51        if restat:
52            self.variable('restat', '1', indent=1)
53        if rspfile:
54            self.variable('rspfile', rspfile, indent=1)
55        if rspfile_content:
56            self.variable('rspfile_content', rspfile_content, indent=1)
57        if deps:
58            self.variable('deps', deps, indent=1)
59
60    def build(self, outputs, rule, inputs=None, implicit=None, order_only=None,
61              variables=None):
62        outputs = self._as_list(outputs)
63        out_outputs = [escape_path(x) for x in outputs]
64        all_inputs = [escape_path(x) for x in self._as_list(inputs)]
65
66        if implicit:
67            implicit = [escape_path(x) for x in self._as_list(implicit)]
68            all_inputs.append('|')
69            all_inputs.extend(implicit)
70        if order_only:
71            order_only = [escape_path(x) for x in self._as_list(order_only)]
72            all_inputs.append('||')
73            all_inputs.extend(order_only)
74
75        self._line('build %s: %s' % (' '.join(out_outputs),
76                                     ' '.join([rule] + all_inputs)))
77
78        if variables:
79            if isinstance(variables, dict):
80                iterator = iter(variables.items())
81            else:
82                iterator = iter(variables)
83
84            for key, val in iterator:
85                self.variable(key, val, indent=1)
86
87        return outputs
88
89    def include(self, path):
90        self._line('include %s' % path)
91
92    def subninja(self, path):
93        self._line('subninja %s' % path)
94
95    def default(self, paths):
96        self._line('default %s' % ' '.join(self._as_list(paths)))
97
98    def _count_dollars_before_index(self, s, i):
99        """Returns the number of '$' characters right in front of s[i]."""
100        dollar_count = 0
101        dollar_index = i - 1
102        while dollar_index > 0 and s[dollar_index] == '$':
103            dollar_count += 1
104            dollar_index -= 1
105        return dollar_count
106
107    def _line(self, text, indent=0):
108        """Write 'text' word-wrapped at self.width characters."""
109        leading_space = '  ' * indent
110        while len(leading_space) + len(text) > self.width:
111            # The text is too wide; wrap if possible.
112
113            # Find the rightmost space that would obey our width constraint and
114            # that's not an escaped space.
115            available_space = self.width - len(leading_space) - len(' $')
116            space = available_space
117            while True:
118                space = text.rfind(' ', 0, space)
119                if (space < 0 or
120                    self._count_dollars_before_index(text, space) % 2 == 0):
121                    break
122
123            if space < 0:
124                # No such space; just use the first unescaped space we can find.
125                space = available_space - 1
126                while True:
127                    space = text.find(' ', space + 1)
128                    if (space < 0 or
129                        self._count_dollars_before_index(text, space) % 2 == 0):
130                        break
131            if space < 0:
132                # Give up on breaking.
133                break
134
135            self.output.write(leading_space + text[0:space] + ' $\n')
136            text = text[space+1:]
137
138            # Subsequent lines are continuations, so indent them.
139            leading_space = '  ' * (indent+2)
140
141        self.output.write(leading_space + text + '\n')
142
143    def _as_list(self, input):
144        if input is None:
145            return []
146        if isinstance(input, list):
147            return input
148        return [input]
149
150
151def escape(string):
152    """Escape a string such that it can be embedded into a Ninja file without
153    further interpretation."""
154    assert '\n' not in string, 'Ninja syntax does not allow newlines'
155    # We only have one special metacharacter: '$'.
156    return string.replace('$', '$$')
157