• 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_gathering:gathering_providers.bzl",
23    "TransitiveLicensesInfo",
24)
25load("@rules_license//rules_gathering:trace.bzl", "TraceInfo")
26
27def _strip_null_repo(label):
28    """Removes the null repo name (e.g. @//) from a string.
29
30    The is to make str(label) compatible between bazel 5.x and 6.x
31    """
32    s = str(label)
33    if s.startswith('@//'):
34        return s[1:]
35    elif s.startswith('@@//'):
36        return s[2:]
37    return s
38
39def _gather_licenses_info_impl(target, ctx):
40    return gather_metadata_info_common(target, ctx, TransitiveLicensesInfo, [], should_traverse)
41
42gather_licenses_info = aspect(
43    doc = """Collects LicenseInfo providers into a single TransitiveLicensesInfo provider.""",
44    implementation = _gather_licenses_info_impl,
45    attr_aspects = ["*"],
46    attrs = {
47        "_trace": attr.label(default = "@rules_license//rules:trace_target"),
48    },
49    provides = [TransitiveLicensesInfo],
50    apply_to_generating_rules = True,
51)
52
53def _write_licenses_info_impl(target, ctx):
54    """Write transitive license info into a JSON file
55
56    Args:
57      target: The target of the aspect.
58      ctx: The aspect evaluation context.
59
60    Returns:
61      OutputGroupInfo
62    """
63
64    if not TransitiveLicensesInfo in target:
65        return [OutputGroupInfo(licenses = depset())]
66    info = target[TransitiveLicensesInfo]
67    outs = []
68
69    # If the result doesn't contain licenses, we simply return the provider
70    if not hasattr(info, "target_under_license"):
71        return [OutputGroupInfo(licenses = depset())]
72
73    # Write the output file for the target
74    name = "%s_licenses_info.json" % ctx.label.name
75    lic_info, _ = licenses_info_to_json(info)
76    content = "[\n%s\n]\n" % ",\n".join(lic_info)
77    out = ctx.actions.declare_file(name)
78    ctx.actions.write(
79        output = out,
80        content = content,
81    )
82    outs.append(out)
83
84    if ctx.attr._trace[TraceInfo].trace:
85        trace = ctx.actions.declare_file("%s_trace_info.json" % ctx.label.name)
86        ctx.actions.write(output = trace, content = "\n".join(info.traces))
87        outs.append(trace)
88
89    return [OutputGroupInfo(licenses = depset(outs))]
90
91gather_licenses_info_and_write = aspect(
92    doc = """Collects TransitiveLicensesInfo providers and writes JSON representation to a file.
93
94    Usage:
95      blaze build //some:target \
96          --aspects=@rules_license//rules:gather_licenses_info.bzl%gather_licenses_info_and_write
97          --output_groups=licenses
98    """,
99    implementation = _write_licenses_info_impl,
100    attr_aspects = ["*"],
101    attrs = {
102        "_trace": attr.label(default = "@rules_license//rules:trace_target"),
103    },
104    provides = [OutputGroupInfo],
105    requires = [gather_licenses_info],
106    apply_to_generating_rules = True,
107)
108
109def write_licenses_info(ctx, deps, json_out):
110    """Writes TransitiveLicensesInfo providers for a set of targets as JSON.
111
112    TODO(aiuto): Document JSON schema. But it is under development, so the current
113    best place to look is at tests/hello_licenses.golden.
114
115    Usage:
116      write_licenses_info must be called from a rule implementation, where the
117      rule has run the gather_licenses_info aspect on its deps to
118      collect the transitive closure of LicenseInfo providers into a
119      LicenseInfo provider.
120
121      foo = rule(
122        implementation = _foo_impl,
123        attrs = {
124           "deps": attr.label_list(aspects = [gather_licenses_info])
125        }
126      )
127
128      def _foo_impl(ctx):
129        ...
130        json_file = ctx.actions.declare_file("%s_licenses.json" % ctx.label.name)
131        license_files = write_licenses_info(ctx, ctx.attr.deps, json_file)
132
133        // process the json file and the license_files referenced by it
134        ctx.actions.run(
135          inputs = [json_file] + license_files
136          executable = ...
137        )
138
139    Args:
140      ctx: context of the caller
141      deps: a list of deps which should have TransitiveLicensesInfo providers.
142            This requires that you have run the gather_licenses_info
143            aspect over them
144      json_out: output handle to write the JSON info
145
146    Returns:
147      A list of License File objects for each of the license paths referenced in the json.
148    """
149    licenses_json = []
150    licenses_files = []
151    for dep in deps:
152        if TransitiveLicensesInfo in dep:
153            transitive_licenses_info = dep[TransitiveLicensesInfo]
154            lic_info, _ = licenses_info_to_json(transitive_licenses_info)
155            licenses_json.extend(lic_info)
156            for info in transitive_licenses_info.licenses.to_list():
157                if info.license_text:
158                    licenses_files.append(info.license_text)
159
160    ctx.actions.write(
161        output = json_out,
162        content = "[\n%s\n]\n" % ",\n".join(licenses_json),
163    )
164    return licenses_files
165
166def licenses_info_to_json(licenses_info):
167    """Render a single LicenseInfo provider to JSON
168
169    Args:
170      licenses_info: A LicenseInfo.
171
172    Returns:
173      [(str)] list of LicenseInfo values rendered as JSON.
174      [(File)] list of Files containing license texts.
175    """
176
177    main_template = """  {{
178    "top_level_target": "{top_level_target}",
179    "dependencies": [{dependencies}
180    ],
181    "licenses": [{licenses}
182    ]\n  }}"""
183
184    dep_template = """
185      {{
186        "target_under_license": "{target_under_license}",
187        "licenses": [
188          {licenses}
189        ]
190      }}"""
191
192    # TODO(aiuto): 'rule' is a duplicate of 'label' until old users are transitioned
193    license_template = """
194      {{
195        "label": "{label}",
196        "rule": "{label}",
197        "license_kinds": [{kinds}
198        ],
199        "copyright_notice": "{copyright_notice}",
200        "package_name": "{package_name}",
201        "package_url": "{package_url}",
202        "package_version": "{package_version}",
203        "license_text": "{license_text}",
204        "used_by": [
205          {used_by}
206        ]
207      }}"""
208
209    kind_template = """
210          {{
211            "target": "{kind_path}",
212            "name": "{kind_name}",
213            "long_name": "{kind_long_name}",
214            "conditions": {kind_conditions}
215          }}"""
216
217    # Build reverse map of license to user
218    used_by = {}
219    for dep in licenses_info.deps.to_list():
220        # Undo the concatenation applied when stored in the provider.
221        dep_licenses = dep.licenses.split(",")
222        for license in dep_licenses:
223            if license not in used_by:
224                used_by[license] = []
225            used_by[license].append(_strip_null_repo(dep.target_under_license))
226
227    all_licenses = []
228    all_license_text_files = []
229    for license in sorted(licenses_info.licenses.to_list(), key = lambda x: x.label):
230        kinds = []
231        for kind in sorted(license.license_kinds, key = lambda x: x.name):
232            if hasattr(kind, "long_name"):
233                long_name = kind.long_name
234            else:
235                long_name = ""
236            kinds.append(kind_template.format(
237                kind_name = kind.name,
238                kind_long_name = long_name,
239                kind_path = kind.label,
240                kind_conditions = kind.conditions,
241            ))
242
243        if license.license_text:
244            # Special handling for synthetic LicenseInfo
245            text_path = (license.license_text.package + "/" + license.license_text.name if type(license.license_text) == "Label" else license.license_text.path)
246            all_licenses.append(license_template.format(
247                copyright_notice = license.copyright_notice,
248                kinds = ",".join(kinds),
249                license_text = text_path,
250                package_name = license.package_name,
251                package_url = license.package_url,
252                package_version = license.package_version,
253                label = _strip_null_repo(license.label),
254                used_by = ",\n          ".join(sorted(['"%s"' % x for x in used_by[str(license.label)]])),
255            ))
256            # Additionally return all File references so that other rules invoking
257            # this method can load license text file contents from external repos
258            # using runfiles
259            all_license_text_files.append(license.license_text)
260    all_deps = []
261    for dep in sorted(licenses_info.deps.to_list(), key = lambda x: x.target_under_license):
262        # Undo the concatenation applied when stored in the provider.
263        dep_licenses = dep.licenses.split(",")
264        all_deps.append(dep_template.format(
265            target_under_license = _strip_null_repo(dep.target_under_license),
266            licenses = ",\n          ".join(sorted(['"%s"' % _strip_null_repo(x) for x in dep_licenses])),
267        ))
268
269    return [main_template.format(
270        top_level_target = _strip_null_repo(licenses_info.target_under_license),
271        dependencies = ",".join(all_deps),
272        licenses = ",".join(all_licenses),
273    )], all_license_text_files
274