Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions rust/private/rustdoc.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,10 @@ def rustdoc_compile_action(
# back to the crate root, which is always a `File` per the provider contract.
rustdoc_crate_info = _rustdoc_crate_info(crate_info, output if output != None else crate_info.root)

# Runtime libs contributed by the cc_toolchain, tracked so `rust_doc_test`
# can strip their (separately configured) root from the runfiles paths.
static_runtime_libs = []

# rustdoc does not understand linker flags like -lstatic that
# `include_link_flags` generates. So we manually build flags that only apply
# to rustdoc.
Expand All @@ -170,6 +174,33 @@ def rustdoc_compile_action(
else:
rustdoc_flags.append("-Clink-arg=%s" % arg)

# The cc_toolchain's runtime libs (libc++ / libunwind on any toolchain
# enabling `static_link_cpp_runtimes`) are NOT part of
# transitive_noncrates: Bazel injects them into C++ link actions, and
# rustdoc never runs one -- it drives the link itself.
# `add_native_link_flags` emits their `-Lnative=` search path
# unconditionally but gates the matching `-lstatic=` behind
# `include_link_flags`, which is False for rustdoc. Without the `-l`
# below the archives sit on the search path with nothing referencing
# them, and every doc test fails to link with undefined `_Unwind_*`.
#
# Mirrors the crate-type split in `collect_inputs`, so the libs named
# here are the ones that were added to the action inputs.
if cc_toolchain:
if crate_info.type in ["dylib", "cdylib"]:
runtime_libs = cc_toolchain.dynamic_runtime_lib(feature_configuration = feature_configuration)
else:
runtime_libs = cc_toolchain.static_runtime_lib(feature_configuration = feature_configuration)
for lib in runtime_libs.to_list():
static_runtime_libs.append(lib)
arg = get_lib_name(lib)
if not for_windows:
arg = "-l" + arg
if type(rustdoc_flags) == "Args":
rustdoc_flags.add("-Clink-arg=%s" % arg)
else:
rustdoc_flags.append("-Clink-arg=%s" % arg)

args, env = construct_arguments(
ctx = ctx,
attr = ctx.attr,
Expand Down Expand Up @@ -214,6 +245,7 @@ def rustdoc_compile_action(
arguments = args.all,
supports_path_mapping = args.supports_path_mapping,
tools = [toolchain.rust_doc],
static_runtime_libs = static_runtime_libs,
)

def _zip_action(ctx, input_dir, output_zip, crate_label):
Expand Down
15 changes: 15 additions & 0 deletions rust/private/rustdoc_test.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,21 @@ def _construct_writer_arguments(ctx, test_runner, opt_test_params, action, crate
if dep_cc_info:
_collect_library_roots(roots, dep_cc_info.linking_context.linker_inputs)

# The cc_toolchain runtime libs (see rustdoc.bzl) are built in their own
# configuration, so their root differs from every crate root collected
# above. Without stripping it too, the `-Lnative=` search path rustc emits
# for them stays an execroot path that does not exist under runfiles, and
# the linker reports "unable to find library".
#
# Source files have an empty root, and they need no stripping: their
# `-Lnative=` path is already workspace-relative. Skip them -- an empty
# root would add `--strip_substring=/`, and the writer applies these as
# plain string replacements, so that would delete every `/` in every
# argument.
for lib in action.static_runtime_libs:
if lib.root.path:
roots.append(lib.root.path)

writer_args.add_all(roots, format_each = "--strip_substring=%s/", uniquify = True)

# Indicate that the rustdoc_test args are over.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ load("@rules_cc//cc:cc_toolchain_config_lib.bzl", "feature")
load("@rules_cc//cc:defs.bzl", "cc_toolchain")
load("@rules_cc//cc/common:cc_common.bzl", "cc_common")
load("@rules_cc//cc/toolchains:cc_toolchain_config_info.bzl", "CcToolchainConfigInfo")
load("//rust:defs.bzl", "rust_shared_library", "rust_static_library")
load("//rust:defs.bzl", "rust_doc_test", "rust_library", "rust_shared_library", "rust_static_library")

def _test_cc_config_impl(ctx):
config_info = cc_common.create_cc_toolchain_config_info(
Expand Down Expand Up @@ -81,6 +81,55 @@ inputs_analysis_test = analysistest.make(
},
)

def _rustdoc_link_args_analysis_test_impl(ctx):
env = analysistest.begin(ctx)
tut = analysistest.target_under_test(env)

actions = tut[DepActionsInfo].actions
action = None
for candidate in actions:
if candidate.mnemonic in ["RustdocTestWriter", "RustdocTestCompile"]:
action = candidate
break

asserts.true(
env,
action != None,
"error: no rustdoc test action found among: {}".format(
[candidate.mnemonic for candidate in actions],
),
)

if action:
# Any one of the accepted spellings is enough: `rustdoc.bzl` omits the
# `-l` prefix when the target ABI is msvc, where link.exe takes bare
# library names.
asserts.true(
env,
any([expected in action.argv for expected in ctx.attr.expected_any_of]),
"error: expected one of {} in the rustdoc test link args: '{}'".format(
ctx.attr.expected_any_of,
action.argv,
),
)

return analysistest.end(env)

rustdoc_link_args_analysis_test = analysistest.make(
impl = _rustdoc_link_args_analysis_test_impl,
doc = """An analysistest to examine the link args of a rust_doc_test target.

rustdoc drives the doc test link itself rather than running a Bazel C++
link action, so the cc_toolchain's runtime libs never arrive through
`static_link_cpp_runtimes` the way they do for a rust_library. They have to
be named explicitly on the rustdoc command line instead, or the doc test
fails to link against a toolchain that supplies its own unwinder.
""",
attrs = {
"expected_any_of": attr.string_list(),
},
)

def runtime_libs_test(name):
"""Produces test shared and static library targets that are set up to use a custom cc_toolchain with custom runtime libs.

Expand Down Expand Up @@ -150,3 +199,38 @@ def runtime_libs_test(name):
target_under_test = "%s/_static_library" % name,
expected_inputs = ["dummy.a"],
)

# A doc test links like a binary, so it needs the static runtime lib named
# on the command line. `dummy.a` yields `dummy` via `get_lib_name`, spelled
# `-ldummy` everywhere except msvc.
rust_library(
name = "%s/__doctest_library" % name,
edition = "2018",
srcs = ["lib.rs"],
tags = ["manual", "nobuild"],
)

rust_doc_test(
name = "%s/__doc_test" % name,
crate = ":%s/__doctest_library" % name,
tags = ["manual", "nobuild"],
)

with_extra_toolchain(
name = "%s/_doc_test" % name,
extra_toolchain = ":%s/test_cc_toolchain" % name,
target = "%s/__doc_test" % name,
tags = ["manual"],
# rust_doc_test is a test rule, so it is testonly.
testonly = True,
)

rustdoc_link_args_analysis_test(
name = "%s/doc_test" % name,
target_under_test = "%s/_doc_test" % name,
expected_any_of = [
"-Clink-arg=-ldummy",
# msvc: link.exe takes bare library names, no `-l`.
"-Clink-arg=dummy",
],
)
Loading