blob: 58152b681a18be17d966bb435bd5ffb092901298 [file] [log] [blame]
# Copyright 2023 The Pigweed Authors
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
"""Utilities for manipulating GN targets."""
from json import loads as json_loads, dumps as json_dumps
from pathlib import PurePosixPath
from typing import Set
from pw_build.bazel_query import BazelRule
from pw_build.gn_config import GnConfig, GN_CONFIG_FLAGS
from pw_build.gn_utils import (
GnLabel,
GnPath,
GnVisibility,
MalformedGnError,
)
class GnTarget: # pylint: disable=too-many-instance-attributes
"""Represents a Pigweed Build target.
Attributes:
config: Variables on the target that can be added to a config, e.g.
those in `pw_build.gn_config.GN_CONFIG_FLAGS.
The following attributes match those of `pw_internal_build_target` in
//pw_build/gn_internal/build_target.gni
remove_configs
The following attributes are analogous to GN variables, see
https://gn.googlesource.com/gn/+/main/docs/reference.md#target_variables:
visibility
testonly
public
sources
inputs
public_configs
configs
public_deps
deps
"""
def __init__(
self,
base_label: str | PurePosixPath | GnLabel,
base_path: str | PurePosixPath | GnPath,
bazel: BazelRule | None = None,
json: str | None = None,
check_includes: bool = True,
) -> None:
"""Creates a GN target
Args:
"""
self._type: str
self._label: GnLabel
self._base_label: GnLabel = GnLabel(base_label)
self._base_path: GnPath = GnPath(base_path)
self._repos: Set[str] = set()
self.visibility: list[GnVisibility] = []
self.testonly: bool = False
self.check_includes = check_includes
self.public: list[GnPath] = []
self.sources: list[GnPath] = []
self.inputs: list[GnPath] = []
self.config: GnConfig = GnConfig()
self.public_configs: Set[GnLabel] = set()
self.configs: Set[GnLabel] = set()
self.remove_configs: Set[GnLabel] = set()
self.public_deps: Set[GnLabel] = set()
self.deps: Set[GnLabel] = set()
if bazel:
self._from_bazel(bazel)
elif json:
self._from_json(json)
else:
self._label = GnLabel(base_label)
def _from_bazel(self, rule: BazelRule) -> None:
"""Populates target info from a Bazel Rule.
Filenames will be relative to the given path to the source directory.
Args:
rule: The Bazel rule to populate this object with.
"""
kind = rule.kind()
linkstatic = rule.get_bool('linkstatic')
visibility = rule.get_list('visibility')
self.testonly = rule.get_bool('testonly')
public = rule.get_list('hdrs')
sources = rule.get_list('srcs')
inputs = rule.get_list('additional_linker_inputs')
include_dirs = rule.get_list('includes')
self.config.add('public_defines', *rule.get_list('defines'))
self.config.add('cflags', *rule.get_list('copts'))
self.config.add('ldflags', *rule.get_list('linkopts'))
self.config.add('defines', *rule.get_list('local_defines'))
public_deps = rule.get_list('deps')
deps = rule.get_list('implementation_deps')
rule_label = rule.label()
if rule_label.startswith('//'):
rule_label = rule_label[2:]
self._label = self._base_label.joinlabel(rule_label)
# Translate Bazel kind to GN target type.
if kind == 'cc_library':
if linkstatic:
self._type = 'pw_static_library'
else:
self._type = 'pw_source_set'
else:
raise MalformedGnError(f'unsupported Bazel kind: {kind}')
# Bazel always implicitly includes private visibility.
visibility.append('//visibility:private')
for scope in visibility:
self.add_visibility(bazel=scope)
# Add includer directories. Bazel implicitly includes the project root.
include_dirs.append('//')
gn_paths = [
GnPath(self._base_path, bazel=file) for file in include_dirs
]
self.config.add('include_dirs', *[str(gn_path) for gn_path in gn_paths])
for path in public:
self.add_path('public', bazel=path)
for path in sources:
self.add_path('sources', bazel=path)
for path in inputs:
self.add_path('inputs', bazel=path)
for label in public_deps:
self.add_dep(public=True, bazel=label)
for label in deps:
self.add_dep(bazel=label)
def _from_json(self, data: str) -> None:
"""Populates this target from a JSON string.
Args:
data: The JSON data to populate this object with.
"""
obj = json_loads(data)
self._type = obj.get('target_type')
name = obj.get('target_name')
package = obj.get('package', '')
self._label = GnLabel(
self._base_label.joinlabel(package), gn=f':{name}'
)
self.visibility = [
GnVisibility(self._base_label, self._label, gn=scope)
for scope in obj.get('visibility', [])
]
self.testonly = bool(obj.get('testonly', False))
self.testonly = bool(obj.get('check_includes', False))
self.public = [GnPath(path) for path in obj.get('public', [])]
self.sources = [GnPath(path) for path in obj.get('sources', [])]
self.inputs = [GnPath(path) for path in obj.get('inputs', [])]
for flag in GN_CONFIG_FLAGS:
if flag in obj:
self.config.add(flag, *obj[flag])
self.public_configs = {
GnLabel(label) for label in obj.get('public_configs', [])
}
self.configs = {GnLabel(label) for label in obj.get('configs', [])}
self.public_deps = {
GnLabel(label) for label in obj.get('public_deps', [])
}
self.deps = {GnLabel(label) for label in obj.get('deps', [])}
def to_json(self) -> str:
"""Returns a JSON representation of this target."""
obj: dict[str, bool | str | list[str]] = {}
if self._type:
obj['target_type'] = self._type
obj['target_name'] = self.name()
obj['package'] = self.package()
if self.visibility:
obj['visibility'] = [str(scope) for scope in self.visibility]
if self.testonly:
obj['testonly'] = self.testonly
if not self.check_includes:
obj['check_includes'] = self.check_includes
if self.public:
obj['public'] = [str(path) for path in self.public]
if self.sources:
obj['sources'] = [str(path) for path in self.sources]
if self.inputs:
obj['inputs'] = [str(path) for path in self.inputs]
for flag in GN_CONFIG_FLAGS:
if self.config.has(flag):
obj[flag] = list(self.config.get(flag))
if self.public_configs:
obj['public_configs'] = [
str(label) for label in self.public_configs
]
if self.configs:
obj['configs'] = [str(label) for label in self.configs]
if self.remove_configs:
obj['remove_configs'] = [
str(label) for label in self.remove_configs
]
if self.public_deps:
obj['public_deps'] = [str(label) for label in self.public_deps]
if self.deps:
obj['deps'] = [str(label) for label in self.deps]
return json_dumps(obj)
def name(self) -> str:
"""Returns the target name."""
return self._label.name()
def label(self) -> GnLabel:
"""Returns the target label."""
return self._label
def type(self) -> str:
"""Returns the target type."""
return self._type
def repos(self) -> Set[str]:
"""Returns any external repositories referenced from Bazel rules."""
return self._repos
def package(self) -> str:
"""Returns the relative path from the base label."""
pkgname = self._label.relative_to(self._base_label).split(':')[0]
return '' if pkgname == '.' else pkgname
def add_path(self, dst: str, **kwargs) -> None:
"""Adds a GN path using this target's source root.
Args:
dst: Variable to add the path to. Should be one of 'public',
'sources', or 'inputs'.
Keyword Args:
Same as `GnPath.__init__`.
"""
getattr(self, dst).append(GnPath(self._base_path, **kwargs))
def add_config(self, label: GnLabel, public: bool = False) -> None:
"""Adds a GN config to this target.
Args:
label: The label of the config to add to this target.
public: If true, the config is added as a `public_config`.
"""
self.remove_configs.discard(label)
if public:
self.public_configs.add(label)
self.configs.discard(label)
else:
self.public_configs.discard(label)
self.configs.add(label)
def remove_config(self, label: GnLabel) -> None:
"""Adds the given config to the list of default configs to remove.
Args:
label: The config to add to `remove_configs`.
"""
self.public_configs.discard(label)
self.configs.discard(label)
self.remove_configs.add(label)
def add_dep(self, **kwargs) -> None:
"""Adds a GN dependency to the target using this target's base label.
Keyword Args:
Same as `GnLabel.__init__`.
"""
dep = GnLabel(self._base_label, **kwargs)
repo = dep.repo()
if repo:
self._repos.add(repo)
if dep.public():
self.public_deps.add(dep)
else:
self.deps.add(dep)
def add_visibility(self, **kwargs) -> None:
"""Adds a GN visibility scope using this target's base label.
This removes redundant scopes:
* If the scope to be added is already within an existing scope, it is
ignored.
* If existing scopes are within the scope being added, they are removed.
Keyword Args:
Same as `GnVisibility.__init__`.
"""
new_scope = GnVisibility(self._base_label, self._label, **kwargs)
if any(new_scope.within(scope) for scope in self.visibility):
return
self.visibility = list(
filter(lambda scope: not scope.within(new_scope), self.visibility)
)
self.visibility.append(new_scope)
def make_relative(self, item: GnLabel | GnVisibility) -> str:
"""Returns a label relative to this target.
Args:
item: The GN item to rebase relative to this target.
"""
return item.relative_to(self._label)