blob: 8d08395c85034a48d9ef13df98357150394d638d [file] [log] [blame]
"""Rules for Cargo build scripts (`build.rs` files)"""
load("@bazel_skylib//lib:paths.bzl", "paths")
load("@bazel_skylib//rules:common_settings.bzl", "BuildSettingInfo")
load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain")
load("@rules_cc//cc:action_names.bzl", "ACTION_NAMES")
load("//rust:defs.bzl", "rust_common")
load("//rust:rust_common.bzl", "BuildInfo")
# buildifier: disable=bzl-visibility
load(
"//rust/private:rustc.bzl",
"get_compilation_mode_opts",
"get_linker_and_args",
)
# buildifier: disable=bzl-visibility
load(
"//rust/private:utils.bzl",
"dedent",
"expand_dict_value_locations",
"find_cc_toolchain",
"find_toolchain",
_name_to_crate_name = "name_to_crate_name",
)
load(":runfiles_enabled.bzl", "is_runfiles_enabled", "runfiles_enabled_attr")
# Reexport for cargo_build_script_wrapper.bzl
name_to_crate_name = _name_to_crate_name
CargoBuildScriptRunfilesInfo = provider(
doc = "Info about a `cargo_build_script.script` target.",
fields = {
"data": "List[Target]: The raw `cargo_build_script_runfiles.data` attribute.",
"tools": "List[Target]: The raw `cargo_build_script_runfiles.tools` attribute.",
},
)
def _cargo_build_script_runfiles_impl(ctx):
script = ctx.executable.script
is_windows = script.extension == "exe"
exe = ctx.actions.declare_file("{}{}".format(ctx.label.name, ".exe" if is_windows else ""))
ctx.actions.symlink(
output = exe,
target_file = script,
is_executable = True,
)
# Tools are ommitted here because they should be within the `script`
# attribute's runfiles.
runfiles = ctx.runfiles(files = ctx.files.data)
return [
DefaultInfo(
files = depset([exe]),
runfiles = runfiles.merge(ctx.attr.script[DefaultInfo].default_runfiles),
executable = exe,
),
CargoBuildScriptRunfilesInfo(
data = ctx.attr.data,
tools = ctx.attr.tools,
),
]
cargo_build_script_runfiles = rule(
doc = """\
A rule for producing `cargo_build_script.script` with proper runfiles.
This rule ensure's the executable for `cargo_build_script` has properly formed runfiles with `cfg=target` and
`cfg=exec` files. This is a challenge becuase had the script binary been directly consumed, it would have been
in either configuration which would have been incorrect for either the `tools` (exec) or `data` (target) attributes.
This is solved here by consuming the script as exec and creating a symlink to consumers of this rule can consume
with `cfg=target` and still get an exec compatible binary.
This rule may not be necessary if it becomes possible to construct runfiles trees within a rule for an action as
we'd be able to build the correct runfiles tree and configure the script runner to run the script in the new runfiles
directory:
https://github.com/bazelbuild/bazel/issues/15486
""",
implementation = _cargo_build_script_runfiles_impl,
attrs = {
"data": attr.label_list(
doc = "Data required by the build script.",
allow_files = True,
),
"script": attr.label(
doc = "The binary script to run, generally a `rust_binary` target.",
executable = True,
mandatory = True,
providers = [rust_common.crate_info],
cfg = "exec",
),
"tools": attr.label_list(
doc = "Tools required by the build script.",
allow_files = True,
cfg = "exec",
),
},
executable = True,
)
def get_cc_compile_args_and_env(cc_toolchain, feature_configuration):
"""Gather cc environment variables from the given `cc_toolchain`
Args:
cc_toolchain (cc_toolchain): The current rule's `cc_toolchain`.
feature_configuration (FeatureConfiguration): Class used to construct command lines from CROSSTOOL features.
Returns:
tuple: A tuple of the following items:
- (sequence): A flattened C command line flags for given action.
- (sequence): A flattened CXX command line flags for given action.
- (dict): C environment variables to be set for given action.
"""
compile_variables = cc_common.create_compile_variables(
feature_configuration = feature_configuration,
cc_toolchain = cc_toolchain,
)
cc_c_args = cc_common.get_memory_inefficient_command_line(
feature_configuration = feature_configuration,
action_name = ACTION_NAMES.c_compile,
variables = compile_variables,
)
cc_cxx_args = cc_common.get_memory_inefficient_command_line(
feature_configuration = feature_configuration,
action_name = ACTION_NAMES.cpp_compile,
variables = compile_variables,
)
cc_env = cc_common.get_environment_variables(
feature_configuration = feature_configuration,
action_name = ACTION_NAMES.c_compile,
variables = compile_variables,
)
return cc_c_args, cc_cxx_args, cc_env
def _pwd_flags_sysroot(args):
"""Prefix execroot-relative paths of known arguments with ${pwd}.
Args:
args (list): List of tool arguments.
Returns:
list: The modified argument list.
"""
res = []
for arg in args:
s, opt, path = arg.partition("--sysroot=")
if s == "" and not paths.is_absolute(path):
res.append("{}${{pwd}}/{}".format(opt, path))
else:
res.append(arg)
return res
def _pwd_flags_isystem(args):
"""Prefix execroot-relative paths of known arguments with ${pwd}.
Args:
args (list): List of tool arguments.
Returns:
list: The modified argument list.
"""
res = []
fix_next_arg = False
for arg in args:
if fix_next_arg and not paths.is_absolute(arg):
res.append("${{pwd}}/{}".format(arg))
else:
res.append(arg)
fix_next_arg = arg == "-isystem"
return res
def _pwd_flags(args):
return _pwd_flags_isystem(_pwd_flags_sysroot(args))
def _feature_enabled(ctx, feature_name, default = False):
"""Check if a feature is enabled.
If the feature is explicitly enabled or disabled, return accordingly.
In the case where the feature is not explicitly enabled or disabled, return the default value.
Args:
ctx: The context object.
feature_name: The name of the feature.
default: The default value to return if the feature is not explicitly enabled or disabled.
Returns:
Boolean defining whether the feature is enabled.
"""
if feature_name in ctx.disabled_features:
return False
if feature_name in ctx.features:
return True
return default
def _rlocationpath(file, workspace_name):
if file.short_path.startswith("../"):
return file.short_path[len("../"):]
return "{}/{}".format(workspace_name, file.short_path)
def _create_runfiles_dir(ctx, script):
runfiles_dir = ctx.actions.declare_directory("{}.cargo_runfiles".format(ctx.label.name))
workspace_name = ctx.label.workspace_name
if not workspace_name:
workspace_name = ctx.workspace_name
def _runfiles_map(file):
return "{}={}".format(file.path, _rlocationpath(file, workspace_name))
runfiles = script[DefaultInfo].default_runfiles
args = ctx.actions.args()
args.use_param_file("@%s", use_always = True)
args.add(runfiles_dir.path)
args.add_all(runfiles.files, map_each = _runfiles_map, allow_closure = True)
ctx.actions.run(
mnemonic = "CargoBuildScriptRunfilesDir",
executable = ctx.executable._runfiles_maker,
arguments = [args],
inputs = runfiles.files,
outputs = [runfiles_dir],
)
return runfiles_dir
def _cargo_build_script_impl(ctx):
"""The implementation for the `cargo_build_script` rule.
Args:
ctx (ctx): The rules context object
Returns:
list: A list containing a BuildInfo provider
"""
script = ctx.executable.script
script_info = ctx.attr.script[CargoBuildScriptRunfilesInfo]
toolchain = find_toolchain(ctx)
out_dir = ctx.actions.declare_directory(ctx.label.name + ".out_dir")
env_out = ctx.actions.declare_file(ctx.label.name + ".env")
dep_env_out = ctx.actions.declare_file(ctx.label.name + ".depenv")
flags_out = ctx.actions.declare_file(ctx.label.name + ".flags")
link_flags = ctx.actions.declare_file(ctx.label.name + ".linkflags")
link_search_paths = ctx.actions.declare_file(ctx.label.name + ".linksearchpaths") # rustc-link-search, propagated from transitive dependencies
compilation_mode_opt_level = get_compilation_mode_opts(ctx, toolchain).opt_level
script_tools = []
script_data = []
for target in script_info.data:
script_data.append(target[DefaultInfo].files)
script_data.append(target[DefaultInfo].default_runfiles.files)
for target in script_info.tools:
script_tools.append(target[DefaultInfo].files)
script_tools.append(target[DefaultInfo].default_runfiles.files)
workspace_name = ctx.label.workspace_name
if not workspace_name:
workspace_name = ctx.workspace_name
manifest_dir = "{}.runfiles/{}/{}".format(script.path, workspace_name, ctx.label.package)
if not is_runfiles_enabled(ctx.attr):
runfiles_dir = _create_runfiles_dir(ctx, ctx.attr.script)
script_data.append(depset([runfiles_dir]))
manifest_dir = "{}/{}/{}".format(runfiles_dir.path, workspace_name, ctx.label.package)
streams = struct(
stdout = ctx.actions.declare_file(ctx.label.name + ".stdout.log"),
stderr = ctx.actions.declare_file(ctx.label.name + ".stderr.log"),
)
pkg_name = ctx.attr.pkg_name
if pkg_name == "":
pkg_name = name_to_pkg_name(ctx.label.name)
toolchain_tools = [toolchain.all_files]
cc_toolchain = find_cpp_toolchain(ctx)
# Start with the default shell env, which contains any --action_env
# settings passed in on the command line.
env = dict(ctx.configuration.default_shell_env)
env.update({
"CARGO_CRATE_NAME": name_to_crate_name(pkg_name),
"CARGO_MANIFEST_DIR": manifest_dir,
"CARGO_PKG_NAME": pkg_name,
"HOST": toolchain.exec_triple.str,
"NUM_JOBS": "1",
"OPT_LEVEL": compilation_mode_opt_level,
"RUSTC": toolchain.rustc.path,
"TARGET": toolchain.target_flag_value,
# OUT_DIR is set by the runner itself, rather than on the action.
})
# This isn't exactly right, but Bazel doesn't have exact views of "debug" and "release", so...
env.update({
"DEBUG": {"dbg": "true", "fastbuild": "true", "opt": "false"}.get(ctx.var["COMPILATION_MODE"], "true"),
"PROFILE": {"dbg": "debug", "fastbuild": "debug", "opt": "release"}.get(ctx.var["COMPILATION_MODE"], "unknown"),
})
if ctx.attr.version:
version = ctx.attr.version.split("+")[0].split(".")
patch = version[2].split("-") if len(version) > 2 else [""]
env["CARGO_PKG_VERSION_MAJOR"] = version[0]
env["CARGO_PKG_VERSION_MINOR"] = version[1] if len(version) > 1 else ""
env["CARGO_PKG_VERSION_PATCH"] = patch[0]
env["CARGO_PKG_VERSION_PRE"] = patch[1] if len(patch) > 1 else ""
env["CARGO_PKG_VERSION"] = ctx.attr.version
# Pull in env vars which may be required for the cc_toolchain to work (e.g. on OSX, the SDK version).
# We hope that the linker env is sufficient for the whole cc_toolchain.
cc_toolchain, feature_configuration = find_cc_toolchain(ctx)
linker, link_args, linker_env = get_linker_and_args(ctx, ctx.attr, "bin", cc_toolchain, feature_configuration, None)
env.update(**linker_env)
env["LD"] = linker
env["LDFLAGS"] = " ".join(_pwd_flags(link_args))
# MSVC requires INCLUDE to be set
cc_c_args, cc_cxx_args, cc_env = get_cc_compile_args_and_env(cc_toolchain, feature_configuration)
include = cc_env.get("INCLUDE")
if include:
env["INCLUDE"] = include
if cc_toolchain:
toolchain_tools.append(cc_toolchain.all_files)
env["CC"] = cc_common.get_tool_for_action(
feature_configuration = feature_configuration,
action_name = ACTION_NAMES.c_compile,
)
env["CXX"] = cc_common.get_tool_for_action(
feature_configuration = feature_configuration,
action_name = ACTION_NAMES.cpp_compile,
)
env["AR"] = cc_common.get_tool_for_action(
feature_configuration = feature_configuration,
action_name = ACTION_NAMES.cpp_link_static_library,
)
# Populate CFLAGS and CXXFLAGS that cc-rs relies on when building from source, in particular
# to determine the deployment target when building for apple platforms (`macosx-version-min`
# for example, itself derived from the `macos_minimum_os` Bazel argument).
env["CFLAGS"] = " ".join(_pwd_flags(cc_c_args))
env["CXXFLAGS"] = " ".join(_pwd_flags(cc_cxx_args))
# Inform build scripts of rustc flags
# https://github.com/rust-lang/cargo/issues/9600
env["CARGO_ENCODED_RUSTFLAGS"] = "\\x1f".join([
# Allow build scripts to locate the generated sysroot
"--sysroot=${{pwd}}/{}".format(toolchain.sysroot),
] + ctx.attr.rustc_flags)
for f in ctx.attr.crate_features:
env["CARGO_FEATURE_" + f.upper().replace("-", "_")] = "1"
links = ctx.attr.links or ""
if links:
env["CARGO_MANIFEST_LINKS"] = links
# Add environment variables from the Rust toolchain.
env.update(toolchain.env)
# Gather data from the `toolchains` attribute.
for target in ctx.attr.toolchains:
if DefaultInfo in target:
toolchain_tools.extend([
target[DefaultInfo].files,
target[DefaultInfo].default_runfiles.files,
])
if platform_common.ToolchainInfo in target:
all_files = getattr(target[platform_common.ToolchainInfo], "all_files", depset([]))
if type(all_files) == "list":
all_files = depset(all_files)
toolchain_tools.append(all_files)
if platform_common.TemplateVariableInfo in target:
variables = getattr(target[platform_common.TemplateVariableInfo], "variables", depset([]))
env.update(variables)
_merge_env_dict(env, expand_dict_value_locations(
ctx,
ctx.attr.build_script_env,
getattr(ctx.attr, "data", []) +
getattr(ctx.attr, "compile_data", []) +
getattr(ctx.attr, "tools", []) +
script_info.data +
script_info.tools,
))
tools = depset(
direct = [
script,
ctx.executable._cargo_build_script_runner,
] + ([toolchain.target_json] if toolchain.target_json else []),
transitive = script_data + script_tools + toolchain_tools,
)
# dep_env_file contains additional environment variables coming from
# direct dependency sys-crates' build scripts. These need to be made
# available to the current crate build script.
# See https://doc.rust-lang.org/cargo/reference/build-scripts.html#-sys-packages
# for details.
args = ctx.actions.args()
args.add(script)
args.add(links)
args.add(out_dir.path)
args.add(env_out)
args.add(flags_out)
args.add(link_flags)
args.add(link_search_paths)
args.add(dep_env_out)
args.add(streams.stdout)
args.add(streams.stderr)
args.add(ctx.attr.rundir)
build_script_inputs = []
for dep in ctx.attr.link_deps:
if rust_common.dep_info in dep and dep[rust_common.dep_info].dep_env:
dep_env_file = dep[rust_common.dep_info].dep_env
args.add(dep_env_file.path)
build_script_inputs.append(dep_env_file)
for dep_build_info in dep[rust_common.dep_info].transitive_build_infos.to_list():
build_script_inputs.append(dep_build_info.out_dir)
for dep in ctx.attr.deps:
for dep_build_info in dep[rust_common.dep_info].transitive_build_infos.to_list():
build_script_inputs.append(dep_build_info.out_dir)
experimental_symlink_execroot = ctx.attr._experimental_symlink_execroot[BuildSettingInfo].value or \
_feature_enabled(ctx, "symlink-exec-root")
if experimental_symlink_execroot:
env["RULES_RUST_SYMLINK_EXEC_ROOT"] = "1"
ctx.actions.run(
executable = ctx.executable._cargo_build_script_runner,
arguments = [args],
outputs = [
out_dir,
env_out,
flags_out,
link_flags,
link_search_paths,
dep_env_out,
streams.stdout,
streams.stderr,
],
tools = tools,
inputs = build_script_inputs,
mnemonic = "CargoBuildScriptRun",
progress_message = "Running Cargo build script {}".format(pkg_name),
env = env,
toolchain = None,
# Set use_default_shell_env so that $PATH is set, as tools like Cmake
# may want to probe $PATH for helper tools.
use_default_shell_env = True,
)
return [
# Although this isn't used anywhere, without this, `bazel build`'ing
# the cargo_build_script label won't actually run the build script
# since bazel is lazy.
DefaultInfo(files = depset([out_dir])),
BuildInfo(
out_dir = out_dir,
rustc_env = env_out,
dep_env = dep_env_out,
flags = flags_out,
linker_flags = link_flags,
link_search_paths = link_search_paths,
compile_data = depset(transitive = script_data),
),
OutputGroupInfo(
streams = depset([streams.stdout, streams.stderr]),
out_dir = depset([out_dir]),
),
]
cargo_build_script = rule(
doc = (
"A rule for running a crate's `build.rs` files to generate build information " +
"which is then used to determine how to compile said crate."
),
implementation = _cargo_build_script_impl,
attrs = {
"build_script_env": attr.string_dict(
doc = "Environment variables for build scripts.",
),
"crate_features": attr.string_list(
doc = "The list of rust features that the build script should consider activated.",
),
"deps": attr.label_list(
doc = "The Rust build-dependencies of the crate",
providers = [rust_common.dep_info],
cfg = "exec",
),
"link_deps": attr.label_list(
doc = dedent("""\
The subset of the Rust (normal) dependencies of the crate that
have the links attribute and therefore provide environment
variables to this build script.
"""),
providers = [rust_common.dep_info],
),
"links": attr.string(
doc = "The name of the native library this crate links against.",
),
"pkg_name": attr.string(
doc = "The name of package being compiled, if not derived from `name`.",
),
"rundir": attr.string(
default = "",
doc = dedent("""\
A directory to cd to before the cargo_build_script is run. This should be a path relative to the exec root.
The default behaviour (and the behaviour if rundir is set to the empty string) is to change to the relative path corresponding to the cargo manifest directory, which replicates the normal behaviour of cargo so it is easy to write compatible build scripts.
If set to `.`, the cargo build script will run in the exec root.
"""),
),
"rustc_flags": attr.string_list(
doc = dedent("""\
List of compiler flags passed to `rustc`.
These strings are subject to Make variable expansion for predefined
source/output path variables like `$location`, `$execpath`, and
`$rootpath`. This expansion is useful if you wish to pass a generated
file of arguments to rustc: `@$(location //package:target)`.
"""),
),
"script": attr.label(
doc = "The binary script to run, generally a `rust_binary` target.",
executable = True,
mandatory = True,
cfg = "target",
providers = [CargoBuildScriptRunfilesInfo],
),
"tools": attr.label_list(
doc = "Tools required by the build script.",
allow_files = True,
cfg = "exec",
),
"version": attr.string(
doc = "The semantic version (semver) of the crate",
),
"_cargo_build_script_runner": attr.label(
executable = True,
allow_files = True,
default = Label("//cargo/cargo_build_script_runner:cargo_build_script_runner"),
cfg = "exec",
),
"_cc_toolchain": attr.label(
default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"),
),
"_experimental_symlink_execroot": attr.label(
default = Label("//cargo/settings:experimental_symlink_execroot"),
),
"_runfiles_maker": attr.label(
cfg = "exec",
executable = True,
default = Label("//cargo/private/runfiles_maker"),
),
} | runfiles_enabled_attr(),
fragments = ["cpp"],
toolchains = [
str(Label("//rust:toolchain_type")),
"@bazel_tools//tools/cpp:toolchain_type",
],
)
def _merge_env_dict(prefix_dict, suffix_dict):
"""Merges suffix_dict into prefix_dict, appending rather than replacing certain env vars."""
for key in ["CFLAGS", "CXXFLAGS", "LDFLAGS"]:
if key in prefix_dict and key in suffix_dict and prefix_dict[key]:
prefix_dict[key] += " " + suffix_dict.pop(key)
prefix_dict.update(suffix_dict)
def name_to_pkg_name(name):
"""Sanitize the name of cargo_build_script targets.
Args:
name (str): The name value pass to the `cargo_build_script` wrapper.
Returns:
str: A cleaned up name for a build script target.
"""
if name.endswith("_bs"):
return name[:-len("_bs")]
return name