blob: 445036bab31ac82d990ce5fe742945e716de788f [file] [log] [blame]
# Copyright 2019 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.
"""Rust Bindgen rules"""
load(
"@bazel_tools//tools/build_defs/cc:action_names.bzl",
"CPP_COMPILE_ACTION_NAME",
)
load("@rules_cc//cc:defs.bzl", "CcInfo")
load("//rust:defs.bzl", "rust_library")
load("//rust:rust_common.bzl", "BuildInfo")
# buildifier: disable=bzl-visibility
load("//rust/private:rustc.bzl", "get_linker_and_args")
# buildifier: disable=bzl-visibility
load("//rust/private:utils.bzl", "find_cc_toolchain", "get_lib_name_default", "get_preferred_artifact")
# TODO(hlopko): use the more robust logic from rustc.bzl also here, through a reasonable API.
def _get_libs_for_static_executable(dep):
"""find the libraries used for linking a static executable.
Args:
dep (Target): A cc_library target.
Returns:
depset: A depset[File]
"""
linker_inputs = dep[CcInfo].linking_context.linker_inputs.to_list()
return depset([get_preferred_artifact(lib, use_pic = False) for li in linker_inputs for lib in li.libraries])
def rust_bindgen_library(
name,
header,
cc_lib,
bindgen_flags = None,
bindgen_features = None,
clang_flags = None,
**kwargs):
"""Generates a rust source file for `header`, and builds a rust_library.
Arguments are the same as `rust_bindgen`, and `kwargs` are passed directly to rust_library.
Args:
name (str): A unique name for this target.
header (str): The label of the .h file to generate bindings for.
cc_lib (str): The label of the cc_library that contains the .h file. This is used to find the transitive includes.
bindgen_flags (list, optional): Flags to pass directly to the bindgen executable. See https://rust-lang.github.io/rust-bindgen/ for details.
bindgen_features (list, optional): The `features` attribute for the `rust_bindgen` target.
clang_flags (list, optional): Flags to pass directly to the clang executable.
**kwargs: Arguments to forward to the underlying `rust_library` rule.
"""
tags = kwargs.get("tags") or []
if "tags" in kwargs:
kwargs.pop("tags")
sub_tags = tags + ([] if "manual" in tags else ["manual"])
deps = kwargs.get("deps") or []
if "deps" in kwargs:
kwargs.pop("deps")
bindgen_kwargs = {}
if "leak_symbols" in kwargs:
bindgen_kwargs.update({"leak_symbols": kwargs.pop("leak_symbols")})
rust_bindgen(
name = name + "__bindgen",
header = header,
cc_lib = cc_lib,
bindgen_flags = bindgen_flags or [],
features = bindgen_features,
clang_flags = clang_flags or [],
tags = sub_tags,
**bindgen_kwargs
)
for custom_tag in ["__bindgen", "no-clippy", "no-rustfmt"]:
tags = tags + ([] if custom_tag in tags else [custom_tag])
rust_library(
name = name,
srcs = [name + "__bindgen.rs"],
deps = deps + [name + "__bindgen"],
tags = tags,
**kwargs
)
def _get_user_link_flags(cc_lib):
linker_flags = []
for linker_input in cc_lib[CcInfo].linking_context.linker_inputs.to_list():
linker_flags.extend(linker_input.user_link_flags)
return linker_flags
def _generate_cc_link_build_info(ctx, cc_lib):
"""Produce the eqivilant cargo_build_script providers for use in linking the library.
Args:
ctx (ctx): The rule's context object
cc_lib (Target): The `rust_bindgen.cc_lib` target.
Returns:
The `BuildInfo` provider.
"""
compile_data = []
rustc_flags = []
linker_search_paths = []
for linker_input in cc_lib[CcInfo].linking_context.linker_inputs.to_list():
for lib in linker_input.libraries:
if lib.static_library:
rustc_flags.append("-lstatic={}".format(get_lib_name_default(lib.static_library)))
linker_search_paths.append(lib.static_library.dirname)
compile_data.append(lib.static_library)
elif lib.pic_static_library:
rustc_flags.append("-lstatic={}".format(get_lib_name_default(lib.pic_static_library)))
linker_search_paths.append(lib.pic_static_library.dirname)
compile_data.append(lib.pic_static_library)
if not compile_data:
fail("No static libraries found in {}".format(
cc_lib.label,
))
rustc_flags_file = ctx.actions.declare_file("{}.rustc_flags".format(ctx.label.name))
ctx.actions.write(
output = rustc_flags_file,
content = "\n".join(rustc_flags),
)
link_search_paths = ctx.actions.declare_file("{}.link_search_paths".format(ctx.label.name))
ctx.actions.write(
output = link_search_paths,
content = "\n".join([
"-Lnative=${{pwd}}/{}".format(path)
for path in depset(linker_search_paths).to_list()
]),
)
return BuildInfo(
compile_data = depset(compile_data),
dep_env = None,
flags = rustc_flags_file,
# linker_flags is provided via CcInfo
linker_flags = None,
link_search_paths = link_search_paths,
out_dir = None,
rustc_env = None,
)
def _rust_bindgen_impl(ctx):
# nb. We can't grab the cc_library`s direct headers, so a header must be provided.
cc_lib = ctx.attr.cc_lib
header = ctx.file.header
cc_header_list = ctx.attr.cc_lib[CcInfo].compilation_context.headers.to_list()
if header not in cc_header_list:
fail("Header {} is not in {}'s transitive headers.".format(ctx.attr.header, cc_lib), "header")
toolchain = ctx.toolchains[Label("//bindgen:toolchain_type")]
bindgen_bin = toolchain.bindgen
clang_bin = toolchain.clang
libclang = toolchain.libclang
libstdcxx = toolchain.libstdcxx
output = ctx.outputs.out
cc_toolchain, feature_configuration = find_cc_toolchain(ctx = ctx)
tools = depset([clang_bin], transitive = [cc_toolchain.all_files])
# libclang should only have 1 output file
libclang_dir = _get_libs_for_static_executable(libclang).to_list()[0].dirname
env = {
"CLANG_PATH": clang_bin.path,
"LIBCLANG_PATH": libclang_dir,
"RUST_BACKTRACE": "1",
}
args = ctx.actions.args()
# Configure Bindgen Arguments
args.add_all(ctx.attr.bindgen_flags)
args.add(header)
args.add("--output", output)
# Vanilla usage of bindgen produces formatted output, here we do the same if we have `rustfmt` in our toolchain.
rustfmt_toolchain = ctx.toolchains[Label("//rust/rustfmt:toolchain_type")]
if toolchain.default_rustfmt:
# Bindgen is able to find rustfmt using the RUSTFMT environment variable
env.update({"RUSTFMT": rustfmt_toolchain.rustfmt.path})
tools = depset(transitive = [tools, rustfmt_toolchain.all_files])
else:
args.add("--no-rustfmt-bindings")
# Configure Clang Arguments
args.add("--")
compile_variables = cc_common.create_compile_variables(
cc_toolchain = cc_toolchain,
feature_configuration = feature_configuration,
include_directories = cc_lib[CcInfo].compilation_context.includes,
quote_include_directories = cc_lib[CcInfo].compilation_context.quote_includes,
system_include_directories = cc_lib[CcInfo].compilation_context.system_includes,
user_compile_flags = ctx.attr.clang_flags,
)
compile_flags = cc_common.get_memory_inefficient_command_line(
feature_configuration = feature_configuration,
action_name = CPP_COMPILE_ACTION_NAME,
variables = compile_variables,
)
# Bindgen forcibly uses clang.
# It's possible that the selected cc_toolchain isn't clang, and may specify flags which clang doesn't recognise.
# Ideally we could depend on a more specific toolchain, requesting one which is specifically clang via some constraint.
# Unfortunately, we can't currently rely on this, so instead we filter only to flags we know clang supports.
# We can add extra flags here as needed.
flags_known_to_clang = ("-I", "-iquote", "-isystem", "--sysroot", "--gcc-toolchain")
open_arg = False
for arg in compile_flags:
if open_arg:
args.add(arg)
open_arg = False
continue
# The cc_toolchain merged these flags into its returned flags - don't strip these out.
if arg in ctx.attr.clang_flags:
args.add(arg)
continue
if not arg.startswith(flags_known_to_clang):
continue
args.add(arg)
if arg in flags_known_to_clang:
open_arg = True
continue
_, _, linker_env = get_linker_and_args(ctx, ctx.attr, "bin", cc_toolchain, feature_configuration, None)
env.update(**linker_env)
# Set the dynamic linker search path so that clang uses the libstdcxx from the toolchain.
# DYLD_LIBRARY_PATH is LD_LIBRARY_PATH on macOS.
if libstdcxx:
env["LD_LIBRARY_PATH"] = ":".join([f.dirname for f in _get_libs_for_static_executable(libstdcxx).to_list()])
env["DYLD_LIBRARY_PATH"] = env["LD_LIBRARY_PATH"]
ctx.actions.run(
executable = bindgen_bin,
inputs = depset(
[header],
transitive = [
cc_lib[CcInfo].compilation_context.headers,
_get_libs_for_static_executable(libclang),
] + ([
_get_libs_for_static_executable(libstdcxx),
] if libstdcxx else []),
),
outputs = [output],
mnemonic = "RustBindgen",
progress_message = "Generating bindings for {}..".format(header.path),
env = env,
arguments = [args],
tools = tools,
)
if ctx.attr.leak_symbols:
# buildifier: disable=print
print("WARN: rust_bindgen.leak_symbols is set to True for {} - please file an issue at https://github.com/bazelbuild/rules_rust/issues explaining why this was necessary, as this support will be removed soon.".format(ctx.label))
providers = [cc_common.merge_cc_infos(
direct_cc_infos = [cc_lib[CcInfo]],
)]
else:
providers = [
_generate_cc_link_build_info(ctx, cc_lib),
# As in https://github.com/bazelbuild/rules_rust/pull/2361, we want
# to link cc_lib to the direct parent (rlib) using `-lstatic=<cc_lib>` rustc flag
# Hence, we do not need to provide the whole CcInfo of cc_lib because
# it will cause the downstream binary to link the cc_lib again
# (same effect as setting `leak_symbols` attribute above)
# The CcInfo here only contains the custom link flags (i.e. linkopts attribute)
# specified by users in cc_lib
CcInfo(
linking_context = cc_common.create_linking_context(
linker_inputs = depset([cc_common.create_linker_input(
owner = ctx.label,
user_link_flags = _get_user_link_flags(cc_lib),
)]),
),
),
]
return providers + [
OutputGroupInfo(
bindgen_bindings = depset([output]),
),
]
rust_bindgen = rule(
doc = "Generates a rust source file from a cc_library and a header.",
implementation = _rust_bindgen_impl,
attrs = {
"bindgen_flags": attr.string_list(
doc = "Flags to pass directly to the bindgen executable. See https://rust-lang.github.io/rust-bindgen/ for details.",
),
"cc_lib": attr.label(
doc = "The cc_library that contains the `.h` file. This is used to find the transitive includes.",
providers = [CcInfo],
mandatory = True,
),
"clang_flags": attr.string_list(
doc = "Flags to pass directly to the clang executable.",
),
"header": attr.label(
doc = "The `.h` file to generate bindings for.",
allow_single_file = True,
mandatory = True,
),
"leak_symbols": attr.bool(
doc = (
"If True, `cc_lib` will be exposed and linked into all downstream consumers of the target vs. the " +
"`rust_library` directly consuming it."
),
default = False,
),
"_cc_toolchain": attr.label(
default = Label("@bazel_tools//tools/cpp:current_cc_toolchain"),
),
"_process_wrapper": attr.label(
default = Label("//util/process_wrapper"),
executable = True,
allow_single_file = True,
cfg = "exec",
),
},
outputs = {"out": "%{name}.rs"},
fragments = ["cpp"],
toolchains = [
str(Label("//bindgen:toolchain_type")),
str(Label("//rust:toolchain_type")),
str(Label("//rust/rustfmt:toolchain_type")),
"@bazel_tools//tools/cpp:toolchain_type",
],
)
def _rust_bindgen_toolchain_impl(ctx):
return platform_common.ToolchainInfo(
bindgen = ctx.executable.bindgen,
clang = ctx.executable.clang,
libclang = ctx.attr.libclang,
libstdcxx = ctx.attr.libstdcxx,
default_rustfmt = ctx.attr.default_rustfmt,
)
rust_bindgen_toolchain = rule(
_rust_bindgen_toolchain_impl,
doc = """\
The tools required for the `rust_bindgen` rule.
This rule depends on the [`bindgen`](https://crates.io/crates/bindgen) binary crate, and it
in turn depends on both a clang binary and the clang library. To obtain these dependencies,
`rust_bindgen_dependencies` imports bindgen and its dependencies.
```python
load("@rules_rust//bindgen:defs.bzl", "rust_bindgen_toolchain")
rust_bindgen_toolchain(
name = "bindgen_toolchain_impl",
bindgen = "//my/rust:bindgen",
clang = "//my/clang:clang",
libclang = "//my/clang:libclang.so",
libstdcxx = "//my/cpp:libstdc++",
)
toolchain(
name = "bindgen_toolchain",
toolchain = "bindgen_toolchain_impl",
toolchain_type = "@rules_rust//bindgen:toolchain_type",
)
```
This toolchain will then need to be registered in the current `WORKSPACE`.
For additional information, see the [Bazel toolchains documentation](https://docs.bazel.build/versions/master/toolchains.html).
""",
attrs = {
"bindgen": attr.label(
doc = "The label of a `bindgen` executable.",
executable = True,
cfg = "exec",
),
"clang": attr.label(
doc = "The label of a `clang` executable.",
executable = True,
cfg = "exec",
allow_files = True,
),
"default_rustfmt": attr.bool(
doc = "If set, `rust_bindgen` targets will always format generated sources with `rustfmt`.",
mandatory = False,
default = True,
),
"libclang": attr.label(
doc = "A cc_library that provides bindgen's runtime dependency on libclang.",
cfg = "exec",
providers = [CcInfo],
allow_files = True,
),
"libstdcxx": attr.label(
doc = "A cc_library that satisfies libclang's libstdc++ dependency. This is used to make the execution of clang hermetic. If None, system libraries will be used instead.",
cfg = "exec",
providers = [CcInfo],
mandatory = False,
allow_files = True,
),
},
)