`rust_test(crate = ...)` does not stage the crate's transitive dynamic libraries into runfiles
Maintainers usually reply within 2 days
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
mainas 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
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from bazelbuild/rules_rust
-
MSVC stdlib_linkflags miss kernel32.lib and ntdll.libPossibly taken @TrapsterDK claimed this 3 days ago. Openneeds-triage
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
bazelbuild/rules_rust#4375 ·
Maintainers usually reply within 2 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
bazelbuild/rules_rust#4266 ·
Maintainers usually reply within 2 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
bazelbuild/rules_rust#4223 ·
Maintainers usually reply within 2 days
-
Difficulty 1/5 1-3 hours Newbie friendliness 90/100
bazelbuild/rules_rust#4152 · 1 reaction ·
Maintainers usually reply within 2 days
-
awaiting-response
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
bazelbuild/rules_rust#3599 · 1 comment ·
Maintainers usually reply within 2 days
All issues in bazelbuild/rules_rust
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 69/100
open-telemetry/opentelemetry-go-compile-instrumentation#1713 ·
Maintainers usually reply within 2 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 77/100
carp-dk/flutter-plugins#1312 ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
jhomlala/betterplayer#1646 ·
Maintainers usually reply within 1 day
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Frogging-Family/linux-tkg#1265 ·