Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move rules/sbom.bzl to rules_gathering/generate_sbom.bzl #97

Merged
merged 4 commits into from
Jun 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/sboms/BUILD
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Demonstrate the generate_sbom rule

load("@rules_license//rules:sbom.bzl", "generate_sbom")
load("@rules_license//rules_gathering:generate_sbom.bzl", "generate_sbom")

# There are not a lot of targets in this rule set to build a SBOM from
# so we will (in a very self-referential way) generate one for the tool
Expand Down
297 changes: 9 additions & 288 deletions rules/gather_metadata.bzl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2022 Google LLC
# Copyright 2023 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -11,294 +11,15 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Rules and macros for collecting LicenseInfo providers."""
"""Forwarder for gather_metadata_info.

To be deleted before version 0.1.0
"""
load(
"@rules_license//rules:licenses_core.bzl",
"gather_metadata_info_common",
"should_traverse",
"@rules_license//rules_gathering:gather_metadata.bzl",
_gather_metadata_info = "gather_metadata_info",
_gather_metadata_info_and_write = "gather_metadata_info_and_write",
)
load(
"@rules_license//rules:providers.bzl",
"ExperimentalMetadataInfo",
"PackageInfo",
)
load(
"@rules_license//rules/private:gathering_providers.bzl",
"TransitiveMetadataInfo",
)
load("@rules_license//rules_gathering:trace.bzl", "TraceInfo")

def _strip_null_repo(label):
"""Removes the null repo name (e.g. @//) from a string.

The is to make str(label) compatible between bazel 5.x and 6.x
"""
s = str(label)
if s.startswith('@//'):
return s[1:]
elif s.startswith('@@//'):
return s[2:]
return s

def _bazel_package(label):
clean_label = _strip_null_repo(label)
return clean_label[0:-(len(label.name) + 1)]

def _gather_metadata_info_impl(target, ctx):
return gather_metadata_info_common(
target,
ctx,
TransitiveMetadataInfo,
[ExperimentalMetadataInfo, PackageInfo],
should_traverse)

gather_metadata_info = aspect(
doc = """Collects LicenseInfo providers into a single TransitiveMetadataInfo provider.""",
implementation = _gather_metadata_info_impl,
attr_aspects = ["*"],
attrs = {
"_trace": attr.label(default = "@rules_license//rules:trace_target"),
},
provides = [TransitiveMetadataInfo],
apply_to_generating_rules = True,
)

def _write_metadata_info_impl(target, ctx):
"""Write transitive license info into a JSON file

Args:
target: The target of the aspect.
ctx: The aspect evaluation context.

Returns:
OutputGroupInfo
"""

if not TransitiveMetadataInfo in target:
return [OutputGroupInfo(licenses = depset())]
info = target[TransitiveMetadataInfo]
outs = []

# If the result doesn't contain licenses, we simply return the provider
if not hasattr(info, "target_under_license"):
return [OutputGroupInfo(licenses = depset())]

# Write the output file for the target
name = "%s_metadata_info.json" % ctx.label.name
content = "[\n%s\n]\n" % ",\n".join(metadata_info_to_json(info))
out = ctx.actions.declare_file(name)
ctx.actions.write(
output = out,
content = content,
)
outs.append(out)

if ctx.attr._trace[TraceInfo].trace:
trace = ctx.actions.declare_file("%s_trace_info.json" % ctx.label.name)
ctx.actions.write(output = trace, content = "\n".join(info.traces))
outs.append(trace)

return [OutputGroupInfo(licenses = depset(outs))]

gather_metadata_info_and_write = aspect(
doc = """Collects TransitiveMetadataInfo providers and writes JSON representation to a file.

Usage:
bazel build //some:target \
--aspects=@rules_license//rules:gather_metadata_info.bzl%gather_metadata_info_and_write
--output_groups=licenses
""",
implementation = _write_metadata_info_impl,
attr_aspects = ["*"],
attrs = {
"_trace": attr.label(default = "@rules_license//rules:trace_target"),
},
provides = [OutputGroupInfo],
requires = [gather_metadata_info],
apply_to_generating_rules = True,
)

def write_metadata_info(ctx, deps, json_out):
"""Writes TransitiveMetadataInfo providers for a set of targets as JSON.

TODO(aiuto): Document JSON schema. But it is under development, so the current
best place to look is at tests/hello_licenses.golden.

Usage:
write_metadata_info must be called from a rule implementation, where the
rule has run the gather_metadata_info aspect on its deps to
collect the transitive closure of LicenseInfo providers into a
LicenseInfo provider.

foo = rule(
implementation = _foo_impl,
attrs = {
"deps": attr.label_list(aspects = [gather_metadata_info])
}
)

def _foo_impl(ctx):
...
out = ctx.actions.declare_file("%s_licenses.json" % ctx.label.name)
write_metadata_info(ctx, ctx.attr.deps, metadata_file)

Args:
ctx: context of the caller
deps: a list of deps which should have TransitiveMetadataInfo providers.
This requires that you have run the gather_metadata_info
aspect over them
json_out: output handle to write the JSON info
"""
licenses = []
for dep in deps:
if TransitiveMetadataInfo in dep:
licenses.extend(metadata_info_to_json(dep[TransitiveMetadataInfo]))
ctx.actions.write(
output = json_out,
content = "[\n%s\n]\n" % ",\n".join(licenses),
)

def metadata_info_to_json(metadata_info):
"""Render a single LicenseInfo provider to JSON

Args:
metadata_info: A LicenseInfo.

Returns:
[(str)] list of LicenseInfo values rendered as JSON.
"""

main_template = """ {{
"top_level_target": "{top_level_target}",
"dependencies": [{dependencies}
],
"licenses": [{licenses}
],
"packages": [{packages}
]\n }}"""

dep_template = """
{{
"target_under_license": "{target_under_license}",
"licenses": [
{licenses}
]
}}"""

license_template = """
{{
"label": "{label}",
"bazel_package": "{bazel_package}",
"license_kinds": [{kinds}
],
"copyright_notice": "{copyright_notice}",
"package_name": "{package_name}",
"package_url": "{package_url}",
"package_version": "{package_version}",
"license_text": "{license_text}",
"used_by": [
{used_by}
]
}}"""

kind_template = """
{{
"target": "{kind_path}",
"name": "{kind_name}",
"conditions": {kind_conditions}
}}"""

package_info_template = """
{{
"target": "{label}",
"bazel_package": "{bazel_package}",
"package_name": "{package_name}",
"package_url": "{package_url}",
"package_version": "{package_version}"
}}"""

# Build reverse map of license to user
used_by = {}
for dep in metadata_info.deps.to_list():
# Undo the concatenation applied when stored in the provider.
dep_licenses = dep.licenses.split(",")
for license in dep_licenses:
if license not in used_by:
used_by[license] = []
used_by[license].append(_strip_null_repo(dep.target_under_license))

all_licenses = []
for license in sorted(metadata_info.licenses.to_list(), key = lambda x: x.label):
kinds = []
for kind in sorted(license.license_kinds, key = lambda x: x.name):
kinds.append(kind_template.format(
kind_name = kind.name,
kind_path = kind.label,
kind_conditions = kind.conditions,
))

if license.license_text:
# Special handling for synthetic LicenseInfo
text_path = (license.license_text.package + "/" + license.license_text.name if type(license.license_text) == "Label" else license.license_text.path)
all_licenses.append(license_template.format(
copyright_notice = license.copyright_notice,
kinds = ",".join(kinds),
license_text = text_path,
package_name = license.package_name,
package_url = license.package_url,
package_version = license.package_version,
label = _strip_null_repo(license.label),
bazel_package = _bazel_package(license.label),
used_by = ",\n ".join(sorted(['"%s"' % x for x in used_by[str(license.label)]])),
))

all_deps = []
for dep in sorted(metadata_info.deps.to_list(), key = lambda x: x.target_under_license):
# Undo the concatenation applied when stored in the provider.
dep_licenses = dep.licenses.split(",")
all_deps.append(dep_template.format(
target_under_license = _strip_null_repo(dep.target_under_license),
licenses = ",\n ".join(sorted(['"%s"' % _strip_null_repo(x) for x in dep_licenses])),
))

all_packages = []
# We would use this if we had distinct depsets for every provider type.
#for package in sorted(metadata_info.package_info.to_list(), key = lambda x: x.label):
# all_packages.append(package_info_template.format(
# label = _strip_null_repo(package.label),
# package_name = package.package_name,
# package_url = package.package_url,
# package_version = package.package_version,
# ))

for mi in sorted(metadata_info.other_metadata.to_list(), key = lambda x: x.label):
# Maybe use a map of provider class to formatter. A generic dict->json function
# in starlark would help

# This format is for using distinct providers. I like the compile time safety.
if mi.type == "package_info":
all_packages.append(package_info_template.format(
label = _strip_null_repo(mi.label),
bazel_package = _bazel_package(mi.label),
package_name = mi.package_name,
package_url = mi.package_url,
package_version = mi.package_version,
))
# experimental: Support the ExperimentalMetadataInfo bag of data
if mi.type == "package_info_alt":
all_packages.append(package_info_template.format(
label = _strip_null_repo(mi.label),
bazel_package = _bazel_package(mi.label),
# data is just a bag, so we need to use get() or ""
package_name = mi.data.get("package_name") or "",
package_url = mi.data.get("package_url") or "",
package_version = mi.data.get("package_version") or "",
))

return [main_template.format(
top_level_target = _strip_null_repo(metadata_info.target_under_license),
dependencies = ",".join(all_deps),
licenses = ",".join(all_licenses),
packages = ",".join(all_packages),
)]
gather_metadata_info = _gather_metadata_info
gather_metadata_info_and_write = _gather_metadata_info_and_write
Loading