blob: cc1dc7268eefba1e7159ad2105ede5512e792cd8 [file] [log] [blame]
# Copyright 2023 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
load("//bazel/portage/build_defs:common.bzl", "BinaryPackageInfo", "SDKInfo")
HashTracerInfo = provider(
"""
Because transitive validations created by aspects are not run by bazel, we
need to attach them all to a real target. This provider contains the files
that will be included in the top-level target's `_validation` output group.
""",
fields = {
"files": """
Depset[File]: The validation output files.
""",
},
)
def _generate_cat_action(ctx, files):
output = ctx.actions.declare_file(ctx.rule.attr.name + ".hash.cat")
args = ctx.actions.args()
args.add(output)
args.add_all(files)
ctx.actions.run_shell(
outputs = [output],
inputs = files,
arguments = [args],
mnemonic = "HashTracer",
execution_requirements = {
# We don't cache this because we want to increase the chance of
# the hash being printed.
"no-cache": "",
},
command = """set -eu -o pipefail
out="$1"
shift
cat "$@"
touch "${out}"
""",
)
return output
def _generate_hash_action(ctx, files):
output = ctx.actions.declare_file(ctx.rule.attr.name + ".hash")
args = ctx.actions.args()
args.add(output)
args.add_all(files, expand_directories = False)
ctx.actions.run_shell(
outputs = [output],
inputs = files,
arguments = [args],
mnemonic = "HashTracer",
execution_requirements = {
# Disable the sandbox to avoid creating a symlink forest.
# The symlink forest will mess up the `find` command.
"no-sandbox": "",
},
command = """set -eu -o pipefail
out="$1"
shift
for FILE in "$@"; do
if [[ -d "${FILE}" ]]; then
pushd "${FILE}" >/dev/null
HASH="$(find . -type f -print0 | sort -z | xargs -0 sha256sum | sha256sum | cut -f1 -d ' ')"
popd >/dev/null
else
HASH="$(sha256sum "${FILE}" | cut -f1 -d ' ')"
fi
SIZE="$(du -bs "${FILE}" | cut -f1 -d$'\t')"
echo "* Hash Tracer: ${FILE} -> ${HASH} (${SIZE} bytes)" > "$out"
done
""",
)
# We split the hashing and printing of the hash into two actions. This
# allows the hash calculation to be cached and avoids putting extra strain
# on the RBE CAS.
return _generate_cat_action(ctx, [output])
def _processes_rule(rule):
"""
Iterates through all the attributes of a rule and extracts all of its
dependencies' `HashTracerInfo`.
"""
depsets = []
def _add(val):
if type(val) in [
"bool",
"int",
"builtin_function_or_method",
"string",
"NoneType",
"Label",
"License",
]:
return
elif type(val) == "Target":
if HashTracerInfo in val:
depsets.append(val[HashTracerInfo].files)
else:
fail("Unknown type: %s" % (type(val)))
attrs = dir(rule.attr)
for key in attrs:
val = getattr(rule.attr, key)
# We can't use recursion, so special case these.
if type(val) == "list":
for item in val:
_add(item)
elif type(val) == "dict":
# label_keyed_string_dict has the labels as keys
for key in val.keys():
_add(key)
else:
_add(val)
return depsets
def _hash_tracer_impl(target, ctx):
direct_outputs = []
transitive_outputs = []
# We consider these terminal nodes since we just want to print out the
# output hash.
if ctx.rule.kind in [
"pkg_tar_impl",
"go_binary",
"rust_binary",
"cc_binary",
"cc_library",
"py_library",
]:
direct_outputs.append(_generate_hash_action(ctx, target.files))
elif ctx.rule.kind in ["build_sdk", "sdk_update", "sdk_install_deps", "sdk_from_archive"]:
layers = target[SDKInfo].layers
# We only want to hash the layer that the rule created.
# TODO: Refactor rules to return only newly created layers in their
# DefaultInfo provider.
last_layer = layers[-1]
direct_outputs.append(_generate_hash_action(ctx, [last_layer]))
transitive_outputs.extend(_processes_rule(ctx.rule))
elif ctx.rule.kind in ["ebuild"]:
files = [target[BinaryPackageInfo].partial]
direct_outputs.append(_generate_hash_action(ctx, files))
transitive_outputs.extend(_processes_rule(ctx.rule))
else:
# For all the intermediary nodes we just propagate the dependencies.
transitive_outputs.extend(_processes_rule(ctx.rule))
return [HashTracerInfo(files = depset(direct_outputs, transitive = transitive_outputs))]
hash_tracer = aspect(
implementation = _hash_tracer_impl,
doc = "Prints out the sha256 of all dependent tar, go_binary, and rust_binary targets, etc.",
attr_aspects = ["*"],
)
def _hash_tracer_validator_impl(target, ctx):
validations = []
depsets = []
if HashTracerInfo in target:
files = target[HashTracerInfo].files
return [
OutputGroupInfo(_validation = files),
]
else:
return []
hash_tracer_validator = aspect(
implementation = _hash_tracer_validator_impl,
doc = """
Attaches the HashTracerInfo to the top-level build targets as an validation
action.
This is necessary because aspects can only attach a validator to a top level
action. See https://github.com/bazelbuild/bazel/issues/19636.
""",
requires = [hash_tracer],
)