diff --git a/rust/private/rustdoc.bzl b/rust/private/rustdoc.bzl index 4c407755f9..cc0a23e5c7 100644 --- a/rust/private/rustdoc.bzl +++ b/rust/private/rustdoc.bzl @@ -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. @@ -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, @@ -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): diff --git a/rust/private/rustdoc_test.bzl b/rust/private/rustdoc_test.bzl index 733444eff7..664878cecd 100644 --- a/rust/private/rustdoc_test.bzl +++ b/rust/private/rustdoc_test.bzl @@ -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. diff --git a/test/unit/cc_toolchain_runtime_lib/cc_toolchain_runtime_lib_test.bzl b/test/unit/cc_toolchain_runtime_lib/cc_toolchain_runtime_lib_test.bzl index b097e1faf0..504cb106a2 100644 --- a/test/unit/cc_toolchain_runtime_lib/cc_toolchain_runtime_lib_test.bzl +++ b/test/unit/cc_toolchain_runtime_lib/cc_toolchain_runtime_lib_test.bzl @@ -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( @@ -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. @@ -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", + ], + )