blob: 0247a1ffc42186902ed15eeb288ea3f8fdc33df6 [file] [log] [blame]
# Copyright 2015 The Bazel Authors. All rights reserved.
#
# 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
#
# http://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.
"""Utility functions not specific to the rust toolchain."""
load("@bazel_tools//tools/cpp:toolchain_utils.bzl", find_rules_cc_toolchain = "find_cpp_toolchain")
load(":providers.bzl", "BuildInfo", "CrateInfo", "DepInfo", "DepVariantInfo")
def find_toolchain(ctx):
"""Finds the first rust toolchain that is configured.
Args:
ctx (ctx): The ctx object for the current target.
Returns:
rust_toolchain: A Rust toolchain context.
"""
return ctx.toolchains[Label("//rust:toolchain")]
def find_cc_toolchain(ctx):
"""Extracts a CcToolchain from the current target's context
Args:
ctx (ctx): The current target's rule context object
Returns:
tuple: A tuple of (CcToolchain, FeatureConfiguration)
"""
cc_toolchain = find_rules_cc_toolchain(ctx)
feature_configuration = cc_common.configure_features(
ctx = ctx,
cc_toolchain = cc_toolchain,
requested_features = ctx.features,
unsupported_features = ctx.disabled_features,
)
return cc_toolchain, feature_configuration
# TODO: Replace with bazel-skylib's `path.dirname`. This requires addressing some
# dependency issues or generating docs will break.
def relativize(path, start):
"""Returns the relative path from start to path.
Args:
path (str): The path to relativize.
start (str): The ancestor path against which to relativize.
Returns:
str: The portion of `path` that is relative to `start`.
"""
src_parts = _path_parts(start)
dest_parts = _path_parts(path)
n = 0
for src_part, dest_part in zip(src_parts, dest_parts):
if src_part != dest_part:
break
n += 1
relative_path = ""
for _ in range(n, len(src_parts)):
relative_path += "../"
relative_path += "/".join(dest_parts[n:])
return relative_path
def _path_parts(path):
"""Takes a path and returns a list of its parts with all "." elements removed.
The main use case of this function is if one of the inputs to relativize()
is a relative path, such as "./foo".
Args:
path (str): A string representing a unix path
Returns:
list: A list containing the path parts with all "." elements removed.
"""
path_parts = path.split("/")
return [part for part in path_parts if part != "."]
def get_lib_name(lib):
"""Returns the name of a library artifact, eg. libabc.a -> abc
Args:
lib (File): A library file
Returns:
str: The name of the library
"""
# On macos and windows, dynamic/static libraries always end with the
# extension and potential versions will be before the extension, and should
# be part of the library name.
# On linux, the version usually comes after the extension.
# So regardless of the platform we want to find the extension and make
# everything left to it the library name.
# Search for the extension - starting from the right - by removing any
# trailing digit.
comps = lib.basename.split(".")
for comp in reversed(comps):
if comp.isdigit():
comps.pop()
else:
break
# The library name is now everything minus the extension.
libname = ".".join(comps[:-1])
if libname.startswith("lib"):
return libname[3:]
else:
return libname
def determine_output_hash(crate_root, label):
"""Generates a hash of the crate root file's path.
Args:
crate_root (File): The crate's root file (typically `lib.rs`).
label (Label): The label of the target.
Returns:
str: A string representation of the hash.
"""
# Take the absolute value of hash() since it could be negative.
h = hash(crate_root.path) + hash(repr(label))
if h < 0:
h = -h
return repr(h)
def get_preferred_artifact(library_to_link):
"""Get the first available library to link from a LibraryToLink object.
Args:
library_to_link (LibraryToLink): See the followg links for additional details:
https://docs.bazel.build/versions/master/skylark/lib/LibraryToLink.html
Returns:
File: Returns the first valid library type (only one is expected)
"""
return (
library_to_link.static_library or
library_to_link.pic_static_library or
library_to_link.interface_library or
library_to_link.dynamic_library
)
def _expand_location(ctx, env, data):
"""A trivial helper for `_expand_locations`
Args:
ctx (ctx): The rule's context object
env (str): The value possibly containing location macros to expand.
data (sequence of Targets): see `_expand_locations`
Returns:
string: The location-macro expanded version of the string.
"""
for directive in ("$(execpath ", "$(location "):
if directive in env:
# build script runner will expand pwd to execroot for us
env = env.replace(directive, "${pwd}/" + directive)
return ctx.expand_location(env, data)
def expand_dict_value_locations(ctx, env, data):
"""Performs location-macro expansion on string values.
$(execroot ...) and $(location ...) are prefixed with ${pwd},
which process_wrapper and build_script_runner will expand at run time
to the absolute path. This is necessary because include_str!() is relative
to the currently compiled file, and build scripts run relative to the
manifest dir, so we can not use execroot-relative paths.
$(rootpath ...) is unmodified, and is useful for passing in paths via
rustc_env that are encoded in the binary with env!(), but utilized at
runtime, such as in tests. The absolute paths are not usable in this case,
as compilation happens in a separate sandbox folder, so when it comes time
to read the file at runtime, the path is no longer valid.
See [`expand_location`](https://docs.bazel.build/versions/main/skylark/lib/ctx.html#expand_location) for detailed documentation.
Args:
ctx (ctx): The rule's context object
env (dict): A dict whose values we iterate over
data (sequence of Targets): The targets which may be referenced by
location macros. This is expected to be the `data` attribute of
the target, though may have other targets or attributes mixed in.
Returns:
dict: A dict of environment variables with expanded location macros
"""
return dict([(k, _expand_location(ctx, v, data)) for (k, v) in env.items()])
def expand_list_element_locations(ctx, args, data):
"""Performs location-macro expansion on a list of string values.
$(execroot ...) and $(location ...) are prefixed with ${pwd},
which process_wrapper and build_script_runner will expand at run time
to the absolute path.
See [`expand_location`](https://docs.bazel.build/versions/main/skylark/lib/ctx.html#expand_location) for detailed documentation.
Args:
ctx (ctx): The rule's context object
args (list): A list we iterate over
data (sequence of Targets): The targets which may be referenced by
location macros. This is expected to be the `data` attribute of
the target, though may have other targets or attributes mixed in.
Returns:
list: A list of arguments with expanded location macros
"""
return [_expand_location(ctx, arg, data) for arg in args]
def name_to_crate_name(name):
"""Converts a build target's name into the name of its associated crate.
Crate names cannot contain certain characters, such as -, which are allowed
in build target names. All illegal characters will be converted to
underscores.
This is a similar conversion as that which cargo does, taking a
`Cargo.toml`'s `package.name` and canonicalizing it
Note that targets can specify the `crate_name` attribute to customize their
crate name; in situations where this is important, use the
compute_crate_name() function instead.
Args:
name (str): The name of the target.
Returns:
str: The name of the crate for this target.
"""
return name.replace("-", "_")
def _invalid_chars_in_crate_name(name):
"""Returns any invalid chars in the given crate name.
Args:
name (str): Name to test.
Returns:
list: List of invalid characters in the crate name.
"""
return dict([(c, ()) for c in name.elems() if not (c.isalnum() or c == "_")]).keys()
def compute_crate_name(label, toolchain, name_override = None):
"""Returns the crate name to use for the current target.
Args:
label (struct): The label of the current target.
toolchain (struct): The toolchain in use for the target.
name_override (String): An optional name to use (as an override of label.name).
Returns:
str: The crate name to use for this target.
"""
if name_override:
invalid_chars = _invalid_chars_in_crate_name(name_override)
if invalid_chars:
fail("Crate name '{}' contains invalid character(s): {}".format(
name_override,
" ".join(invalid_chars),
))
return name_override
if toolchain and label and toolchain._rename_first_party_crates:
if should_encode_label_in_crate_name(label.package, toolchain._third_party_dir):
crate_name = label.name
else:
crate_name = encode_label_as_crate_name(label.package, label.name)
else:
crate_name = name_to_crate_name(label.name)
invalid_chars = _invalid_chars_in_crate_name(crate_name)
if invalid_chars:
fail(
"Crate name '{}' ".format(crate_name) +
"derived from Bazel target name '{}' ".format(label.name) +
"contains invalid character(s): {}\n".format(" ".join(invalid_chars)) +
"Consider adding a crate_name attribute to set a valid crate name",
)
return crate_name
def dedent(doc_string):
"""Remove any common leading whitespace from every line in text.
This functionality is similar to python's `textwrap.dedent` functionality
https://docs.python.org/3/library/textwrap.html#textwrap.dedent
Args:
doc_string (str): A docstring style string
Returns:
str: A string optimized for stardoc rendering
"""
lines = doc_string.splitlines()
if not lines:
return doc_string
# If the first line is empty, use the second line
first_line = lines[0]
if not first_line:
first_line = lines[1]
# Detect how much space prepends the first line and subtract that from all lines
space_count = len(first_line) - len(first_line.lstrip())
# If there are no leading spaces, do not alter the docstring
if space_count == 0:
return doc_string
else:
# Remove the leading block of spaces from the current line
block = " " * space_count
return "\n".join([line.replace(block, "", 1).rstrip() for line in lines])
def make_static_lib_symlink(actions, rlib_file):
"""Add a .a symlink to an .rlib file.
The name of the symlink is derived from the <name> of the <name>.rlib file as follows:
* `<name>.a`, if <name> starts with `lib`
* `lib<name>.a`, otherwise.
For example, the name of the symlink for
* `libcratea.rlib` is `libcratea.a`
* `crateb.rlib` is `libcrateb.a`.
Args:
actions (actions): The rule's context actions object.
rlib_file (File): The file to symlink, which must end in .rlib.
Returns:
The symlink's File.
"""
if not rlib_file.basename.endswith(".rlib"):
fail("file is not an .rlib: ", rlib_file.basename)
basename = rlib_file.basename[:-5]
if not basename.startswith("lib"):
basename = "lib" + basename
dot_a = actions.declare_file(basename + ".a", sibling = rlib_file)
actions.symlink(output = dot_a, target_file = rlib_file)
return dot_a
def is_exec_configuration(ctx):
"""Determine if a context is building for the exec configuration.
This is helpful when processing command line flags that should apply
to the target configuration but not the exec configuration.
Args:
ctx (ctx): The ctx object for the current target.
Returns:
True if the exec configuration is detected, False otherwise.
"""
# TODO(djmarcin): Is there any better way to determine cfg=exec?
return ctx.genfiles_dir.path.find("-exec-") != -1
def transform_deps(deps):
"""Conditionally transform a [Target] into [DepVariantInfo].
This helper function is used to transform ctx.attr.deps and ctx.attr.proc_macro_deps into
[DepVariantInfo] when --//rust/settings:incompatible_make_rust_providers_target_independent
is set to true (https://github.com/bazelbuild/rules_rust/issues/966).
Do not rely on this function - it will be removed after the flag is flipped.
Args:
deps (list of Targets): Dependencies comming from ctx.attr.deps or ctx.attr.proc_macro_deps
Returns:
list of DepVariantInfos if --//rust/settings:incompatible_make_rust_providers_target has
been flipped, list of Targets otherwise.
"""
return [DepVariantInfo(
crate_info = dep[CrateInfo] if CrateInfo in dep else None,
dep_info = dep[DepInfo] if DepInfo in dep else None,
build_info = dep[BuildInfo] if BuildInfo in dep else None,
cc_info = dep[CcInfo] if CcInfo in dep else None,
) for dep in deps]
def should_encode_label_in_crate_name(package, third_party_dir):
"""Determines if the crate's name should include the Bazel label, encoded.
Names of third-party crates do not encode the full label.
Args:
package (string): The package in question.
third_party_dir (string): The directory in which third-party packages are kept.
Returns:
True if the crate name should encode the label, False otherwise.
"""
# TODO(hlopko): This code assumes a monorepo; make it work with external
# repositories as well.
return ("//" + package + "/").startswith(third_party_dir + "/")
# This is a list of pairs, where the first element of the pair is a character
# that is allowed in Bazel package or target names but not in crate names; and
# the second element is an encoding of that char suitable for use in a crate
# name.
_encodings = (
(":", "colon"),
("!", "bang"),
("%", "percent"),
("@", "at"),
("^", "caret"),
("`", "backtick"),
(" ", "space"),
("\"", "quote"),
("#", "hash"),
("$", "dollar"),
("&", "ampersand"),
("'", "backslash"),
("(", "lparen"),
(")", "rparen"),
("*", "star"),
("-", "dash"),
("+", "plus"),
(",", "comma"),
(";", "semicolon"),
("<", "langle"),
("=", "equal"),
(">", "rangle"),
("?", "question"),
("[", "lbracket"),
("]", "rbracket"),
("{", "lbrace"),
("|", "pipe"),
("}", "rbrace"),
("~", "tilde"),
("/", "slash"),
(".", "dot"),
)
# For each of the above encodings, we generate two substitution rules: one that
# ensures any occurrences of the encodings themselves in the package/target
# aren't clobbered by this translation, and one that does the encoding itself.
# We also include a rule that protects the clobbering-protection rules from
# getting clobbered.
_substitutions = [("_quote", "_quotequote_")] + [
subst
for (pattern, replacement) in _encodings
for subst in (
("_{}_".format(replacement), "_quote{}_".format(replacement)),
(pattern, "_{}_".format(replacement)),
)
]
def encode_label_as_crate_name(package, name):
"""Encodes the package and target names in a format suitable for a crate name.
Args:
package (string): The package of the target in question.
name (string): The name of the target in question.
Returns:
A string that encodes the package and target name, to be used as the crate's name.
"""
full_name = package + ":" + name
return _replace_all(full_name, _substitutions)
def decode_crate_name_as_label_for_testing(crate_name):
"""Decodes a crate_name that was encoded by encode_label_as_crate_name.
This is used to check that the encoding is bijective; it is expected to only
be used in tests.
Args:
crate_name (string): The name of the crate.
Returns:
A string representing the Bazel label (package and target).
"""
return _replace_all(crate_name, [(t[1], t[0]) for t in _substitutions])
def _replace_all(string, substitutions):
"""Replaces occurrences of the given patterns in `string`.
There are a few reasons this looks complicated:
* The substitutions are performed with some priority, i.e. patterns that are
listed first in `substitutions` are higher priority than patterns that are
listed later.
* We also take pains to avoid doing replacements that overlap with each
other, since overlaps invalidate pattern matches.
* To avoid hairy offset invalidation, we apply the substitutions
right-to-left.
* To avoid the "_quote" -> "_quotequote_" rule introducing new pattern
matches later in the string during decoding, we take the leftmost
replacement, in cases of overlap. (Note that no rule can induce new
pattern matches *earlier* in the string.) (E.g. "_quotedot_" encodes to
"_quotequote_dot_". Note that "_quotequote_" and "_dot_" both occur in
this string, and overlap.).
Args:
string (string): the string in which the replacements should be performed.
substitutions: the list of patterns and replacements to apply.
Returns:
A string with the appropriate substitutions performed.
"""
# Find the highest-priority pattern matches.
plan = {}
for pattern, replacement in substitutions:
for pattern_start in range(len(string)):
if not pattern_start in plan and string.startswith(pattern, pattern_start):
plan[pattern_start] = (len(pattern), replacement)
# Drop replacements that overlap with a replacement earlier in the string.
replaced_indices_set = {}
leftmost_plan = {}
for pattern_start in sorted(plan.keys()):
length, _ = plan[pattern_start]
pattern_indices = list(range(pattern_start, pattern_start + length))
if any([i in replaced_indices_set for i in pattern_indices]):
continue
replaced_indices_set.update([(i, True) for i in pattern_indices])
leftmost_plan[pattern_start] = plan[pattern_start]
plan = leftmost_plan
# Execute the replacement plan, working from right to left.
for pattern_start in sorted(plan.keys(), reverse = True):
length, replacement = plan[pattern_start]
after_pattern = pattern_start + length
string = string[:pattern_start] + replacement + string[after_pattern:]
return string