diff options
Diffstat (limited to 'rules/gather_licenses_info.bzl')
-rw-r--r-- | rules/gather_licenses_info.bzl | 270 |
1 files changed, 192 insertions, 78 deletions
diff --git a/rules/gather_licenses_info.bzl b/rules/gather_licenses_info.bzl index bd8c210..9dd1cbc 100644 --- a/rules/gather_licenses_info.bzl +++ b/rules/gather_licenses_info.bzl @@ -1,4 +1,4 @@ -# Copyright 2020 Google LLC +# Copyright 2022 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,66 +11,115 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """Rules and macros for collecting LicenseInfo providers.""" load( - "@rules_license//rules:providers.bzl", - "LicenseInfo", - "LicensesInfo", + "@rules_license//rules:licenses_core.bzl", + "TraceInfo", + "gather_metadata_info_common", + "should_traverse", +) +load( + "@rules_license//rules/private:gathering_providers.bzl", + "TransitiveLicensesInfo", ) -# Debugging verbosity -_VERBOSITY = 0 +# Definition for compliance namespace, used for filtering licenses +# based on the namespace to which they belong. +NAMESPACES = ["compliance"] -def _debug(loglevel, msg): - if _VERBOSITY > loglevel: - print(msg) # buildifier: disable=print +def _strip_null_repo(label): + """Removes the null repo name (e.g. @//) from a string. -def _get_transitive_licenses(deps, licenses, trans): - for dep in deps: - if LicenseInfo in dep: - license = dep[LicenseInfo] - _debug(1, " depends on license: %s" % license.rule) - licenses.append(license) - if LicensesInfo in dep: - license_list = dep[LicensesInfo].licenses - if license_list: - _debug(1, " transitively depends on: %s" % licenses) - trans.append(license_list) + The is to make str(label) compatible between bazel 5.x and 6.x + """ + s = str(label) + if s.startswith('@//'): + return s[1:] + elif s.startswith('@@//'): + return s[2:] + return s def _gather_licenses_info_impl(target, ctx): - licenses = [] - trans = [] - if hasattr(ctx.rule.attr, "applicable_licenses"): - _get_transitive_licenses(ctx.rule.attr.applicable_licenses, licenses, trans) - if hasattr(ctx.rule.attr, "deps"): - _get_transitive_licenses(ctx.rule.attr.deps, licenses, trans) - if hasattr(ctx.rule.attr, "srcs"): - _get_transitive_licenses(ctx.rule.attr.srcs, licenses, trans) - return [LicensesInfo(licenses = depset(tuple(licenses), transitive = trans))] + return gather_metadata_info_common(target, ctx, TransitiveLicensesInfo, NAMESPACES, [], should_traverse) gather_licenses_info = aspect( - doc = """Collects LicenseInfo providers into a single LicensesInfo provider.""", + doc = """Collects LicenseInfo providers into a single TransitiveLicensesInfo provider.""", implementation = _gather_licenses_info_impl, - attr_aspects = ["applicable_licenses", "deps", "srcs"], + attr_aspects = ["*"], + attrs = { + "_trace": attr.label(default = "@rules_license//rules:trace_target"), + }, + provides = [TransitiveLicensesInfo], apply_to_generating_rules = True, ) -def _quotes_or_null(s): - if not s: - return "null" - return '"%s"' % s +def _write_licenses_info_impl(target, ctx): + """Write transitive license info into a JSON file + + Args: + target: The target of the aspect. + ctx: The aspect evaluation context. + + Returns: + OutputGroupInfo + """ + + if not TransitiveLicensesInfo in target: + return [OutputGroupInfo(licenses = depset())] + info = target[TransitiveLicensesInfo] + outs = [] + + # If the result doesn't contain licenses, we simply return the provider + if not hasattr(info, "target_under_license"): + return [OutputGroupInfo(licenses = depset())] + + # Write the output file for the target + name = "%s_licenses_info.json" % ctx.label.name + content = "[\n%s\n]\n" % ",\n".join(licenses_info_to_json(info)) + out = ctx.actions.declare_file(name) + ctx.actions.write( + output = out, + content = content, + ) + outs.append(out) + + if ctx.attr._trace[TraceInfo].trace: + trace = ctx.actions.declare_file("%s_trace_info.json" % ctx.label.name) + ctx.actions.write(output = trace, content = "\n".join(info.traces)) + outs.append(trace) + + return [OutputGroupInfo(licenses = depset(outs))] + +gather_licenses_info_and_write = aspect( + doc = """Collects TransitiveLicensesInfo providers and writes JSON representation to a file. + + Usage: + blaze build //some:target \ + --aspects=@rules_license//rules:gather_licenses_info.bzl%gather_licenses_info_and_write + --output_groups=licenses + """, + implementation = _write_licenses_info_impl, + attr_aspects = ["*"], + attrs = { + "_trace": attr.label(default = "@rules_license//rules:trace_target"), + }, + provides = [OutputGroupInfo], + requires = [gather_licenses_info], + apply_to_generating_rules = True, +) def write_licenses_info(ctx, deps, json_out): - """Writes LicensesInfo providers for a set of targets as JSON. + """Writes TransitiveLicensesInfo providers for a set of targets as JSON. - TODO(aiuto): Document JSON schema. + TODO(aiuto): Document JSON schema. But it is under development, so the current + best place to look is at tests/hello_licenses.golden. Usage: write_licenses_info must be called from a rule implementation, where the - rule has run the gather_licenses_info aspect on its deps to collect the - transitive closure of LicenseInfo providers into a LicenseInfo provider. + rule has run the gather_licenses_info aspect on its deps to + collect the transitive closure of LicenseInfo providers into a + LicenseInfo provider. foo = rule( implementation = _foo_impl, @@ -86,51 +135,116 @@ def write_licenses_info(ctx, deps, json_out): Args: ctx: context of the caller - deps: a list of deps which should have LicensesInfo providers. + deps: a list of deps which should have TransitiveLicensesInfo providers. This requires that you have run the gather_licenses_info aspect over them json_out: output handle to write the JSON info """ - - rule_template = """ {{ - "rule": "{rule}", - "license_kinds": [{kinds} - ], - "copyright_notice": "{copyright_notice}", - "package_name": "{package_name}", - "package_url": {package_url}, - "package_version": {package_version}, - "license_text": "{license_text}"\n }}""" - - kind_template = """ - {{ - "target": "{kind_path}", - "name": "{kind_name}", - "conditions": {kind_conditions} - }}""" - licenses = [] for dep in deps: - if LicensesInfo in dep: - for license in dep[LicensesInfo].licenses.to_list(): - _debug(0, " Requires license: %s" % license) - kinds = [] - for kind in license.license_kinds: - kinds.append(kind_template.format( - kind_name = kind.name, - kind_path = kind.label, - kind_conditions = kind.conditions, - )) - licenses.append(rule_template.format( - rule = license.rule, - copyright_notice = license.copyright_notice, - package_name = license.package_name, - package_url = _quotes_or_null(license.package_url), - package_version = _quotes_or_null(license.package_version), - license_text = license.license_text.path, - kinds = ",\n".join(kinds), - )) + if TransitiveLicensesInfo in dep: + licenses.extend(licenses_info_to_json(dep[TransitiveLicensesInfo])) ctx.actions.write( output = json_out, content = "[\n%s\n]\n" % ",\n".join(licenses), ) + +def licenses_info_to_json(licenses_info): + """Render a single LicenseInfo provider to JSON + + Args: + licenses_info: A LicenseInfo. + + Returns: + [(str)] list of LicenseInfo values rendered as JSON. + """ + + main_template = """ {{ + "top_level_target": "{top_level_target}", + "dependencies": [{dependencies} + ], + "licenses": [{licenses} + ]\n }}""" + + dep_template = """ + {{ + "target_under_license": "{target_under_license}", + "licenses": [ + {licenses} + ] + }}""" + + # TODO(aiuto): 'rule' is a duplicate of 'label' until old users are transitioned + license_template = """ + {{ + "label": "{label}", + "rule": "{label}", + "license_kinds": [{kinds} + ], + "copyright_notice": "{copyright_notice}", + "package_name": "{package_name}", + "package_url": "{package_url}", + "package_version": "{package_version}", + "license_text": "{license_text}", + "used_by": [ + {used_by} + ] + }}""" + + kind_template = """ + {{ + "target": "{kind_path}", + "name": "{kind_name}", + "conditions": {kind_conditions} + }}""" + + # Build reverse map of license to user + used_by = {} + for dep in licenses_info.deps.to_list(): + # Undo the concatenation applied when stored in the provider. + dep_licenses = dep.licenses.split(",") + for license in dep_licenses: + if license not in used_by: + used_by[license] = [] + used_by[license].append(_strip_null_repo(dep.target_under_license)) + + all_licenses = [] + for license in sorted(licenses_info.licenses.to_list(), key = lambda x: x.label): + kinds = [] + for kind in sorted(license.license_kinds, key = lambda x: x.name): + kinds.append(kind_template.format( + kind_name = kind.name, + kind_path = kind.label, + kind_conditions = kind.conditions, + )) + + if license.license_text: + # Special handling for synthetic LicenseInfo + text_path = (license.license_text.package + "/" + license.license_text.name if type(license.license_text) == "Label" else license.license_text.path) + all_licenses.append(license_template.format( + copyright_notice = license.copyright_notice, + kinds = ",".join(kinds), + license_text = text_path, + package_name = license.package_name, + package_url = license.package_url, + package_version = license.package_version, + label = _strip_null_repo(license.label), + used_by = ",\n ".join(sorted(['"%s"' % x for x in used_by[str(license.label)]])), + )) + + all_deps = [] + for dep in sorted(licenses_info.deps.to_list(), key = lambda x: x.target_under_license): + licenses_used = [] + + # Undo the concatenation applied when stored in the provider. + dep_licenses = dep.licenses.split(",") + all_deps.append(dep_template.format( + target_under_license = _strip_null_repo(dep.target_under_license), + licenses = ",\n ".join(sorted(['"%s"' % _strip_null_repo(x) for x in dep_licenses])), + )) + + return [main_template.format( + top_level_target = _strip_null_repo(licenses_info.target_under_license), + dependencies = ",".join(all_deps), + licenses = ",".join(all_licenses), + )] |