Skip to content
Open
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
47 changes: 46 additions & 1 deletion compiler/rustc_codegen_ssa/src/back/link.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ use rustc_errors::DiagCtxtHandle;
use rustc_fs_util::{TempDirBuilder, fix_windows_verbatim_for_gcc, try_canonicalize};
use rustc_hir::attrs::NativeLibKind;
use rustc_hir::def_id::{CrateNum, LOCAL_CRATE};
use rustc_lint_defs::builtin::LINKER_INFO;
use rustc_lint_defs::builtin::{LINKER_INFO, LINKER_STATIC_ARCHIVE_ORDER};
use rustc_macros::Diagnostic;
use rustc_metadata::EncodedMetadata;
use rustc_metadata::fs::{METADATA_FILENAME, copy_to_stdout, emit_wrapper_file};
Expand Down Expand Up @@ -1219,6 +1219,10 @@ fn link_natively(
let (linker_path, flavor) = linker_and_flavor(sess);
let self_contained_components = self_contained_components(sess, crate_type, &linker_path);

// Surface the `-Clink-arg` static-archive ordering issue before building the linker command
// line. See <https://github.com/rust-lang/rust/issues/154975>.
warn_static_archive_order(sess, crate_info, flavor);

// On AIX, we ship all libraries as .a big_af archive
// the expected format is lib<name>.a(libname.so) for the actual
// dynamic library. So we link to a temporary .so file to be archived
Expand Down Expand Up @@ -2398,6 +2402,47 @@ fn add_user_defined_link_args(cmd: &mut dyn Linker, sess: &Session) {
cmd.verbatim_args(&sess.opts.cg.link_args);
}

/// Detect static archives (`.a`/`.o` paths) passed via `-Clink-arg`, which rustc appends after
/// its own native libraries. With `--as-needed`, strict left-to-right linkers like GNU `ld.bfd`
/// may then drop a dynamic library referenced only by that archive. rustc can't know whether the
/// back-reference exists, so this is a suppressed-by-default lint. Even with `lld` (which tolerates
/// back-references) the risky ordering is the same, so it is flagged too — that way the lint
/// surfaces the latent issue before a switch back to `ld.bfd`. See
/// <https://github.com/rust-lang/rust/issues/154975>.
fn warn_static_archive_order(sess: &Session, crate_info: &CrateInfo, flavor: LinkerFlavor) {
// `--as-needed` is only emitted for GNU non-Windows linkers (`GccLinker::add_as_needed`);
// Darwin uses a different model.
if !flavor.is_gnu() || sess.target.is_like_windows || sess.target.is_like_darwin {
return;
}

let static_archives: Vec<&String> = sess
.opts
.cg
.link_args
.iter()
.filter(|arg| {
let p = Path::new(arg);
matches!(p.extension().and_then(|e| e.to_str()), Some("a" | "o"))
})
.collect();

if static_archives.is_empty() {
return;
}

let levels = &crate_info.lint_level_specs;
for archive in static_archives {
emit_lint_base(
sess,
LINKER_STATIC_ARCHIVE_ORDER,
levels.linker_static_archive_order,
None,
diagnostics::LinkerStaticArchiveOrder { archive: archive.as_str() },
);
}
}

/// Add arbitrary "late link" args defined by the target spec.
/// FIXME: Determine where exactly these args need to be inserted.
fn add_late_link_args(
Expand Down
15 changes: 15 additions & 0 deletions compiler/rustc_codegen_ssa/src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1377,3 +1377,18 @@ impl<'a> MissingNativeLibrary<'a> {
pub(crate) struct SuggestLibraryName<'a> {
suggested_name: &'a str,
}

#[derive(Diagnostic)]
#[diag(
"static archive `{$archive}` passed via `-Clink-arg` may be ordered before a dynamic \
library it references"
)]
#[note(
"rustc appends `-Clink-arg` arguments after its own native libraries; with `--as-needed`, \
linkers that resolve symbols strictly left-to-right (such as GNU `ld.bfd`) may drop a dynamic \
library that only a static archive to its left references"
)]
#[help("use `-l static={$archive}` so the archive is placed with the other native libraries")]
pub(crate) struct LinkerStaticArchiveOrder<'a> {
pub archive: &'a str,
}
5 changes: 4 additions & 1 deletion compiler/rustc_codegen_ssa/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use rustc_data_structures::unord::UnordMap;
use rustc_hir::CRATE_HIR_ID;
use rustc_hir::attrs::{CfgEntry, NativeLibKind, WindowsSubsystemKind};
use rustc_hir::def_id::CrateNum;
use rustc_lint_defs::builtin::LINKER_INFO;
use rustc_lint_defs::builtin::{LINKER_INFO, LINKER_STATIC_ARCHIVE_ORDER};
use rustc_macros::{Decodable, Encodable};
use rustc_metadata::EncodedMetadata;
use rustc_middle::dep_graph::WorkProduct;
Expand Down Expand Up @@ -421,13 +421,16 @@ impl CompiledModules {
pub struct CodegenLintLevelSpecs {
linker_messages: StableLevelSpec,
linker_info: StableLevelSpec,
linker_static_archive_order: StableLevelSpec,
}

impl CodegenLintLevelSpecs {
pub fn from_tcx(tcx: TyCtxt<'_>) -> Self {
Self {
linker_messages: tcx.lint_level_spec_at_node(LINKER_MESSAGES, CRATE_HIR_ID),
linker_info: tcx.lint_level_spec_at_node(LINKER_INFO, CRATE_HIR_ID),
linker_static_archive_order: tcx
.lint_level_spec_at_node(LINKER_STATIC_ARCHIVE_ORDER, CRATE_HIR_ID),
}
}
}
36 changes: 36 additions & 0 deletions compiler/rustc_lint_defs/src/builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ pub mod hardwired {
LEGACY_DERIVE_HELPERS,
LINKER_INFO,
LINKER_MESSAGES,
LINKER_STATIC_ARCHIVE_ORDER,
LONG_RUNNING_CONST_EVAL,
MACRO_EXPANDED_MACRO_EXPORTS_ACCESSED_BY_ABSOLUTE_PATHS,
MACRO_USE_EXTERN_CRATE,
Expand Down Expand Up @@ -4089,6 +4090,41 @@ declare_lint! {
"linker warnings known to be informational-only and not indicative of a problem"
}

declare_lint! {
/// The `linker_static_archive_order` lint detects static archives (`.a`/`.o`) passed via
/// `-Clink-arg` instead of the idiomatic `-l static=` option, which places the archive with
/// rustc's other native libraries.
///
/// ### Example
///
/// ```rust,ignore (needs CLI args, platform-specific)
/// // rustc -Clink-arg=/path/to/libfoo.a foo.rs
/// fn main() {}
/// ```
///
/// `-Clink-arg` arguments are appended after those libraries, so the archive can end up ordered
/// before a dynamic library it references. Under `--as-needed`, strict left-to-right linkers
/// such as GNU `ld.bfd` may then drop that library, causing undefined references. `lld`
/// tolerates this, masking the problem; the lint still fires there so the issue surfaces before
/// a switch back to `ld.bfd`.
///
/// ### Explanation
///
/// This lint is heuristic: without symbol resolution, rustc can't know whether the archive
/// actually back-references a later dynamic library, so it may fire on link orders that succeed.
/// It is allowed by default so that `-D warnings` does not turn a possible false positive into a
/// hard error. See <https://github.com/rust-lang/rust/issues/154975>.
pub LINKER_STATIC_ARCHIVE_ORDER,
Allow,
"static archive passed via `-Clink-arg` may be ordered before a dynamic library it references, \
which `--as-needed` linkers such as GNU `ld.bfd` can then drop",
// The lint is heuristic: rustc can't know whether a back-reference actually exists, so the
// diagnostic may fire on link orders that link successfully. Prevent `-D warnings` from
// turning a possible false positive into a hard error. `-D linker-static-archive-order` still
// applies.

@bjorn3 bjorn3 Aug 11, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Even if it isn't strictly needed, I think the lint should still fire.

View changes since the review

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

OK.

ignore_deny_warnings
}

declare_lint! {
/// The `named_arguments_used_positionally` lint detects cases where named arguments are only
/// used positionally in format strings. This usage is valid but potentially very confusing.
Expand Down
1 change: 1 addition & 0 deletions tests/run-make/link-arg-static-archive-order/empty.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
fn main() {}
117 changes: 117 additions & 0 deletions tests/run-make/link-arg-static-archive-order/rmake.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Tests for the `linker_static_archive_order` lint.
// See <https://github.com/rust-lang/rust/issues/154975>.

//@ only-linux
// The lint applies to GNU `ld`-family linkers (including `lld`) on non-Windows, non-Darwin.

use run_make_support::{cwd, is_darwin, is_windows, rustc};

fn main() {
// The diagnostic carries the full archive path, so assert on the stable filename substring.
let archive = cwd().join("libdoesnotexist.a");
let archive_str = archive.to_str().unwrap();

// 1. A `.a` path via `-Clink-arg` on a bare `ld` flavor fires the lint.
rustc()
.input("empty.rs")
.linker_flavor("ld")
.link_arg(archive_str)
.arg("-Wlinker-static-archive-order")
.print("link-args")
.run_unchecked()
.assert_stderr_contains("warning: static archive")
.assert_stderr_contains("libdoesnotexist.a");

// 2. A `.o` path is flagged the same way.
let object = cwd().join("doesnotexist.o");
rustc()
.input("empty.rs")
.linker_flavor("ld")
.link_arg(object.to_str().unwrap())
.arg("-Wlinker-static-archive-order")
.print("link-args")
.run_unchecked()
.assert_stderr_contains("warning: static archive")
.assert_stderr_contains("doesnotexist.o");

// 3. A non-archive arg like `-lm` does not fire.
rustc()
.input("empty.rs")
.linker_flavor("ld")
.link_arg("-lm")
.arg("-Wlinker-static-archive-order")
.print("link-args")
.run_unchecked()
.assert_stderr_not_contains("linker_static_archive_order");

// 4. `Allow` by default: no `-W`, no output.
rustc()
.input("empty.rs")
.linker_flavor("ld")
.link_arg(archive_str)
.print("link-args")
.run_unchecked()
.assert_stderr_not_contains("linker_static_archive_order");

// 5. `ignore_deny_warnings`: `-Dwarnings` does not promote the `-W` to an error.
rustc()
.input("empty.rs")
.linker_flavor("ld")
.link_arg(archive_str)
.arg("-Wlinker-static-archive-order")
.arg("-Dwarnings")
.print("link-args")
.run_unchecked()
.assert_stderr_contains("warning: static archive")
.assert_stderr_contains("the `linker_static_archive_order` lint ignores `-D warnings`");

// 6. `-Dlinker-static-archive-order` (specific) does promote to an error.
rustc()
.input("empty.rs")
.linker_flavor("ld")
.link_arg(archive_str)
.arg("-Dlinker-static-archive-order")
.print("link-args")
.run_fail()
.assert_stderr_contains("error: static archive")
.assert_stderr_contains("libdoesnotexist.a");

// 7. `#![allow]` suppresses even an explicit `-W`.
rustc()
.input("-")
.linker_flavor("ld")
.link_arg(archive_str)
.arg("-Wlinker-static-archive-order")
.arg("--crate-type=lib")
.stdin_buf("#![allow(linker_static_archive_order)] fn main() {}")
.print("link-args")
.run_unchecked()
.assert_stderr_not_contains("static archive");

// 8. The `gnu-cc` flavor (cc-driven, `Gnu(Cc::Yes, Lld::No)`) also fires: it uses real `ld`
// behind `cc` and the same `--as-needed` + left-to-right interaction applies.
rustc()
.input("empty.rs")
.arg("-Zunstable-options")
.linker_flavor("gnu-cc")
.link_arg(archive_str)
.arg("-Wlinker-static-archive-order")
.print("link-args")
.run_unchecked()
.assert_stderr_contains("warning: static archive")
.assert_stderr_contains("libdoesnotexist.a");

// 9. The lint also fires with `lld` (`Gnu(.., Lld::Yes)`): the risky ordering is the same, so
// the latent issue surfaces before a switch back to `ld.bfd`.
rustc()
.input("empty.rs")
.linker_flavor("ld.lld")
.link_arg(archive_str)
.arg("-Wlinker-static-archive-order")
.print("link-args")
.run_unchecked()
.assert_stderr_contains("warning: static archive")
.assert_stderr_contains("libdoesnotexist.a");

assert!(!is_windows() && !is_darwin());
}
Loading