xref: /aosp_15_r20/external/bazelbuild-rules_license/rules/licenses_core.bzl (revision f578df4fd057ffe2023728444759535685631548)
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:user_filtered_rule_kinds.bzl", "user_aspect_filters")
18load(
19    "@rules_license//rules:providers.bzl",
20    "LicenseInfo",
21)
22load(
23    "@rules_license//rules/private:gathering_providers.bzl",
24    "LicensedTargetInfo",
25    "TransitiveLicensesInfo",
26)
27
28
29TraceInfo = provider(
30    doc = """Provides a target (as a string) to assist in debugging dependency issues.""",
31    fields = {
32        "trace": "String: a target to trace dependency edges to.",
33    },
34)
35
36def _trace_impl(ctx):
37    return TraceInfo(trace = ctx.build_setting_value)
38
39trace = rule(
40    doc = """Used to allow the specification of a target to trace while collecting license dependencies.""",
41    implementation = _trace_impl,
42    build_setting = config.string(flag = True),
43)
44
45def should_traverse(ctx, attr):
46    """Checks if the dependent attribute should be traversed.
47
48    Args:
49      ctx: The aspect evaluation context.
50      attr: The name of the attribute to be checked.
51
52    Returns:
53      True iff the attribute should be traversed.
54    """
55    k = ctx.rule.kind
56
57    for filters in [aspect_filters, user_aspect_filters]:
58        always_ignored = filters.get("*", [])
59        if k in filters:
60            attr_matches = filters[k]
61            if (attr in attr_matches or
62                "*" in attr_matches or
63                ("_*" in attr_matches and attr.startswith("_")) or
64                attr in always_ignored):
65                return False
66
67            for m in attr_matches:
68                if attr == m:
69                    return False
70
71    return True
72
73def _get_transitive_metadata(ctx, trans_licenses, trans_other_metadata, trans_package_info, trans_deps, traces, provider, filter_func):
74    attrs = [a for a in dir(ctx.rule.attr)]
75    for name in attrs:
76        if not filter_func(ctx, name):
77            continue
78        a = getattr(ctx.rule.attr, name)
79
80        # Make anything singleton into a list for convenience.
81        if type(a) != type([]):
82            a = [a]
83        for dep in a:
84            # Ignore anything that isn't a target
85            if type(dep) != "Target":
86                continue
87
88            # Targets can also include things like input files that won't have the
89            # aspect, so we additionally check for the aspect rather than assume
90            # it's on all targets.  Even some regular targets may be synthetic and
91            # not have the aspect. This provides protection against those outlier
92            # cases.
93            if provider in dep:
94                info = dep[provider]
95                if info.licenses:
96                    trans_licenses.append(info.licenses)
97                if info.deps:
98                    trans_deps.append(info.deps)
99                if info.traces:
100                    for trace in info.traces:
101                        traces.append("(" + ", ".join([str(ctx.label), ctx.rule.kind, name]) + ") -> " + trace)
102
103                # We only need one or the other of these stanzas.
104                # If we use a polymorphic approach to metadata providers, then
105                # this works.
106                if hasattr(info, "other_metadata"):
107                    if info.other_metadata:
108                        trans_other_metadata.append(info.other_metadata)
109                # But if we want more precise type safety, we would have a
110                # trans_* for each type of metadata. That is not user
111                # extensibile.
112                if hasattr(info, "package_info"):
113                    if info.package_info:
114                        trans_package_info.append(info.package_info)
115
116def gather_metadata_info_common(target, ctx, provider_factory, namespaces, metadata_providers, filter_func):
117    """Collect license and other metadata info from myself and my deps.
118
119    Any single target might directly depend on a license, or depend on
120    something that transitively depends on a license, or neither.
121    This aspect bundles all those into a single provider. At each level, we add
122    in new direct license deps found and forward up the transitive information
123    collected so far.
124
125    This is a common abstraction for crawling the dependency graph. It is parameterized
126    to allow specifying the provider that is populated with results. It is
127    configurable to select only licenses matching a certain namespace. It is also
128    configurable to specify which dependency edges should not be traced for the
129    purpose of tracing the graph.
130
131    Args:
132      target: The target of the aspect.
133      ctx: The aspect evaluation context.
134      provider_factory: abstracts the provider returned by this aspect
135      namespaces: a list of namespaces licenses must match to be included
136      metadata_providers: a list of other providers of interest
137      filter_func: a function that returns true iff the dep edge should be ignored
138
139    Returns:
140      provider of parameterized type
141    """
142
143    # First we gather my direct license attachments
144    licenses = []
145    other_metadata = []
146    package_info = []
147    if ctx.rule.kind == "_license":
148        # Don't try to gather licenses from the license rule itself. We'll just
149        # blunder into the text file of the license and pick up the default
150        # attribute of the package, which we don't want.
151        pass
152    else:
153        if hasattr(ctx.rule.attr, "applicable_licenses"):
154            for dep in ctx.rule.attr.applicable_licenses:
155                if LicenseInfo in dep:
156                    lic = dep[LicenseInfo]
157
158                    # This check shouldn't be necessary since any license created
159                    # by the official code will have this set. However, one of the
160                    # tests has its own implementation of license that had to be fixed
161                    # so this is just a conservative safety check.
162                    if hasattr(lic, "namespace"):
163                        if lic.namespace in namespaces:
164                            licenses.append(lic)
165                    else:
166                        fail("should have a namespace")
167                for m_p in metadata_providers:
168                    if m_p in dep:
169                        other_metadata.append(dep[m_p])
170
171    # Now gather transitive collection of providers from the targets
172    # this target depends upon.
173    trans_licenses = []
174    trans_other_metadata = []
175    trans_package_info = []
176    trans_deps = []
177    traces = []
178    _get_transitive_metadata(ctx, trans_licenses, trans_other_metadata, trans_package_info, trans_deps, traces, provider_factory, filter_func)
179
180    if not licenses and not trans_licenses:
181        return [provider_factory(deps = depset(), licenses = depset(), traces = [])]
182
183    # If this is the target, start the sequence of traces.
184    if ctx.attr._trace[TraceInfo].trace and ctx.attr._trace[TraceInfo].trace in str(ctx.label):
185        traces = [ctx.attr._trace[TraceInfo].trace]
186
187    # Trim the number of traces accumulated since the output can be quite large.
188    # A few representative traces are generally sufficient to identify why a dependency
189    # is incorrectly incorporated.
190    if len(traces) > 10:
191        traces = traces[0:10]
192
193    if licenses:
194        # At this point we have a target and a list of directly used licenses.
195        # Bundle those together so we can report the exact targets that cause the
196        # dependency on each license. Since a list cannot be stored in a
197        # depset, even inside a provider, the list is concatenated into a
198        # string and will be unconcatenated in the output phase.
199        direct_license_uses = [LicensedTargetInfo(
200            target_under_license = target.label,
201            licenses = ",".join([str(x.label) for x in licenses]),
202        )]
203    else:
204        direct_license_uses = None
205
206    # This is a bit of a hack for bazel 5.x.  We can not pass extra fields to
207    # the provider constructor, so we need to do something special for each.
208    # In Bazel 6.x we can use a provider initializer function that would take
209    # all the args and only use the ones it wants.
210    if provider_factory == TransitiveLicensesInfo:
211        return [provider_factory(
212            target_under_license = target.label,
213            licenses = depset(tuple(licenses), transitive = trans_licenses),
214            deps = depset(direct = direct_license_uses, transitive = trans_deps),
215            traces = traces,
216        )]
217
218    return [provider_factory(
219        target_under_license = target.label,
220        licenses = depset(tuple(licenses), transitive = trans_licenses),
221        other_metadata = depset(tuple(other_metadata), transitive = trans_other_metadata),
222        deps = depset(direct = direct_license_uses, transitive = trans_deps),
223        traces = traces,
224    )]
225