# 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), }, )