xref: /aosp_15_r20/external/pigweed/pw_build/py/pw_build/gn_writer.py (revision 61c4878ac05f98d0ceed94b57d316916de578985)
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 PurePath, PurePosixPath
21from types import TracebackType
22from typing import IO, Iterable, Set, Type
23
24from pw_build.gn_target import GnTarget
25
26COPYRIGHT_HEADER = f'''
27# Copyright {datetime.now().year} The Pigweed Authors
28#
29# Licensed under the Apache License, Version 2.0 (the "License"); you may not
30# use this file except in compliance with the License. You may obtain a copy of
31# the License at
32#
33#     https://www.apache.org/licenses/LICENSE-2.0
34#
35# Unless required by applicable law or agreed to in writing, software
36# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
37# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
38# License for the specific language governing permissions and limitations under
39# the License.
40
41# DO NOT MANUALLY EDIT!'''
42
43
44class MalformedGnError(Exception):
45    """Raised when creating a GN object fails."""
46
47
48class GnWriter:
49    """Represents a partial BUILD.gn file being constructed.
50
51    Except for testing , callers should prefer using `GnFile`. That
52    object wraps this one, and ensures that the GN file produced includes the
53    relevant copyright header and is formatted correctly.
54    """
55
56    def __init__(self, file: IO) -> None:
57        self._file: IO = file
58        self._scopes: list[str] = []
59        self._margin: str = ''
60
61    def write_comment(self, comment: str | None = None) -> None:
62        """Adds a GN comment.
63
64        Args:
65            comment: The comment string to write.
66        """
67        if not comment:
68            self.write('#')
69            return
70        while len(comment) > 78:
71            index = comment.rfind(' ', 0, 78)
72            if index < 0:
73                break
74            self.write(f'# {comment[:index]}')
75            comment = comment[index + 1 :]
76        self.write(f'# {comment}')
77
78    def write_import(self, gni: str | PurePosixPath) -> None:
79        """Adds a GN import.
80
81        Args:
82            gni: The source-relative path to a GN import file.
83        """
84        self.write(f'import("{str(gni)}")')
85
86    def write_target(self, gn_target: GnTarget) -> None:
87        """Write a GN target.
88
89        Args:
90            target: The GN target data to write.
91        """
92        if gn_target.origin:
93            self.write_comment(f'Generated from {gn_target.origin}')
94        self.write(f'{gn_target.type()}("{gn_target.name()}") {{')
95        self._margin += '  '
96
97        # inclusive-language: disable
98        # See https://gn.googlesource.com/gn/+/master/docs/style_guide.md
99        # inclusive-language: enable
100        ordered_names = [
101            'public',
102            'sources',
103            'inputs',
104            'cflags',
105            'include_dirs',
106            'defines',
107            'public_configs',
108            'configs',
109            'public_deps',
110            'deps',
111        ]
112        for attr in ordered_names:
113            self.write_list(attr, gn_target.attrs.get(attr, []))
114        self._margin = self._margin[2:]
115        self.write('}')
116
117    def write_list(self, var_name: str, items: Iterable[str]) -> None:
118        """Adds a named GN list of the given items, if non-empty.
119
120        Args:
121            var_name: The name of the GN list variable.
122            items: The list items to write as strings.
123        """
124        items = list(items)
125        if not items:
126            return
127        self.write(f'{var_name} = [')
128        self._margin += '  '
129        for item in sorted(items):
130            self.write(f'"{str(item)}",')
131        self._margin = self._margin[2:]
132        self.write(']')
133
134    def write_blank(self) -> None:
135        """Adds a blank line."""
136        print('', file=self._file)
137
138    def write_preformatted(self, preformatted: str) -> None:
139        """Adds text with minimal formatting.
140
141        The only formatting applied to the given text is to strip any leading
142        whitespace. This allows calls to be more readable by allowing
143        preformatted text to start on a new line, e.g.
144
145            _write_preformatted('''
146          preformatted line 1
147          preformatted line 2
148          preformatted line 3''')
149
150        Args:
151            preformatted: The text to write.
152        """
153        print(preformatted.lstrip(), file=self._file)
154
155    def write_file(self, imports: Set[str], gn_targets: list[GnTarget]) -> None:
156        """Write a complete BUILD.gn file.
157
158        Args:
159            imports: A list of GNI files needed by targets.
160            gn_targets: A list of GN targets to add to the file.
161        """
162        self.write_import('//build_overrides/pigweed.gni')
163        if imports:
164            self.write_blank()
165        for gni in sorted(list(imports)):
166            self.write_import(gni)
167        for target in sorted(gn_targets, key=lambda t: t.name()):
168            self.write_blank()
169            self.write_target(target)
170
171    def write(self, text: str) -> None:
172        """Writes to the file, appropriately indented.
173
174        Args:
175            text: The text to indent and write.
176        """
177        print(f'{self._margin}{text}', file=self._file)
178
179
180class GnFile:
181    """Represents an open BUILD.gn file that is formatted on close.
182
183    Typical usage:
184
185        with GnFile('/path/to/BUILD.gn', 'my-package') as build_gn:
186          build_gn.write_...
187
188    where "write_..." refers to any of the "write" methods of `GnWriter`.
189    """
190
191    def __init__(self, pathname: PurePath) -> None:
192        if pathname.name != 'BUILD.gn' and pathname.suffix != '.gni':
193            raise MalformedGnError(f'invalid GN filename: {pathname}')
194        os.makedirs(pathname.parent, exist_ok=True)
195        self._pathname: PurePath = pathname
196        self._file: IO
197        self._writer: GnWriter
198
199    def __enter__(self) -> GnWriter:
200        """Opens the GN file."""
201        self._file = open(self._pathname, 'w+')
202        self._writer = GnWriter(self._file)
203        self._writer.write_preformatted(COPYRIGHT_HEADER)
204        file = PurePath(*PurePath(__file__).parts[-2:])
205        self._writer.write_comment(
206            f'This file was automatically generated by {file}'
207        )
208        self._writer.write_blank()
209        return self._writer
210
211    def __exit__(
212        self,
213        exc_type: Type[BaseException] | None,
214        exc_val: BaseException | None,
215        exc_tb: TracebackType | None,
216    ) -> None:
217        """Closes the GN file and formats it."""
218        self._file.close()
219        subprocess.check_call(['gn', 'format', self._pathname])
220