diff options
Diffstat (limited to 'rules/licenses_core.bzl')
-rw-r--r-- | rules/licenses_core.bzl | 187 |
1 files changed, 187 insertions, 0 deletions
diff --git a/rules/licenses_core.bzl b/rules/licenses_core.bzl new file mode 100644 index 0000000..42702bd --- /dev/null +++ b/rules/licenses_core.bzl @@ -0,0 +1,187 @@ +# 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", + "LicensedTargetInfo", +) + + +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_licenses(ctx, trans_licenses, 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) + +def gather_licenses_info_common(target, ctx, provider_factory, namespaces, filter_func): + """Collect license 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 + 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 = [] + 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") + + + # Now gather transitive collection of providers from the targets + # this target depends upon. + trans_licenses = [] + trans_deps = [] + traces = [] + _get_transitive_licenses(ctx, trans_licenses, 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 + + 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, + )] |