comparison windows-dev/ninja_syntax.py @ 571:aa240af27589

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