blob: e8dfe8d09929583744a74f54db7156701bef41cf [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_skylib//lib:paths.bzl", "paths")
load("@bazel_tools//tools/cpp:toolchain_utils.bzl", find_rules_cc_toolchain = "find_cpp_toolchain")
load(":providers.bzl", "BuildInfo", "CrateGroupInfo", "CrateInfo", "DepInfo", "DepVariantInfo", "RustcOutputDiagnosticsInfo")
UNSUPPORTED_FEATURES = [
"thin_lto",
"module_maps",
"use_header_modules",
"fdo_instrument",
"fdo_optimize",
# This feature is unsupported by definition. The authors of C++ toolchain
# configuration can place any linker flags that should not be applied when
# linking Rust targets in a feature with this name.
"rules_rust_unsupported_feature",
]
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_type")]
def find_cc_toolchain(ctx, extra_unsupported_features = tuple()):
"""Extracts a CcToolchain from the current target's context
Args:
ctx (ctx): The current target's rule context object
extra_unsupported_features (sequence of str): Extra featrures to disable
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 = UNSUPPORTED_FEATURES + ctx.disabled_features +
list(extra_unsupported_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_default(lib):
"""Returns the name of a library artifact.
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
# TODO: Could we remove this function in favor of a "windows" parameter in the
# above function? It looks like currently lambdas cannot accept local parameters
# so the following doesn't work:
# args.add_all(
# cc_toolchain.dynamic_runtime_lib(feature_configuration = feature_configuration),
# map_each = lambda x: get_lib_name(x, for_windows = toolchain.target_os.startswith("windows)),
# format_each = "-ldylib=%s",
# )
def get_lib_name_for_windows(lib):
"""Returns the name of a library artifact for Windows builds.
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])
return libname
def abs(value):
"""Returns the absolute value of a number.
Args:
value (int): A number.
Returns:
int: The absolute value of the number.
"""
if value < 0:
return -value
return value
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 = abs(hash(crate_root.path) + hash(repr(label)))
return repr(h)
def get_preferred_artifact(library_to_link, use_pic):
"""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
use_pic: If set, prefers pic_static_library over static_library.
Returns:
File: Returns the first valid library type (only one is expected)
"""
if use_pic:
# Order consistent with https://github.com/bazelbuild/bazel/blob/815dfdabb7df31d4e99b6fc7616ff8e2f9385d98/src/main/java/com/google/devtools/build/lib/rules/cpp/CcLinkingContext.java#L437.
return (
library_to_link.pic_static_library or
library_to_link.static_library or
library_to_link.interface_library or
library_to_link.dynamic_library
)
else:
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
)
# The normal ctx.expand_location, but with an additional deduplication step.
# We do this to work around a potential crash, see
# https://github.com/bazelbuild/bazel/issues/16664
def dedup_expand_location(ctx, input, targets = []):
return ctx.expand_location(input, _deduplicate(targets))
def _deduplicate(xs):
return {x: True for x in xs}.keys()
def concat(xss):
return [x for xs in xss for x in xs]
def _expand_location_for_build_script_runner(ctx, env, data, known_variables):
"""A trivial helper for `expand_dict_value_locations` and `expand_list_element_locations`
Args:
ctx (ctx): The rule's context object
env (str): The value possibly containing location macros to expand.
data (sequence of Targets): See one of the parent functions.
known_variables (dict): Make variables (probably from toolchains) to substitute in when doing make variable expansion.
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_make_variables(
env,
dedup_expand_location(ctx, env, data),
known_variables,
)
def expand_dict_value_locations(ctx, env, data, known_variables):
"""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.
For detailed documentation, see:
- [`expand_location`](https://bazel.build/rules/lib/ctx#expand_location)
- [`expand_make_variables`](https://bazel.build/rules/lib/ctx#expand_make_variables)
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.
known_variables (dict): Make variables (probably from toolchains) to substitute in when doing make variable expansion.
Returns:
dict: A dict of environment variables with expanded location macros
"""
return dict([(k, _expand_location_for_build_script_runner(ctx, v, data, known_variables)) for (k, v) in env.items()])
def expand_list_element_locations(ctx, args, data, known_variables):
"""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.
For detailed documentation, see:
- [`expand_location`](https://bazel.build/rules/lib/ctx#expand_location)
- [`expand_make_variables`](https://bazel.build/rules/lib/ctx#expand_make_variables)
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.
known_variables (dict): Make variables (probably from toolchains) to substitute in when doing make variable expansion.
Returns:
list: A list of arguments with expanded location macros
"""
return [_expand_location_for_build_script_runner(ctx, arg, data, known_variables) 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.
"""
for illegal in ("-", "/"):
name = name.replace(illegal, "_")
return name
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(workspace_name, label, toolchain, name_override = None):
"""Returns the crate name to use for the current target.
Args:
workspace_name (string): The current workspace name.
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 and
should_encode_label_in_crate_name(workspace_name, label, toolchain._third_party_dir)):
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(ctx_package, 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:
ctx_package (string): The rule's context package name.
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
# The .a symlink below is created as a sibling to the .rlib file.
# Bazel doesn't allow creating a symlink outside of the rule's package,
# so if the .rlib file comes from a different package, first symlink it
# to the rule's package. The name of the new .rlib symlink is derived
# as the name of the original .rlib relative to its package.
if rlib_file.owner.package != ctx_package:
new_path = rlib_file.short_path.removeprefix(rlib_file.owner.package).removeprefix("/")
new_rlib_file = actions.declare_file(new_path)
actions.symlink(output = new_rlib_file, target_file = rlib_file)
rlib_file = new_rlib_file
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):
"""Transforms a [Target] into [DepVariantInfo].
This helper function is used to transform ctx.attr.deps and ctx.attr.proc_macro_deps into
[DepVariantInfo].
Args:
deps (list of Targets): Dependencies coming from ctx.attr.deps or ctx.attr.proc_macro_deps
Returns:
list of DepVariantInfos.
"""
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,
crate_group_info = dep[CrateGroupInfo] if CrateGroupInfo in dep else None,
) for dep in deps]
def get_import_macro_deps(ctx):
"""Returns a list of targets to be added to proc_macro_deps.
Args:
ctx (struct): the ctx of the current target.
Returns:
list of Targets. Either empty (if the fake import macro implementation
is being used), or a singleton list with the real implementation.
"""
if not hasattr(ctx.attr, "_import_macro_dep"):
return []
if ctx.attr._import_macro_dep.label.name == "fake_import_macro_impl":
return []
return [ctx.attr._import_macro_dep]
def should_encode_label_in_crate_name(workspace_name, label, third_party_dir):
"""Determines if the crate's name should include the Bazel label, encoded.
Crate names may only encode the label if the target is in the current repo,
the target is not in the third_party_dir, and the current repo is not
rules_rust.
Args:
workspace_name (string): The name of the current workspace.
label (Label): 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 (
workspace_name != "rules_rust" and
not label.workspace_root and
not ("//" + label.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 = (
(":", "x"),
("!", "excl"),
("%", "prc"),
("@", "ao"),
("^", "caret"),
("`", "bt"),
(" ", "sp"),
("\"", "dq"),
("#", "octo"),
("$", "dllr"),
("&", "amp"),
("'", "sq"),
("(", "lp"),
(")", "rp"),
("*", "astr"),
("-", "d"),
("+", "pl"),
(",", "cm"),
(";", "sm"),
("<", "la"),
("=", "eq"),
(">", "ra"),
("?", "qm"),
("[", "lbk"),
("]", "rbk"),
("{", "lbe"),
("|", "pp"),
("}", "rbe"),
("~", "td"),
("/", "y"),
(".", "pd"),
)
# 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 = [("_z", "_zz_")] + [
subst
for (pattern, replacement) in _encodings
for subst in (
("_{}_".format(replacement), "_z{}_".format(replacement)),
(pattern, "_{}_".format(replacement)),
)
]
# Expose the substitutions for testing only.
substitutions_for_testing = _substitutions
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.
"""
return _encode_raw_string(package + ":" + name)
def _encode_raw_string(str):
"""Encodes a string using the above encoding format.
Args:
str (string): The string to be encoded.
Returns:
An encoded version of the input string.
"""
return _replace_all(str, _substitutions)
# Expose the underlying encoding function for testing only.
encode_raw_string_for_testing = _encode_raw_string
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 for each string index, going
# left-to-right and skipping indices that are already involved in a
# pattern match.
plan = {}
matched_indices_set = {}
for pattern_start in range(len(string)):
if pattern_start in matched_indices_set:
continue
for (pattern, replacement) in substitutions:
if not string.startswith(pattern, pattern_start):
continue
length = len(pattern)
plan[pattern_start] = (length, replacement)
matched_indices_set.update([(pattern_start + i, True) for i in range(length)])
break
# 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
def can_build_metadata(toolchain, ctx, crate_type):
"""Can we build metadata for this rust_library?
Args:
toolchain (toolchain): The rust toolchain
ctx (ctx): The rule's context object
crate_type (String): one of lib|rlib|dylib|staticlib|cdylib|proc-macro
Returns:
bool: whether we can build metadata for this rule.
"""
# In order to enable pipelined compilation we require that:
# 1) The _pipelined_compilation flag is enabled,
# 2) the OS running the rule is something other than windows as we require sandboxing (for now),
# 3) process_wrapper is enabled (this is disabled when compiling process_wrapper itself),
# 4) the crate_type is rlib or lib.
return toolchain._pipelined_compilation and \
toolchain.exec_triple.system != "windows" and \
ctx.attr._process_wrapper and \
crate_type in ("rlib", "lib")
def crate_root_src(name, crate_name, srcs, crate_type):
"""Determines the source file for the crate root, should it not be specified in `attr.crate_root`.
Args:
name (str): The name of the target.
crate_name (str): The target's `crate_name` attribute.
srcs (list): A list of all sources for the target Crate.
crate_type (str): The type of this crate ("bin", "lib", "rlib", "cdylib", etc).
Returns:
File: The root File object for a given crate. See the following links for more details:
- https://doc.rust-lang.org/cargo/reference/cargo-targets.html#library
- https://doc.rust-lang.org/cargo/reference/cargo-targets.html#binaries
"""
default_crate_root_filename = "main.rs" if crate_type == "bin" else "lib.rs"
if not crate_name:
crate_name = name
crate_root = (
(srcs[0] if len(srcs) == 1 else None) or
_shortest_src_with_basename(srcs, default_crate_root_filename) or
_shortest_src_with_basename(srcs, name + ".rs") or
_shortest_src_with_basename(srcs, crate_name + ".rs")
)
if not crate_root:
file_names = [default_crate_root_filename, name + ".rs"]
fail("Couldn't find {} among `srcs`, please use `crate_root` to specify the root file.".format(" or ".join(file_names)))
return crate_root
def _shortest_src_with_basename(srcs, basename):
"""Finds the shortest among the paths in srcs that match the desired basename.
Args:
srcs (list): A list of File objects
basename (str): The target basename to match against.
Returns:
File: The File object with the shortest path that matches `basename`
"""
shortest = None
for f in srcs:
if f.basename == basename:
if not shortest or len(f.dirname) < len(shortest.dirname):
shortest = f
return shortest
def determine_lib_name(name, crate_type, toolchain, lib_hash = None):
"""See https://github.com/bazelbuild/rules_rust/issues/405
Args:
name (str): The name of the current target
crate_type (str): The `crate_type`
toolchain (rust_toolchain): The current `rust_toolchain`
lib_hash (str, optional): The hashed crate root path
Returns:
str: A unique library name
"""
extension = None
prefix = ""
if crate_type in ("dylib", "cdylib", "proc-macro"):
extension = toolchain.dylib_ext
elif crate_type == "staticlib":
extension = toolchain.staticlib_ext
elif crate_type in ("lib", "rlib"):
# All platforms produce 'rlib' here
extension = ".rlib"
prefix = "lib"
elif crate_type == "bin":
fail("crate_type of 'bin' was detected in a rust_library. Please compile " +
"this crate as a rust_binary instead.")
if not extension:
fail(("Unknown crate_type: {}. If this is a cargo-supported crate type, " +
"please file an issue!").format(crate_type))
prefix = "lib"
if toolchain.target_triple and toolchain.target_os == "windows" and crate_type not in ("lib", "rlib"):
prefix = ""
if toolchain.target_arch in ("wasm32", "wasm64") and crate_type == "cdylib":
prefix = ""
return "{prefix}{name}{lib_hash}{extension}".format(
prefix = prefix,
name = name,
lib_hash = "-" + lib_hash if lib_hash else "",
extension = extension,
)
def transform_sources(ctx, srcs, crate_root):
"""Creates symlinks of the source files if needed.
Rustc assumes that the source files are located next to the crate root.
In case of a mix between generated and non-generated source files, this
we violate this assumption, as part of the sources will be located under
bazel-out/... . In order to allow for targets that contain both generated
and non-generated source files, we generate symlinks for all non-generated
files.
Args:
ctx (struct): The current rule's context.
srcs (List[File]): The sources listed in the `srcs` attribute
crate_root (File): The file specified in the `crate_root` attribute,
if it exists, otherwise None
Returns:
Tuple(List[File], File): The transformed srcs and crate_root
"""
has_generated_sources = len([src for src in srcs if not src.is_source]) > 0
if not has_generated_sources:
return srcs, crate_root
package_root = paths.join(ctx.label.workspace_root, ctx.label.package)
generated_sources = [_symlink_for_non_generated_source(ctx, src, package_root) for src in srcs if src != crate_root]
generated_root = crate_root
if crate_root:
generated_root = _symlink_for_non_generated_source(ctx, crate_root, package_root)
generated_sources.append(generated_root)
return generated_sources, generated_root
def get_edition(attr, toolchain, label):
"""Returns the Rust edition from either the current rule's attributes or the current `rust_toolchain`
Args:
attr (struct): The current rule's attributes
toolchain (rust_toolchain): The `rust_toolchain` for the current target
label (Label): The label of the target being built
Returns:
str: The target Rust edition
"""
if getattr(attr, "edition"):
return attr.edition
elif not toolchain.default_edition:
fail("Attribute `edition` is required for {}.".format(label))
else:
return toolchain.default_edition
def _symlink_for_non_generated_source(ctx, src_file, package_root):
"""Creates and returns a symlink for non-generated source files.
This rule uses the full path to the source files and the rule directory to compute
the relative paths. This is needed, instead of using `short_path`, because of non-generated
source files in external repositories possibly returning relative paths depending on the
current version of Bazel.
Args:
ctx (struct): The current rule's context.
src_file (File): The source file.
package_root (File): The full path to the directory containing the current rule.
Returns:
File: The created symlink if a non-generated file, or the file itself.
"""
if src_file.is_source or src_file.root.path != ctx.bin_dir.path:
src_short_path = paths.relativize(src_file.path, src_file.root.path)
src_symlink = ctx.actions.declare_file(paths.relativize(src_short_path, package_root))
ctx.actions.symlink(
output = src_symlink,
target_file = src_file,
progress_message = "Creating symlink to source file: {}".format(src_file.path),
)
return src_symlink
else:
return src_file
def generate_output_diagnostics(ctx, sibling, require_process_wrapper = True):
"""Generates a .rustc-output file if it's required.
Args:
ctx: (ctx): The current rule's context object
sibling: (File): The file to generate the diagnostics for.
require_process_wrapper: (bool): Whether to require the process wrapper
in order to generate the .rustc-output file.
Returns:
Optional[File] The .rustc-object file, if generated.
"""
# Since this feature requires error_format=json, we usually need
# process_wrapper, since it can write the json here, then convert it to the
# regular error format so the user can see the error properly.
if require_process_wrapper and not ctx.attr._process_wrapper:
return None
provider = ctx.attr._rustc_output_diagnostics[RustcOutputDiagnosticsInfo]
if not provider.rustc_output_diagnostics:
return None
return ctx.actions.declare_file(
sibling.basename + ".rustc-output",
sibling = sibling,
)
def is_std_dylib(file):
"""Whether the file is a dylib crate for std
"""
basename = file.basename
return (
# for linux and darwin
basename.startswith("libstd-") and (basename.endswith(".so") or basename.endswith(".dylib")) or
# for windows
basename.startswith("std-") and basename.endswith(".dll")
)