blob: 1adb970b78fb3d0ab4b786028478cb89e7c6e4c1 [file] [log] [blame]
# Copyright 2023 The Bazel Authors. All rights reserved.
#
# 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:providers.bzl",
"LicenseInfo",
"PackageInfo",
)
load("@rules_license//rules_gathering:trace.bzl", "TraceInfo")
load(":user_filtered_rule_kinds.bzl", "user_aspect_filters")
load(":to_json.bzl", "labels_to_json", "licenses_to_json", "package_infos_to_json")
TransitivePackageInfo = provider(
"""Transitive list of all SBOM relevant dependencies.""",
fields = {
"top_level_target": "Label: The top level target label we are examining.",
"license_info": "depset(LicenseInfo)",
"package_info": "depset(PackageInfo)",
"packages": "depset(label)",
"target_under_license": "Label: A target which will be associated with some licenses.",
"traces": "list(string) - diagnostic for tracing a dependency relationship to a target.",
},
)
# Singleton instance of nothing present. Each level of the aspect must return something,
# so we use one to save space instead of making a lot of empty ones.
NULL_INFO = TransitivePackageInfo(license_info = depset(), package_info = depset(), packages = depset())
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_license_info, trans_package_info, trans_packages, traces, provider, filter_func):
"""Pulls the transitive data up from attributes we care about.
Collapses all the TransitivePackageInfo providers from my deps into lists
that we can then turn into a single TPI.
"""
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.license_info:
trans_license_info.append(info.license_info)
if info.package_info:
trans_package_info.append(info.package_info)
if info.packages:
trans_packages.append(info.packages)
if hasattr(info, "traces"):
if info.traces:
for trace in info.traces:
traces.append("(" + ", ".join([str(ctx.label), ctx.rule.kind, name]) + ") -> " + trace)
def gather_package_common(target, ctx, provider_factory, 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 a subset of providers. 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
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
"""
# A hack until https://github.com/bazelbuild/rules_license/issues/89 is
# fully resolved. If exec is in the bin_dir path, then the current
# configuration is probably cfg = exec.
if "-exec-" in ctx.bin_dir.path:
return [NULL_INFO]
# First we gather my direct license attachments
licenses = []
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
elif hasattr(ctx.rule.attr, "applicable_licenses"):
for dep in ctx.rule.attr.applicable_licenses:
if LicenseInfo in dep:
licenses.append(dep[LicenseInfo])
if PackageInfo in dep:
package_info.depend(dep[LicenseInfo])
# Record all the external repos anyway.
target_name = str(target.label)
packages = []
if target_name.startswith("@") and target_name[1] != "/":
packages.append(target.label)
# DBG print(str(target.label))
elif hasattr(ctx.rule.attr, "tags"):
for tag in ctx.rule.attr.tags:
if tag.startswith("maven_coordinates="):
packages.append(target.label)
# Now gather transitive collection of providers from the targets
# this target depends upon.
trans_license_info = []
trans_package_info = []
trans_packages = []
traces = []
_get_transitive_metadata(ctx, trans_license_info, trans_package_info, trans_packages, traces, provider_factory, filter_func)
if (not licenses and
not package_info and
not packages and
not trans_license_info and
not trans_package_info and
not trans_packages):
return [NULL_INFO]
# 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]
return [provider_factory(
target_under_license = target.label,
license_info = depset(direct = licenses, transitive = trans_license_info),
package_info = depset(direct = package_info, transitive = trans_package_info),
packages = depset(direct = packages, transitive = trans_packages),
traces = traces,
)]
def _gather_package_impl(target, ctx):
ret = gather_package_common(
target,
ctx,
TransitivePackageInfo,
# [ExperimentalMetadataInfo, PackageInfo],
[PackageInfo],
should_traverse,
)
# print(ret)
return ret
gather_package_info = aspect(
doc = """Collects License and Package providers into a single TransitivePackageInfo provider.""",
implementation = _gather_package_impl,
attr_aspects = ["*"],
attrs = {
"_trace": attr.label(default = "@rules_license//rules:trace_target"),
},
provides = [TransitivePackageInfo],
apply_to_generating_rules = True,
)
def _packages_used_impl(ctx):
"""Write the TransitivePackageInfo as JSON."""
tpi = ctx.attr.target[TransitivePackageInfo]
licenses_json = licenses_to_json(tpi.license_info)
package_info_json = package_infos_to_json(tpi.package_info)
packages = labels_to_json(tpi.packages.to_list())
# Create a single dict of all the info.
main_template = """{{
"top_level_target": "{top_level_target}",
"licenses": {licenses},
"package_info": {package_info},
"packages": {packages}
\n}}"""
content = main_template.format(
top_level_target = ctx.attr.target.label,
licenses = licenses_json,
package_info = package_info_json,
packages = packages,
)
ctx.actions.write(
output = ctx.outputs.out,
content = content,
)
packages_used = rule(
doc = """Gather transitive package information for a target and write as JSON.""",
implementation = _packages_used_impl,
attrs = {
"target": attr.label(
aspects = [gather_package_info],
allow_files = True,
),
"out": attr.output(mandatory = True),
},
)