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