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