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