OILS / vendor / ninja_syntax.py View on Github | oilshell.org

233 lines, 150 significant
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
17Note that this is emphatically not a required piece of Ninja; it's
18just a helpful utility for build-file-generation systems that already
19use Python.
20"""
21
22import collections
23import re
24import textwrap
25
26def escape_path(word):
27 return word.replace('$ ', '$$ ').replace(' ', '$ ').replace(':', '$:')
28
29
30BuildCall = collections.namedtuple(
31 'BuildCall', 'outputs rule inputs implicit variables')
32
33
34class 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
57class 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
206def as_list(input):
207 if input is None:
208 return []
209 if isinstance(input, list):
210 return input
211 return [input]
212
213
214def 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
222def 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)