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"""Utilities for manipulating GN targets.""" 15 16from json import loads as json_loads, dumps as json_dumps 17from pathlib import PurePosixPath 18from typing import Set 19 20from pw_build.bazel_query import BazelRule 21from pw_build.gn_config import GnConfig, GN_CONFIG_FLAGS 22from pw_build.gn_utils import ( 23 GnLabel, 24 GnPath, 25 GnVisibility, 26 MalformedGnError, 27) 28 29 30class GnTarget: # pylint: disable=too-many-instance-attributes 31 """Represents a Pigweed Build target. 32 33 Attributes: 34 config: Variables on the target that can be added to a config, e.g. 35 those in `pw_build.gn_config.GN_CONFIG_FLAGS. 36 37 The following attributes match those of `pw_internal_build_target` in 38 //pw_build/gn_internal/build_target.gni 39 remove_configs 40 41 The following attributes are analogous to GN variables, see 42 https://gn.googlesource.com/gn/+/main/docs/reference.md#target_variables: 43 visibility 44 testonly 45 public 46 sources 47 inputs 48 public_configs 49 configs 50 public_deps 51 deps 52 """ 53 54 def __init__( 55 self, 56 base_label: str | PurePosixPath | GnLabel, 57 base_path: str | PurePosixPath | GnPath, 58 bazel: BazelRule | None = None, 59 json: str | None = None, 60 check_includes: bool = True, 61 ) -> None: 62 """Creates a GN target 63 64 Args: 65 66 """ 67 self._type: str 68 self._label: GnLabel 69 self._base_label: GnLabel = GnLabel(base_label) 70 self._base_path: GnPath = GnPath(base_path) 71 self._repos: Set[str] = set() 72 self.visibility: list[GnVisibility] = [] 73 self.testonly: bool = False 74 self.check_includes = check_includes 75 self.public: list[GnPath] = [] 76 self.sources: list[GnPath] = [] 77 self.inputs: list[GnPath] = [] 78 self.config: GnConfig = GnConfig() 79 self.public_configs: Set[GnLabel] = set() 80 self.configs: Set[GnLabel] = set() 81 self.remove_configs: Set[GnLabel] = set() 82 self.public_deps: Set[GnLabel] = set() 83 self.deps: Set[GnLabel] = set() 84 if bazel: 85 self._from_bazel(bazel) 86 elif json: 87 self._from_json(json) 88 else: 89 self._label = GnLabel(base_label) 90 91 def _from_bazel(self, rule: BazelRule) -> None: 92 """Populates target info from a Bazel Rule. 93 94 Filenames will be relative to the given path to the source directory. 95 96 Args: 97 rule: The Bazel rule to populate this object with. 98 """ 99 kind = rule.kind() 100 linkstatic = rule.get_bool('linkstatic') 101 visibility = rule.get_list('visibility') 102 103 self.testonly = rule.get_bool('testonly') 104 public = rule.get_list('hdrs') 105 sources = rule.get_list('srcs') 106 inputs = rule.get_list('additional_linker_inputs') 107 108 include_dirs = rule.get_list('includes') 109 self.config.add('public_defines', *rule.get_list('defines')) 110 self.config.add('cflags', *rule.get_list('copts')) 111 self.config.add('ldflags', *rule.get_list('linkopts')) 112 self.config.add('defines', *rule.get_list('local_defines')) 113 114 public_deps = rule.get_list('deps') 115 deps = rule.get_list('implementation_deps') 116 117 rule_label = rule.label() 118 if rule_label.startswith('//'): 119 rule_label = rule_label[2:] 120 self._label = self._base_label.joinlabel(rule_label) 121 122 # Translate Bazel kind to GN target type. 123 if kind == 'cc_library': 124 if linkstatic: 125 self._type = 'pw_static_library' 126 else: 127 self._type = 'pw_source_set' 128 else: 129 raise MalformedGnError(f'unsupported Bazel kind: {kind}') 130 131 # Bazel always implicitly includes private visibility. 132 visibility.append('//visibility:private') 133 for scope in visibility: 134 self.add_visibility(bazel=scope) 135 136 # Add includer directories. Bazel implicitly includes the project root. 137 include_dirs.append('//') 138 gn_paths = [ 139 GnPath(self._base_path, bazel=file) for file in include_dirs 140 ] 141 self.config.add('include_dirs', *[str(gn_path) for gn_path in gn_paths]) 142 143 for path in public: 144 self.add_path('public', bazel=path) 145 for path in sources: 146 self.add_path('sources', bazel=path) 147 for path in inputs: 148 self.add_path('inputs', bazel=path) 149 for label in public_deps: 150 self.add_dep(public=True, bazel=label) 151 for label in deps: 152 self.add_dep(bazel=label) 153 154 def _from_json(self, data: str) -> None: 155 """Populates this target from a JSON string. 156 157 Args: 158 data: The JSON data to populate this object with. 159 """ 160 obj = json_loads(data) 161 self._type = obj.get('target_type') 162 name = obj.get('target_name') 163 package = obj.get('package', '') 164 self._label = GnLabel( 165 self._base_label.joinlabel(package), gn=f':{name}' 166 ) 167 self.visibility = [ 168 GnVisibility(self._base_label, self._label, gn=scope) 169 for scope in obj.get('visibility', []) 170 ] 171 self.testonly = bool(obj.get('testonly', False)) 172 self.testonly = bool(obj.get('check_includes', False)) 173 self.public = [GnPath(path) for path in obj.get('public', [])] 174 self.sources = [GnPath(path) for path in obj.get('sources', [])] 175 self.inputs = [GnPath(path) for path in obj.get('inputs', [])] 176 for flag in GN_CONFIG_FLAGS: 177 if flag in obj: 178 self.config.add(flag, *obj[flag]) 179 self.public_configs = { 180 GnLabel(label) for label in obj.get('public_configs', []) 181 } 182 self.configs = {GnLabel(label) for label in obj.get('configs', [])} 183 self.public_deps = { 184 GnLabel(label) for label in obj.get('public_deps', []) 185 } 186 self.deps = {GnLabel(label) for label in obj.get('deps', [])} 187 188 def to_json(self) -> str: 189 """Returns a JSON representation of this target.""" 190 obj: dict[str, bool | str | list[str]] = {} 191 if self._type: 192 obj['target_type'] = self._type 193 obj['target_name'] = self.name() 194 obj['package'] = self.package() 195 if self.visibility: 196 obj['visibility'] = [str(scope) for scope in self.visibility] 197 if self.testonly: 198 obj['testonly'] = self.testonly 199 if not self.check_includes: 200 obj['check_includes'] = self.check_includes 201 if self.public: 202 obj['public'] = [str(path) for path in self.public] 203 if self.sources: 204 obj['sources'] = [str(path) for path in self.sources] 205 if self.inputs: 206 obj['inputs'] = [str(path) for path in self.inputs] 207 for flag in GN_CONFIG_FLAGS: 208 if self.config.has(flag): 209 obj[flag] = list(self.config.get(flag)) 210 if self.public_configs: 211 obj['public_configs'] = [ 212 str(label) for label in self.public_configs 213 ] 214 if self.configs: 215 obj['configs'] = [str(label) for label in self.configs] 216 if self.remove_configs: 217 obj['remove_configs'] = [ 218 str(label) for label in self.remove_configs 219 ] 220 if self.public_deps: 221 obj['public_deps'] = [str(label) for label in self.public_deps] 222 if self.deps: 223 obj['deps'] = [str(label) for label in self.deps] 224 return json_dumps(obj) 225 226 def name(self) -> str: 227 """Returns the target name.""" 228 return self._label.name() 229 230 def label(self) -> GnLabel: 231 """Returns the target label.""" 232 return self._label 233 234 def type(self) -> str: 235 """Returns the target type.""" 236 return self._type 237 238 def repos(self) -> Set[str]: 239 """Returns any external repositories referenced from Bazel rules.""" 240 return self._repos 241 242 def package(self) -> str: 243 """Returns the relative path from the base label.""" 244 pkgname = self._label.relative_to(self._base_label).split(':')[0] 245 return '' if pkgname == '.' else pkgname 246 247 def add_path(self, dst: str, **kwargs) -> None: 248 """Adds a GN path using this target's source root. 249 250 Args: 251 dst: Variable to add the path to. Should be one of 'public', 252 'sources', or 'inputs'. 253 254 Keyword Args: 255 Same as `GnPath.__init__`. 256 """ 257 getattr(self, dst).append(GnPath(self._base_path, **kwargs)) 258 259 def add_config(self, label: GnLabel, public: bool = False) -> None: 260 """Adds a GN config to this target. 261 262 Args: 263 label: The label of the config to add to this target. 264 public: If true, the config is added as a `public_config`. 265 """ 266 self.remove_configs.discard(label) 267 if public: 268 self.public_configs.add(label) 269 self.configs.discard(label) 270 else: 271 self.public_configs.discard(label) 272 self.configs.add(label) 273 274 def remove_config(self, label: GnLabel) -> None: 275 """Adds the given config to the list of default configs to remove. 276 277 Args: 278 label: The config to add to `remove_configs`. 279 """ 280 self.public_configs.discard(label) 281 self.configs.discard(label) 282 self.remove_configs.add(label) 283 284 def add_dep(self, **kwargs) -> None: 285 """Adds a GN dependency to the target using this target's base label. 286 287 Keyword Args: 288 Same as `GnLabel.__init__`. 289 """ 290 dep = GnLabel(self._base_label, **kwargs) 291 repo = dep.repo() 292 if repo: 293 self._repos.add(repo) 294 if dep.public(): 295 self.public_deps.add(dep) 296 else: 297 self.deps.add(dep) 298 299 def add_visibility(self, **kwargs) -> None: 300 """Adds a GN visibility scope using this target's base label. 301 302 This removes redundant scopes: 303 * If the scope to be added is already within an existing scope, it is 304 ignored. 305 * If existing scopes are within the scope being added, they are removed. 306 307 Keyword Args: 308 Same as `GnVisibility.__init__`. 309 """ 310 new_scope = GnVisibility(self._base_label, self._label, **kwargs) 311 if any(new_scope.within(scope) for scope in self.visibility): 312 return 313 self.visibility = list( 314 filter(lambda scope: not scope.within(new_scope), self.visibility) 315 ) 316 self.visibility.append(new_scope) 317 318 def make_relative(self, item: GnLabel | GnVisibility) -> str: 319 """Returns a label relative to this target. 320 321 Args: 322 item: The GN item to rebase relative to this target. 323 """ 324 return item.relative_to(self._label) 325