`rust_test(crate = ...)` does not stage the crate's transitive dynamic libraries into runfiles

Open Beginner friendly
#4,309 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 2 days

@Wito-1 is already working on this.

Since Sep 30, 2026.

  • #4310 by @Wito-1 — open

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
88/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
rust
Domain
build-system

Research direction

Start in rust/private/rustc.bzl at rustc_compile_action and compare its dynamic-library runfiles loop with _compute_rpaths and dep_info.transitive_noncrates. Run the provided Bazel reproduction with bazel test //repro:all --cache_test_results=no; done means the crate-based test stages libfoo.so and passes without changing the deps-based test behavior.

Written by the indexing model from the issue text.

Description

Summary

rustc_compile_action stages a binary's CcInfo dynamic libraries into its runfiles by walking only the rule's own deps attribute, while _compute_rpaths walks the full transitive linker-input set. A rust_test that reaches a dynamic library only through its crate attribute therefore links against it, has DT_NEEDED for it and a RUNPATH into the _solib_<cpu> directory where Bazel would stage it, but the file is never put in the runfiles. The test then fails at loader time on any host that has no copy of the library on the system search path (a sandbox, a remote executor, CI).

The same crate exposed through a rust_library listed in deps works, because establish_cc_info merges the transitive CcInfo into the library's own provider, so the deps-only walk happens to see it.

Reproduction

# BUILD.bazel
load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_import")
load("@rules_rust//rust:defs.bzl", "rust_library", "rust_test")

cc_binary(
    name = "libfoo.so",
    srcs = ["foo.c"],
    linkshared = True,
)

cc_import(
    name = "foo",
    shared_library = ":libfoo.so",
)

rust_library(
    name = "uses_foo",
    srcs = ["lib.rs"],
    deps = [":foo"],
)

# Reaches libfoo.so only through `crate`.
rust_test(
    name = "uses_foo_crate_test",
    crate = ":uses_foo",
)

# Reaches libfoo.so through a rust_library in `deps`.
rust_test(
    name = "uses_foo_deps_test",
    srcs = ["test.rs"],
    deps = [":uses_foo"],
)
// foo.c
int foo_answer(void) { return 42; }
// lib.rs
unsafe extern "C" {
    fn foo_answer() -> i32;
}

pub fn answer() -> i32 {
    unsafe { foo_answer() }
}

#[cfg(test)]
mod tests {
    #[test]
    fn answers() {
        assert_eq!(super::answer(), 42);
    }
}
// test.rs
#[test]
fn answers() {
    assert_eq!(uses_foo::answer(), 42);
}
$ bazel test //repro:all --cache_test_results=no
//repro:uses_foo_deps_test     PASSED in 0.0s
//repro:uses_foo_crate_test    FAILED in 0.0s

$ cat bazel-testlogs/repro/uses_foo_crate_test/test.log
.../uses_foo_crate_test: error while loading shared libraries: libfoo.so: cannot open shared object file: No such file or directory

Observed

Both binaries link identically. readelf -d on each shows the same entries:

(RUNPATH)  Library runpath: [$ORIGIN/../../../_solib_k8/_U_S_Srepro_Cfoo___Urepro]
(NEEDED)   Shared library: [libfoo.so]

Only the runfiles differ. From bazel cquery --output=starlark over providers(target)["DefaultInfo"].default_runfiles.files:

//repro:uses_foo_crate_test: uses_foo_crate_test
//repro:uses_foo_deps_test:  libfoo.so, uses_foo_deps_test

Expected

A binary's runfiles contain every dynamic library its link consumed, i.e. the same set the rpath computation already walks. Whether a dylib is reached via deps or via crate should not change what is shipped.

Cause

rust/private/rustc.bzl, in rustc_compile_action (0.70.0, lines 1753–1762; main has the same loop plus link_deps):

    if crate_info.type in ["bin", "cdylib", "staticlib"]:
        dynamic_libraries = ctx.runfiles(files = [
            library_to_link.dynamic_library
            for dep in getattr(ctx.attr, "deps", [])
            if CcInfo in dep
            for linker_input in dep[CcInfo].linking_context.linker_inputs.to_list()
            for library_to_link in linker_input.libraries
            if _is_dylib(library_to_link) and library_to_link.dynamic_library
        ])
        transitive_runfiles.append(dynamic_libraries)

ctx.attr.crate is not in ctx.attr.deps, and the crate's own DefaultInfo.default_runfiles (merged a few lines above) carry no dylibs because an rlib is not in the ["bin", "cdylib", "staticlib"] set. _compute_rpaths, by contrast, iterates dep_info.transitive_noncrates, which collect_deps builds from crate_info.deps and therefore does include the crate's dependencies.

Proposed fix

Iterate the same transitive set the rpath computation uses. dep_info is already in scope. This cannot add anything the binary does not need: _is_dylib keeps only libraries that were linked dynamically, which are exactly the ones the binary has as DT_NEEDED.

--- a/rust/private/rustc.bzl
+++ b/rust/private/rustc.bzl
@@ -1751,11 +1751,15 @@
         for target in runfiles_attr:
             transitive_runfiles.append(target[DefaultInfo].default_runfiles)
     if crate_info.type in ["bin", "cdylib", "staticlib"]:
+        # Stage every dynamic library the link consumed, walking the same
+        # transitive set `_compute_rpaths` does. Walking only `ctx.attr.deps`
+        # misses a dylib reached solely through a `rust_test(crate = ...)`'s
+        # crate: the binary NEEDs it and its rpath points at the solib dir, but
+        # the file is never shipped, so it fails at loader time wherever the
+        # host has no copy.
         dynamic_libraries = ctx.runfiles(files = [
             library_to_link.dynamic_library
-            for dep in getattr(ctx.attr, "deps", [])
-            if CcInfo in dep
-            for linker_input in dep[CcInfo].linking_context.linker_inputs.to_list()
+            for linker_input in dep_info.transitive_noncrates.to_list()
             for library_to_link in linker_input.libraries
             if _is_dylib(library_to_link) and library_to_link.dynamic_library
         ])

With this applied, the crate = test above stages libfoo.so and passes. In a larger repository, diffing default_runfiles for 16 rust_test/rust_binary targets before and after the change altered only the one target that used crate =; every other target's runfiles were byte-identical, as expected from the establish_cc_info merge.

Environment

  • rules_rust 0.70.0 (bzlmod); the loop is unchanged on main as of 0.74.0
  • Bazel 9.2.0, Linux x86_64
  • Reproduces locally under sandboxing and on remote execution; passes on a host that happens to have the library on the default loader path, which is why it is easy to miss
Dominant language
Starlark
Stars
854
Forks
684
Avg merge
2d 22h
Merged PRs (30d)
21

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from bazelbuild/rules_rust

All issues in bazelbuild/rules_rust

Similar issues

More Build System issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.