1695b41eeSopenharmony_ci#!/usr/bin/python
2695b41eeSopenharmony_ci
3695b41eeSopenharmony_ci# Copyright 2011 Google Inc. All Rights Reserved.
4695b41eeSopenharmony_ci#
5695b41eeSopenharmony_ci# Licensed under the Apache License, Version 2.0 (the "License");
6695b41eeSopenharmony_ci# you may not use this file except in compliance with the License.
7695b41eeSopenharmony_ci# You may obtain a copy of the License at
8695b41eeSopenharmony_ci#
9695b41eeSopenharmony_ci#     http://www.apache.org/licenses/LICENSE-2.0
10695b41eeSopenharmony_ci#
11695b41eeSopenharmony_ci# Unless required by applicable law or agreed to in writing, software
12695b41eeSopenharmony_ci# distributed under the License is distributed on an "AS IS" BASIS,
13695b41eeSopenharmony_ci# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14695b41eeSopenharmony_ci# See the License for the specific language governing permissions and
15695b41eeSopenharmony_ci# limitations under the License.
16695b41eeSopenharmony_ci
17695b41eeSopenharmony_ci"""Python module for generating .ninja files.
18695b41eeSopenharmony_ci
19695b41eeSopenharmony_ciNote that this is emphatically not a required piece of Ninja; it's
20695b41eeSopenharmony_cijust a helpful utility for build-file-generation systems that already
21695b41eeSopenharmony_ciuse Python.
22695b41eeSopenharmony_ci"""
23695b41eeSopenharmony_ci
24695b41eeSopenharmony_ciimport re
25695b41eeSopenharmony_ciimport textwrap
26695b41eeSopenharmony_cifrom io import TextIOWrapper
27695b41eeSopenharmony_cifrom typing import Dict, List, Match, Optional, Tuple, Union
28695b41eeSopenharmony_ci
29695b41eeSopenharmony_cidef escape_path(word: str) -> str:
30695b41eeSopenharmony_ci    return word.replace('$ ', '$$ ').replace(' ', '$ ').replace(':', '$:')
31695b41eeSopenharmony_ci
32695b41eeSopenharmony_ciclass Writer(object):
33695b41eeSopenharmony_ci    def __init__(self, output: TextIOWrapper, width: int = 78) -> None:
34695b41eeSopenharmony_ci        self.output = output
35695b41eeSopenharmony_ci        self.width = width
36695b41eeSopenharmony_ci
37695b41eeSopenharmony_ci    def newline(self) -> None:
38695b41eeSopenharmony_ci        self.output.write('\n')
39695b41eeSopenharmony_ci
40695b41eeSopenharmony_ci    def comment(self, text: str) -> None:
41695b41eeSopenharmony_ci        for line in textwrap.wrap(text, self.width - 2, break_long_words=False,
42695b41eeSopenharmony_ci                                  break_on_hyphens=False):
43695b41eeSopenharmony_ci            self.output.write('# ' + line + '\n')
44695b41eeSopenharmony_ci
45695b41eeSopenharmony_ci    def variable(
46695b41eeSopenharmony_ci        self,
47695b41eeSopenharmony_ci        key: str,
48695b41eeSopenharmony_ci        value: Optional[Union[bool, int, float, str, List[str]]],
49695b41eeSopenharmony_ci        indent: int = 0,
50695b41eeSopenharmony_ci    ) -> None:
51695b41eeSopenharmony_ci        if value is None:
52695b41eeSopenharmony_ci            return
53695b41eeSopenharmony_ci        if isinstance(value, list):
54695b41eeSopenharmony_ci            value = ' '.join(filter(None, value))  # Filter out empty strings.
55695b41eeSopenharmony_ci        self._line('%s = %s' % (key, value), indent)
56695b41eeSopenharmony_ci
57695b41eeSopenharmony_ci    def pool(self, name: str, depth: int) -> None:
58695b41eeSopenharmony_ci        self._line('pool %s' % name)
59695b41eeSopenharmony_ci        self.variable('depth', depth, indent=1)
60695b41eeSopenharmony_ci
61695b41eeSopenharmony_ci    def rule(
62695b41eeSopenharmony_ci        self,
63695b41eeSopenharmony_ci        name: str,
64695b41eeSopenharmony_ci        command: str,
65695b41eeSopenharmony_ci        description: Optional[str] = None,
66695b41eeSopenharmony_ci        depfile: Optional[str] = None,
67695b41eeSopenharmony_ci        generator: bool = False,
68695b41eeSopenharmony_ci        pool: Optional[str] = None,
69695b41eeSopenharmony_ci        restat: bool = False,
70695b41eeSopenharmony_ci        rspfile: Optional[str] = None,
71695b41eeSopenharmony_ci        rspfile_content: Optional[str] = None,
72695b41eeSopenharmony_ci        deps: Optional[Union[str, List[str]]] = None,
73695b41eeSopenharmony_ci    ) -> None:
74695b41eeSopenharmony_ci        self._line('rule %s' % name)
75695b41eeSopenharmony_ci        self.variable('command', command, indent=1)
76695b41eeSopenharmony_ci        if description:
77695b41eeSopenharmony_ci            self.variable('description', description, indent=1)
78695b41eeSopenharmony_ci        if depfile:
79695b41eeSopenharmony_ci            self.variable('depfile', depfile, indent=1)
80695b41eeSopenharmony_ci        if generator:
81695b41eeSopenharmony_ci            self.variable('generator', '1', indent=1)
82695b41eeSopenharmony_ci        if pool:
83695b41eeSopenharmony_ci            self.variable('pool', pool, indent=1)
84695b41eeSopenharmony_ci        if restat:
85695b41eeSopenharmony_ci            self.variable('restat', '1', indent=1)
86695b41eeSopenharmony_ci        if rspfile:
87695b41eeSopenharmony_ci            self.variable('rspfile', rspfile, indent=1)
88695b41eeSopenharmony_ci        if rspfile_content:
89695b41eeSopenharmony_ci            self.variable('rspfile_content', rspfile_content, indent=1)
90695b41eeSopenharmony_ci        if deps:
91695b41eeSopenharmony_ci            self.variable('deps', deps, indent=1)
92695b41eeSopenharmony_ci
93695b41eeSopenharmony_ci    def build(
94695b41eeSopenharmony_ci        self,
95695b41eeSopenharmony_ci        outputs: Union[str, List[str]],
96695b41eeSopenharmony_ci        rule: str,
97695b41eeSopenharmony_ci        inputs: Optional[Union[str, List[str]]] = None,
98695b41eeSopenharmony_ci        implicit: Optional[Union[str, List[str]]] = None,
99695b41eeSopenharmony_ci        order_only: Optional[Union[str, List[str]]] = None,
100695b41eeSopenharmony_ci        variables: Optional[
101695b41eeSopenharmony_ci            Union[
102695b41eeSopenharmony_ci                List[Tuple[str, Optional[Union[str, List[str]]]]],
103695b41eeSopenharmony_ci                Dict[str, Optional[Union[str, List[str]]]],
104695b41eeSopenharmony_ci            ]
105695b41eeSopenharmony_ci        ] = None,
106695b41eeSopenharmony_ci        implicit_outputs: Optional[Union[str, List[str]]] = None,
107695b41eeSopenharmony_ci        pool: Optional[str] = None,
108695b41eeSopenharmony_ci        dyndep: Optional[str] = None,
109695b41eeSopenharmony_ci    ) -> List[str]:
110695b41eeSopenharmony_ci        outputs = as_list(outputs)
111695b41eeSopenharmony_ci        out_outputs = [escape_path(x) for x in outputs]
112695b41eeSopenharmony_ci        all_inputs = [escape_path(x) for x in as_list(inputs)]
113695b41eeSopenharmony_ci
114695b41eeSopenharmony_ci        if implicit:
115695b41eeSopenharmony_ci            implicit = [escape_path(x) for x in as_list(implicit)]
116695b41eeSopenharmony_ci            all_inputs.append('|')
117695b41eeSopenharmony_ci            all_inputs.extend(implicit)
118695b41eeSopenharmony_ci        if order_only:
119695b41eeSopenharmony_ci            order_only = [escape_path(x) for x in as_list(order_only)]
120695b41eeSopenharmony_ci            all_inputs.append('||')
121695b41eeSopenharmony_ci            all_inputs.extend(order_only)
122695b41eeSopenharmony_ci        if implicit_outputs:
123695b41eeSopenharmony_ci            implicit_outputs = [escape_path(x)
124695b41eeSopenharmony_ci                                for x in as_list(implicit_outputs)]
125695b41eeSopenharmony_ci            out_outputs.append('|')
126695b41eeSopenharmony_ci            out_outputs.extend(implicit_outputs)
127695b41eeSopenharmony_ci
128695b41eeSopenharmony_ci        self._line('build %s: %s' % (' '.join(out_outputs),
129695b41eeSopenharmony_ci                                     ' '.join([rule] + all_inputs)))
130695b41eeSopenharmony_ci        if pool is not None:
131695b41eeSopenharmony_ci            self._line('  pool = %s' % pool)
132695b41eeSopenharmony_ci        if dyndep is not None:
133695b41eeSopenharmony_ci            self._line('  dyndep = %s' % dyndep)
134695b41eeSopenharmony_ci
135695b41eeSopenharmony_ci        if variables:
136695b41eeSopenharmony_ci            if isinstance(variables, dict):
137695b41eeSopenharmony_ci                iterator = iter(variables.items())
138695b41eeSopenharmony_ci            else:
139695b41eeSopenharmony_ci                iterator = iter(variables)
140695b41eeSopenharmony_ci
141695b41eeSopenharmony_ci            for key, val in iterator:
142695b41eeSopenharmony_ci                self.variable(key, val, indent=1)
143695b41eeSopenharmony_ci
144695b41eeSopenharmony_ci        return outputs
145695b41eeSopenharmony_ci
146695b41eeSopenharmony_ci    def include(self, path: str) -> None:
147695b41eeSopenharmony_ci        self._line('include %s' % path)
148695b41eeSopenharmony_ci
149695b41eeSopenharmony_ci    def subninja(self, path: str) -> None:
150695b41eeSopenharmony_ci        self._line('subninja %s' % path)
151695b41eeSopenharmony_ci
152695b41eeSopenharmony_ci    def default(self, paths: Union[str, List[str]]) -> None:
153695b41eeSopenharmony_ci        self._line('default %s' % ' '.join(as_list(paths)))
154695b41eeSopenharmony_ci
155695b41eeSopenharmony_ci    def _count_dollars_before_index(self, s: str, i: int) -> int:
156695b41eeSopenharmony_ci        """Returns the number of '$' characters right in front of s[i]."""
157695b41eeSopenharmony_ci        dollar_count = 0
158695b41eeSopenharmony_ci        dollar_index = i - 1
159695b41eeSopenharmony_ci        while dollar_index > 0 and s[dollar_index] == '$':
160695b41eeSopenharmony_ci            dollar_count += 1
161695b41eeSopenharmony_ci            dollar_index -= 1
162695b41eeSopenharmony_ci        return dollar_count
163695b41eeSopenharmony_ci
164695b41eeSopenharmony_ci    def _line(self, text: str, indent: int = 0) -> None:
165695b41eeSopenharmony_ci        """Write 'text' word-wrapped at self.width characters."""
166695b41eeSopenharmony_ci        leading_space = '  ' * indent
167695b41eeSopenharmony_ci        while len(leading_space) + len(text) > self.width:
168695b41eeSopenharmony_ci            # The text is too wide; wrap if possible.
169695b41eeSopenharmony_ci
170695b41eeSopenharmony_ci            # Find the rightmost space that would obey our width constraint and
171695b41eeSopenharmony_ci            # that's not an escaped space.
172695b41eeSopenharmony_ci            available_space = self.width - len(leading_space) - len(' $')
173695b41eeSopenharmony_ci            space = available_space
174695b41eeSopenharmony_ci            while True:
175695b41eeSopenharmony_ci                space = text.rfind(' ', 0, space)
176695b41eeSopenharmony_ci                if (space < 0 or
177695b41eeSopenharmony_ci                    self._count_dollars_before_index(text, space) % 2 == 0):
178695b41eeSopenharmony_ci                    break
179695b41eeSopenharmony_ci
180695b41eeSopenharmony_ci            if space < 0:
181695b41eeSopenharmony_ci                # No such space; just use the first unescaped space we can find.
182695b41eeSopenharmony_ci                space = available_space - 1
183695b41eeSopenharmony_ci                while True:
184695b41eeSopenharmony_ci                    space = text.find(' ', space + 1)
185695b41eeSopenharmony_ci                    if (space < 0 or
186695b41eeSopenharmony_ci                        self._count_dollars_before_index(text, space) % 2 == 0):
187695b41eeSopenharmony_ci                        break
188695b41eeSopenharmony_ci            if space < 0:
189695b41eeSopenharmony_ci                # Give up on breaking.
190695b41eeSopenharmony_ci                break
191695b41eeSopenharmony_ci
192695b41eeSopenharmony_ci            self.output.write(leading_space + text[0:space] + ' $\n')
193695b41eeSopenharmony_ci            text = text[space+1:]
194695b41eeSopenharmony_ci
195695b41eeSopenharmony_ci            # Subsequent lines are continuations, so indent them.
196695b41eeSopenharmony_ci            leading_space = '  ' * (indent+2)
197695b41eeSopenharmony_ci
198695b41eeSopenharmony_ci        self.output.write(leading_space + text + '\n')
199695b41eeSopenharmony_ci
200695b41eeSopenharmony_ci    def close(self) -> None:
201695b41eeSopenharmony_ci        self.output.close()
202695b41eeSopenharmony_ci
203695b41eeSopenharmony_ci
204695b41eeSopenharmony_cidef as_list(input: Optional[Union[str, List[str]]]) -> List[str]:
205695b41eeSopenharmony_ci    if input is None:
206695b41eeSopenharmony_ci        return []
207695b41eeSopenharmony_ci    if isinstance(input, list):
208695b41eeSopenharmony_ci        return input
209695b41eeSopenharmony_ci    return [input]
210695b41eeSopenharmony_ci
211695b41eeSopenharmony_ci
212695b41eeSopenharmony_cidef escape(string: str) -> str:
213695b41eeSopenharmony_ci    """Escape a string such that it can be embedded into a Ninja file without
214695b41eeSopenharmony_ci    further interpretation."""
215695b41eeSopenharmony_ci    assert '\n' not in string, 'Ninja syntax does not allow newlines'
216695b41eeSopenharmony_ci    # We only have one special metacharacter: '$'.
217695b41eeSopenharmony_ci    return string.replace('$', '$$')
218695b41eeSopenharmony_ci
219695b41eeSopenharmony_ci
220695b41eeSopenharmony_cidef expand(string: str, vars: Dict[str, str], local_vars: Dict[str, str] = {}) -> str:
221695b41eeSopenharmony_ci    """Expand a string containing $vars as Ninja would.
222695b41eeSopenharmony_ci
223695b41eeSopenharmony_ci    Note: doesn't handle the full Ninja variable syntax, but it's enough
224695b41eeSopenharmony_ci    to make configure.py's use of it work.
225695b41eeSopenharmony_ci    """
226695b41eeSopenharmony_ci    def exp(m: Match[str]) -> str:
227695b41eeSopenharmony_ci        var = m.group(1)
228695b41eeSopenharmony_ci        if var == '$':
229695b41eeSopenharmony_ci            return '$'
230695b41eeSopenharmony_ci        return local_vars.get(var, vars.get(var, ''))
231695b41eeSopenharmony_ci    return re.sub(r'\$(\$|\w*)', exp, string)
232