1 | # Copyright 2011 Google Inc. All Rights Reserved.
|
2 | #
|
3 | # Licensed under the Apache License, Version 2.0 (the "License");
|
4 | # you may not use this file except in compliance with the License.
|
5 | # You may obtain a copy of the License at
|
6 | #
|
7 | # http://www.apache.org/licenses/LICENSE-2.0
|
8 | #
|
9 | # Unless required by applicable law or agreed to in writing, software
|
10 | # distributed under the License is distributed on an "AS IS" BASIS,
|
11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12 | # See the License for the specific language governing permissions and
|
13 | # limitations under the License.
|
14 |
|
15 | """Python module for generating .ninja files.
|
16 |
|
17 | Note that this is emphatically not a required piece of Ninja; it's
|
18 | just a helpful utility for build-file-generation systems that already
|
19 | use Python.
|
20 | """
|
21 |
|
22 | import collections
|
23 | import re
|
24 | import textwrap
|
25 |
|
26 | def escape_path(word):
|
27 | return word.replace('$ ', '$$ ').replace(' ', '$ ').replace(':', '$:')
|
28 |
|
29 |
|
30 | BuildCall = collections.namedtuple(
|
31 | 'BuildCall', 'outputs rule inputs implicit variables')
|
32 |
|
33 |
|
34 | class FakeWriter(object):
|
35 |
|
36 | def __init__(self, writer):
|
37 | """
|
38 | Args:
|
39 | n: Writer to delegate to
|
40 | """
|
41 | self.writer = writer
|
42 | self.build_calls = [] # recorded
|
43 |
|
44 | def build(self, outputs, rule, inputs=None, implicit=None, order_only=None,
|
45 | variables=None, implicit_outputs=None, pool=None, dyndep=None):
|
46 | b = BuildCall(outputs, rule, inputs=inputs, implicit=implicit, variables=variables)
|
47 | self.build_calls.append(b)
|
48 | self.writer.build(outputs, rule, inputs=inputs, implicit=implicit, variables=variables)
|
49 |
|
50 | def newline(self):
|
51 | self.writer.newline()
|
52 |
|
53 | def num_build_targets(self):
|
54 | return self.writer.num_build_targets()
|
55 |
|
56 |
|
57 | class Writer(object):
|
58 | def __init__(self, output, width=78):
|
59 | self.output = output
|
60 | self.width = width
|
61 |
|
62 | self._num_build_targets = 0 # number of times we call n.build()
|
63 |
|
64 | def newline(self):
|
65 | self.output.write('\n')
|
66 |
|
67 | def comment(self, text):
|
68 | for line in textwrap.wrap(text, self.width - 2, break_long_words=False,
|
69 | break_on_hyphens=False):
|
70 | self.output.write('# ' + line + '\n')
|
71 |
|
72 | def variable(self, key, value, indent=0):
|
73 | if value is None:
|
74 | return
|
75 | if isinstance(value, list):
|
76 | value = ' '.join(filter(None, value)) # Filter out empty strings.
|
77 | self._line('%s = %s' % (key, value), indent)
|
78 |
|
79 | def pool(self, name, depth):
|
80 | self._line('pool %s' % name)
|
81 | self.variable('depth', depth, indent=1)
|
82 |
|
83 | def rule(self, name, command, description=None, depfile=None,
|
84 | generator=False, pool=None, restat=False, rspfile=None,
|
85 | rspfile_content=None, deps=None):
|
86 | self._line('rule %s' % name)
|
87 | self.variable('command', command, indent=1)
|
88 | if description:
|
89 | self.variable('description', description, indent=1)
|
90 | if depfile:
|
91 | self.variable('depfile', depfile, indent=1)
|
92 | if generator:
|
93 | self.variable('generator', '1', indent=1)
|
94 | if pool:
|
95 | self.variable('pool', pool, indent=1)
|
96 | if restat:
|
97 | self.variable('restat', '1', indent=1)
|
98 | if rspfile:
|
99 | self.variable('rspfile', rspfile, indent=1)
|
100 | if rspfile_content:
|
101 | self.variable('rspfile_content', rspfile_content, indent=1)
|
102 | if deps:
|
103 | self.variable('deps', deps, indent=1)
|
104 |
|
105 | def build(self, outputs, rule, inputs=None, implicit=None, order_only=None,
|
106 | variables=None, implicit_outputs=None, pool=None, dyndep=None):
|
107 | outputs = as_list(outputs)
|
108 | out_outputs = [escape_path(x) for x in outputs]
|
109 | all_inputs = [escape_path(x) for x in as_list(inputs)]
|
110 |
|
111 | if implicit:
|
112 | implicit = [escape_path(x) for x in as_list(implicit)]
|
113 | all_inputs.append('|')
|
114 | all_inputs.extend(implicit)
|
115 | if order_only:
|
116 | order_only = [escape_path(x) for x in as_list(order_only)]
|
117 | all_inputs.append('||')
|
118 | all_inputs.extend(order_only)
|
119 | if implicit_outputs:
|
120 | implicit_outputs = [escape_path(x)
|
121 | for x in as_list(implicit_outputs)]
|
122 | out_outputs.append('|')
|
123 | out_outputs.extend(implicit_outputs)
|
124 |
|
125 | self._line('build %s: %s' % (' '.join(out_outputs),
|
126 | ' '.join([rule] + all_inputs)))
|
127 | if pool is not None:
|
128 | self._line(' pool = %s' % pool)
|
129 | if dyndep is not None:
|
130 | self._line(' dyndep = %s' % dyndep)
|
131 |
|
132 | if variables:
|
133 | if isinstance(variables, dict):
|
134 | iterator = iter(variables.items())
|
135 | else:
|
136 | iterator = iter(variables)
|
137 |
|
138 | for key, val in iterator:
|
139 | self.variable(key, val, indent=1)
|
140 |
|
141 | self._num_build_targets += 1
|
142 |
|
143 | return outputs
|
144 |
|
145 | def num_build_targets(self):
|
146 | return self._num_build_targets
|
147 |
|
148 | def include(self, path):
|
149 | self._line('include %s' % path)
|
150 |
|
151 | def subninja(self, path):
|
152 | self._line('subninja %s' % path)
|
153 |
|
154 | def default(self, paths):
|
155 | self._line('default %s' % ' '.join(as_list(paths)))
|
156 |
|
157 | def _count_dollars_before_index(self, s, i):
|
158 | """Returns the number of '$' characters right in front of s[i]."""
|
159 | dollar_count = 0
|
160 | dollar_index = i - 1
|
161 | while dollar_index > 0 and s[dollar_index] == '$':
|
162 | dollar_count += 1
|
163 | dollar_index -= 1
|
164 | return dollar_count
|
165 |
|
166 | def _line(self, text, indent=0):
|
167 | """Write 'text' word-wrapped at self.width characters."""
|
168 | leading_space = ' ' * indent
|
169 | while len(leading_space) + len(text) > self.width:
|
170 | # The text is too wide; wrap if possible.
|
171 |
|
172 | # Find the rightmost space that would obey our width constraint and
|
173 | # that's not an escaped space.
|
174 | available_space = self.width - len(leading_space) - len(' $')
|
175 | space = available_space
|
176 | while True:
|
177 | space = text.rfind(' ', 0, space)
|
178 | if (space < 0 or
|
179 | self._count_dollars_before_index(text, space) % 2 == 0):
|
180 | break
|
181 |
|
182 | if space < 0:
|
183 | # No such space; just use the first unescaped space we can find.
|
184 | space = available_space - 1
|
185 | while True:
|
186 | space = text.find(' ', space + 1)
|
187 | if (space < 0 or
|
188 | self._count_dollars_before_index(text, space) % 2 == 0):
|
189 | break
|
190 | if space < 0:
|
191 | # Give up on breaking.
|
192 | break
|
193 |
|
194 | self.output.write(leading_space + text[0:space] + ' $\n')
|
195 | text = text[space+1:]
|
196 |
|
197 | # Subsequent lines are continuations, so indent them.
|
198 | leading_space = ' ' * (indent+2)
|
199 |
|
200 | self.output.write(leading_space + text + '\n')
|
201 |
|
202 | def close(self):
|
203 | self.output.close()
|
204 |
|
205 |
|
206 | def as_list(input):
|
207 | if input is None:
|
208 | return []
|
209 | if isinstance(input, list):
|
210 | return input
|
211 | return [input]
|
212 |
|
213 |
|
214 | def escape(string):
|
215 | """Escape a string such that it can be embedded into a Ninja file without
|
216 | further interpretation."""
|
217 | assert '\n' not in string, 'Ninja syntax does not allow newlines'
|
218 | # We only have one special metacharacter: '$'.
|
219 | return string.replace('$', '$$')
|
220 |
|
221 |
|
222 | def expand(string, vars, local_vars={}):
|
223 | """Expand a string containing $vars as Ninja would.
|
224 |
|
225 | Note: doesn't handle the full Ninja variable syntax, but it's enough
|
226 | to make configure.py's use of it work.
|
227 | """
|
228 | def exp(m):
|
229 | var = m.group(1)
|
230 | if var == '$':
|
231 | return '$'
|
232 | return local_vars.get(var, vars.get(var, ''))
|
233 | return re.sub(r'\$(\$|\w*)', exp, string)
|