unplugged-system/build/bazel/rules/license/license_aspect.bzl

157 lines
5.5 KiB
Python
Raw Normal View History

load("@rules_license//rules:providers.bzl", "LicenseInfo")
load("//build/bazel/rules:metadata.bzl", "MetadataFileInfo")
RuleLicensedDependenciesInfo = provider(
doc = """Rule's licensed dependencies.""",
fields = dict(
license_closure = "depset(license) for the rule and its licensed dependencies",
),
)
def _maybe_expand(rule, transitive_licenses):
if not RuleLicensedDependenciesInfo in rule:
return
dep_info = rule[RuleLicensedDependenciesInfo]
if hasattr(dep_info, "license_closure"):
transitive_licenses.append(dep_info.license_closure)
def create_metadata_file_info(ctx):
if hasattr(ctx.rule.attr, "applicable_licenses"):
for lic in ctx.rule.attr.applicable_licenses:
files = lic.files.to_list()
if len(files) == 1 and files[0].basename == "METADATA":
return MetadataFileInfo(metadata_file = files[0])
return MetadataFileInfo(metadata_file = None)
def _rule_licenses_aspect_impl(_rule, ctx):
if ctx.rule.kind == "_license":
return [RuleLicensedDependenciesInfo(), MetadataFileInfo()]
licenses = []
transitive_licenses = []
if hasattr(ctx.rule.attr, "applicable_licenses"):
licenses.extend(ctx.rule.attr.applicable_licenses)
for a in dir(ctx.rule.attr):
# Ignore private attributes
if a.startswith("_"):
continue
value = getattr(ctx.rule.attr, a)
vlist = value if type(value) == type([]) else [value]
for item in vlist:
if type(item) == "Target" and RuleLicensedDependenciesInfo in item:
_maybe_expand(item, transitive_licenses)
return [
RuleLicensedDependenciesInfo(license_closure = depset(licenses, transitive = transitive_licenses)),
create_metadata_file_info(ctx),
]
license_aspect = aspect(
doc = """Collect transitive license closure.""",
implementation = _rule_licenses_aspect_impl,
attr_aspects = ["*"],
apply_to_generating_rules = True,
provides = [RuleLicensedDependenciesInfo, MetadataFileInfo],
)
_license_kind_template = """
{{
"target": "{kind_path}",
"name": "{kind_name}",
"conditions": {kind_conditions}
}}"""
def _license_kind_to_json(kind):
return _license_kind_template.format(kind_name = kind.name, kind_path = kind.label, kind_conditions = kind.conditions)
def _quotes_or_null(s):
if not s:
return "null"
return s
def _license_file(license_rule):
file = license_rule[LicenseInfo].license_text
return file if file and file.basename != "__NO_LICENSE__" else struct(path = "")
def _divine_package_name(license):
if license.package_name:
return license.package_name.removeprefix("external").removesuffix("BUILD.bazel").replace("/", " ").strip()
return license.rule.name.removeprefix("external_").removesuffix("_license").replace("_", " ")
def license_map(deps):
"""Collects license to licensees map for the given set of rule targets.
TODO(asmundak): at the moment licensees lists are all empty because collecting
the licensees turned out to be too slow. Restore this later.
Args:
deps: list of rule targets
Returns:
dictionary mapping a license to its licensees
"""
transitive_licenses = []
for d in deps:
_maybe_expand(d, transitive_licenses)
# Each rule provides the closure of its licenses, let us build the
# reverse map. A minor quirk is that for some reason there may be
# multiple license instances with with the same label. Use the
# intermediary dict to map rule's label to its first instance
license_by_label = dict()
licensees = dict()
for lic in depset(transitive = transitive_licenses).to_list():
if not LicenseInfo in lic:
continue
label = lic[LicenseInfo].label.name
if not label in license_by_label:
license_by_label[label] = lic
licensees[lic] = []
return licensees
_license_template = """ {{
"rule": "{rule}",
"license_kinds": [{kinds}
],
"copyright_notice": "{copyright_notice}",
"package_name": "{package_name}",
"package_url": {package_url},
"package_version": {package_version},
"license_text": "{license_text}",
"licensees": [
"{licensees}"
]
\n }}"""
def _used_license_to_json(license_rule, licensed_rules):
license = license_rule[LicenseInfo]
return _license_template.format(
rule = license.label.name,
copyright_notice = license.copyright_notice,
package_name = _divine_package_name(license),
package_url = _quotes_or_null(license.package_url),
package_version = _quotes_or_null(license.package_version),
license_text = _license_file(license_rule).path,
kinds = ",\n".join([_license_kind_to_json(kind) for kind in license.license_kinds]),
licensees = "\",\n \"".join([r for r in licensed_rules]),
)
def license_map_to_json(licensees):
"""Returns an array of JSON representations of a license and its licensees. """
return [_used_license_to_json(lic, rules) for lic, rules in licensees.items()]
def license_map_notice_files(licensees):
"""Returns an array of license text files for the given licensee map.
Args:
licensees: dict returned by license_map() call
Returns:
the list of notice files this licensees map depends on.
"""
notice_files = []
for lic in licensees.keys():
file = _license_file(lic)
if file.path:
notice_files.append(file)
return notice_files