Skip to content

Preserve native final-link requirements through rust_static_library - #4232

Merged
UebelAndre merged 1 commit into
bazelbuild:mainfrom
cerisier:cerisier/staticlib-final-link-inputs
Aug 19, 2026
Merged

Preserve native final-link requirements through rust_static_library#4232
UebelAndre merged 1 commit into
bazelbuild:mainfrom
cerisier:cerisier/staticlib-final-link-inputs

Conversation

@cerisier

Copy link
Copy Markdown
Contributor

Summary

Preserve native final-link requirements when a rust_static_library is consumed by C or C++.

The existing CcInfo construction correctly avoids propagating dependency static archives that rustc has already bundled into the Rust staticlib. However, it rebuilds each dependency LinkerInput using only dynamic libraries, which also drops user_link_flags and additional_inputs. Those values cannot be encoded in a .a archive and are still required by the eventual foreign-language final link.

Rustc behavior being ported

This change maps rustc's staticlib orchestration onto Bazel's linking-provider model rather than inventing new linkage behavior.

At rust revision 67854e511de21d881bb16426996cd4259d44aa2e:

The Rust Reference describes the same contract: a staticlib contains local and upstream Rust code, but its system and dynamic dependencies must be supplied when another linker consumes it. --print=native-static-libs exists to communicate those requirements: https://doc.rust-lang.org/reference/linkage.html#static-and-dynamic-runtimes

Rust tests this behavior directly:

  • print-native-static-libs builds an upstream rlib and a staticlib, then asserts that --print=native-static-libs contains native requirements from both the current crate and the transitive crate, including command-line -l arguments.
  • staticlib-dylib-linkage takes the emitted native link arguments, passes them to a C compiler together with the Rust staticlib, and runs the resulting executable.

Mapping to Bazel

Rustc and Bazel represent the same boundary differently:

rustc staticlib behavior rules_rust/Bazel representation
Bundle upstream Rust objects and bundled native static libraries Keep dependency static LibraryToLink values out of the exported CcInfo; they are already in the Rust archive
Leave dynamic/native system requirements for the foreign final linker Preserve dynamic LibraryToLink values and dependency user_link_flags
Return final-link requirements through --print=native-static-libs Return them transitively through the rust_static_library target's CcInfo
No equivalent filesystem dependency model Preserve Bazel additional_inputs, such as linker scripts, so the final action remains hermetic

Bazel defines LinkerInput as the libraries, flags, and other files passed to a linker, and defines additional_linker_inputs as files required specifically by the link action: https://bazel.build/rules/lib/builtins/LinkerInput and https://bazel.build/reference/be/c-cpp

The implementation preserves the complete user_link_flags field instead of parsing only -l flags. Rustc has semantic NativeLibKind information when producing its list; rules_rust receives platform- and toolchain-encoded Bazel linker flags. Parsing those strings here would duplicate C++ toolchain behavior and mishandle constructs such as frameworks, MSVC options, library search paths, and linker scripts.

Root cause

_collect_nonstatic_linker_inputs was shared by staticlib and cdylib. It reconstructed dependency LinkerInput values with only non-static libraries:

  • Dropping static LibraryToLink values is correct for both outputs because those archives were consumed while creating the Rust output.
  • Dropping user_link_flags and additional_inputs is correct for cdylib, whose final link has already consumed them.
  • Dropping them for staticlib is incorrect because archive creation is not the foreign final link.

Consequently, a native dependency such as cc_library(linkopts = ["-lresolv"]) could be used while compiling a Rust staticlib, but its requirement disappeared from the CcInfo seen by a downstream cc_binary or cc_test.

Change

  • Continue propagating dependency dynamic libraries.
  • Preserve dependency user_link_flags and additional_inputs for staticlib only.
  • Continue excluding dependency static archives already bundled into the Rust staticlib.
  • Keep cdylib propagation unchanged.
  • Add an analysis regression covering flags, additional inputs, and exclusion of the bundled static archive.
  • Add a Linux end-to-end regression using libresolv.

Proof

On current upstream main, with only the new analysis fixture applied, the regression fails because the final CppLink action contains neither -L/doesnotexist nor empty.so. With the implementation change applied, the same test passes and confirms that the dependency static archive remains excluded.

The Linux regression is:

cc_library(linkopts = ["-lresolv"])
        -> rust_static_library(link_deps = [...])
        -> cc_test

The Rust staticlib exports a function returning the address of ns_initparse; the C++ test references that function, forcing the relevant Rust object out of the archive.

The same production change was additionally tested in the hermeticbuild fork with its hermetic LLVM C/C++ toolchain. With the fixture but without the fix, the Linux x86_64 link fails:

ld.lld: error: undefined symbol: ns_initparse
referenced by ... in archive libstaticlib_uses_resolver_linkopts.a

The archive contains U ns_initparse, confirming that the system dependency was not bundled. With the fix, the final C++ link contains -lresolv, succeeds, and produces an x86_64 ELF recording:

NEEDED Shared library: [libresolv.so.2]
U ns_initparse

The undefined dynamic symbol is expected because libresolv.so.2 is now recorded as the provider that resolves it at load time.

Tests

bazel test //test/unit/linker_inputs_propagation:linker_inputs_propagation_test_suite

Baseline with test fixture only: the new regression fails. With this change: two analysis tests pass and four platform-incompatible tests are skipped on macOS.

Additional Linux x86_64 cross-link verification in the hermeticbuild fork:

bazel build --config=remote \
  --extra_toolchains=@llvm//toolchain:all \
  --platforms=@llvm//platforms:linux_x86_64 \
  //test/linker_inputs_propagation:depends_on_resolver_linkopts_via_staticlib

The Linux executable was cross-linked from a macOS host and inspected as an ELF; it was not executed on that host.

@cerisier
cerisier marked this pull request as ready for review August 16, 2026 04:46

@UebelAndre UebelAndre left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the links and docs to upstream rustc behavior! This looks good to me 😄

@UebelAndre
UebelAndre added this pull request to the merge queue Aug 19, 2026
Merged via the queue into bazelbuild:main with commit e22086c Aug 19, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants