xref: /aosp_15_r20/external/bazelbuild-rules_license/rules/gather_licenses_info.bzl (revision f578df4fd057ffe2023728444759535685631548)
1# Copyright 2022 Google LLC
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# https://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14"""Rules and macros for collecting LicenseInfo providers."""
15
16load(
17    "@rules_license//rules:licenses_core.bzl",
18    "TraceInfo",
19    "gather_metadata_info_common",
20    "should_traverse",
21)
22load(
23    "@rules_license//rules/private:gathering_providers.bzl",
24    "TransitiveLicensesInfo",
25)
26
27# Definition for compliance namespace, used for filtering licenses
28# based on the namespace to which they belong.
29NAMESPACES = ["compliance"]
30
31def _strip_null_repo(label):
32    """Removes the null repo name (e.g. @//) from a string.
33
34    The is to make str(label) compatible between bazel 5.x and 6.x
35    """
36    s = str(label)
37    if s.startswith('@//'):
38        return s[1:]
39    elif s.startswith('@@//'):
40        return s[2:]
41    return s
42
43def _gather_licenses_info_impl(target, ctx):
44    return gather_metadata_info_common(target, ctx, TransitiveLicensesInfo, NAMESPACES, [], should_traverse)
45
46gather_licenses_info = aspect(
47    doc = """Collects LicenseInfo providers into a single TransitiveLicensesInfo provider.""",
48    implementation = _gather_licenses_info_impl,
49    attr_aspects = ["*"],
50    attrs = {
51        "_trace": attr.label(default = "@rules_license//rules:trace_target"),
52    },
53    provides = [TransitiveLicensesInfo],
54    apply_to_generating_rules = True,
55)
56
57def _write_licenses_info_impl(target, ctx):
58    """Write transitive license info into a JSON file
59
60    Args:
61      target: The target of the aspect.
62      ctx: The aspect evaluation context.
63
64    Returns:
65      OutputGroupInfo
66    """
67
68    if not TransitiveLicensesInfo in target:
69        return [OutputGroupInfo(licenses = depset())]
70    info = target[TransitiveLicensesInfo]
71    outs = []
72
73    # If the result doesn't contain licenses, we simply return the provider
74    if not hasattr(info, "target_under_license"):
75        return [OutputGroupInfo(licenses = depset())]
76
77    # Write the output file for the target
78    name = "%s_licenses_info.json" % ctx.label.name
79    content = "[\n%s\n]\n" % ",\n".join(licenses_info_to_json(info))
80    out = ctx.actions.declare_file(name)
81    ctx.actions.write(
82        output = out,
83        content = content,
84    )
85    outs.append(out)
86
87    if ctx.attr._trace[TraceInfo].trace:
88        trace = ctx.actions.declare_file("%s_trace_info.json" % ctx.label.name)
89        ctx.actions.write(output = trace, content = "\n".join(info.traces))
90        outs.append(trace)
91
92    return [OutputGroupInfo(licenses = depset(outs))]
93
94gather_licenses_info_and_write = aspect(
95    doc = """Collects TransitiveLicensesInfo providers and writes JSON representation to a file.
96
97    Usage:
98      blaze build //some:target \
99          --aspects=@rules_license//rules:gather_licenses_info.bzl%gather_licenses_info_and_write
100          --output_groups=licenses
101    """,
102    implementation = _write_licenses_info_impl,
103    attr_aspects = ["*"],
104    attrs = {
105        "_trace": attr.label(default = "@rules_license//rules:trace_target"),
106    },
107    provides = [OutputGroupInfo],
108    requires = [gather_licenses_info],
109    apply_to_generating_rules = True,
110)
111
112def write_licenses_info(ctx, deps, json_out):
113    """Writes TransitiveLicensesInfo providers for a set of targets as JSON.
114
115    TODO(aiuto): Document JSON schema. But it is under development, so the current
116    best place to look is at tests/hello_licenses.golden.
117
118    Usage:
119      write_licenses_info must be called from a rule implementation, where the
120      rule has run the gather_licenses_info aspect on its deps to
121      collect the transitive closure of LicenseInfo providers into a
122      LicenseInfo provider.
123
124      foo = rule(
125        implementation = _foo_impl,
126        attrs = {
127           "deps": attr.label_list(aspects = [gather_licenses_info])
128        }
129      )
130
131      def _foo_impl(ctx):
132        ...
133        out = ctx.actions.declare_file("%s_licenses.json" % ctx.label.name)
134        write_licenses_info(ctx, ctx.attr.deps, licenses_file)
135
136    Args:
137      ctx: context of the caller
138      deps: a list of deps which should have TransitiveLicensesInfo providers.
139            This requires that you have run the gather_licenses_info
140            aspect over them
141      json_out: output handle to write the JSON info
142    """
143    licenses = []
144    for dep in deps:
145        if TransitiveLicensesInfo in dep:
146            licenses.extend(licenses_info_to_json(dep[TransitiveLicensesInfo]))
147    ctx.actions.write(
148        output = json_out,
149        content = "[\n%s\n]\n" % ",\n".join(licenses),
150    )
151
152def licenses_info_to_json(licenses_info):
153    """Render a single LicenseInfo provider to JSON
154
155    Args:
156      licenses_info: A LicenseInfo.
157
158    Returns:
159      [(str)] list of LicenseInfo values rendered as JSON.
160    """
161
162    main_template = """  {{
163    "top_level_target": "{top_level_target}",
164    "dependencies": [{dependencies}
165    ],
166    "licenses": [{licenses}
167    ]\n  }}"""
168
169    dep_template = """
170      {{
171        "target_under_license": "{target_under_license}",
172        "licenses": [
173          {licenses}
174        ]
175      }}"""
176
177    # TODO(aiuto): 'rule' is a duplicate of 'label' until old users are transitioned
178    license_template = """
179      {{
180        "label": "{label}",
181        "rule": "{label}",
182        "license_kinds": [{kinds}
183        ],
184        "copyright_notice": "{copyright_notice}",
185        "package_name": "{package_name}",
186        "package_url": "{package_url}",
187        "package_version": "{package_version}",
188        "license_text": "{license_text}",
189        "used_by": [
190          {used_by}
191        ]
192      }}"""
193
194    kind_template = """
195          {{
196            "target": "{kind_path}",
197            "name": "{kind_name}",
198            "conditions": {kind_conditions}
199          }}"""
200
201    # Build reverse map of license to user
202    used_by = {}
203    for dep in licenses_info.deps.to_list():
204        # Undo the concatenation applied when stored in the provider.
205        dep_licenses = dep.licenses.split(",")
206        for license in dep_licenses:
207            if license not in used_by:
208                used_by[license] = []
209            used_by[license].append(_strip_null_repo(dep.target_under_license))
210
211    all_licenses = []
212    for license in sorted(licenses_info.licenses.to_list(), key = lambda x: x.label):
213        kinds = []
214        for kind in sorted(license.license_kinds, key = lambda x: x.name):
215            kinds.append(kind_template.format(
216                kind_name = kind.name,
217                kind_path = kind.label,
218                kind_conditions = kind.conditions,
219            ))
220
221        if license.license_text:
222            # Special handling for synthetic LicenseInfo
223            text_path = (license.license_text.package + "/" + license.license_text.name if type(license.license_text) == "Label" else license.license_text.path)
224            all_licenses.append(license_template.format(
225                copyright_notice = license.copyright_notice,
226                kinds = ",".join(kinds),
227                license_text = text_path,
228                package_name = license.package_name,
229                package_url = license.package_url,
230                package_version = license.package_version,
231                label = _strip_null_repo(license.label),
232                used_by = ",\n          ".join(sorted(['"%s"' % x for x in used_by[str(license.label)]])),
233            ))
234
235    all_deps = []
236    for dep in sorted(licenses_info.deps.to_list(), key = lambda x: x.target_under_license):
237        licenses_used = []
238
239        # Undo the concatenation applied when stored in the provider.
240        dep_licenses = dep.licenses.split(",")
241        all_deps.append(dep_template.format(
242            target_under_license = _strip_null_repo(dep.target_under_license),
243            licenses = ",\n          ".join(sorted(['"%s"' % _strip_null_repo(x) for x in dep_licenses])),
244        ))
245
246    return [main_template.format(
247        top_level_target = _strip_null_repo(licenses_info.target_under_license),
248        dependencies = ",".join(all_deps),
249        licenses = ",".join(all_licenses),
250    )]
251