1load("@rules_license//rules:providers.bzl", "LicenseInfo")
2load("//build/bazel/rules:metadata.bzl", "MetadataFileInfo")
3
4RuleLicensedDependenciesInfo = provider(
5    doc = """Rule's licensed dependencies.""",
6    fields = dict(
7        license_closure = "depset(license) for the rule and its licensed dependencies",
8    ),
9)
10
11def _maybe_expand(rule, transitive_licenses):
12    if not RuleLicensedDependenciesInfo in rule:
13        return
14    dep_info = rule[RuleLicensedDependenciesInfo]
15    if hasattr(dep_info, "license_closure"):
16        transitive_licenses.append(dep_info.license_closure)
17
18def create_metadata_file_info(ctx):
19    if hasattr(ctx.rule.attr, "applicable_licenses"):
20        for lic in ctx.rule.attr.applicable_licenses:
21            files = lic.files.to_list()
22            if len(files) == 1 and files[0].basename == "METADATA":
23                return MetadataFileInfo(metadata_file = files[0])
24
25    return MetadataFileInfo(metadata_file = None)
26
27def _rule_licenses_aspect_impl(_rule, ctx):
28    if ctx.rule.kind == "_license":
29        return [RuleLicensedDependenciesInfo(), MetadataFileInfo()]
30
31    licenses = []
32    transitive_licenses = []
33    if hasattr(ctx.rule.attr, "applicable_licenses"):
34        licenses.extend(ctx.rule.attr.applicable_licenses)
35
36    for a in dir(ctx.rule.attr):
37        # Ignore private attributes
38        if a.startswith("_"):
39            continue
40        value = getattr(ctx.rule.attr, a)
41        vlist = value if type(value) == type([]) else [value]
42        for item in vlist:
43            if type(item) == "Target" and RuleLicensedDependenciesInfo in item:
44                _maybe_expand(item, transitive_licenses)
45
46    return [
47        RuleLicensedDependenciesInfo(license_closure = depset(licenses, transitive = transitive_licenses)),
48        create_metadata_file_info(ctx),
49    ]
50
51license_aspect = aspect(
52    doc = """Collect transitive license closure.""",
53    implementation = _rule_licenses_aspect_impl,
54    attr_aspects = ["*"],
55    apply_to_generating_rules = True,
56    provides = [RuleLicensedDependenciesInfo, MetadataFileInfo],
57)
58
59_license_kind_template = """
60      {{
61        "target": "{kind_path}",
62        "name": "{kind_name}",
63        "conditions": {kind_conditions}
64      }}"""
65
66def _license_kind_to_json(kind):
67    return _license_kind_template.format(kind_name = kind.name, kind_path = kind.label, kind_conditions = kind.conditions)
68
69def _quotes_or_null(s):
70    if not s:
71        return "null"
72    return s
73
74def _license_file(license_rule):
75    file = license_rule[LicenseInfo].license_text
76    return file if file and file.basename != "__NO_LICENSE__" else struct(path = "")
77
78def _divine_package_name(license):
79    if license.package_name:
80        return license.package_name.removeprefix("external").removesuffix("BUILD.bazel").replace("/", " ").strip()
81    return license.rule.name.removeprefix("external_").removesuffix("_license").replace("_", " ")
82
83def license_map(deps):
84    """Collects license to licensees map for the given set of rule targets.
85
86    TODO(asmundak): at the moment licensees lists are all empty because collecting
87    the licensees turned out to be too slow. Restore this later.
88    Args:
89        deps: list of rule targets
90    Returns:
91        dictionary mapping a license to its licensees
92    """
93    transitive_licenses = []
94    for d in deps:
95        _maybe_expand(d, transitive_licenses)
96
97    # Each rule provides the closure of its licenses, let us build the
98    # reverse map. A minor quirk is that for some reason there may be
99    # multiple license instances with with the same label. Use the
100    # intermediary dict to map rule's label to its first instance
101    license_by_label = dict()
102    licensees = dict()
103    for lic in depset(transitive = transitive_licenses).to_list():
104        if not LicenseInfo in lic:
105            continue
106        label = lic[LicenseInfo].label.name
107        if not label in license_by_label:
108            license_by_label[label] = lic
109            licensees[lic] = []
110    return licensees
111
112_license_template = """  {{
113    "rule": "{rule}",
114    "license_kinds": [{kinds}
115    ],
116    "copyright_notice": "{copyright_notice}",
117    "package_name": "{package_name}",
118    "package_url": {package_url},
119    "package_version": {package_version},
120    "license_text": "{license_text}",
121    "licensees": [
122        "{licensees}"
123    ]
124    \n  }}"""
125
126def _used_license_to_json(license_rule, licensed_rules):
127    license = license_rule[LicenseInfo]
128    return _license_template.format(
129        rule = license.label.name,
130        copyright_notice = license.copyright_notice,
131        package_name = _divine_package_name(license),
132        package_url = _quotes_or_null(license.package_url),
133        package_version = _quotes_or_null(license.package_version),
134        license_text = _license_file(license_rule).path,
135        kinds = ",\n".join([_license_kind_to_json(kind) for kind in license.license_kinds]),
136        licensees = "\",\n        \"".join([r for r in licensed_rules]),
137    )
138
139def license_map_to_json(licensees):
140    """Returns an array of JSON representations of a license and its licensees. """
141    return [_used_license_to_json(lic, rules) for lic, rules in licensees.items()]
142
143def license_map_notice_files(licensees):
144    """Returns an array of license text files for the given licensee map.
145
146    Args:
147        licensees: dict returned by license_map() call
148    Returns:
149        the list of notice files this licensees map depends on.
150    """
151    notice_files = []
152    for lic in licensees.keys():
153        file = _license_file(lic)
154        if file.path:
155            notice_files.append(file)
156    return notice_files
157