• 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"""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