Skip to content

Commit

Permalink
Introduce target mapping
Browse files Browse the repository at this point in the history
Sometimes users don't want to follow the DWYU rules for all targets or
have to work with external dependencies not following the DWYU principles.
For such cases DWYU allows now creating a mapping which for a chosen
target makes more headers available as the target actually provides.
In other words, one can combine virtually for the DWYU analysis multiple
targets. Doing so allows using headers from transitive dependencies
without DWYU raising an error for select cases.
  • Loading branch information
martis42 committed Nov 1, 2023
1 parent 2e2d52e commit 025b504
Show file tree
Hide file tree
Showing 21 changed files with 418 additions and 10 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,28 @@ your_aspect = dwyu_aspect_factory(use_implementation_deps = True)

Examples for this can be seen at the [implementation_deps test cases](test/aspect/implementation_deps).

## Target mapping

Sometimes users don't want to follow the DWYU rules for all targets or have to work with external dependencies not following the DWYU principles.
For such cases DWYU allows creating a mapping which for a chosen target makes more headers available as the target actually provides.
In other words, one can combine virtually for the DWYU analysis multiple targets.
Doing so allows using headers from transitive dependencies without DWYU raising an error for select cases.

Such a mapping is created with the [dwyu_make_cc_info_mapping](src/cc_info_mapping/cc_info_mapping.bzl) rule.
This offers multiple wys of mapping targets:

1. Explicitly providing a list of targets which are mapped into a single target.
1. Specifying that all direct dependencies of a given target are mapped into the target.
1. Specifying that all transitive dependencies of a given target are mapped into the target.

Activate this behavior via:

```starlark
your_aspect = dwyu_aspect_factory(target_mapping = "<mapping_target_created_by_the_user>")
```

Examples for this can be seen at the [target_mapping test cases](test/aspect/target_mapping).

## Applying automatic fixes

DWYU offers a tool to automatically fix some detected problems.
Expand Down
45 changes: 36 additions & 9 deletions src/aspect/dwyu.bzl
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
load("@bazel_tools//tools/build_defs/cc:action_names.bzl", "CPP_COMPILE_ACTION_NAME")
load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain")
load("@depend_on_what_you_use//src/cc_info_mapping:cc_info_mapping.bzl", "DwyuCcInfoRemappingsInfo")

def _get_target_sources(rule):
public_files = []
Expand All @@ -22,7 +23,7 @@ def _get_relevant_header(target_context, is_target_under_inspection):

def _process_target(ctx, target, defines, output_path, is_target_under_inspection, verbose):
processing_output = ctx.actions.declare_file(output_path)
cc_context = target[CcInfo].compilation_context
cc_context = target.cc_info.compilation_context
header_files = _get_relevant_header(
target_context = cc_context,
is_target_under_inspection = is_target_under_inspection,
Expand Down Expand Up @@ -114,6 +115,34 @@ def _gather_defines(ctx, target_compilation_context):

return extract_defines_from_compiler_flags(compiler_command_line_flags)

def _exchange_cc_info(deps, mapping):
transformed = []
mapping_info = mapping[0][DwyuCcInfoRemappingsInfo].mapping
for dep in deps:
if dep.label in mapping_info:
transformed.append(struct(label = dep.label, cc_info = mapping_info[dep.label]))
else:
transformed.append(struct(label = dep.label, cc_info = dep[CcInfo]))
return transformed

def _preprocess_deps(ctx):
"""
Normally this function does nothing and simply stores dependencies and their CcInfo providers in a specific format.
If the user chooses to use the target mapping feature, we exchange here the CcInf provider for some targets with a
different one.
"""
target_impl_deps = []
if ctx.attr._target_mapping:
target_deps = _exchange_cc_info(deps = ctx.rule.attr.deps, mapping = ctx.attr._target_mapping)
if hasattr(ctx.rule.attr, "implementation_deps"):
pass
else:
target_deps = [struct(label = dep.label, cc_info = dep[CcInfo]) for dep in ctx.rule.attr.deps]
if hasattr(ctx.rule.attr, "implementation_deps"):
target_impl_deps = [struct(label = dep.label, cc_info = dep[CcInfo]) for dep in ctx.rule.attr.implementation_deps]

return target_deps, target_impl_deps

def _do_ensure_private_deps(ctx):
"""
The implementation_deps feature is only meaningful and available for cc_library, where in contrast to cc_binary
Expand Down Expand Up @@ -158,22 +187,20 @@ def dwyu_aspect_impl(target, ctx):

processed_target = _process_target(
ctx,
target = target,
target = struct(label = target.label, cc_info = target[CcInfo]),
defines = _gather_defines(ctx, target_compilation_context = target[CcInfo].compilation_context),
output_path = "{}_processed_target_under_inspection.json".format(target.label.name),
is_target_under_inspection = True,
verbose = False,
)

# TODO consistently use impl_deps as variable name and only use long name in docs and APIs
target_deps, target_impl_deps = _preprocess_deps(ctx)

# TODO Investigate if we can prevent running this multiple times for the same dep if multiple
# target_under_inspection have the same dependency
processed_deps = _process_dependencies(ctx, target = target, deps = ctx.rule.attr.deps, verbose = False)
processed_implementation_deps = _process_dependencies(
ctx,
target = target,
deps = ctx.rule.attr.implementation_deps if hasattr(ctx.rule.attr, "implementation_deps") else [],
verbose = False,
)
processed_deps = _process_dependencies(ctx, target = target, deps = target_deps, verbose = False)
processed_impl_deps = _process_dependencies(ctx, target = target, deps = target_impl_deps, verbose = False)

report_file = ctx.actions.declare_file("{}_dwyu_report.json".format(target.label.name))
args = ctx.actions.args()
Expand Down
11 changes: 10 additions & 1 deletion src/aspect/factory.bzl
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
load("@depend_on_what_you_use//src/cc_info_mapping:cc_info_mapping.bzl", "DwyuCcInfoRemappingsInfo")
load(":dwyu.bzl", "dwyu_aspect_impl")

def dwyu_aspect_factory(
config = None,
recursive = False,
target_mapping = None,
use_implementation_deps = False):
"""
Create a "Depend on What You Use" (DWYU) aspect.
Expand All @@ -14,14 +16,17 @@ def dwyu_aspect_factory(
config: Configuration file for the tool comparing the include statements to the dependencies.
recursive: If true, execute the aspect on all transitive dependencies.
If false, analyze only the target the aspect is being executed on.
target_mapping: A target providing a map of target labels to alternative CcInfo provider objects for those
targets. Typically created with the dwyu_make_cc_info_mapping rule.
use_implementation_deps: If true, ensure cc_library dependencies which are used only in private files are
listed in implementation_deps. Only available for Bazel >= 5.0.0 and if flag
listed in implementation_deps. Only available if flag
'--experimental_cc_implementation_deps' is provided.
Returns:
Configured DWYU aspect
"""
attr_aspects = ["deps"] if recursive else []
aspect_config = [config] if config else []
aspect_target_mapping = [target_mapping] if target_mapping else []
return aspect(
implementation = dwyu_aspect_impl,
attr_aspects = attr_aspects,
Expand Down Expand Up @@ -53,6 +58,10 @@ def dwyu_aspect_factory(
"_recursive": attr.bool(
default = recursive,
),
"_target_mapping": attr.label_list(
providers = [DwyuCcInfoRemappingsInfo],
default = aspect_target_mapping,
),
"_use_implementation_deps": attr.bool(
default = use_implementation_deps,
),
Expand Down
Empty file added src/cc_info_mapping/BUILD
Empty file.
70 changes: 70 additions & 0 deletions src/cc_info_mapping/cc_info_mapping.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
load("@depend_on_what_you_use//src/cc_info_mapping/private:direct_deps.bzl", "mapping_to_direct_deps")
load("@depend_on_what_you_use//src/cc_info_mapping/private:explicit.bzl", "explicit_mapping")
load("@depend_on_what_you_use//src/cc_info_mapping/private:providers.bzl", "DwyuCcInfoRemapInfo")
load("@depend_on_what_you_use//src/cc_info_mapping/private:transitive_deps.bzl", "mapping_to_transitive_deps")
load("@depend_on_what_you_use//src/utils:utils.bzl", "label_to_name")

MAP_DIRECT_DEPS = "__DWYU_MAP_DIRECT_DEPS__"
MAP_TRANSITIVE_DEPS = "__DWYU_MAP_TRANSITIVE_DEPS__"

DwyuCcInfoRemappingsInfo = provider(
"Dictionary of targets labels wnd which CcInfo provider DWYU should use for analysing them.",
fields = {
"mapping": "Dictionary with structure {'target label': CcInfo provider which should be used by DWYU}.",
},
)

def _make_remapping_info_impl(ctx):
return DwyuCcInfoRemappingsInfo(mapping = {
remap[DwyuCcInfoRemapInfo].target: remap[DwyuCcInfoRemapInfo].cc_info
for remap in ctx.attr.remappings
})

_make_remapping_info = rule(
implementation = _make_remapping_info_impl,
provides = [DwyuCcInfoRemappingsInfo],
attrs = {
"remappings": attr.label_list(providers = [DwyuCcInfoRemapInfo]),
},
)

def dwyu_make_cc_info_mapping(name, mapping):
"""
Create a mapping which allows treating targets as if they themselves would offer header files, which in fact are
coming from their dependencies. This enables the DWYU analysis to skip over some usage of headers provided by
transitive dependencies without raising an error.
Args:
name: Unique name for this target. Will be the prefix for all private intermediate targets.
mapping: Dictionary containing various targets and how they should be mapped. Possible mappings are:
- An explicit list of targets which are mapped to the main target. Be careful only to choose targets
which are dependencies of the main target!
- The MAP_DIRECT_DEPS token which tells the rule to map all direct dependencies to the main target.
- The MAP_TRANSITIVE_DEPS token which tells the rule to map recursively all transitive dependencies to
the main target.
"""
mappings = []
for target, map_to in mapping.items():
mapping_action = "{}_mapping_{}".format(name, label_to_name(target))
if map_to == MAP_DIRECT_DEPS:
mapping_to_direct_deps(
name = mapping_action,
target = target,
)
elif map_to == MAP_TRANSITIVE_DEPS:
mapping_to_transitive_deps(
name = mapping_action,
target = target,
)
else:
explicit_mapping(
name = mapping_action,
target = target,
map_to = map_to,
)
mappings.append(mapping_action)

_make_remapping_info(
name = name,
remappings = mappings,
)
Empty file.
34 changes: 34 additions & 0 deletions src/cc_info_mapping/private/direct_deps.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
load(":providers.bzl", "DwyuCcInfoRemapInfo")

def _aggregate_direct_deps_aspect_impl(target, ctx):
aggregated_compilation_context = cc_common.merge_compilation_contexts(
compilation_contexts =
[tgt[CcInfo].compilation_context for tgt in [target] + ctx.rule.attr.deps],
)

return DwyuCcInfoRemapInfo(target = target.label, cc_info = CcInfo(
compilation_context = aggregated_compilation_context,
linking_context = target[CcInfo].linking_context,
))

_aggregate_direct_deps_aspect = aspect(
implementation = _aggregate_direct_deps_aspect_impl,
provides = [DwyuCcInfoRemapInfo],
attr_aspects = [],
)

def _mapping_to_direct_deps_impl(ctx):
return ctx.attr.target[DwyuCcInfoRemapInfo]

mapping_to_direct_deps = rule(
implementation = _mapping_to_direct_deps_impl,
provides = [DwyuCcInfoRemapInfo],
attrs = {
"target": attr.label(aspects = [_aggregate_direct_deps_aspect], providers = [CcInfo]),
},
doc = """
Make headers from all direct dependencies available as if they where provided by the main target itself.
We do so by merging the compilation_context information from the direct dependencies into the main target's CcInfo.
We explicitly ignore implementation_deps, as allowing to map them would break their design of not being visible to users of the target.
""",
)
25 changes: 25 additions & 0 deletions src/cc_info_mapping/private/explicit.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
load(":providers.bzl", "DwyuCcInfoRemapInfo")

def _explicit_mapping_impl(ctx):
aggregated_compilation_context = cc_common.merge_compilation_contexts(
compilation_contexts =
[tgt[CcInfo].compilation_context for tgt in [ctx.attr.target] + ctx.attr.map_to],
)

return DwyuCcInfoRemapInfo(target = ctx.attr.target.label, cc_info = CcInfo(
compilation_context = aggregated_compilation_context,
linking_context = ctx.attr.target[CcInfo].linking_context,
))

explicit_mapping = rule(
implementation = _explicit_mapping_impl,
provides = [DwyuCcInfoRemapInfo],
attrs = {
"map_to": attr.label_list(providers = [CcInfo]),
"target": attr.label(providers = [CcInfo]),
},
doc = """
Make headers from all explicitly listed targets available as if they where provided by the main target itself.
We do so by merging the compilation_context information from the listed targets into the main target's CcInfo.
""",
)
7 changes: 7 additions & 0 deletions src/cc_info_mapping/private/providers.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
DwyuCcInfoRemapInfo = provider(
"An alternative CcInfo object for a target which can be used by DWYU during the analysis.",
fields = {
"cc_info": "CcInfo provider.",
"target": "Label of target which should use the cc_info object.",
},
)
35 changes: 35 additions & 0 deletions src/cc_info_mapping/private/transitive_deps.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
load(":providers.bzl", "DwyuCcInfoRemapInfo")

def _aggregate_transitive_deps_aspect_impl(target, ctx):
all_cc_info = [target[CcInfo]]
all_cc_info.extend([dep[DwyuCcInfoRemapInfo].cc_info for dep in ctx.rule.attr.deps])
aggregated_compilation_context = cc_common.merge_compilation_contexts(
compilation_contexts = [cci.compilation_context for cci in all_cc_info],
)

return DwyuCcInfoRemapInfo(target = target.label, cc_info = CcInfo(
compilation_context = aggregated_compilation_context,
linking_context = target[CcInfo].linking_context,
))

_aggregate_transitive_deps_aspect = aspect(
implementation = _aggregate_transitive_deps_aspect_impl,
provides = [DwyuCcInfoRemapInfo],
attr_aspects = ["deps"],
)

def _mapping_to_transitive_deps_impl(ctx):
return ctx.attr.target[DwyuCcInfoRemapInfo]

mapping_to_transitive_deps = rule(
implementation = _mapping_to_transitive_deps_impl,
provides = [DwyuCcInfoRemapInfo],
attrs = {
"target": attr.label(aspects = [_aggregate_transitive_deps_aspect], providers = [CcInfo]),
},
doc = """
Make headers from all transitive dependencies available as if they where provided by the main target itself.
We do so by recursively merging the compilation_context information from the dependencies into the main target's CcInfo.
We explicitly ignore implementation_deps, as allowing to map them would break their design of not being visible to users of the target.
""",
)
Loading

0 comments on commit 025b504

Please sign in to comment.