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