diff --git a/examples/sboms/BUILD b/examples/sboms/BUILD index 0c31a04..5d70e9c 100644 --- a/examples/sboms/BUILD +++ b/examples/sboms/BUILD @@ -1,6 +1,6 @@ # Demonstrate the generate_sbom rule -load("@rules_license//rules:sbom.bzl", "generate_sbom") +load("@rules_license//rules_gathering:generate_sbom.bzl", "generate_sbom") # There are not a lot of targets in this rule set to build a SBOM from # so we will (in a very self-referential way) generate one for the tool diff --git a/rules/gather_metadata.bzl b/rules/gather_metadata.bzl index f64fe63..2be8bfa 100644 --- a/rules/gather_metadata.bzl +++ b/rules/gather_metadata.bzl @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2023 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,294 +11,15 @@ # 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.""" +"""Forwarder for gather_metadata_info. +To be deleted before version 0.1.0 +""" load( - "@rules_license//rules:licenses_core.bzl", - "gather_metadata_info_common", - "should_traverse", + "@rules_license//rules_gathering:gather_metadata.bzl", + _gather_metadata_info = "gather_metadata_info", + _gather_metadata_info_and_write = "gather_metadata_info_and_write", ) -load( - "@rules_license//rules:providers.bzl", - "ExperimentalMetadataInfo", - "PackageInfo", -) -load( - "@rules_license//rules/private:gathering_providers.bzl", - "TransitiveMetadataInfo", -) -load("@rules_license//rules_gathering:trace.bzl", "TraceInfo") - -def _strip_null_repo(label): - """Removes the null repo name (e.g. @//) from a string. - - 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 _bazel_package(label): - clean_label = _strip_null_repo(label) - return clean_label[0:-(len(label.name) + 1)] - -def _gather_metadata_info_impl(target, ctx): - return gather_metadata_info_common( - target, - ctx, - TransitiveMetadataInfo, - [ExperimentalMetadataInfo, PackageInfo], - should_traverse) - -gather_metadata_info = aspect( - doc = """Collects LicenseInfo providers into a single TransitiveMetadataInfo provider.""", - implementation = _gather_metadata_info_impl, - attr_aspects = ["*"], - attrs = { - "_trace": attr.label(default = "@rules_license//rules:trace_target"), - }, - provides = [TransitiveMetadataInfo], - apply_to_generating_rules = True, -) - -def _write_metadata_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 TransitiveMetadataInfo in target: - return [OutputGroupInfo(licenses = depset())] - info = target[TransitiveMetadataInfo] - 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_metadata_info.json" % ctx.label.name - content = "[\n%s\n]\n" % ",\n".join(metadata_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_metadata_info_and_write = aspect( - doc = """Collects TransitiveMetadataInfo providers and writes JSON representation to a file. - - Usage: - bazel build //some:target \ - --aspects=@rules_license//rules:gather_metadata_info.bzl%gather_metadata_info_and_write - --output_groups=licenses - """, - implementation = _write_metadata_info_impl, - attr_aspects = ["*"], - attrs = { - "_trace": attr.label(default = "@rules_license//rules:trace_target"), - }, - provides = [OutputGroupInfo], - requires = [gather_metadata_info], - apply_to_generating_rules = True, -) - -def write_metadata_info(ctx, deps, json_out): - """Writes TransitiveMetadataInfo providers for a set of targets as JSON. - - 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_metadata_info must be called from a rule implementation, where the - rule has run the gather_metadata_info aspect on its deps to - collect the transitive closure of LicenseInfo providers into a - LicenseInfo provider. - - foo = rule( - implementation = _foo_impl, - attrs = { - "deps": attr.label_list(aspects = [gather_metadata_info]) - } - ) - - def _foo_impl(ctx): - ... - out = ctx.actions.declare_file("%s_licenses.json" % ctx.label.name) - write_metadata_info(ctx, ctx.attr.deps, metadata_file) - - Args: - ctx: context of the caller - deps: a list of deps which should have TransitiveMetadataInfo providers. - This requires that you have run the gather_metadata_info - aspect over them - json_out: output handle to write the JSON info - """ - licenses = [] - for dep in deps: - if TransitiveMetadataInfo in dep: - licenses.extend(metadata_info_to_json(dep[TransitiveMetadataInfo])) - ctx.actions.write( - output = json_out, - content = "[\n%s\n]\n" % ",\n".join(licenses), - ) - -def metadata_info_to_json(metadata_info): - """Render a single LicenseInfo provider to JSON - - Args: - metadata_info: A LicenseInfo. - - Returns: - [(str)] list of LicenseInfo values rendered as JSON. - """ - - main_template = """ {{ - "top_level_target": "{top_level_target}", - "dependencies": [{dependencies} - ], - "licenses": [{licenses} - ], - "packages": [{packages} - ]\n }}""" - - dep_template = """ - {{ - "target_under_license": "{target_under_license}", - "licenses": [ - {licenses} - ] - }}""" - - license_template = """ - {{ - "label": "{label}", - "bazel_package": "{bazel_package}", - "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} - }}""" - - package_info_template = """ - {{ - "target": "{label}", - "bazel_package": "{bazel_package}", - "package_name": "{package_name}", - "package_url": "{package_url}", - "package_version": "{package_version}" - }}""" - - # Build reverse map of license to user - used_by = {} - for dep in metadata_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(metadata_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), - bazel_package = _bazel_package(license.label), - used_by = ",\n ".join(sorted(['"%s"' % x for x in used_by[str(license.label)]])), - )) - - all_deps = [] - for dep in sorted(metadata_info.deps.to_list(), key = lambda x: x.target_under_license): - # 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])), - )) - - all_packages = [] - # We would use this if we had distinct depsets for every provider type. - #for package in sorted(metadata_info.package_info.to_list(), key = lambda x: x.label): - # all_packages.append(package_info_template.format( - # label = _strip_null_repo(package.label), - # package_name = package.package_name, - # package_url = package.package_url, - # package_version = package.package_version, - # )) - - for mi in sorted(metadata_info.other_metadata.to_list(), key = lambda x: x.label): - # Maybe use a map of provider class to formatter. A generic dict->json function - # in starlark would help - - # This format is for using distinct providers. I like the compile time safety. - if mi.type == "package_info": - all_packages.append(package_info_template.format( - label = _strip_null_repo(mi.label), - bazel_package = _bazel_package(mi.label), - package_name = mi.package_name, - package_url = mi.package_url, - package_version = mi.package_version, - )) - # experimental: Support the ExperimentalMetadataInfo bag of data - if mi.type == "package_info_alt": - all_packages.append(package_info_template.format( - label = _strip_null_repo(mi.label), - bazel_package = _bazel_package(mi.label), - # data is just a bag, so we need to use get() or "" - package_name = mi.data.get("package_name") or "", - package_url = mi.data.get("package_url") or "", - package_version = mi.data.get("package_version") or "", - )) - return [main_template.format( - top_level_target = _strip_null_repo(metadata_info.target_under_license), - dependencies = ",".join(all_deps), - licenses = ",".join(all_licenses), - packages = ",".join(all_packages), - )] +gather_metadata_info = _gather_metadata_info +gather_metadata_info_and_write = _gather_metadata_info_and_write diff --git a/rules/sbom.bzl b/rules/sbom.bzl deleted file mode 100644 index 73c1861..0000000 --- a/rules/sbom.bzl +++ /dev/null @@ -1,136 +0,0 @@ -# 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. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# 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. -"""SBOM generation""" - -load( - "@rules_license//rules:gather_metadata.bzl", - "gather_metadata_info", - "gather_metadata_info_and_write", - "write_metadata_info", -) -load( - "@rules_license//rules/private:gathering_providers.bzl", - "TransitiveLicensesInfo", -) - -# This rule is proof of concept, and may not represent the final -# form of a rule for compliance validation. -def _generate_sbom_impl(ctx): - # Gather all licenses and write information to one place - - licenses_file = ctx.actions.declare_file("_%s_licenses_info.json" % ctx.label.name) - write_metadata_info(ctx, ctx.attr.deps, licenses_file) - - # Now turn the big blob of data into something consumable. - inputs = [licenses_file] - outputs = [ctx.outputs.out] - args = ctx.actions.args() - args.add("--licenses_info", licenses_file.path) - args.add("--out", ctx.outputs.out.path) - ctx.actions.run( - mnemonic = "CreateSBOM", - progress_message = "Creating SBOM for %s" % ctx.label, - inputs = inputs, - outputs = outputs, - executable = ctx.executable._sbom_generator, - arguments = [args], - ) - outputs.append(licenses_file) # also make the json file available. - return [DefaultInfo(files = depset(outputs))] - -_generate_sbom = rule( - implementation = _generate_sbom_impl, - attrs = { - "deps": attr.label_list( - aspects = [gather_metadata_info], - ), - "out": attr.output(mandatory = True), - "_sbom_generator": attr.label( - default = Label("@rules_license//tools:write_sbom"), - executable = True, - allow_files = True, - cfg = "exec", - ), - }, -) - -def generate_sbom(**kwargs): - _generate_sbom(**kwargs) - -def _manifest_impl(ctx): - # Gather all licenses and make it available as deps for downstream rules - # Additionally write the list of license filenames to a file that can - # also be used as an input to downstream rules. - licenses_file = ctx.actions.declare_file(ctx.attr.out.name) - mappings = get_licenses_mapping(ctx.attr.deps, ctx.attr.warn_on_legacy_licenses) - ctx.actions.write( - output = licenses_file, - content = "\n".join([",".join([f.path, p]) for (f, p) in mappings.items()]), - ) - return [DefaultInfo(files = depset(mappings.keys()))] - -_manifest = rule( - implementation = _manifest_impl, - doc = """Internal tmplementation method for manifest().""", - attrs = { - "deps": attr.label_list( - doc = """List of targets to collect license files for.""", - aspects = [gather_metadata_info], - ), - "out": attr.output( - doc = """Output file.""", - mandatory = True, - ), - "warn_on_legacy_licenses": attr.bool(default = False), - }, -) - -def manifest(name, deps, out = None, **kwargs): - if not out: - out = name + ".manifest" - - _manifest(name = name, deps = deps, out = out, **kwargs) - -def get_licenses_mapping(deps, warn = False): - """Creates list of entries representing all licenses for the deps. - - Args: - - deps: a list of deps which should have TransitiveLicensesInfo providers. - This requires that you have run the gather_licenses_info - aspect over them - - warn: boolean, if true, display output about legacy targets that need - update - - Returns: - {File:package_name} - """ - tls = [] - for dep in deps: - lds = dep[TransitiveLicensesInfo].licenses - tls.append(lds) - - ds = depset(transitive = tls) - - # Ignore any legacy licenses that may be in the report - mappings = {} - for lic in ds.to_list(): - if type(lic.license_text) == "File": - mappings[lic.license_text] = lic.package_name - elif warn: - # buildifier: disable=print - print("Legacy license %s not included, rule needs updating" % lic.license_text) - - return mappings diff --git a/rules/sbom.bzl b/rules/sbom.bzl new file mode 120000 index 0000000..8485c5f --- /dev/null +++ b/rules/sbom.bzl @@ -0,0 +1 @@ +../rules_gathering/generate_sbom.bzl \ No newline at end of file diff --git a/rules_gathering/gather_metadata.bzl b/rules_gathering/gather_metadata.bzl new file mode 100644 index 0000000..8269241 --- /dev/null +++ b/rules_gathering/gather_metadata.bzl @@ -0,0 +1,305 @@ +# 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. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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:licenses_core.bzl", + "gather_metadata_info_common", + "should_traverse", +) +load( + "@rules_license//rules:providers.bzl", + "ExperimentalMetadataInfo", + "PackageInfo", +) +load( + "@rules_license//rules/private:gathering_providers.bzl", + "TransitiveMetadataInfo", +) +load("@rules_license//rules_gathering:trace.bzl", "TraceInfo") + +def _strip_null_repo(label): + """Removes the null repo name (e.g. @//) from a string. + + 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 _bazel_package(label): + clean_label = _strip_null_repo(label) + return clean_label[0:-(len(label.name) + 1)] + +def _gather_metadata_info_impl(target, ctx): + return gather_metadata_info_common( + target, + ctx, + TransitiveMetadataInfo, + [ExperimentalMetadataInfo, PackageInfo], + should_traverse) + +gather_metadata_info = aspect( + doc = """Collects LicenseInfo providers into a single TransitiveMetadataInfo provider.""", + implementation = _gather_metadata_info_impl, + attr_aspects = ["*"], + attrs = { + "_trace": attr.label(default = "@rules_license//rules:trace_target"), + }, + provides = [TransitiveMetadataInfo], + apply_to_generating_rules = True, +) + +def _write_metadata_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 TransitiveMetadataInfo in target: + return [OutputGroupInfo(licenses = depset())] + info = target[TransitiveMetadataInfo] + 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_metadata_info.json" % ctx.label.name + content = "[\n%s\n]\n" % ",\n".join(metadata_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_metadata_info_and_write = aspect( + doc = """Collects TransitiveMetadataInfo providers and writes JSON representation to a file. + + Usage: + bazel build //some:target \ + --aspects=@rules_license//rules:gather_metadata_info.bzl%gather_metadata_info_and_write + --output_groups=licenses + """, + implementation = _write_metadata_info_impl, + attr_aspects = ["*"], + attrs = { + "_trace": attr.label(default = "@rules_license//rules:trace_target"), + }, + provides = [OutputGroupInfo], + requires = [gather_metadata_info], + apply_to_generating_rules = True, +) + +def write_metadata_info(ctx, deps, json_out): + """Writes TransitiveMetadataInfo providers for a set of targets as JSON. + + 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_metadata_info must be called from a rule implementation, where the + rule has run the gather_metadata_info aspect on its deps to + collect the transitive closure of LicenseInfo providers into a + LicenseInfo provider. + + foo = rule( + implementation = _foo_impl, + attrs = { + "deps": attr.label_list(aspects = [gather_metadata_info]) + } + ) + + def _foo_impl(ctx): + ... + out = ctx.actions.declare_file("%s_licenses.json" % ctx.label.name) + write_metadata_info(ctx, ctx.attr.deps, metadata_file) + + Args: + ctx: context of the caller + deps: a list of deps which should have TransitiveMetadataInfo providers. + This requires that you have run the gather_metadata_info + aspect over them + json_out: output handle to write the JSON info + """ + licenses = [] + for dep in deps: + if TransitiveMetadataInfo in dep: + licenses.extend(metadata_info_to_json(dep[TransitiveMetadataInfo])) + ctx.actions.write( + output = json_out, + content = "[\n%s\n]\n" % ",\n".join(licenses), + ) + +def metadata_info_to_json(metadata_info): + """Render a single LicenseInfo provider to JSON + + Args: + metadata_info: A LicenseInfo. + + Returns: + [(str)] list of LicenseInfo values rendered as JSON. + """ + + main_template = """ {{ + "top_level_target": "{top_level_target}", + "dependencies": [{dependencies} + ], + "licenses": [{licenses} + ], + "packages": [{packages} + ]\n }}""" + + dep_template = """ + {{ + "target_under_license": "{target_under_license}", + "licenses": [ + {licenses} + ] + }}""" + + license_template = """ + {{ + "label": "{label}", + "bazel_package": "{bazel_package}", + "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} + }}""" + + package_info_template = """ + {{ + "target": "{label}", + "bazel_package": "{bazel_package}", + "package_name": "{package_name}", + "package_url": "{package_url}", + "package_version": "{package_version}" + }}""" + + # Build reverse map of license to user + used_by = {} + for dep in metadata_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(metadata_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), + bazel_package = _bazel_package(license.label), + used_by = ",\n ".join(sorted(['"%s"' % x for x in used_by[str(license.label)]])), + )) + + all_deps = [] + for dep in sorted(metadata_info.deps.to_list(), key = lambda x: x.target_under_license): + # 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])), + )) + + all_packages = [] + # We would use this if we had distinct depsets for every provider type. + #for package in sorted(metadata_info.package_info.to_list(), key = lambda x: x.label): + # all_packages.append(package_info_template.format( + # label = _strip_null_repo(package.label), + # package_name = package.package_name, + # package_url = package.package_url, + # package_version = package.package_version, + # )) + + for mi in sorted(metadata_info.other_metadata.to_list(), key = lambda x: x.label): + # Maybe use a map of provider class to formatter. A generic dict->json function + # in starlark would help + + # This format is for using distinct providers. I like the compile time safety. + if mi.type == "package_info": + all_packages.append(package_info_template.format( + label = _strip_null_repo(mi.label), + bazel_package = _bazel_package(mi.label), + package_name = mi.package_name, + package_url = mi.package_url, + package_version = mi.package_version, + )) + # experimental: Support the ExperimentalMetadataInfo bag of data + # WARNING: Do not depend on this. It will change without notice. + if mi.type == "package_info_alt": + all_packages.append(package_info_template.format( + label = _strip_null_repo(mi.label), + bazel_package = _bazel_package(mi.label), + # data is just a bag, so we need to use get() or "" + package_name = mi.data.get("package_name") or "", + package_url = mi.data.get("package_url") or "", + package_version = mi.data.get("package_version") or "", + )) + + return [main_template.format( + top_level_target = _strip_null_repo(metadata_info.target_under_license), + dependencies = ",".join(all_deps), + licenses = ",".join(all_licenses), + packages = ",".join(all_packages), + )] diff --git a/rules_gathering/generate_sbom.bzl b/rules_gathering/generate_sbom.bzl new file mode 100644 index 0000000..00131ec --- /dev/null +++ b/rules_gathering/generate_sbom.bzl @@ -0,0 +1,136 @@ +# 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. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# 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. +"""SBOM generation""" + +load( + "@rules_license//rules_gathering:gather_metadata.bzl", + "gather_metadata_info", + "gather_metadata_info_and_write", + "write_metadata_info", +) +load( + "@rules_license//rules/private:gathering_providers.bzl", + "TransitiveLicensesInfo", +) + +# This rule is proof of concept, and may not represent the final +# form of a rule for compliance validation. +def _generate_sbom_impl(ctx): + # Gather all licenses and write information to one place + + licenses_file = ctx.actions.declare_file("_%s_licenses_info.json" % ctx.label.name) + write_metadata_info(ctx, ctx.attr.deps, licenses_file) + + # Now turn the big blob of data into something consumable. + inputs = [licenses_file] + outputs = [ctx.outputs.out] + args = ctx.actions.args() + args.add("--licenses_info", licenses_file.path) + args.add("--out", ctx.outputs.out.path) + ctx.actions.run( + mnemonic = "CreateSBOM", + progress_message = "Creating SBOM for %s" % ctx.label, + inputs = inputs, + outputs = outputs, + executable = ctx.executable._sbom_generator, + arguments = [args], + ) + outputs.append(licenses_file) # also make the json file available. + return [DefaultInfo(files = depset(outputs))] + +_generate_sbom = rule( + implementation = _generate_sbom_impl, + attrs = { + "deps": attr.label_list( + aspects = [gather_metadata_info], + ), + "out": attr.output(mandatory = True), + "_sbom_generator": attr.label( + default = Label("@rules_license//tools:write_sbom"), + executable = True, + allow_files = True, + cfg = "exec", + ), + }, +) + +def generate_sbom(**kwargs): + _generate_sbom(**kwargs) + +def _manifest_impl(ctx): + # Gather all licenses and make it available as deps for downstream rules + # Additionally write the list of license filenames to a file that can + # also be used as an input to downstream rules. + licenses_file = ctx.actions.declare_file(ctx.attr.out.name) + mappings = get_licenses_mapping(ctx.attr.deps, ctx.attr.warn_on_legacy_licenses) + ctx.actions.write( + output = licenses_file, + content = "\n".join([",".join([f.path, p]) for (f, p) in mappings.items()]), + ) + return [DefaultInfo(files = depset(mappings.keys()))] + +_manifest = rule( + implementation = _manifest_impl, + doc = """Internal tmplementation method for manifest().""", + attrs = { + "deps": attr.label_list( + doc = """List of targets to collect license files for.""", + aspects = [gather_metadata_info], + ), + "out": attr.output( + doc = """Output file.""", + mandatory = True, + ), + "warn_on_legacy_licenses": attr.bool(default = False), + }, +) + +def manifest(name, deps, out = None, **kwargs): + if not out: + out = name + ".manifest" + + _manifest(name = name, deps = deps, out = out, **kwargs) + +def get_licenses_mapping(deps, warn = False): + """Creates list of entries representing all licenses for the deps. + + Args: + + deps: a list of deps which should have TransitiveLicensesInfo providers. + This requires that you have run the gather_licenses_info + aspect over them + + warn: boolean, if true, display output about legacy targets that need + update + + Returns: + {File:package_name} + """ + tls = [] + for dep in deps: + lds = dep[TransitiveLicensesInfo].licenses + tls.append(lds) + + ds = depset(transitive = tls) + + # Ignore any legacy licenses that may be in the report + mappings = {} + for lic in ds.to_list(): + if type(lic.license_text) == "File": + mappings[lic.license_text] = lic.package_name + elif warn: + # buildifier: disable=print + print("Legacy license %s not included, rule needs updating" % lic.license_text) + + return mappings