aboutsummaryrefslogtreecommitdiff
path: root/rules/licenses_core.bzl
blob: 9bb37cba6dfad3b8cb7ed20907f2b71b7964ad70 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
# Copyright 2022 Google LLC
#
# 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.
"""Rules and macros for collecting LicenseInfo providers."""

load("@rules_license//rules:filtered_rule_kinds.bzl", "aspect_filters")
load("@rules_license//rules:user_filtered_rule_kinds.bzl", "user_aspect_filters")
load(
    "@rules_license//rules:providers.bzl",
    "LicenseInfo",
)
load(
    "@rules_license//rules/private:gathering_providers.bzl",
    "LicensedTargetInfo",
    "TransitiveLicensesInfo",
)


TraceInfo = provider(
    doc = """Provides a target (as a string) to assist in debugging dependency issues.""",
    fields = {
        "trace": "String: a target to trace dependency edges to.",
    },
)

def _trace_impl(ctx):
    return TraceInfo(trace = ctx.build_setting_value)

trace = rule(
    doc = """Used to allow the specification of a target to trace while collecting license dependencies.""",
    implementation = _trace_impl,
    build_setting = config.string(flag = True),
)

def should_traverse(ctx, attr):
    """Checks if the dependent attribute should be traversed.

    Args:
      ctx: The aspect evaluation context.
      attr: The name of the attribute to be checked.

    Returns:
      True iff the attribute should be traversed.
    """
    k = ctx.rule.kind

    for filters in [aspect_filters, user_aspect_filters]:
        always_ignored = filters.get("*", [])
        if k in filters:
            attr_matches = filters[k]
            if (attr in attr_matches or
                "*" in attr_matches or
                ("_*" in attr_matches and attr.startswith("_")) or
                attr in always_ignored):
                return False

            for m in attr_matches:
                if attr == m:
                    return False

    return True

def _get_transitive_metadata(ctx, trans_licenses, trans_other_metadata, trans_package_info, trans_deps, traces, provider, filter_func):
    attrs = [a for a in dir(ctx.rule.attr)]
    for name in attrs:
        if not filter_func(ctx, name):
            continue
        a = getattr(ctx.rule.attr, name)

        # Make anything singleton into a list for convenience.
        if type(a) != type([]):
            a = [a]
        for dep in a:
            # Ignore anything that isn't a target
            if type(dep) != "Target":
                continue

            # Targets can also include things like input files that won't have the
            # aspect, so we additionally check for the aspect rather than assume
            # it's on all targets.  Even some regular targets may be synthetic and
            # not have the aspect. This provides protection against those outlier
            # cases.
            if provider in dep:
                info = dep[provider]
                if info.licenses:
                    trans_licenses.append(info.licenses)
                if info.deps:
                    trans_deps.append(info.deps)
                if info.traces:
                    for trace in info.traces:
                        traces.append("(" + ", ".join([str(ctx.label), ctx.rule.kind, name]) + ") -> " + trace)

                # We only need one or the other of these stanzas.
                # If we use a polymorphic approach to metadata providers, then
                # this works.
                if hasattr(info, "other_metadata"):
                    if info.other_metadata:
                        trans_other_metadata.append(info.other_metadata)
                # But if we want more precise type safety, we would have a
                # trans_* for each type of metadata. That is not user
                # extensibile.
                if hasattr(info, "package_info"):
                    if info.package_info:
                        trans_package_info.append(info.package_info)

def gather_metadata_info_common(target, ctx, provider_factory, namespaces, metadata_providers, filter_func):
    """Collect license and other metadata info from myself and my deps.

    Any single target might directly depend on a license, or depend on
    something that transitively depends on a license, or neither.
    This aspect bundles all those into a single provider. At each level, we add
    in new direct license deps found and forward up the transitive information
    collected so far.

    This is a common abstraction for crawling the dependency graph. It is parameterized
    to allow specifying the provider that is populated with results. It is
    configurable to select only licenses matching a certain namespace. It is also
    configurable to specify which dependency edges should not be traced for the
    purpose of tracing the graph.

    Args:
      target: The target of the aspect.
      ctx: The aspect evaluation context.
      provider_factory: abstracts the provider returned by this aspect
      namespaces: a list of namespaces licenses must match to be included
      metadata_providers: a list of other providers of interest
      filter_func: a function that returns true iff the dep edge should be ignored

    Returns:
      provider of parameterized type
    """

    # First we gather my direct license attachments
    licenses = []
    other_metadata = []
    package_info = []
    if ctx.rule.kind == "_license":
        # Don't try to gather licenses from the license rule itself. We'll just
        # blunder into the text file of the license and pick up the default
        # attribute of the package, which we don't want.
        pass
    else:
        if hasattr(ctx.rule.attr, "applicable_licenses"):
            for dep in ctx.rule.attr.applicable_licenses:
                if LicenseInfo in dep:
                    lic = dep[LicenseInfo]

                    # This check shouldn't be necessary since any license created
                    # by the official code will have this set. However, one of the
                    # tests has its own implementation of license that had to be fixed
                    # so this is just a conservative safety check.
                    if hasattr(lic, "namespace"):
                        if lic.namespace in namespaces:
                            licenses.append(lic)
                    else:
                        fail("should have a namespace")
                for m_p in metadata_providers:
                    if m_p in dep:
                        other_metadata.append(dep[m_p])

    # Now gather transitive collection of providers from the targets
    # this target depends upon.
    trans_licenses = []
    trans_other_metadata = []
    trans_package_info = []
    trans_deps = []
    traces = []
    _get_transitive_metadata(ctx, trans_licenses, trans_other_metadata, trans_package_info, trans_deps, traces, provider_factory, filter_func)

    if not licenses and not trans_licenses:
        return [provider_factory(deps = depset(), licenses = depset(), traces = [])]

    # If this is the target, start the sequence of traces.
    if ctx.attr._trace[TraceInfo].trace and ctx.attr._trace[TraceInfo].trace in str(ctx.label):
        traces = [ctx.attr._trace[TraceInfo].trace]

    # Trim the number of traces accumulated since the output can be quite large.
    # A few representative traces are generally sufficient to identify why a dependency
    # is incorrectly incorporated.
    if len(traces) > 10:
        traces = traces[0:10]

    if licenses:
        # At this point we have a target and a list of directly used licenses.
        # Bundle those together so we can report the exact targets that cause the
        # dependency on each license. Since a list cannot be stored in a
        # depset, even inside a provider, the list is concatenated into a
        # string and will be unconcatenated in the output phase.
        direct_license_uses = [LicensedTargetInfo(
            target_under_license = target.label,
            licenses = ",".join([str(x.label) for x in licenses]),
        )]
    else:
        direct_license_uses = None

    # This is a bit of a hack for bazel 5.x.  We can not pass extra fields to
    # the provider constructor, so we need to do something special for each.
    # In Bazel 6.x we can use a provider initializer function that would take
    # all the args and only use the ones it wants.
    if provider_factory == TransitiveLicensesInfo:
        return [provider_factory(
            target_under_license = target.label,
            licenses = depset(tuple(licenses), transitive = trans_licenses),
            deps = depset(direct = direct_license_uses, transitive = trans_deps),
            traces = traces,
        )]

    return [provider_factory(
        target_under_license = target.label,
        licenses = depset(tuple(licenses), transitive = trans_licenses),
        other_metadata = depset(tuple(other_metadata), transitive = trans_other_metadata),
        deps = depset(direct = direct_license_uses, transitive = trans_deps),
        traces = traces,
    )]