From faf91b51e5e783c802dde83c06e236fd8cd01f07 Mon Sep 17 00:00:00 2001 From: Tony Aiuto Date: Mon, 15 May 2023 16:46:28 -0400 Subject: [PATCH 1/2] temp save ttt --- rules/gather_metadata.bzl | 297 +-------------------------- rules_gathering/gather_metadata.bzl | 305 ++++++++++++++++++++++++++++ 2 files changed, 314 insertions(+), 288 deletions(-) create mode 100644 rules_gathering/gather_metadata.bzl 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_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), + )] From d167df2e411efe26d4658315f6ad94e79402d99f Mon Sep 17 00:00:00 2001 From: Tony Aiuto Date: Fri, 2 Jun 2023 15:35:24 -0400 Subject: [PATCH 2/2] Move rules/sbom.bzl to rules_gathering/generate_sbom.bzl This is part of a continuing cleanup to make license and package metadata declarations distinct from the rules which create SBOMs and other reports. That will make it easier for the declarations to have global consistency, while individual organizations can define their own SBOM creators based on local compliance constraints. Forwarders for moved files are left in place. They will be deleted by 0.1.0 at the latest. --- examples/sboms/BUILD | 2 +- rules/sbom.bzl | 137 +----------------------------- rules_gathering/generate_sbom.bzl | 136 +++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 137 deletions(-) mode change 100644 => 120000 rules/sbom.bzl create mode 100644 rules_gathering/generate_sbom.bzl 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/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/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