xref: /aosp_15_r20/external/bazelbuild-rules_license/rules/gather_metadata.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:providers.bzl",
24    "ExperimentalMetadataInfo",
25    "PackageInfo",
26)
27load(
28    "@rules_license//rules/private:gathering_providers.bzl",
29    "TransitiveMetadataInfo",
30)
31
32# Definition for compliance namespace, used for filtering licenses
33# based on the namespace to which they belong.
34NAMESPACES = ["compliance"]
35
36def _strip_null_repo(label):
37    """Removes the null repo name (e.g. @//) from a string.
38
39    The is to make str(label) compatible between bazel 5.x and 6.x
40    """
41    s = str(label)
42    if s.startswith('@//'):
43        return s[1:]
44    elif s.startswith('@@//'):
45        return s[2:]
46    return s
47
48def _bazel_package(label):
49    clean_label = _strip_null_repo(label)
50    return clean_label[0:-(len(label.name) + 1)]
51
52def _gather_metadata_info_impl(target, ctx):
53    return gather_metadata_info_common(
54        target,
55        ctx,
56        TransitiveMetadataInfo,
57        NAMESPACES,
58        [ExperimentalMetadataInfo, PackageInfo],
59        should_traverse)
60
61gather_metadata_info = aspect(
62    doc = """Collects LicenseInfo providers into a single TransitiveMetadataInfo provider.""",
63    implementation = _gather_metadata_info_impl,
64    attr_aspects = ["*"],
65    attrs = {
66        "_trace": attr.label(default = "@rules_license//rules:trace_target"),
67    },
68    provides = [TransitiveMetadataInfo],
69    apply_to_generating_rules = True,
70)
71
72def _write_metadata_info_impl(target, ctx):
73    """Write transitive license info into a JSON file
74
75    Args:
76      target: The target of the aspect.
77      ctx: The aspect evaluation context.
78
79    Returns:
80      OutputGroupInfo
81    """
82
83    if not TransitiveMetadataInfo in target:
84        return [OutputGroupInfo(licenses = depset())]
85    info = target[TransitiveMetadataInfo]
86    outs = []
87
88    # If the result doesn't contain licenses, we simply return the provider
89    if not hasattr(info, "target_under_license"):
90        return [OutputGroupInfo(licenses = depset())]
91
92    # Write the output file for the target
93    name = "%s_metadata_info.json" % ctx.label.name
94    content = "[\n%s\n]\n" % ",\n".join(metadata_info_to_json(info))
95    out = ctx.actions.declare_file(name)
96    ctx.actions.write(
97        output = out,
98        content = content,
99    )
100    outs.append(out)
101
102    if ctx.attr._trace[TraceInfo].trace:
103        trace = ctx.actions.declare_file("%s_trace_info.json" % ctx.label.name)
104        ctx.actions.write(output = trace, content = "\n".join(info.traces))
105        outs.append(trace)
106
107    return [OutputGroupInfo(licenses = depset(outs))]
108
109gather_metadata_info_and_write = aspect(
110    doc = """Collects TransitiveMetadataInfo providers and writes JSON representation to a file.
111
112    Usage:
113      bazel build //some:target \
114          --aspects=@rules_license//rules:gather_metadata_info.bzl%gather_metadata_info_and_write
115          --output_groups=licenses
116    """,
117    implementation = _write_metadata_info_impl,
118    attr_aspects = ["*"],
119    attrs = {
120        "_trace": attr.label(default = "@rules_license//rules:trace_target"),
121    },
122    provides = [OutputGroupInfo],
123    requires = [gather_metadata_info],
124    apply_to_generating_rules = True,
125)
126
127def write_metadata_info(ctx, deps, json_out):
128    """Writes TransitiveMetadataInfo providers for a set of targets as JSON.
129
130    TODO(aiuto): Document JSON schema. But it is under development, so the current
131    best place to look is at tests/hello_licenses.golden.
132
133    Usage:
134      write_metadata_info must be called from a rule implementation, where the
135      rule has run the gather_metadata_info aspect on its deps to
136      collect the transitive closure of LicenseInfo providers into a
137      LicenseInfo provider.
138
139      foo = rule(
140        implementation = _foo_impl,
141        attrs = {
142           "deps": attr.label_list(aspects = [gather_metadata_info])
143        }
144      )
145
146      def _foo_impl(ctx):
147        ...
148        out = ctx.actions.declare_file("%s_licenses.json" % ctx.label.name)
149        write_metadata_info(ctx, ctx.attr.deps, metadata_file)
150
151    Args:
152      ctx: context of the caller
153      deps: a list of deps which should have TransitiveMetadataInfo providers.
154            This requires that you have run the gather_metadata_info
155            aspect over them
156      json_out: output handle to write the JSON info
157    """
158    licenses = []
159    for dep in deps:
160        if TransitiveMetadataInfo in dep:
161            licenses.extend(metadata_info_to_json(dep[TransitiveMetadataInfo]))
162    ctx.actions.write(
163        output = json_out,
164        content = "[\n%s\n]\n" % ",\n".join(licenses),
165    )
166
167def metadata_info_to_json(metadata_info):
168    """Render a single LicenseInfo provider to JSON
169
170    Args:
171      metadata_info: A LicenseInfo.
172
173    Returns:
174      [(str)] list of LicenseInfo values rendered as JSON.
175    """
176
177    main_template = """  {{
178    "top_level_target": "{top_level_target}",
179    "dependencies": [{dependencies}
180    ],
181    "licenses": [{licenses}
182    ],
183    "packages": [{packages}
184    ]\n  }}"""
185
186    dep_template = """
187      {{
188        "target_under_license": "{target_under_license}",
189        "licenses": [
190          {licenses}
191        ]
192      }}"""
193
194    license_template = """
195      {{
196        "label": "{label}",
197        "bazel_package": "{bazel_package}",
198        "license_kinds": [{kinds}
199        ],
200        "copyright_notice": "{copyright_notice}",
201        "package_name": "{package_name}",
202        "package_url": "{package_url}",
203        "package_version": "{package_version}",
204        "license_text": "{license_text}",
205        "used_by": [
206          {used_by}
207        ]
208      }}"""
209
210    kind_template = """
211          {{
212            "target": "{kind_path}",
213            "name": "{kind_name}",
214            "conditions": {kind_conditions}
215          }}"""
216
217    package_info_template = """
218          {{
219            "target": "{label}",
220            "bazel_package": "{bazel_package}",
221            "package_name": "{package_name}",
222            "package_url": "{package_url}",
223            "package_version": "{package_version}"
224          }}"""
225
226    # Build reverse map of license to user
227    used_by = {}
228    for dep in metadata_info.deps.to_list():
229        # Undo the concatenation applied when stored in the provider.
230        dep_licenses = dep.licenses.split(",")
231        for license in dep_licenses:
232            if license not in used_by:
233                used_by[license] = []
234            used_by[license].append(_strip_null_repo(dep.target_under_license))
235
236    all_licenses = []
237    for license in sorted(metadata_info.licenses.to_list(), key = lambda x: x.label):
238        kinds = []
239        for kind in sorted(license.license_kinds, key = lambda x: x.name):
240            kinds.append(kind_template.format(
241                kind_name = kind.name,
242                kind_path = kind.label,
243                kind_conditions = kind.conditions,
244            ))
245
246        if license.license_text:
247            # Special handling for synthetic LicenseInfo
248            text_path = (license.license_text.package + "/" + license.license_text.name if type(license.license_text) == "Label" else license.license_text.path)
249            all_licenses.append(license_template.format(
250                copyright_notice = license.copyright_notice,
251                kinds = ",".join(kinds),
252                license_text = text_path,
253                package_name = license.package_name,
254                package_url = license.package_url,
255                package_version = license.package_version,
256                label = _strip_null_repo(license.label),
257                bazel_package =  _bazel_package(license.label),
258                used_by = ",\n          ".join(sorted(['"%s"' % x for x in used_by[str(license.label)]])),
259            ))
260
261    all_deps = []
262    for dep in sorted(metadata_info.deps.to_list(), key = lambda x: x.target_under_license):
263        # Undo the concatenation applied when stored in the provider.
264        dep_licenses = dep.licenses.split(",")
265        all_deps.append(dep_template.format(
266            target_under_license = _strip_null_repo(dep.target_under_license),
267            licenses = ",\n          ".join(sorted(['"%s"' % _strip_null_repo(x) for x in dep_licenses])),
268        ))
269
270    all_packages = []
271    # We would use this if we had distinct depsets for every provider type.
272    #for package in sorted(metadata_info.package_info.to_list(), key = lambda x: x.label):
273    #    all_packages.append(package_info_template.format(
274    #        label = _strip_null_repo(package.label),
275    #        package_name = package.package_name,
276    #        package_url = package.package_url,
277    #        package_version = package.package_version,
278    #    ))
279
280    for mi in sorted(metadata_info.other_metadata.to_list(), key = lambda x: x.label):
281        # Maybe use a map of provider class to formatter.  A generic dict->json function
282        # in starlark would help
283
284        # This format is for using distinct providers.  I like the compile time safety.
285        if mi.type == "package_info":
286            all_packages.append(package_info_template.format(
287                label = _strip_null_repo(mi.label),
288                bazel_package =  _bazel_package(mi.label),
289                package_name = mi.package_name,
290                package_url = mi.package_url,
291                package_version = mi.package_version,
292            ))
293        # experimental: Support the ExperimentalMetadataInfo bag of data
294        if mi.type == "package_info_alt":
295            all_packages.append(package_info_template.format(
296                label = _strip_null_repo(mi.label),
297                bazel_package =  _bazel_package(mi.label),
298                # data is just a bag, so we need to use get() or ""
299                package_name = mi.data.get("package_name") or "",
300                package_url = mi.data.get("package_url") or "",
301                package_version = mi.data.get("package_version") or "",
302            ))
303
304    return [main_template.format(
305        top_level_target = _strip_null_repo(metadata_info.target_under_license),
306        dependencies = ",".join(all_deps),
307        licenses = ",".join(all_licenses),
308        packages = ",".join(all_packages),
309    )]
310