• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 2022 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of 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,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""Rules and macros for collecting LicenseInfo providers."""
15
16load("@rules_license//rules:filtered_rule_kinds.bzl", "aspect_filters")
17load("@rules_license//rules:providers.bzl", "LicenseInfo")
18load("@rules_license//rules:user_filtered_rule_kinds.bzl", "user_aspect_filters")
19load(
20    "@rules_license//rules_gathering:gathering_providers.bzl",
21    "LicensedTargetInfo",
22    "TransitiveLicensesInfo",
23)
24load("@rules_license//rules_gathering:trace.bzl", "TraceInfo")
25
26def should_traverse(ctx, attr):
27    """Checks if the dependent attribute should be traversed.
28
29    Args:
30      ctx: The aspect evaluation context.
31      attr: The name of the attribute to be checked.
32
33    Returns:
34      True iff the attribute should be traversed.
35    """
36    k = ctx.rule.kind
37
38    for filters in [aspect_filters, user_aspect_filters]:
39        always_ignored = filters.get("*", [])
40        if k in filters:
41            attr_matches = filters[k]
42            if (attr in attr_matches or
43                "*" in attr_matches or
44                ("_*" in attr_matches and attr.startswith("_")) or
45                attr in always_ignored):
46                return False
47
48            for m in attr_matches:
49                if attr == m:
50                    return False
51
52    return True
53
54def _get_transitive_metadata(ctx, trans_licenses, trans_other_metadata, trans_package_info, trans_deps, traces, provider, filter_func):
55    attrs = [a for a in dir(ctx.rule.attr)]
56    for name in attrs:
57        if not filter_func(ctx, name):
58            continue
59        a = getattr(ctx.rule.attr, name)
60
61        # Make anything singleton into a list for convenience.
62        if type(a) != type([]):
63            a = [a]
64        for dep in a:
65            # Ignore anything that isn't a target
66            if type(dep) != "Target":
67                continue
68
69            # Targets can also include things like input files that won't have the
70            # aspect, so we additionally check for the aspect rather than assume
71            # it's on all targets.  Even some regular targets may be synthetic and
72            # not have the aspect. This provides protection against those outlier
73            # cases.
74            if provider in dep:
75                info = dep[provider]
76                if info.licenses:
77                    trans_licenses.append(info.licenses)
78                if info.deps:
79                    trans_deps.append(info.deps)
80                if info.traces:
81                    for trace in info.traces:
82                        traces.append("(" + ", ".join([str(ctx.label), ctx.rule.kind, name]) + ") -> " + trace)
83
84                # We only need one or the other of these stanzas.
85                # If we use a polymorphic approach to metadata providers, then
86                # this works.
87                if hasattr(info, "other_metadata"):
88                    if info.other_metadata:
89                        trans_other_metadata.append(info.other_metadata)
90
91                # But if we want more precise type safety, we would have a
92                # trans_* for each type of metadata. That is not user
93                # extensibile.
94                if hasattr(info, "package_info"):
95                    if info.package_info:
96                        trans_package_info.append(info.package_info)
97
98def gather_metadata_info_common(target, ctx, provider_factory, metadata_providers, filter_func):
99    """Collect license and other metadata info from myself and my deps.
100
101    Any single target might directly depend on a license, or depend on
102    something that transitively depends on a license, or neither.
103    This aspect bundles all those into a single provider. At each level, we add
104    in new direct license deps found and forward up the transitive information
105    collected so far.
106
107    This is a common abstraction for crawling the dependency graph. It is
108    parameterized to allow specifying the provider that is populated with
109    results. It is configurable to select only a subset of providers. It
110    is also configurable to specify which dependency edges should not
111    be traced for the purpose of tracing the graph.
112
113    Args:
114      target: The target of the aspect.
115      ctx: The aspect evaluation context.
116      provider_factory: abstracts the provider returned by this aspect
117      metadata_providers: a list of other providers of interest
118      filter_func: a function that returns true iff the dep edge should be ignored
119
120    Returns:
121      provider of parameterized type
122    """
123
124    # First we gather my direct license attachments
125    licenses = []
126    other_metadata = []
127    package_info = []
128    if ctx.rule.kind == "_license":
129        # Don't try to gather licenses from the license rule itself. We'll just
130        # blunder into the text file of the license and pick up the default
131        # attribute of the package, which we don't want.
132        pass
133    else:
134        if hasattr(ctx.rule.attr, "applicable_licenses"):
135            package_metadata = ctx.rule.attr.applicable_licenses
136        elif hasattr(ctx.rule.attr, "package_metadata"):
137            package_metadata = ctx.rule.attr.package_metadata
138        else:
139            package_metadata = []
140
141        for dep in package_metadata:
142            if LicenseInfo in dep:
143                lic = dep[LicenseInfo]
144                licenses.append(lic)
145
146            for m_p in metadata_providers:
147                if m_p in dep:
148                    other_metadata.append(dep[m_p])
149
150    # A hack until https://github.com/bazelbuild/rules_license/issues/89 is
151    # fully resolved. If exec is in the bin_dir path, then the current
152    # configuration is probably cfg = exec.
153    if "-exec-" in ctx.bin_dir.path:
154        return [provider_factory(deps = depset(), licenses = depset(), traces = [])]
155
156    # Now gather transitive collection of providers from the targets
157    # this target depends upon.
158    trans_licenses = []
159    trans_other_metadata = []
160    trans_package_info = []
161    trans_deps = []
162    traces = []
163
164    _get_transitive_metadata(ctx, trans_licenses, trans_other_metadata, trans_package_info, trans_deps, traces, provider_factory, filter_func)
165
166    if not licenses and not trans_licenses:
167        return [provider_factory(deps = depset(), licenses = depset(), traces = [])]
168
169    # If this is the target, start the sequence of traces.
170    if ctx.attr._trace[TraceInfo].trace and ctx.attr._trace[TraceInfo].trace in str(ctx.label):
171        traces = [ctx.attr._trace[TraceInfo].trace]
172
173    # Trim the number of traces accumulated since the output can be quite large.
174    # A few representative traces are generally sufficient to identify why a dependency
175    # is incorrectly incorporated.
176    if len(traces) > 10:
177        traces = traces[0:10]
178
179    if licenses:
180        # At this point we have a target and a list of directly used licenses.
181        # Bundle those together so we can report the exact targets that cause the
182        # dependency on each license. Since a list cannot be stored in a
183        # depset, even inside a provider, the list is concatenated into a
184        # string and will be unconcatenated in the output phase.
185        direct_license_uses = [LicensedTargetInfo(
186            target_under_license = target.label,
187            licenses = ",".join([str(x.label) for x in licenses]),
188        )]
189    else:
190        direct_license_uses = None
191
192    # This is a bit of a hack for bazel 5.x.  We can not pass extra fields to
193    # the provider constructor, so we need to do something special for each.
194    # In Bazel 6.x we can use a provider initializer function that would take
195    # all the args and only use the ones it wants.
196    if provider_factory == TransitiveLicensesInfo:
197        return [provider_factory(
198            target_under_license = target.label,
199            licenses = depset(tuple(licenses), transitive = trans_licenses),
200            deps = depset(direct = direct_license_uses, transitive = trans_deps),
201            traces = traces,
202        )]
203
204    return [provider_factory(
205        target_under_license = target.label,
206        licenses = depset(tuple(licenses), transitive = trans_licenses),
207        other_metadata = depset(tuple(other_metadata), transitive = trans_other_metadata),
208        deps = depset(direct = direct_license_uses, transitive = trans_deps),
209        traces = traces,
210    )]
211