aboutsummaryrefslogtreecommitdiff
path: root/rules
diff options
context:
space:
mode:
Diffstat (limited to 'rules')
-rw-r--r--rules/BUILD22
-rw-r--r--rules/check_licenses_shim.bzl30
-rw-r--r--rules/compliance.bzl101
-rw-r--r--rules/default_license.bzl55
-rw-r--r--rules/filtered_rule_kinds.bzl47
-rw-r--r--rules/gather_licenses_info.bzl270
-rw-r--r--rules/gather_metadata.bzl309
-rw-r--r--rules/license.bzl117
-rw-r--r--rules/license_impl.bzl47
-rw-r--r--rules/license_kind.bzl19
-rw-r--r--rules/license_policy.bzl53
-rw-r--r--rules/license_policy_check.bzl80
-rw-r--r--rules/licenses_core.bzl224
-rw-r--r--rules/package_info.bzl104
-rw-r--r--rules/private/BUILD35
-rw-r--r--rules/private/gathering_providers.bzl54
-rw-r--r--rules/providers.bzl53
-rw-r--r--rules/sbom.bzl136
-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 = {
+}