• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2023 The Pigweed Authors
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may not
4# use this file except in compliance with the License. You may obtain a copy of
5# the License at
6#
7#     https://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, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations under
13# the License.
14"""Writes a formatted BUILD.gn _file."""
15
16import os
17import subprocess
18
19from datetime import datetime
20from pathlib import Path, PurePath, PurePosixPath
21from types import TracebackType
22from typing import IO, Iterable, Iterator, Type
23
24from pw_build.gn_config import GnConfig, GN_CONFIG_FLAGS
25from pw_build.gn_target import GnTarget
26from pw_build.gn_utils import GnLabel, GnPath, MalformedGnError
27
28COPYRIGHT_HEADER = f'''
29# Copyright {datetime.now().year} The Pigweed Authors
30#
31# Licensed under the Apache License, Version 2.0 (the "License"); you may not
32# use this file except in compliance with the License. You may obtain a copy of
33# the License at
34#
35#     https://www.apache.org/licenses/LICENSE-2.0
36#
37# Unless required by applicable law or agreed to in writing, software
38# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
39# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
40# License for the specific language governing permissions and limitations under
41# the License.
42
43# DO NOT MANUALLY EDIT!'''
44
45
46class GnWriter:
47    """Represents a partial BUILD.gn file being constructed.
48
49    Except for testing , callers should prefer using `GnFile`. That
50    object wraps this one, and ensures that the GN file produced includes the
51    relevant copyright header and is formatted correctly.
52
53    Attributes:
54        repos: A mapping of repository names to build args. These are used to
55            replace repository names when writing labels.
56        aliases: A mapping of label names to build args. These can be used to
57            rewrite labels with alternate names, e.g. "gtest" to "googletest".
58    """
59
60    def __init__(self, file: IO) -> None:
61        self._file: IO = file
62        self._scopes: list[str] = []
63        self._margin: str = ''
64        self._needs_blank: bool = False
65        self.repos: dict[str, str] = {}
66        self.aliases: dict[str, str] = {}
67
68    def write_comment(self, comment: str | None = None) -> None:
69        """Adds a GN comment.
70
71        Args:
72            comment: The comment string to write.
73        """
74        if not comment:
75            self.write('#')
76            return
77        while len(comment) > 78:
78            index = comment.rfind(' ', 0, 78)
79            if index < 0:
80                break
81            self.write(f'# {comment[:index]}')
82            comment = comment[index + 1 :]
83        self.write(f'# {comment}')
84
85    def write_import(self, gni: str | PurePosixPath | GnPath) -> None:
86        """Adds a GN import.
87
88        Args:
89            gni: The source-relative path to a GN import file.
90        """
91        self._needs_blank = False
92        self.write(f'import("{str(gni)}")')
93        self._needs_blank = True
94
95    def write_imports(self, imports: Iterable[str]) -> None:
96        """Adds a list of GN imports.
97
98        Args:
99            imports: A list of GN import files.
100        """
101        for gni in imports:
102            self.write_import(gni)
103
104    def write_config(self, config: GnConfig) -> None:
105        """Adds a GN config.
106
107        Args:
108            config: The GN config data to write.
109        """
110        if not config:
111            return
112        if not config.label:
113            raise MalformedGnError('missing label for `config`')
114        self.write_target_start('config', config.label.name())
115        for flag in GN_CONFIG_FLAGS:
116            self.write_list(flag, config.get(flag))
117        self.write_end()
118
119    def write_target(self, target: GnTarget) -> None:
120        """Write a GN target.
121
122        Args:
123            target: The GN target data to write.
124        """
125        self.write_comment(
126            f'Generated from //{target.package()}:{target.name()}'
127        )
128        self.write_target_start(target.type(), target.name())
129
130        # GN use no `visibility` to indicate publicly visibile.
131        scopes = filter(lambda s: str(s) != '//*', target.visibility)
132        visibility = [target.make_relative(scope) for scope in scopes]
133        self.write_list('visibility', visibility)
134
135        if not target.check_includes:
136            self.write('check_includes = false')
137        self.write_list('public', [str(path) for path in target.public])
138        self.write_list('sources', [str(path) for path in target.sources])
139        self.write_list('inputs', [str(path) for path in target.inputs])
140
141        for flag in GN_CONFIG_FLAGS:
142            self.write_list(flag, target.config.get(flag))
143        self._write_relative('public_configs', target, target.public_configs)
144        self._write_relative('configs', target, target.configs)
145        self._write_relative('remove_configs', target, target.remove_configs)
146
147        self._write_relative('public_deps', target, target.public_deps)
148        self._write_relative('deps', target, target.deps)
149        self.write_end()
150
151    def _write_relative(
152        self, var_name: str, target: GnTarget, labels: Iterable[GnLabel]
153    ) -> None:
154        """Write a list of labels relative to a target.
155
156        Args:
157            var_name: The name of the GN list variable.
158            target: The GN target to rebase the labels to.
159            labels: The labels to write to the list.
160        """
161        self.write_list(var_name, self._resolve(target, labels))
162
163    def _resolve(
164        self, target: GnTarget, labels: Iterable[GnLabel]
165    ) -> Iterator[str]:
166        """Returns rewritten labels.
167
168        If this label has a repo, it must be a key in this object's `repos` and
169        will be replaced by the corresponding value. If this label  is a key in
170        this object's `aliases`, it will be replaced by the corresponding value.
171
172        Args:
173            labels: The labels to resolve.
174        """
175        for label in labels:
176            repo = label.repo()
177            if repo:
178                label.resolve_repo(self.repos[repo])
179            label = GnLabel(self.aliases.get(str(label), str(label)))
180            yield target.make_relative(label)
181
182    def write_target_start(
183        self, target_type: str, target_name: str | None = None
184    ) -> None:
185        """Begins a GN target of the given type.
186
187        Args:
188            target_type: The type of the GN target.
189            target_name: The name of the GN target.
190        """
191        if target_name:
192            self.write(f'{target_type}("{target_name}") {{')
193            self._indent(target_name)
194        else:
195            self.write(f'{target_type}() {{')
196            self._indent(target_type)
197
198    def write_list(
199        self, var_name: str, items: Iterable[str], reorder: bool = True
200    ) -> None:
201        """Adds a named GN list of the given items, if non-empty.
202
203        Args:
204            var_name: The name of the GN list variable.
205            items: The list items to write as strings.
206            reorder: If true, the list is sorted lexicographically.
207        """
208        items = list(items)
209        if not items:
210            return
211        self.write(f'{var_name} = [')
212        self._indent(var_name)
213        if reorder:
214            items = sorted(items)
215        for item in items:
216            self.write(f'"{str(item)}",')
217        self._outdent()
218        self.write(']')
219
220    def write_scope(self, var_name: str) -> None:
221        """Begins a named GN scope.
222
223        Args:
224            var_name: The name of the GN scope variable.
225        """
226        self.write(f'{var_name} = {{')
227        self._indent(var_name)
228
229    def write_if(self, cond: str) -> None:
230        """Begins a GN 'if' condition.
231
232        Args:
233            cond: The conditional expression.
234        """
235        self.write(f'if ({cond}) {{')
236        self._indent(cond)
237
238    def write_else_if(self, cond: str) -> None:
239        """Adds another GN 'if' condition to a previous 'if' condition.
240
241        Args:
242            cond: The conditional expression.
243        """
244        self._outdent()
245        self.write(f'}} else if ({cond}) {{')
246        self._indent(cond)
247
248    def write_else(self) -> None:
249        """Adds a GN 'else' clause to a previous 'if' condition."""
250        last = self._outdent()
251        self.write('} else {')
252        self._indent(f'!({last})')
253
254    def write_end(self) -> None:
255        """Ends a target, scope, or 'if' condition'."""
256        self._outdent()
257        self.write('}')
258        self._needs_blank = True
259
260    def write_blank(self) -> None:
261        """Adds a blank line."""
262        print('', file=self._file)
263        self._needs_blank = False
264
265    def write_preformatted(self, preformatted: str) -> None:
266        """Adds text with minimal formatting.
267
268        The only formatting applied to the given text is to strip any leading
269        whitespace. This allows calls to be more readable by allowing
270        preformatted text to start on a new line, e.g.
271
272            _write_preformatted('''
273          preformatted line 1
274          preformatted line 2
275          preformatted line 3''')
276
277        Args:
278            preformatted: The text to write.
279        """
280        print(preformatted.lstrip(), file=self._file)
281
282    def write(self, text: str) -> None:
283        """Writes to the file, appropriately indented.
284
285        Args:
286            text: The text to indent and write.
287        """
288        if self._needs_blank:
289            self.write_blank()
290        print(f'{self._margin}{text}', file=self._file)
291
292    def _indent(self, scope: str) -> None:
293        """Increases the current margin.
294
295        Saves the scope of indent to aid in debugging. For example, trying to
296        use incorrect code such as
297
298        ```
299          self.write_if('foo')
300          self.write_comment('bar')
301          self.write_else_if('baz')
302        ```
303
304        will throw an exception due to the missing `write_end`. The exception
305        will note that 'baz' was opened but not closed.
306
307        Args:
308            scope: The name of the scope (for debugging).
309        """
310        self._scopes.append(scope)
311        self._margin += '  '
312
313    def _outdent(self) -> str:
314        """Decreases the current margin."""
315        if not self._scopes:
316            raise MalformedGnError('scope closed unexpectedly')
317        last = self._scopes.pop()
318        self._margin = self._margin[2:]
319        self._needs_blank = False
320        return last
321
322    def seal(self) -> None:
323        """Instructs the object that no more writes will occur."""
324        if self._scopes:
325            raise MalformedGnError(f'unclosed scope(s): {self._scopes}')
326
327
328def gn_format(gn_file: Path) -> None:
329    """Calls `gn format` on a BUILD.gn or GN import file."""
330    subprocess.check_call(['gn', 'format', gn_file])
331
332
333class GnFile:
334    """Represents an open BUILD.gn file that is formatted on close.
335
336    Typical usage:
337
338        with GnFile('/path/to/BUILD.gn', 'my-package') as build_gn:
339          build_gn.write_...
340
341    where "write_..." refers to any of the "write" methods of `GnWriter`.
342    """
343
344    def __init__(self, pathname: PurePath, package: str | None = None) -> None:
345        if pathname.name != 'BUILD.gn' and pathname.suffix != '.gni':
346            raise MalformedGnError(f'invalid GN filename: {pathname}')
347        os.makedirs(pathname.parent, exist_ok=True)
348        self._pathname: PurePath = pathname
349        self._package: str | None = package
350        self._file: IO
351        self._writer: GnWriter
352
353    def __enter__(self) -> GnWriter:
354        """Opens the GN file."""
355        self._file = open(self._pathname, 'w+')
356        self._writer = GnWriter(self._file)
357        self._writer.write_preformatted(COPYRIGHT_HEADER)
358        file = PurePath(*PurePath(__file__).parts[-2:])
359        self._writer.write_comment(
360            f'This file was automatically generated by {file}'
361        )
362        if self._package:
363            self._writer.write_comment(
364                f'It contains GN build targets for {self._package}.'
365            )
366        self._writer.write_blank()
367        return self._writer
368
369    def __exit__(
370        self,
371        exc_type: Type[BaseException] | None,
372        exc_val: BaseException | None,
373        exc_tb: TracebackType | None,
374    ) -> None:
375        """Closes the GN file and formats it."""
376        self._file.close()
377        gn_format(Path(self._pathname))
378