Skip to content

Commit d7bcfdb

Browse files
Rollup merge of rust-lang#155338 - cezarbbb:staticlib-symbol-hygiene, r=petrochenkov
Staticlib hide internal symbols According to issue rust-lang#104707, when building a staticlib, all Rust internal symbols — mangled symbols, `#[rustc_std_internal_symbol]` items, allocator shims, etc. — leak out of the static archive. In contrast, cdylib correctly exports only `#[no_mangle]` symbols via a linker version script. `-Zstaticlib-hide-internal-symbols` directly post-processes ELF object files in the archive: parsing the `SHT_SYMTAB` sections and setting `STV_HIDDEN` visibility on any `GLOBAL/WEAK` defined symbol that is not in the exported symbol set, without changing the binding. This is an in-place modification (only writing the st_other byte per matching entry), with zero overhead. Supported on ELF targets (Linux, BSD, etc.) and Apple targets (macOS, iOS, etc.). On unsupported targets (Windows), a warning is emitted and the flag has no effect. **Update**: The rename counterpart (`-Zstaticlib-rename-internal-symbols`) is in rust-lang#156950. The test code are as follows: 1.a std rust staticlib: ```rust use std::collections::HashMap; use std::panic::{catch_unwind, AssertUnwindSafe}; #[no_mangle] pub extern "C" fn my_add(a: i32, b: i32) -> i32 { a + b } #[no_mangle] pub extern "C" fn my_hash_lookup(key: u64) -> u64 { let mut map = HashMap::new(); for i in 0..100u64 { map.insert(i, i.wrapping_mul(2654435761)); } *map.get(&key).unwrap_or(&0) } pub fn internal_reverse(s: &str) -> String { s.chars().rev().collect() } #[no_mangle] pub extern "C" fn my_format_number(n: i32) -> i32 { let s = format!("number: {}", n); s.len() as i32 } #[no_mangle] pub extern "C" fn my_safe_div(a: i32, b: i32) -> i32 { match catch_unwind(AssertUnwindSafe(|| { if b == 0 { panic!("division by zero!"); } a / b })) { Ok(result) => result, Err(_) => -1, } } #[no_mangle] pub extern "C" fn my_uncaught_panic() { panic!("uncaught panic across FFI"); } ``` 1.b downstream c program: ```c extern int my_add(int a, int b); extern unsigned long my_hash_lookup(unsigned long key); extern int my_format_number(int n); extern int my_safe_div(int a, int b); extern void my_uncaught_panic(void); int main() { int failures = 0; if (my_add(10, 20) != 30) failures++; if (my_hash_lookup(5) != 5UL * 2654435761UL) failures++; if (my_format_number(42) != 10) failures++; if (my_safe_div(100, 5) != 20) failures++; if (my_safe_div(100, 0) != -1) failures++; pid_t pid = fork(); if (pid == 0) { alarm(5); my_uncaught_panic(); _exit(0); } else { waitpid(pid, &status, 0); } return failures; } ``` The test results with different compiler flags(which might cause binary size reduction) are as follows: 1.c result with `-Zstaticlib-hide-internal-symbols` ``` settings OFF ON -Zsave ALL OFF.dynsym ON.dynsym ------------------------------------------------------------------------ default 1.7M 1.5M 204K (12%) 1735 5 1730 lto_thin 616K 584K 33K (5%) 246 5 241 lto_fat 525K 525K 0 (0%) 6 5 1 opt_s 1.7M 1.5M 204K (12%) 1735 5 1730 opt_z 1.7M 1.5M 204K (12%) 1735 5 1730 lto_thin_z 602K 570K 32K (5%) 246 5 241 lto_fat_z 514K 514K 0 (0%) 6 5 1 full 514K 514K 0 (0%) 6 5 1 ``` 1.d result with `-Zstaticlib-hide-internal-symbols + -Zstaticlib-rename-internal-symbols` ``` settings OFF ON -Zsave ALL OFF.dynsym ON.dynsym ------------------------------------------------------------------------ default 1.7M 1.5M 162K (9%) 1735 5 1730 lto_thin 616K 599K 18K (2%) 246 5 241 lto_fat 525K 535K -1% (-1%) 6 5 1 opt_s 1.7M 1.5M 162K (9%) 1735 5 1730 opt_z 1.7M 1.5M 162K (9%) 1735 5 1730 lto_thin_z 602K 585K 18K (2%) 246 5 241 lto_fat_z 514K 524K -1% (-1%) 6 5 1 full 514K 523K -1% (-1%) 6 5 1 ``` 2.a no_std rust staticlib ```rust #![no_std] #![feature(core_intrinsics)] use core::panic::PanicInfo; #[panic_handler] fn panic(_info: &PanicInfo) -> ! { loop {} } #[no_mangle] pub extern "C" fn embedded_add(a: i32, b: i32) -> i32 { a.wrapping_add(b) } #[no_mangle] pub extern "C" fn embedded_checksum(data: *const u8, len: usize) -> u8 { if data.is_null() { return 0; } let slice = unsafe { core::slice::from_raw_parts(data, len) }; let mut sum: u8 = 0; for &byte in slice { sum = sum.wrapping_add(byte); } sum } fn internal_helper() -> i32 { 42 } #[no_mangle] pub extern "C" fn call_internal() -> i32 { internal_helper() } #[no_mangle] pub extern "C" fn embedded_trigger_abort() { core::intrinsics::abort(); } ``` 2.b downstream c program ```c extern int embedded_add(int a, int b); extern unsigned char embedded_checksum(const unsigned char *data, unsigned long len); extern int call_internal(void); extern void embedded_trigger_abort(void); int main() { int failures = 0; if (embedded_add(10, 20) != 30) failures++; unsigned char data[] = {1, 2, 3}; if (embedded_checksum(data, 3) != 6) failures++; if (call_internal() != 42) failures++; pid_t pid = fork(); if (pid == 0) { embedded_trigger_abort(); _exit(0); } else { waitpid(pid, &status, 0); } return failures; } ``` The test results with different compiler flags(which might cause binary size reduction) are as follows: 2.c result with `-Zstaticlib-hide-internal-symbols` ``` settings OFF ON -Zsave ALL OFF.dynsym ON.dynsym ------------------------------------------------------------------------ default 485K 429K 56K (11%) 490 4 486 lto_thin 180K 180K 0 (0%) 4 4 0 lto_fat 179K 179K 0 (0%) 4 4 0 opt_s 485K 429K 56K (11%) 490 4 486 opt_z 485K 429K 56K (11%) 490 4 486 lto_thin_z 180K 180K 0 (0%) 4 4 0 lto_fat_z 179K 179K 0 (0%) 4 4 0 full 179K 179K 0 (0%) 4 4 0 ``` 2.d result with `-Zstaticlib-hide-internal-symbols + -Zstaticlib-rename-internal-symbols` ``` settings OFF ON -Zsave ALL OFF.dynsym ON.dynsym ------------------------------------------------------------------------ default 485K 447K 39K (7%) 490 4 486 lto_thin 180K 189K -5% (-5%) 4 4 0 lto_fat 179K 189K -5% (-5%) 4 4 0 opt_s 485K 448K 38K (7%) 490 4 486 opt_z 485K 448K 38K (7%) 490 4 486 lto_thin_z 180K 189K -5% (-5%) 4 4 0 lto_fat_z 179K 189K -5% (-5%) 4 4 0 full 179K 189K -5% (-5%) 4 4 0 ``` Test results show that this compiler option is beneficial for scenarios where LTO cannot be enabled. r? @bjorn3 @petrochenkov
2 parents 3898939 + 6caae67 commit d7bcfdb

14 files changed

Lines changed: 593 additions & 44 deletions

File tree

compiler/rustc_codegen_ssa/src/back/archive.rs

Lines changed: 205 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1-
use std::env;
21
use std::error::Error;
32
use std::ffi::OsString;
43
use std::fs::{self, File};
54
use std::io::{self, BufWriter, Write};
65
use std::path::{Path, PathBuf};
6+
use std::{env, mem};
77

88
use ar_archive_writer::{
99
ArchiveKind, COFFShortExport, MachineTypes, NewArchiveMember, write_archive_to_stream,
1010
};
1111
pub use ar_archive_writer::{DEFAULT_OBJECT_READER, ObjectReader};
1212
use object::read::archive::{ArchiveFile, ArchiveKind as ObjectArchiveKind};
13-
use object::read::macho::FatArch;
14-
use rustc_data_structures::fx::FxIndexSet;
13+
use object::read::elf::Sym as _;
14+
use object::read::macho::{FatArch, Nlist};
15+
use object::{Endianness, elf, macho};
16+
use rustc_data_structures::fx::{FxHashSet, FxIndexSet};
1517
use rustc_data_structures::memmap::Mmap;
1618
use rustc_fs_util::TempDirBuilder;
1719
use rustc_metadata::EncodedMetadata;
@@ -318,7 +320,7 @@ pub trait ArchiveBuilder {
318320

319321
fn add_archive(&mut self, archive: &Path, kind: AddArchiveKind<'_>) -> io::Result<()>;
320322

321-
fn build(self: Box<Self>, output: &Path) -> bool;
323+
fn build(self: Box<Self>, output: &Path, exported_symbols: Option<FxHashSet<String>>) -> bool;
322324
}
323325

324326
fn target_archive_format_to_object_kind(format: &str) -> Option<ObjectArchiveKind> {
@@ -401,7 +403,6 @@ enum ArchiveEntrySource {
401403
#[derive(Debug)]
402404
struct ArchiveEntry {
403405
source: ArchiveEntrySource,
404-
#[expect(dead_code)] // used in #155338
405406
kind: ArchiveEntryKind,
406407
}
407408

@@ -534,9 +535,9 @@ impl<'a> ArchiveBuilder for ArArchiveBuilder<'a> {
534535

535536
/// Combine the provided files, rlibs, and native libraries into a single
536537
/// `Archive`.
537-
fn build(self: Box<Self>, output: &Path) -> bool {
538+
fn build(self: Box<Self>, output: &Path, exported_symbols: Option<FxHashSet<String>>) -> bool {
538539
let sess = self.sess;
539-
match self.build_inner(output) {
540+
match self.build_inner(output, exported_symbols) {
540541
Ok(any_members) => any_members,
541542
Err(error) => {
542543
sess.dcx().emit_fatal(ArchiveBuildFailure { path: output.to_owned(), error })
@@ -546,7 +547,11 @@ impl<'a> ArchiveBuilder for ArArchiveBuilder<'a> {
546547
}
547548

548549
impl<'a> ArArchiveBuilder<'a> {
549-
fn build_inner(self, output: &Path) -> io::Result<bool> {
550+
fn build_inner(
551+
self,
552+
output: &Path,
553+
exported_symbols: Option<FxHashSet<String>>,
554+
) -> io::Result<bool> {
550555
let archive_kind = match &*self.sess.target.archive_format {
551556
"gnu" => ArchiveKind::Gnu,
552557
"bsd" => ArchiveKind::Bsd,
@@ -561,40 +566,51 @@ impl<'a> ArArchiveBuilder<'a> {
561566
let mut entries = Vec::new();
562567

563568
for (entry_name, entry) in self.entries {
564-
let data =
565-
match entry.source {
566-
ArchiveEntrySource::Archive { archive_index, file_range } => {
567-
let src_archive = &self.src_archives[archive_index];
568-
let archive_data = &src_archive.1;
569-
let start = file_range.0 as usize;
570-
let end = start + file_range.1 as usize;
571-
let Some(data) = archive_data.get(start..end) else {
572-
return Err(io_error_context(
573-
"invalid archive member",
574-
io::Error::new(
575-
io::ErrorKind::InvalidData,
576-
format!(
577-
"archive member at offset {start} with size {} \
569+
let data: Box<dyn AsRef<[u8]>> = match entry.source {
570+
ArchiveEntrySource::Archive { archive_index, file_range } => {
571+
let src_archive = &self.src_archives[archive_index];
572+
let archive_data = &src_archive.1;
573+
let start = file_range.0 as usize;
574+
let end = start + file_range.1 as usize;
575+
let Some(data) = archive_data.get(start..end) else {
576+
return Err(io_error_context(
577+
"invalid archive member",
578+
io::Error::new(
579+
io::ErrorKind::InvalidData,
580+
format!(
581+
"archive member at offset {start} with size {} \
578582
exceeds archive size {} in `{}`",
579-
file_range.1,
580-
archive_data.len(),
581-
src_archive.0.display(),
582-
),
583+
file_range.1,
584+
archive_data.len(),
585+
src_archive.0.display(),
583586
),
584-
));
585-
};
586-
587-
Box::new(data) as Box<dyn AsRef<[u8]>>
587+
),
588+
));
589+
};
590+
591+
if entry.kind == ArchiveEntryKind::RustObj
592+
&& let Some(exported) = &exported_symbols
593+
{
594+
Box::new(apply_hide(data, exported))
595+
} else {
596+
Box::new(data)
588597
}
589-
ArchiveEntrySource::File(file) => unsafe {
590-
Box::new(
591-
Mmap::map(File::open(file).map_err(|err| {
592-
io_error_context("failed to open object file", err)
593-
})?)
594-
.map_err(|err| io_error_context("failed to map object file", err))?,
595-
) as Box<dyn AsRef<[u8]>>
596-
},
597-
};
598+
}
599+
ArchiveEntrySource::File(file) => unsafe {
600+
let mmap = Mmap::map(
601+
File::open(file)
602+
.map_err(|err| io_error_context("failed to open object file", err))?,
603+
)
604+
.map_err(|err| io_error_context("failed to map object file", err))?;
605+
if entry.kind == ArchiveEntryKind::RustObj
606+
&& let Some(exported) = &exported_symbols
607+
{
608+
Box::new(apply_hide(&mmap, exported))
609+
} else {
610+
Box::new(mmap) as Box<dyn AsRef<[u8]>>
611+
}
612+
},
613+
};
598614

599615
entries.push(NewArchiveMember {
600616
buf: data,
@@ -655,3 +671,153 @@ impl<'a> ArArchiveBuilder<'a> {
655671
fn io_error_context(context: &str, err: io::Error) -> io::Error {
656672
io::Error::new(io::ErrorKind::Other, format!("{context}: {err}"))
657673
}
674+
675+
// We use the `object` crate for the read-only pass over ELF/Mach-O object files
676+
// because its `Sym`/`Nlist` traits provide clean access to symbol properties without
677+
// manual byte parsing. However, `object` does not expose mutable views into the data,
678+
// so we cannot use it to modify symbol fields in place. Instead, the read-only pass
679+
// collects byte-level patches (offset + new value), and the write pass
680+
// (`apply_patches`) applies them to a copy of the byte buffer without any ELF/Mach-O
681+
// parsing — similar to how linker relocations work.
682+
683+
/// A byte-level patch collected in the read-only pass and applied in the write pass.
684+
struct Patch {
685+
offset: usize,
686+
value: u8,
687+
}
688+
689+
/// Apply a list of byte patches to `data`, returning the (possibly modified) bytes.
690+
fn apply_patches(data: &[u8], patches: &[Patch]) -> Vec<u8> {
691+
let mut buf = data.to_vec();
692+
for p in patches {
693+
buf[p.offset] = p.value;
694+
}
695+
buf
696+
}
697+
698+
// ---------------------------------------------------------------------------
699+
// ELF hide – read-only pass uses `object` crate, write pass uses `Patch` list
700+
// ---------------------------------------------------------------------------
701+
702+
fn elf_hide_patches_impl<'data, Elf: object::read::elf::FileHeader<Endian = Endianness>>(
703+
data: &'data [u8],
704+
st_other_offset: usize,
705+
exported: &FxHashSet<String>,
706+
) -> Option<Vec<Patch>>
707+
where
708+
u64: From<Elf::Word>,
709+
{
710+
let header = Elf::parse(data).ok()?;
711+
let endian = header.endian().ok()?;
712+
let sections = header.sections(endian, data).ok()?;
713+
let symtab = sections.symbols(endian, data, elf::SHT_SYMTAB).ok()?;
714+
715+
let data_ptr = data.as_ptr() as usize;
716+
let strings = symtab.strings();
717+
let mut patches = Vec::new();
718+
719+
for sym in symtab.iter() {
720+
let binding = sym.st_bind();
721+
if binding != elf::STB_GLOBAL && binding != elf::STB_WEAK {
722+
continue;
723+
}
724+
if sym.is_undefined(endian) {
725+
continue;
726+
}
727+
let Ok(name_bytes) = sym.name(endian, strings) else { continue };
728+
let Ok(name) = str::from_utf8(name_bytes) else { continue };
729+
if !exported.contains(name) {
730+
let sym_addr = sym as *const Elf::Sym as usize;
731+
let offset = sym_addr - data_ptr + st_other_offset;
732+
let new_vis = (sym.st_other() & !0x03) | elf::STV_HIDDEN;
733+
patches.push(Patch { offset, value: new_vis });
734+
}
735+
}
736+
737+
Some(patches)
738+
}
739+
740+
// ---------------------------------------------------------------------------
741+
// Mach-O hide – same architecture: read-only pass via `object`, write via patches
742+
// ---------------------------------------------------------------------------
743+
744+
fn macho_hide_patches_impl<'data, Mach: object::read::macho::MachHeader<Endian = Endianness>>(
745+
data: &'data [u8],
746+
n_type_offset: usize,
747+
exported: &FxHashSet<String>,
748+
) -> Option<Vec<Patch>> {
749+
let header = Mach::parse(data, 0).ok()?;
750+
let endian = header.endian().ok()?;
751+
let mut commands = header.load_commands(endian, data, 0).ok()?;
752+
753+
let symtab_cmd = loop {
754+
let cmd = commands.next().ok()??;
755+
if let Some(st) = cmd.symtab().ok().flatten() {
756+
break st;
757+
}
758+
};
759+
let symtab: object::read::macho::SymbolTable<'_, Mach, &_> =
760+
symtab_cmd.symbols(endian, data).ok()?;
761+
762+
let data_ptr = data.as_ptr() as usize;
763+
let strings = symtab.strings();
764+
let mut patches = Vec::new();
765+
766+
for nlist in symtab.iter() {
767+
if nlist.is_stab() {
768+
continue;
769+
}
770+
if nlist.is_undefined() {
771+
continue;
772+
}
773+
if nlist.n_type() & macho::N_EXT == 0 {
774+
continue;
775+
}
776+
let Ok(name_bytes) = nlist.name(endian, strings) else { continue };
777+
let Ok(name) = str::from_utf8(name_bytes) else { continue };
778+
let name = name.strip_prefix('_').unwrap_or(name);
779+
if !exported.contains(name) {
780+
let nlist_addr = nlist as *const Mach::Nlist as usize;
781+
let offset = nlist_addr - data_ptr + n_type_offset;
782+
patches.push(Patch { offset, value: nlist.n_type() | macho::N_PEXT });
783+
}
784+
}
785+
786+
Some(patches)
787+
}
788+
789+
// ---------------------------------------------------------------------------
790+
// Unified dispatch: top-level detection via `object::File::parse`
791+
// ---------------------------------------------------------------------------
792+
793+
fn hide_patches(data: &[u8], exported: &FxHashSet<String>) -> Option<Vec<Patch>> {
794+
let file = object::File::parse(data).ok()?;
795+
match file {
796+
object::File::Elf64(_) => elf_hide_patches_impl::<elf::FileHeader64<Endianness>>(
797+
data,
798+
mem::offset_of!(elf::Sym64<Endianness>, st_other),
799+
exported,
800+
),
801+
object::File::Elf32(_) => elf_hide_patches_impl::<elf::FileHeader32<Endianness>>(
802+
data,
803+
mem::offset_of!(elf::Sym32<Endianness>, st_other),
804+
exported,
805+
),
806+
object::File::MachO64(_) => macho_hide_patches_impl::<macho::MachHeader64<Endianness>>(
807+
data,
808+
mem::offset_of!(macho::Nlist64<Endianness>, n_type),
809+
exported,
810+
),
811+
object::File::MachO32(_) => macho_hide_patches_impl::<macho::MachHeader32<Endianness>>(
812+
data,
813+
mem::offset_of!(macho::Nlist32<Endianness>, n_type),
814+
exported,
815+
),
816+
_ => None,
817+
}
818+
}
819+
820+
fn apply_hide(data: &[u8], exported: &FxHashSet<String>) -> Vec<u8> {
821+
let patches = hide_patches(data, exported).unwrap_or_default();
822+
apply_patches(data, &patches)
823+
}

compiler/rustc_codegen_ssa/src/back/link.rs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ pub fn link_binary(
131131
RlibFlavor::Normal,
132132
&path,
133133
)
134-
.build(&out_filename);
134+
.build(&out_filename, None);
135135
}
136136
CrateType::StaticLib => {
137137
link_staticlib(
@@ -566,7 +566,22 @@ fn link_staticlib(
566566
sess.dcx().emit_fatal(e);
567567
}
568568

569-
ab.build(out_filename);
569+
let exported_symbols = if sess.opts.unstable_opts.staticlib_hide_internal_symbols {
570+
if !matches!(sess.target.binary_format, BinaryFormat::Elf | BinaryFormat::MachO) {
571+
sess.dcx().emit_warn(errors::StaticlibHideInternalSymbolsUnsupported {
572+
binary_format: sess.target.archive_format.to_string(),
573+
});
574+
None
575+
} else {
576+
crate_info
577+
.exported_symbols
578+
.get(&CrateType::StaticLib)
579+
.map(|symbols| symbols.iter().map(|(s, _)| s.clone()).collect())
580+
}
581+
} else {
582+
None
583+
};
584+
ab.build(out_filename, exported_symbols);
570585

571586
let crates = crate_info.used_crates.iter();
572587

@@ -1265,7 +1280,7 @@ fn link_natively(
12651280
if should_archive {
12661281
let mut ab = archive_builder_builder.new_archive_builder(sess);
12671282
ab.add_file(temp_filename, ArchiveEntryKind::Other);
1268-
ab.build(out_filename);
1283+
ab.build(out_filename, None);
12691284
}
12701285
}
12711286

@@ -3265,7 +3280,7 @@ fn add_static_crate(
32653280
sess.dcx()
32663281
.emit_fatal(errors::RlibArchiveBuildFailure { path: cratepath.clone(), error });
32673282
}
3268-
if archive.build(&dst) {
3283+
if archive.build(&dst, None) {
32693284
link_upstream(&dst);
32703285
}
32713286
});

compiler/rustc_codegen_ssa/src/errors.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,14 @@ pub(crate) struct IncompatibleArchiveFormat {
693693
#[diag("linking static libraries is not supported for BPF")]
694694
pub(crate) struct BpfStaticlibNotSupported;
695695

696+
#[derive(Diagnostic)]
697+
#[diag(
698+
"-Zstaticlib-hide-internal-symbols only supports ELF and Mach-O targets, but the target uses `{$binary_format}`"
699+
)]
700+
pub(crate) struct StaticlibHideInternalSymbolsUnsupported {
701+
pub binary_format: String,
702+
}
703+
696704
#[derive(Diagnostic)]
697705
#[diag("entry symbol `main` declared multiple times")]
698706
#[help(

compiler/rustc_interface/src/tests.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -866,6 +866,7 @@ fn test_unstable_options_tracking_hash() {
866866
tracked!(split_lto_unit, Some(true));
867867
tracked!(src_hash_algorithm, Some(SourceFileHashAlgorithm::Sha1));
868868
tracked!(stack_protector, StackProtector::All);
869+
tracked!(staticlib_hide_internal_symbols, true);
869870
tracked!(teach, true);
870871
tracked!(thinlto, Some(true));
871872
tracked!(tiny_const_eval_limit, true);

compiler/rustc_interface/src/util.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,7 @@ impl ArchiveBuilderBuilder for DummyArchiveBuilderBuilder {
470470
output_path: &Path,
471471
) {
472472
// Build an empty static library to avoid calling an external dlltool on mingw
473-
ArArchiveBuilderBuilder.new_archive_builder(sess).build(output_path);
473+
ArArchiveBuilderBuilder.new_archive_builder(sess).build(output_path, None);
474474
}
475475
}
476476

compiler/rustc_session/src/config.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2465,6 +2465,14 @@ pub fn build_session_options(early_dcx: &mut EarlyDiagCtxt, matches: &getopts::M
24652465
let mut collected_options = Default::default();
24662466

24672467
let mut unstable_opts = UnstableOptions::build(early_dcx, matches, &mut collected_options);
2468+
2469+
if unstable_opts.staticlib_hide_internal_symbols && !crate_types.contains(&CrateType::StaticLib)
2470+
{
2471+
early_dcx.early_warn(
2472+
"-Zstaticlib-hide-internal-symbols has no effect without `--crate-type staticlib`",
2473+
);
2474+
}
2475+
24682476
let (lint_opts, describe_lints, lint_cap) = get_cmd_lint_options(early_dcx, matches);
24692477

24702478
if !unstable_opts.unstable_options && json_timings {

0 commit comments

Comments
 (0)