diff options
Diffstat (limited to 'rules')
-rw-r--r-- | rules/BUILD | 22 | ||||
-rw-r--r-- | rules/check_licenses_shim.bzl | 30 | ||||
-rw-r--r-- | rules/compliance.bzl | 101 | ||||
-rw-r--r-- | rules/default_license.bzl | 55 | ||||
-rw-r--r-- | rules/filtered_rule_kinds.bzl | 47 | ||||
-rw-r--r-- | rules/gather_licenses_info.bzl | 270 | ||||
-rw-r--r-- | rules/gather_metadata.bzl | 309 | ||||
-rw-r--r-- | rules/license.bzl | 117 | ||||
-rw-r--r-- | rules/license_impl.bzl | 47 | ||||
-rw-r--r-- | rules/license_kind.bzl | 19 | ||||
-rw-r--r-- | rules/license_policy.bzl | 53 | ||||
-rw-r--r-- | rules/license_policy_check.bzl | 80 | ||||
-rw-r--r-- | rules/licenses_core.bzl | 224 | ||||
-rw-r--r-- | rules/package_info.bzl | 104 | ||||
-rw-r--r-- | rules/private/BUILD | 35 | ||||
-rw-r--r-- | rules/private/gathering_providers.bzl | 54 | ||||
-rw-r--r-- | rules/providers.bzl | 53 | ||||
-rw-r--r-- | rules/sbom.bzl | 136 | ||||
-rw-r--r-- | rules/user_filtered_rule_kinds.bzl (renamed from rules/license_policy_provider.bzl) | 24 |
19 files changed, 1407 insertions, 373 deletions
diff --git a/rules/BUILD b/rules/BUILD index f0fd218..83e8c14 100644 --- a/rules/BUILD +++ b/rules/BUILD @@ -15,12 +15,34 @@ # limitations under the License. """Rules for making license declarations.""" +load("@rules_license//rules:licenses_core.bzl", "trace") + package( default_applicable_licenses = ["//:license"], default_visibility = ["//visibility:public"], ) +licenses(["notice"]) + +# This target controls the value of the traced target used during dependency collection. +# This value should always be the empty string! +# Specify this value with a flag, like --@rules_license//rules:trace_target=//target/to:trace +trace( + name = "trace_target", + build_setting_default = "", # TRACE-TARGET-SHOULD-BE-EMPTY + visibility = ["//visibility:public"], +) + filegroup( name = "standard_package", srcs = glob(["**"]), ) + +# Do not create a bzl_library(). That would create a dependency loop back +# to bazel-skylib. We export the .bzl files to the documentation maker. +exports_files( + glob([ + "*.bzl", + ]), + visibility = ["//doc_build:__pkg__"], +) diff --git a/rules/check_licenses_shim.bzl b/rules/check_licenses_shim.bzl new file mode 100644 index 0000000..3dcfe2c --- /dev/null +++ b/rules/check_licenses_shim.bzl @@ -0,0 +1,30 @@ +# 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. +"""This module provides a custom Starlark rule used to create wrappers for targets that +can have blaze build --check_licenses executed against them.""" + +def _shim_rule_impl(ctx): + # This rule doesn't need to return anything. It only exists to propagate the dependency supplied + # by the label_flag + return [] + +shim_rule = rule( + doc = """This rule exists to configure a dependent target via label. An instantiation of this + rule is then used as a dependency for the legacy_check_target rule, which can be built with --check_licenses + to get the effect of running --check_licenses on an arbitrary target which may or may not have a distribs + attribute""", + implementation = _shim_rule_impl, + # The definition of this attribute creates a dependency relationship on the manually provided label. + attrs = {"target": attr.label(default = ":check_licenses_target")}, +) diff --git a/rules/compliance.bzl b/rules/compliance.bzl index a30de9e..2fb04ab 100644 --- a/rules/compliance.bzl +++ b/rules/compliance.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,40 +11,30 @@ # 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. - -"""Proof of concept. License compliance checking.""" +"""License compliance checking.""" load( "@rules_license//rules:gather_licenses_info.bzl", "gather_licenses_info", + "gather_licenses_info_and_write", "write_licenses_info", ) load( - "@rules_license//rules:providers.bzl", - "LicensesInfo", + "@rules_license//rules/private:gathering_providers.bzl", + "TransitiveLicensesInfo", ) -# Debugging verbosity -_VERBOSITY = 0 - -def _debug(loglevel, msg): - if _VERBOSITY > loglevel: - print(msg) # buildifier: disable=print - +# This rule is proof of concept, and may not represent the final +# form of a rule for compliance validation. def _check_license_impl(ctx): # Gather all licenses and write information to one place - _debug(0, "Check license: %s" % ctx.label) - licenses_file = ctx.actions.declare_file("_%s_licenses_info.json" % ctx.label.name) write_licenses_info(ctx, ctx.attr.deps, licenses_file) license_files = [] if ctx.outputs.license_texts: - for dep in ctx.attr.deps: - if LicensesInfo in dep: - for license in dep[LicensesInfo].licenses.to_list(): - license_files.append(license.license_text) + license_files = get_licenses_mapping(ctx.attr.deps).keys() # Now run the checker on it inputs = [licenses_file] @@ -75,11 +65,11 @@ def _check_license_impl(ctx): _check_license = rule( implementation = _check_license_impl, attrs = { - "check_conditions": attr.bool(default = True, mandatory = False), - "copyright_notices": attr.output(mandatory = False), "deps": attr.label_list( aspects = [gather_licenses_info], ), + "check_conditions": attr.bool(default = True, mandatory = False), + "copyright_notices": attr.output(mandatory = False), "license_texts": attr.output(mandatory = False), "report": attr.output(mandatory = True), "_checker": attr.label( @@ -91,11 +81,46 @@ _check_license = rule( }, ) +# TODO(b/152546336): Update the check to take a pointer to a condition list. def check_license(**kwargs): _check_license(**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_licenses_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 _licenses_used_impl(ctx): - """Gather all licenses and make it available as JSON.""" + # Gather all licenses and make it available as JSON write_licenses_info(ctx, ctx.attr.deps, ctx.outputs.out) return [DefaultInfo(files = depset([ctx.outputs.out]))] @@ -105,7 +130,7 @@ _licenses_used = rule( attrs = { "deps": attr.label_list( doc = """List of targets to collect LicenseInfo for.""", - aspects = [gather_licenses_info], + aspects = [gather_licenses_info_and_write], ), "out": attr.output( doc = """Output file.""", @@ -114,6 +139,38 @@ _licenses_used = rule( }, ) +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: + print("Legacy license %s not included, rule needs updating" % lic.license_text) + + return mappings + def licenses_used(name, deps, out = None, **kwargs): """Collects LicensedInfo providers for a set of targets and writes as JSON. diff --git a/rules/default_license.bzl b/rules/default_license.bzl deleted file mode 100644 index 57a7147..0000000 --- a/rules/default_license.bzl +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright 2020 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. - -"""Proof of concept. License restriction.""" - -load( - "@rules_license//rules:providers.bzl", - "LicenseInfo", - "LicensesInfo", -) - -# An experiment to provide license defaults via a rule. This is far from -# working and should not be considered part of the current design. -# - -def _default_licenses_impl(ctx): - licenses = [] - for dep in ctx.attr.deps: - if LicenseInfo in dep: - licenses.append(dep[LicenseInfo]) - return [LicensesInfo(licenses = licenses)] - -_default_licenses = rule( - implementation = _default_licenses_impl, - attrs = { - "conditions": attr.string_list( - doc = "TBD", - ), - "deps": attr.label_list( - mandatory = True, - doc = "Licenses", - providers = [LicenseInfo], - cfg = "exec", - ), - }, -) - -# buildifier: disable=unnamed-macro -def default_licenses(licenses, conditions = None): - _default_licenses( - name = "__default_licenses", - deps = ["%s_license" % license for license in licenses], - conditions = conditions, - ) diff --git a/rules/filtered_rule_kinds.bzl b/rules/filtered_rule_kinds.bzl new file mode 100644 index 0000000..1d6f01c --- /dev/null +++ b/rules/filtered_rule_kinds.bzl @@ -0,0 +1,47 @@ +# 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. + +"""Filtered rule kinds for aspect inspection. +The format of this dictionary is: + + rule_name: [attr, attr, ...] + +Only filters for rules that are part of the Bazel distribution should be added +to this file. Other filters should be added in user_filtered_rule_kinds.bzl + +Attributes are either the explicit list of attributes to filter, or '_*' which +would ignore all attributes prefixed with a _. +""" + +# Rule kinds with attributes the aspect currently needs to ignore +aspect_filters = { + "*": ["linter"], + "_constant_gen": ["_generator"], + "cc_binary": ["_*"], + "cc_embed_data": ["_*"], + "cc_grpc_library": ["_*"], + "cc_library": ["_*"], + "cc_toolchain_alias": ["_cc_toolchain"], + "genrule": ["tools", "exec_tools", "toolchains"], + "genyacc": ["_*"], + "go_binary": ["_*"], + "go_library": ["_*"], + "go_wrap_cc": ["_*"], + "java_binary": ["_*", "plugins", "exported_plugins"], + "java_library": ["plugins", "exported_plugins"], + "java_wrap_cc": ["_cc_toolchain", "swig_top"], + "py_binary": ["_*"], + "py_extension": ["_cc_toolchain"], + "sh_binary": ["_bash_binary"], +} 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), + )] diff --git a/rules/gather_metadata.bzl b/rules/gather_metadata.bzl new file mode 100644 index 0000000..162ea97 --- /dev/null +++ b/rules/gather_metadata.bzl @@ -0,0 +1,309 @@ +# 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", + "TraceInfo", + "gather_metadata_info_common", + "should_traverse", +) +load( + "@rules_license//rules:providers.bzl", + "ExperimentalMetadataInfo", + "PackageInfo", +) +load( + "@rules_license//rules/private:gathering_providers.bzl", + "TransitiveMetadataInfo", +) + +# Definition for compliance namespace, used for filtering licenses +# based on the namespace to which they belong. +NAMESPACES = ["compliance"] + +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, + NAMESPACES, + [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), + )] diff --git a/rules/license.bzl b/rules/license.bzl index f726be1..032599d 100644 --- a/rules/license.bzl +++ b/rules/license.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,54 +11,43 @@ # 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 for declaring the compliance licenses used by a package. -"""Rules for declaring the licenses used by a package.""" +""" load( "@rules_license//rules:providers.bzl", - "LicenseInfo", "LicenseKindInfo", ) +load( + "@rules_license//rules:license_impl.bzl", + "license_rule_impl", +) -# Debugging verbosity -_VERBOSITY = 0 - -def _debug(loglevel, msg): - if _VERBOSITY > loglevel: - print(msg) # buildifier: disable=print - -# -# license() -# - -def _license_impl(ctx): - provider = LicenseInfo( - license_kinds = tuple([k[LicenseKindInfo] for k in ctx.attr.license_kinds]), - copyright_notice = ctx.attr.copyright_notice, - package_name = ctx.attr.package_name, - package_url = ctx.attr.package_url, - package_version = ctx.attr.package_version, - license_text = ctx.file.license_text, - rule = ctx.label, - ) - _debug(0, provider) - return [provider] +# Enable this if your organization requires the license text to be a file +# checked into source control instead of, possibly, another rule. +_require_license_text_is_a_file = False +# This rule must be named "_license" for backwards compatability with older +# or Bazel that checked that name explicitly. See +# https://github.com/bazelbuild/bazel/commit/bbc221f60bc8c9177470529d85c3e47a5d9aaf21 +# TODO(after bazel 7.0 release): Feel free to rename the rule and move. _license = rule( - implementation = _license_impl, + implementation = license_rule_impl, attrs = { - "copyright_notice": attr.string( - doc = "Copyright notice.", - ), "license_kinds": attr.label_list( - mandatory = True, + mandatory = False, doc = "License kind(s) of this license. If multiple license kinds are" + " listed in the LICENSE file, and they all apply, then all" + " should be listed here. If the user can choose a single one" + " of many, then only list one here.", providers = [LicenseKindInfo], + # This should be the null configuration, not the exec. cfg = "exec", ), + "copyright_notice": attr.string( + doc = "Copyright notice.", + ), "license_text": attr.label( allow_single_file = True, default = "LICENSE", @@ -80,51 +69,71 @@ _license = rule( " by an applicatation. It should be a value that" + " increases over time, rather than a commit hash." ), + "namespace": attr.string( + doc = "A human readable name used to organize licenses into categories." + + " This is used in google3 to differentiate third party licenses used" + + " for compliance versus internal licenses used by SLAsan for internal" + + " teams' SLAs.", + ), }, ) # buildifier: disable=function-docstring-args -def license(name, - copyright_notice = None, - license_kinds = None, - license_text = None, - package_name = None, - package_url = None, - package_version = None, - tags = None, - **kwargs): +def license( + name, + license_text = "LICENSE", + license_kind = None, + license_kinds = None, + copyright_notice = None, + package_name = None, + package_url = None, + package_version = None, + namespace = "compliance", + tags = [], + visibility = ["//visibility:public"]): """Wrapper for license rule. + @wraps(_license) + Args: name: str target name. + license_text: str Filename of the license file + license_kind: label a single license_kind. Only one of license_kind or license_kinds may + be specified license_kinds: list(label) list of license_kind targets. - license_kind: label a single license_kind. Only one of license_kind or - license_kinds may be specified copyright_notice: str Copyright notice associated with this package. package_name: str A human readable name identifying this package. This may be used to produce an index of OSS packages used by - an applicatation. - package_url: The URL this instance was downloaded from. - package_version: The version number of this package. This should be a - value that increases over time, rather than a commit - hash. - kwargs: Other things may be specified, but they are explicitly ignored. + an application. + package_url: str The canonical URL this package was downloaded from. + package_version: str The version corresponding the the URL. + namespace: str Undocumened. Internal. + tags: list(str) tags applied to the rule + visibility: list(label) visibility spec. """ - single_kind = kwargs.pop("license_kind", default = None) - if single_kind: + if license_kind: if license_kinds: fail("Can not use both license_kind and license_kinds") - license_kinds = [single_kind] - tags = tags or [] + license_kinds = [license_kind] + + if _require_license_text_is_a_file: + # Make sure the file exists as named in the rule. A glob expression that + # expands to the name of the file is not acceptable. + srcs = native.glob([license_text]) + if len(srcs) != 1 or srcs[0] != license_text: + fail("Specified license file doesn't exist: %s" % license_text) + _license( name = name, license_kinds = license_kinds, - license_text = license_text or "LICENSE", + license_text = license_text, copyright_notice = copyright_notice, package_name = package_name, package_url = package_url, package_version = package_version, + namespace = namespace, applicable_licenses = [], + visibility = visibility, tags = tags, - visibility = ["//visibility:public"], + testonly = 0, ) diff --git a/rules/license_impl.bzl b/rules/license_impl.bzl new file mode 100644 index 0000000..03477c6 --- /dev/null +++ b/rules/license_impl.bzl @@ -0,0 +1,47 @@ +# 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 for declaring the licenses used by a package. + +""" + +load( + "@rules_license//rules:providers.bzl", + "LicenseInfo", + "LicenseKindInfo", +) + +# Debugging verbosity +_VERBOSITY = 0 + +def _debug(loglevel, msg): + if _VERBOSITY > loglevel: + print(msg) # buildifier: disable=print + +# +# license() +# + +def license_rule_impl(ctx): + provider = LicenseInfo( + license_kinds = tuple([k[LicenseKindInfo] for k in ctx.attr.license_kinds]), + copyright_notice = ctx.attr.copyright_notice, + package_name = ctx.attr.package_name or ctx.build_file_path.rstrip("/BUILD"), + package_url = ctx.attr.package_url, + package_version = ctx.attr.package_version, + license_text = ctx.file.license_text, + label = ctx.label, + namespace = ctx.attr.namespace, + ) + _debug(0, provider) + return [provider] diff --git a/rules/license_kind.bzl b/rules/license_kind.bzl index 47b7639..7e6c024 100644 --- a/rules/license_kind.bzl +++ b/rules/license_kind.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,7 +11,6 @@ # 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. - """Proof of concept. License restriction.""" load("@rules_license//rules:providers.bzl", "LicenseKindInfo") @@ -30,6 +29,7 @@ def _license_kind_impl(ctx): ctx.label.package, ctx.label.name, ), + long_name = ctx.attr.long_name, conditions = ctx.attr.conditions, ) return [provider] @@ -37,22 +37,29 @@ def _license_kind_impl(ctx): _license_kind = rule( implementation = _license_kind_impl, attrs = { - "canonical_text": attr.label( - doc = "File containing the canonical text for this license. Must be UTF-8 encoded.", - allow_single_file = True, - ), "conditions": attr.string_list( doc = "Conditions to be met when using software under this license." + " Conditions are defined by the organization using this license.", mandatory = True, ), + "canonical_text": attr.label( + doc = "File containing the canonical text for this license. Must be UTF-8 encoded.", + allow_single_file = True, + ), + "long_name": attr.string(doc = "Human readable long name of license."), "url": attr.string(doc = "URL pointing to canonical license definition"), }, ) def license_kind(name, **kwargs): + """Wrapper for license_kind. + + @wraps(_license_kind) + """ if "conditions" not in kwargs: kwargs["conditions"] = [] + if "long_name" not in kwargs: + kwargs["long_name"] = name _license_kind( name = name, applicable_licenses = [], diff --git a/rules/license_policy.bzl b/rules/license_policy.bzl deleted file mode 100644 index 0539301..0000000 --- a/rules/license_policy.bzl +++ /dev/null @@ -1,53 +0,0 @@ -# Copyright 2020 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. - -"""license_policy rule. - -A license_policy names a set of conditions allowed in the union of all -license_kinds use by a target. The name of the rule is typically an -application type (e.g. production_server, mobile_application, ...) - -""" - -load("@rules_license//rules:license_policy_provider.bzl", "LicensePolicyInfo") - -def _license_policy_impl(ctx): - provider = LicensePolicyInfo( - name = ctx.attr.name, - label = "@%s//%s:%s" % ( - ctx.label.workspace_name, - ctx.label.package, - ctx.label.name, - ), - conditions = ctx.attr.conditions, - ) - return [provider] - -_license_policy = rule( - implementation = _license_policy_impl, - attrs = { - "conditions": attr.string_list( - doc = "Conditions to be met when using software under this license." + - " Conditions are defined by the organization using this license.", - mandatory = True, - ), - }, -) - -def license_policy(name, conditions): - _license_policy( - name = name, - conditions = conditions, - applicable_licenses = [], - ) diff --git a/rules/license_policy_check.bzl b/rules/license_policy_check.bzl deleted file mode 100644 index be46913..0000000 --- a/rules/license_policy_check.bzl +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright 2020 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. - -"""License compliance checking at analysis time.""" - -load( - "@rules_license//rules:gather_licenses_info.bzl", - "gather_licenses_info", -) -load( - "@rules_license//rules:license_policy_provider.bzl", - "LicensePolicyInfo", -) -load( - "@rules_license//rules:providers.bzl", - "LicensesInfo", -) - -def _license_policy_check_impl(ctx): - policy = ctx.attr.policy[LicensePolicyInfo] - allowed_conditions = policy.conditions - if LicensesInfo in ctx.attr.target: - for license in ctx.attr.target[LicensesInfo].licenses.to_list(): - for kind in license.license_kinds: - # print(kind.conditions) - for condition in kind.conditions: - if condition not in allowed_conditions: - fail("Condition %s violates policy %s" % ( - condition, - policy.label, - )) - return [DefaultInfo()] - -_license_policy_check = rule( - implementation = _license_policy_check_impl, - doc = """Internal tmplementation method for license_policy_check().""", - attrs = { - "policy": attr.label( - doc = """Policy definition.""", - mandatory = True, - providers = [LicensePolicyInfo], - ), - "target": attr.label( - doc = """Target to collect LicenseInfo for.""", - aspects = [gather_licenses_info], - mandatory = True, - allow_single_file = True, - ), - }, -) - -def license_policy_check(name, target, policy, **kwargs): - """Checks a target against a policy. - - Args: - name: The target. - target: A target to test for compliance with a policy - policy: A rule providing LicensePolicyInfo. - **kwargs: other args. - - Usage: - - license_policy_check( - name = "license_info", - target = ":my_app", - policy = "//my_org/compliance/policies:mobile_application", - ) - """ - _license_policy_check(name = name, target = target, policy = policy, **kwargs) diff --git a/rules/licenses_core.bzl b/rules/licenses_core.bzl new file mode 100644 index 0000000..9bb37cb --- /dev/null +++ b/rules/licenses_core.bzl @@ -0,0 +1,224 @@ +# 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:filtered_rule_kinds.bzl", "aspect_filters") +load("@rules_license//rules:user_filtered_rule_kinds.bzl", "user_aspect_filters") +load( + "@rules_license//rules:providers.bzl", + "LicenseInfo", +) +load( + "@rules_license//rules/private:gathering_providers.bzl", + "LicensedTargetInfo", + "TransitiveLicensesInfo", +) + + +TraceInfo = provider( + doc = """Provides a target (as a string) to assist in debugging dependency issues.""", + fields = { + "trace": "String: a target to trace dependency edges to.", + }, +) + +def _trace_impl(ctx): + return TraceInfo(trace = ctx.build_setting_value) + +trace = rule( + doc = """Used to allow the specification of a target to trace while collecting license dependencies.""", + implementation = _trace_impl, + build_setting = config.string(flag = True), +) + +def should_traverse(ctx, attr): + """Checks if the dependent attribute should be traversed. + + Args: + ctx: The aspect evaluation context. + attr: The name of the attribute to be checked. + + Returns: + True iff the attribute should be traversed. + """ + k = ctx.rule.kind + + for filters in [aspect_filters, user_aspect_filters]: + always_ignored = filters.get("*", []) + if k in filters: + attr_matches = filters[k] + if (attr in attr_matches or + "*" in attr_matches or + ("_*" in attr_matches and attr.startswith("_")) or + attr in always_ignored): + return False + + for m in attr_matches: + if attr == m: + return False + + return True + +def _get_transitive_metadata(ctx, trans_licenses, trans_other_metadata, trans_package_info, trans_deps, traces, provider, filter_func): + attrs = [a for a in dir(ctx.rule.attr)] + for name in attrs: + if not filter_func(ctx, name): + continue + a = getattr(ctx.rule.attr, name) + + # Make anything singleton into a list for convenience. + if type(a) != type([]): + a = [a] + for dep in a: + # Ignore anything that isn't a target + if type(dep) != "Target": + continue + + # Targets can also include things like input files that won't have the + # aspect, so we additionally check for the aspect rather than assume + # it's on all targets. Even some regular targets may be synthetic and + # not have the aspect. This provides protection against those outlier + # cases. + if provider in dep: + info = dep[provider] + if info.licenses: + trans_licenses.append(info.licenses) + if info.deps: + trans_deps.append(info.deps) + if info.traces: + for trace in info.traces: + traces.append("(" + ", ".join([str(ctx.label), ctx.rule.kind, name]) + ") -> " + trace) + + # We only need one or the other of these stanzas. + # If we use a polymorphic approach to metadata providers, then + # this works. + if hasattr(info, "other_metadata"): + if info.other_metadata: + trans_other_metadata.append(info.other_metadata) + # But if we want more precise type safety, we would have a + # trans_* for each type of metadata. That is not user + # extensibile. + if hasattr(info, "package_info"): + if info.package_info: + trans_package_info.append(info.package_info) + +def gather_metadata_info_common(target, ctx, provider_factory, namespaces, metadata_providers, filter_func): + """Collect license and other metadata info from myself and my deps. + + Any single target might directly depend on a license, or depend on + something that transitively depends on a license, or neither. + This aspect bundles all those into a single provider. At each level, we add + in new direct license deps found and forward up the transitive information + collected so far. + + This is a common abstraction for crawling the dependency graph. It is parameterized + to allow specifying the provider that is populated with results. It is + configurable to select only licenses matching a certain namespace. It is also + configurable to specify which dependency edges should not be traced for the + purpose of tracing the graph. + + Args: + target: The target of the aspect. + ctx: The aspect evaluation context. + provider_factory: abstracts the provider returned by this aspect + namespaces: a list of namespaces licenses must match to be included + metadata_providers: a list of other providers of interest + filter_func: a function that returns true iff the dep edge should be ignored + + Returns: + provider of parameterized type + """ + + # First we gather my direct license attachments + licenses = [] + other_metadata = [] + package_info = [] + if ctx.rule.kind == "_license": + # Don't try to gather licenses from the license rule itself. We'll just + # blunder into the text file of the license and pick up the default + # attribute of the package, which we don't want. + pass + else: + if hasattr(ctx.rule.attr, "applicable_licenses"): + for dep in ctx.rule.attr.applicable_licenses: + if LicenseInfo in dep: + lic = dep[LicenseInfo] + + # This check shouldn't be necessary since any license created + # by the official code will have this set. However, one of the + # tests has its own implementation of license that had to be fixed + # so this is just a conservative safety check. + if hasattr(lic, "namespace"): + if lic.namespace in namespaces: + licenses.append(lic) + else: + fail("should have a namespace") + for m_p in metadata_providers: + if m_p in dep: + other_metadata.append(dep[m_p]) + + # Now gather transitive collection of providers from the targets + # this target depends upon. + trans_licenses = [] + trans_other_metadata = [] + trans_package_info = [] + trans_deps = [] + traces = [] + _get_transitive_metadata(ctx, trans_licenses, trans_other_metadata, trans_package_info, trans_deps, traces, provider_factory, filter_func) + + if not licenses and not trans_licenses: + return [provider_factory(deps = depset(), licenses = depset(), traces = [])] + + # If this is the target, start the sequence of traces. + if ctx.attr._trace[TraceInfo].trace and ctx.attr._trace[TraceInfo].trace in str(ctx.label): + traces = [ctx.attr._trace[TraceInfo].trace] + + # Trim the number of traces accumulated since the output can be quite large. + # A few representative traces are generally sufficient to identify why a dependency + # is incorrectly incorporated. + if len(traces) > 10: + traces = traces[0:10] + + if licenses: + # At this point we have a target and a list of directly used licenses. + # Bundle those together so we can report the exact targets that cause the + # dependency on each license. Since a list cannot be stored in a + # depset, even inside a provider, the list is concatenated into a + # string and will be unconcatenated in the output phase. + direct_license_uses = [LicensedTargetInfo( + target_under_license = target.label, + licenses = ",".join([str(x.label) for x in licenses]), + )] + else: + direct_license_uses = None + + # This is a bit of a hack for bazel 5.x. We can not pass extra fields to + # the provider constructor, so we need to do something special for each. + # In Bazel 6.x we can use a provider initializer function that would take + # all the args and only use the ones it wants. + if provider_factory == TransitiveLicensesInfo: + return [provider_factory( + target_under_license = target.label, + licenses = depset(tuple(licenses), transitive = trans_licenses), + deps = depset(direct = direct_license_uses, transitive = trans_deps), + traces = traces, + )] + + return [provider_factory( + target_under_license = target.label, + licenses = depset(tuple(licenses), transitive = trans_licenses), + other_metadata = depset(tuple(other_metadata), transitive = trans_other_metadata), + deps = depset(direct = direct_license_uses, transitive = trans_deps), + traces = traces, + )] diff --git a/rules/package_info.bzl b/rules/package_info.bzl new file mode 100644 index 0000000..c79545f --- /dev/null +++ b/rules/package_info.bzl @@ -0,0 +1,104 @@ +# 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 for declaring metadata about a package.""" + +load( + "@rules_license//rules:providers.bzl", + "ExperimentalMetadataInfo", + "PackageInfo", +) + +# +# package_info() +# + +def _package_info_impl(ctx): + provider = PackageInfo( + # Metadata providers must include a type discriminator. We don't need it + # to collect the providers, but we do need it to write the JSON. We + # key on the type field to look up the correct block of code to pull + # data out and format it. We can't to the lookup on the provider class. + type = "package_info", + label = ctx.label, + package_name = ctx.attr.package_name or ctx.build_file_path.rstrip("/BUILD"), + package_url = ctx.attr.package_url, + package_version = ctx.attr.package_version, + ) + # Experimental alternate design, using a generic 'data' back to hold things + generic_provider = ExperimentalMetadataInfo( + type = "package_info_alt", + label = ctx.label, + data = { + "package_name": ctx.attr.package_name or ctx.build_file_path.rstrip("/BUILD"), + "package_url": ctx.attr.package_url, + "package_version": ctx.attr.package_version + } + ) + return [provider, generic_provider] + +_package_info = rule( + implementation = _package_info_impl, + attrs = { + "package_name": attr.string( + doc = "A human readable name identifying this package." + + " This may be used to produce an index of OSS packages used by" + + " an applicatation.", + ), + "package_url": attr.string( + doc = "The URL this instance of the package was download from." + + " This may be used to produce an index of OSS packages used by" + + " an applicatation.", + ), + "package_version": attr.string( + doc = "A human readable version string identifying this package." + + " This may be used to produce an index of OSS packages used" + + " by an applicatation. It should be a value that" + + " increases over time, rather than a commit hash." + ), + }, +) + +# buildifier: disable=function-docstring-args +def package_info( + name, + package_name = None, + package_url = None, + package_version = None, + **kwargs): + """Wrapper for package_info rule. + + @wraps(_package_info) + + Args: + name: str target name. + package_name: str A human readable name identifying this package. This + may be used to produce an index of OSS packages used by + an application. + package_url: str The canoncial URL this package distribution was retrieved from. + Note that, because of local mirroring, that might not be the + physical URL it was retrieved from. + package_version: str A human readable name identifying version of this package. + kwargs: other args. Most are ignored. + """ + visibility = kwargs.get("visibility") or ["//visibility:public"] + _package_info( + name = name, + package_name = package_name, + package_url = package_url, + package_version = package_version, + applicable_licenses = [], + visibility = visibility, + tags = [], + testonly = 0, + ) diff --git a/rules/private/BUILD b/rules/private/BUILD new file mode 100644 index 0000000..452e623 --- /dev/null +++ b/rules/private/BUILD @@ -0,0 +1,35 @@ +# 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. +# 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 for making license declarations.""" + +package( + default_applicable_licenses = ["//:license"], + default_visibility = ["//visibility:public"], +) + +licenses(["notice"]) + +filegroup( + name = "standard_package", + srcs = glob(["**"]), +) + +# Do not create a bzl_library(). That would create a dependency loop back +# to bazel-skylib. We export the .bzl files to the documentation maker. +exports_files( + glob([ + "*.bzl", + ]), + visibility = ["//doc_build:__pkg__"], +) diff --git a/rules/private/gathering_providers.bzl b/rules/private/gathering_providers.bzl new file mode 100644 index 0000000..1c3740f --- /dev/null +++ b/rules/private/gathering_providers.bzl @@ -0,0 +1,54 @@ +# 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. +"""Providers for transitively gathering all license and package_info targets. + +Warning: This is private to the aspect that walks the tree. The API is subject +to change at any release. +""" + +LicensedTargetInfo = provider( + doc = """Lists the licenses directly used by a single target.""", + fields = { + "target_under_license": "Label: The target label", + "licenses": "list(label of a license rule)", + }, +) + +def licenses_info(): + return provider( + doc = """The transitive set of licenses used by a target.""", + fields = { + "target_under_license": "Label: The top level target label.", + "deps": "depset(LicensedTargetInfo): The transitive list of dependencies that have licenses.", + "licenses": "depset(LicenseInfo)", + "traces": "list(string) - diagnostic for tracing a dependency relationship to a target.", + }, + ) + +# This provider is used by the aspect that is used by manifest() rules. +TransitiveLicensesInfo = licenses_info() + +TransitiveMetadataInfo = provider( + doc = """The transitive set of licenses used by a target.""", + fields = { + "top_level_target": "Label: The top level target label we are examining.", + "other_metadata": "depset(ExperimentalMetatdataInfo)", + "licenses": "depset(LicenseInfo)", + "package_info": "depset(PackageInfo)", + + "target_under_license": "Label: A target which will be associated with some licenses.", + "deps": "depset(LicensedTargetInfo): The transitive list of dependencies that have licenses.", + "traces": "list(string) - diagnostic for tracing a dependency relationship to a target.", + }, +) diff --git a/rules/providers.bzl b/rules/providers.bzl index 9e830ce..33a7fb5 100644 --- a/rules/providers.bzl +++ b/rules/providers.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,34 +11,57 @@ # 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. +"""Basic providers for license rules. -"""Providers for license rules.""" +This file should only contain the basic providers needed to create +license and package_info declarations. Providers needed to gather +them are declared in other places. +""" LicenseKindInfo = provider( - doc = """Provides information about a license kind.""", + doc = """Provides information about a license_kind instance.""", fields = { - "conditions": "List of conditions to be met when using this software.", - "label": "The full path to the license kind definition.", - "name": "License Name", + "conditions": "list(string): List of conditions to be met when using this packages under this license.", + "label": "Label: The full path to the license kind definition.", + "long_name": "string: Human readable license name", + "name": "string: Canonical license name", }, ) LicenseInfo = provider( - doc = """Provides information about an instance of a license.""", + doc = """Provides information about a license instance.""", fields = { - "copyright_notice": "Human readable short copyright notice", - "license_kinds": "License kinds", - "license_text": "License file", - "package_name": "Human readable package name", + "copyright_notice": "string: Human readable short copyright notice", + "label": "Label: label of the license rule", + "license_kinds": "list(LicenseKindInfo): License kinds ", + "license_text": "string: The license file path", + "namespace": "string: namespace of the license rule", + # TODO(aiuto): move to PackageInfo + "package_name": "string: Human readable package name", "package_url": "URL from which this package was downloaded.", "package_version": "Human readable version string", - "rule": "From whence this came", }, ) -LicensesInfo = provider( - doc = """The set of license instances used in a target.""", +PackageInfo = provider( + doc = """Provides information about a package.""", fields = { - "licenses": "list(LicenseInfo).", + "type": "string: How to interpret data", + "label": "Label: label of the package_info rule", + "package_name": "string: Human readable package name", + "package_url": "string: URL from which this package was downloaded.", + "package_version": "string: Human readable version string", }, ) + +# This is more extensible. Because of the provider implementation, having a big +# dict of values rather than named fields is not much more costly. +# Design choice. Replace data with actual providers, such as PackageInfo +ExperimentalMetadataInfo = provider( + doc = """Generic bag of metadata.""", + fields = { + "type": "string: How to interpret data", + "label": "Label: label of the metadata rule", + "data": "String->any: Map of names to values", + } +) diff --git a/rules/sbom.bzl b/rules/sbom.bzl new file mode 100644 index 0000000..73c1861 --- /dev/null +++ b/rules/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: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/license_policy_provider.bzl b/rules/user_filtered_rule_kinds.bzl index caecce8..a099794 100644 --- a/rules/license_policy_provider.bzl +++ b/rules/user_filtered_rule_kinds.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,14 +11,18 @@ # 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. +"""Filtered rule kinds for aspect inspection. -"""LicensePolicyProvider.""" +The format of this dictionary is: + rule_name: [attr, attr, ...] -LicensePolicyInfo = provider( - doc = """Declares a policy name and the license conditions allowable under it.""", - fields = { - "conditions": "List of conditions to be met when using this software.", - "label": "The full path to the license policy definition.", - "name": "License policy name", - }, -) +Filters for rules that are not part of the Bazel distribution should be added +to this file. + +Attributes are either the explicit list of attributes to filter, or '_*' which +would ignore all attributes prefixed with a _. +""" + +# Rule kinds with attributes the aspect currently needs to ignore +user_aspect_filters = { +} |