bazel-lib/lib/private/utils.bzl

390 lines
13 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_bazel_7_or_greater():
"""Detects if the Bazel version being used is greater than or equal to 7 (including Bazel 7 pre-releases and RCs).
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 7 (including pre-releases and RCs)
"""
# Hacky way to check if the we're using at least Bazel 7. 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) and "cc_host_toolchain_alias" 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_bazel_7_or_greater = _is_bazel_7_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,
)