225 lines
9.2 KiB
Python
225 lines
9.2 KiB
Python
# 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,
|
|
)]
|