mirror of
https://github.com/bazel-contrib/bazel-lib
synced 2024-12-01 07:15:24 +00:00
a4e997de1f
* feat: add `propagate_common_{,test_,binary_}rule_attributes` to lib/utils. * lib/private/utils: PR feedback: extract COMMON_BINARY_RULE_ATTRIBUTES and COMMON_TEST_RULE_ATTRIBUTES to global constants. * lib/private/utils: PR feedback: remove default value for attrs param to propagate_common_*rule_attributes * fix: lib/private/utils: buildifier.
357 lines
12 KiB
Python
357 lines
12 KiB
Python
"""General utility functions"""
|
|
|
|
load("@bazel_tools//tools/build_defs/repo:http.bzl", _http_archive = "http_archive")
|
|
load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe")
|
|
|
|
def _propagate_well_known_tags(tags = []):
|
|
"""Returns a list of tags filtered from the input set that only contains the ones that are considered "well known"
|
|
|
|
These are listed in Bazel's documentation:
|
|
https://docs.bazel.build/versions/main/test-encyclopedia.html#tag-conventions
|
|
https://docs.bazel.build/versions/main/be/common-definitions.html#common-attributes
|
|
|
|
Args:
|
|
tags: List of tags to filter
|
|
|
|
Returns:
|
|
List of tags that only contains the well known set
|
|
"""
|
|
|
|
WELL_KNOWN_TAGS = [
|
|
"no-sandbox",
|
|
"no-cache",
|
|
"no-remote-cache",
|
|
"no-remote-exec",
|
|
"no-remote",
|
|
"local",
|
|
"requires-network",
|
|
"block-network",
|
|
"requires-fakeroot",
|
|
"exclusive",
|
|
"manual",
|
|
"external",
|
|
]
|
|
|
|
# cpu:n tags allow setting the requested number of CPUs for a test target.
|
|
# More info at https://docs.bazel.build/versions/main/test-encyclopedia.html#other-resources
|
|
CPU_PREFIX = "cpu:"
|
|
|
|
return [
|
|
tag
|
|
for tag in tags
|
|
if tag in WELL_KNOWN_TAGS or tag.startswith(CPU_PREFIX)
|
|
]
|
|
|
|
def _to_label(param):
|
|
"""Converts a string to a Label. If Label is supplied, the same label is returned.
|
|
|
|
Args:
|
|
param: a string representing a label or a Label
|
|
|
|
Returns:
|
|
a Label
|
|
"""
|
|
root_repo = "@@" if _is_bazel_6_or_greater() else "@"
|
|
param_type = type(param)
|
|
if param_type == "string":
|
|
if param.startswith("@"):
|
|
return Label(param)
|
|
if param.startswith("//"):
|
|
return Label("{}{}".format(root_repo, param))
|
|
if param.startswith(":"):
|
|
param = param[1:]
|
|
return Label("{}//{}:{}".format(root_repo, native.package_name(), param))
|
|
elif param_type == "Label":
|
|
return param
|
|
else:
|
|
msg = "Expected 'string' or 'Label' but got '{}'".format(param_type)
|
|
fail(msg)
|
|
|
|
def _consistent_label_str(ctx, label):
|
|
"""Generate a consistent label string for all Bazel versions.
|
|
|
|
Starting in Bazel 6, the workspace name is empty for the local workspace and there's no other
|
|
way to determine it. This behavior differs from Bazel 5 where the local workspace name was fully
|
|
qualified in str(label).
|
|
|
|
This utility function is meant for use in rules and requires the rule context to determine the
|
|
user's workspace name (`ctx.workspace_name`).
|
|
|
|
Args:
|
|
ctx: The rule context.
|
|
label: A Label.
|
|
|
|
Returns:
|
|
String representation of the label including the repository name if the label is from an
|
|
external repository. For labels in the user's repository the label will start with `@//`.
|
|
"""
|
|
return "@{}//{}:{}".format(
|
|
"" if label.workspace_name == ctx.workspace_name else label.workspace_name,
|
|
label.package,
|
|
label.name,
|
|
)
|
|
|
|
def _is_external_label(param):
|
|
"""Returns True if the given Label (or stringy version of a label) represents a target outside of the workspace
|
|
|
|
Args:
|
|
param: a string or label
|
|
|
|
Returns:
|
|
a bool
|
|
"""
|
|
if not _is_bazel_6_or_greater() and str(param).startswith("@@//"):
|
|
# Work-around for https://github.com/bazelbuild/bazel/issues/16528
|
|
return False
|
|
return len(_to_label(param).workspace_root) > 0
|
|
|
|
# Path to the root of the workspace
|
|
def _path_to_workspace_root():
|
|
""" Returns the path to the workspace root under bazel
|
|
|
|
Returns:
|
|
Path to the workspace root
|
|
"""
|
|
return "/".join([".."] * len(native.package_name().split("/")))
|
|
|
|
# Like glob() but returns directories only
|
|
def _glob_directories(include, **kwargs):
|
|
all = native.glob(include, exclude_directories = 0, **kwargs)
|
|
files = native.glob(include, **kwargs)
|
|
directories = [p for p in all if p not in files]
|
|
return directories
|
|
|
|
def _file_exists(path):
|
|
"""Check whether a file exists.
|
|
|
|
Useful in macros to set defaults for a configuration file if it is present.
|
|
This can only be called during the loading phase, not from a rule implementation.
|
|
|
|
Args:
|
|
path: a label, or a string which is a path relative to this package
|
|
"""
|
|
label = _to_label(path)
|
|
file_abs = "%s/%s" % (label.package, label.name)
|
|
file_rel = file_abs[len(native.package_name()) + 1:]
|
|
file_glob = native.glob([file_rel], exclude_directories = 1, allow_empty = True)
|
|
return len(file_glob) > 0
|
|
|
|
def _default_timeout(size, timeout):
|
|
"""Provide a sane default for *_test timeout attribute.
|
|
|
|
The [test-encyclopedia](https://bazel.build/reference/test-encyclopedia) says:
|
|
|
|
> Tests may return arbitrarily fast regardless of timeout.
|
|
> A test is not penalized for an overgenerous timeout, although a warning may be issued:
|
|
> you should generally set your timeout as tight as you can without incurring any flakiness.
|
|
|
|
However Bazel's default for timeout is medium, which is dumb given this guidance.
|
|
|
|
It also says:
|
|
|
|
> Tests which do not explicitly specify a timeout have one implied based on the test's size as follows
|
|
|
|
Therefore if size is specified, we should allow timeout to take its implied default.
|
|
If neither is set, then we can fix Bazel's wrong default here to avoid warnings under
|
|
`--test_verbose_timeout_warnings`.
|
|
|
|
This function can be used in a macro which wraps a testing rule.
|
|
|
|
Args:
|
|
size: the size attribute of a test target
|
|
timeout: the timeout attribute of a test target
|
|
|
|
Returns:
|
|
"short" if neither is set, otherwise timeout
|
|
"""
|
|
|
|
if size == None and timeout == None:
|
|
return "short"
|
|
|
|
return timeout
|
|
|
|
def _is_bazel_6_or_greater():
|
|
"""Detects if the Bazel version being used is greater than or equal to 6 (including Bazel 6 pre-releases and RCs).
|
|
|
|
Detecting Bazel 6 or greater is particularly useful in rules as slightly different code paths may be needed to
|
|
support bzlmod which was added in Bazel 6.
|
|
|
|
Unlike the undocumented `native.bazel_version`, which only works in WORKSPACE and repository rules, this function can
|
|
be used in rules and BUILD files.
|
|
|
|
An alternate approach to make the Bazel version available in BUILD files and rules would be to
|
|
use the [host_repo](https://github.com/aspect-build/bazel-lib/blob/main/docs/host_repo.md) repository rule
|
|
which contains the bazel_version in the exported `host` struct:
|
|
|
|
WORKSPACE:
|
|
```
|
|
load("@aspect_bazel_lib//lib:host_repo.bzl", "host_repo")
|
|
host_repo(name = "aspect_bazel_lib_host")
|
|
```
|
|
|
|
BUILD.bazel:
|
|
```
|
|
load("@aspect_bazel_lib_host//:defs.bzl", "host")
|
|
print(host.bazel_version)
|
|
```
|
|
|
|
That approach, however, incurs a cost in the user's WORKSPACE.
|
|
|
|
Returns:
|
|
True if the Bazel version being used is greater than or equal to 6 (including pre-releases and RCs)
|
|
"""
|
|
|
|
# Hacky way to check if the we're using at least Bazel 6. Would be nice if there was a ctx.bazel_version instead.
|
|
# native.bazel_version only works in repository rules.
|
|
return "apple_binary" not in dir(native)
|
|
|
|
def is_bzlmod_enabled():
|
|
"""Detect the value of the --enable_bzlmod flag"""
|
|
return str(Label("@//:BUILD.bazel")).startswith("@@")
|
|
|
|
def _maybe_http_archive(**kwargs):
|
|
"""Adapts a maybe(http_archive, ...) to look like an http_archive.
|
|
|
|
This makes WORKSPACE dependencies easier to read and update.
|
|
|
|
Typical usage looks like,
|
|
|
|
```
|
|
load("//lib:utils.bzl", http_archive = "maybe_http_archive")
|
|
|
|
http_archive(
|
|
name = "aspect_rules_js",
|
|
sha256 = "5bb643d9e119832a383e67f946dc752b6d719d66d1df9b46d840509ceb53e1f1",
|
|
strip_prefix = "rules_js-1.6.2",
|
|
url = "https://github.com/aspect-build/rules_js/archive/refs/tags/v1.6.2.tar.gz",
|
|
)
|
|
```
|
|
|
|
instead of the classic maybe pattern of,
|
|
|
|
```
|
|
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
|
|
load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe")
|
|
|
|
maybe(
|
|
http_archive,
|
|
name = "aspect_rules_js",
|
|
sha256 = "5bb643d9e119832a383e67f946dc752b6d719d66d1df9b46d840509ceb53e1f1",
|
|
strip_prefix = "rules_js-1.6.2",
|
|
url = "https://github.com/aspect-build/rules_js/archive/refs/tags/v1.6.2.tar.gz",
|
|
)
|
|
```
|
|
|
|
Args:
|
|
**kwargs: all arguments to pass-forward to http_archive
|
|
"""
|
|
maybe(_http_archive, **kwargs)
|
|
|
|
_COMMON_RULE_ATTRIBUTES = [
|
|
"compatible_with",
|
|
"deprecation",
|
|
"distribs",
|
|
"exec_compatible_with",
|
|
"exec_properties",
|
|
"features",
|
|
"restricted_to",
|
|
"tags",
|
|
"target_compatible_with",
|
|
"testonly",
|
|
"toolchains",
|
|
"visibility",
|
|
]
|
|
|
|
_COMMON_TEST_RULE_ATTRIBUTES = _COMMON_RULE_ATTRIBUTES + [
|
|
"args",
|
|
"env",
|
|
"env_inherit",
|
|
"size",
|
|
"timeout",
|
|
"flaky",
|
|
"shard_count",
|
|
"local",
|
|
]
|
|
|
|
_COMMON_BINARY_RULE_ATTRIBUTES = _COMMON_RULE_ATTRIBUTES + [
|
|
"args",
|
|
"env",
|
|
"output_licenses",
|
|
]
|
|
|
|
def _propagate_common_rule_attributes(attrs):
|
|
"""Returns a dict of rule parameters filtered from the input dict that only contains the onces that are common to all rules
|
|
|
|
These are listed in Bazel's documentation:
|
|
https://bazel.build/reference/be/common-definitions#common-attributes
|
|
|
|
Args:
|
|
attrs: Dict of parameters to filter
|
|
|
|
Returns:
|
|
The dict of parameters, containing only common attributes
|
|
"""
|
|
|
|
return {
|
|
k: attrs[k]
|
|
for k in attrs
|
|
if k in _COMMON_RULE_ATTRIBUTES
|
|
}
|
|
|
|
def _propagate_common_test_rule_attributes(attrs):
|
|
"""Returns a dict of rule parameters filtered from the input dict that only contains the onces that are common to all test rules
|
|
|
|
These are listed in Bazel's documentation:
|
|
https://bazel.build/reference/be/common-definitions#common-attributes
|
|
https://bazel.build/reference/be/common-definitions#common-attributes-tests
|
|
|
|
Args:
|
|
attrs: Dict of parameters to filter
|
|
|
|
Returns:
|
|
The dict of parameters, containing only common test attributes
|
|
"""
|
|
|
|
return {
|
|
k: attrs[k]
|
|
for k in attrs
|
|
if k in _COMMON_TEST_RULE_ATTRIBUTES
|
|
}
|
|
|
|
def _propagate_common_binary_rule_attributes(attrs):
|
|
"""Returns a dict of rule parameters filtered from the input dict that only contains the onces that are common to all binary rules
|
|
|
|
These are listed in Bazel's documentation:
|
|
https://bazel.build/reference/be/common-definitions#common-attributes
|
|
https://bazel.build/reference/be/common-definitions#common-attributes-binary
|
|
|
|
Args:
|
|
attrs: Dict of parameters to filter
|
|
|
|
Returns:
|
|
The dict of parameters, containing only common binary attributes
|
|
"""
|
|
|
|
return {
|
|
k: attrs[k]
|
|
for k in attrs
|
|
if k in _COMMON_RULE_ATTRIBUTES or k in _COMMON_BINARY_RULE_ATTRIBUTES
|
|
}
|
|
|
|
utils = struct(
|
|
default_timeout = _default_timeout,
|
|
file_exists = _file_exists,
|
|
glob_directories = _glob_directories,
|
|
is_bazel_6_or_greater = _is_bazel_6_or_greater,
|
|
is_bzlmod_enabled = is_bzlmod_enabled,
|
|
is_external_label = _is_external_label,
|
|
maybe_http_archive = _maybe_http_archive,
|
|
path_to_workspace_root = _path_to_workspace_root,
|
|
propagate_well_known_tags = _propagate_well_known_tags,
|
|
propagate_common_rule_attributes = _propagate_common_rule_attributes,
|
|
propagate_common_test_rule_attributes = _propagate_common_test_rule_attributes,
|
|
propagate_common_binary_rule_attributes = _propagate_common_binary_rule_attributes,
|
|
to_label = _to_label,
|
|
consistent_label_str = _consistent_label_str,
|
|
)
|