rustc_codegen_ssa/back/
archive.rs

1use std::env;
2use std::error::Error;
3use std::ffi::OsString;
4use std::fs::{self, File};
5use std::io::{self, BufWriter, Write};
6use std::path::{Path, PathBuf};
7
8use ar_archive_writer::{
9    ArchiveKind, COFFShortExport, MachineTypes, NewArchiveMember, write_archive_to_stream,
10};
11pub use ar_archive_writer::{DEFAULT_OBJECT_READER, ObjectReader};
12use object::read::archive::ArchiveFile;
13use object::read::macho::FatArch;
14use rustc_data_structures::fx::FxIndexSet;
15use rustc_data_structures::memmap::Mmap;
16use rustc_fs_util::TempDirBuilder;
17use rustc_metadata::EncodedMetadata;
18use rustc_session::Session;
19use rustc_span::Symbol;
20use rustc_target::spec::Arch;
21use tracing::trace;
22
23use super::metadata::{create_compressed_metadata_file, search_for_section};
24use crate::common;
25// Re-exporting for rustc_codegen_llvm::back::archive
26pub use crate::errors::{ArchiveBuildFailure, ExtractBundledLibsError, UnknownArchiveKind};
27use crate::errors::{
28    DlltoolFailImportLibrary, ErrorCallingDllTool, ErrorCreatingImportLibrary, ErrorWritingDEFFile,
29};
30
31/// An item to be included in an import library.
32/// This is a slimmed down version of `COFFShortExport` from `ar-archive-writer`.
33pub struct ImportLibraryItem {
34    /// The name to be exported.
35    pub name: String,
36    /// The ordinal to be exported, if any.
37    pub ordinal: Option<u16>,
38    /// The original, decorated name if `name` is not decorated.
39    pub symbol_name: Option<String>,
40    /// True if this is a data export, false if it is a function export.
41    pub is_data: bool,
42}
43
44impl ImportLibraryItem {
45    fn into_coff_short_export(self, sess: &Session) -> COFFShortExport {
46        let import_name = (sess.target.arch == Arch::Arm64EC).then(|| self.name.clone());
47        COFFShortExport {
48            name: self.name,
49            ext_name: None,
50            symbol_name: self.symbol_name,
51            import_name,
52            export_as: None,
53            ordinal: self.ordinal.unwrap_or(0),
54            noname: self.ordinal.is_some(),
55            data: self.is_data,
56            private: false,
57            constant: false,
58        }
59    }
60}
61
62pub trait ArchiveBuilderBuilder {
63    fn new_archive_builder<'a>(&self, sess: &'a Session) -> Box<dyn ArchiveBuilder + 'a>;
64
65    fn create_dylib_metadata_wrapper(
66        &self,
67        sess: &Session,
68        metadata: &EncodedMetadata,
69        symbol_name: &str,
70    ) -> Vec<u8> {
71        create_compressed_metadata_file(sess, metadata, symbol_name)
72    }
73
74    /// Creates a DLL Import Library <https://docs.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-creation#creating-an-import-library>.
75    /// and returns the path on disk to that import library.
76    /// This functions doesn't take `self` so that it can be called from
77    /// `linker_with_args`, which is specialized on `ArchiveBuilder` but
78    /// doesn't take or create an instance of that type.
79    fn create_dll_import_lib(
80        &self,
81        sess: &Session,
82        lib_name: &str,
83        items: Vec<ImportLibraryItem>,
84        output_path: &Path,
85    ) {
86        if common::is_mingw_gnu_toolchain(&sess.target) {
87            // The binutils linker used on -windows-gnu targets cannot read the import
88            // libraries generated by LLVM: in our attempts, the linker produced an .EXE
89            // that loaded but crashed with an AV upon calling one of the imported
90            // functions. Therefore, use binutils to create the import library instead,
91            // by writing a .DEF file to the temp dir and calling binutils's dlltool.
92            create_mingw_dll_import_lib(sess, lib_name, items, output_path);
93        } else {
94            trace!("creating import library");
95            trace!("  dll_name {:#?}", lib_name);
96            trace!("  output_path {}", output_path.display());
97            trace!(
98                "  import names: {}",
99                items
100                    .iter()
101                    .map(|ImportLibraryItem { name, .. }| name.clone())
102                    .collect::<Vec<_>>()
103                    .join(", "),
104            );
105
106            // All import names are Rust identifiers and therefore cannot contain \0 characters.
107            // FIXME: when support for #[link_name] is implemented, ensure that the import names
108            // still don't contain any \0 characters. Also need to check that the names don't
109            // contain substrings like " @" or "NONAME" that are keywords or otherwise reserved
110            // in definition files.
111
112            let mut file = match fs::File::create_new(&output_path) {
113                Ok(file) => file,
114                Err(error) => sess
115                    .dcx()
116                    .emit_fatal(ErrorCreatingImportLibrary { lib_name, error: error.to_string() }),
117            };
118
119            let exports =
120                items.into_iter().map(|item| item.into_coff_short_export(sess)).collect::<Vec<_>>();
121            let machine = match &sess.target.arch {
122                Arch::X86_64 => MachineTypes::AMD64,
123                Arch::X86 => MachineTypes::I386,
124                Arch::AArch64 => MachineTypes::ARM64,
125                Arch::Arm64EC => MachineTypes::ARM64EC,
126                Arch::Arm => MachineTypes::ARMNT,
127                cpu => panic!("unsupported cpu type {cpu}"),
128            };
129
130            if let Err(error) = ar_archive_writer::write_import_library(
131                &mut file,
132                lib_name,
133                &exports,
134                machine,
135                !sess.target.is_like_msvc,
136                // Enable compatibility with MSVC's `/WHOLEARCHIVE` flag.
137                // Without this flag a duplicate symbol error would be emitted
138                // when linking a rust staticlib using `/WHOLEARCHIVE`.
139                // See #129020
140                true,
141                &[],
142            ) {
143                sess.dcx()
144                    .emit_fatal(ErrorCreatingImportLibrary { lib_name, error: error.to_string() });
145            }
146        }
147    }
148
149    fn extract_bundled_libs<'a>(
150        &'a self,
151        rlib: &'a Path,
152        outdir: &Path,
153        bundled_lib_file_names: &FxIndexSet<Symbol>,
154    ) -> Result<(), ExtractBundledLibsError<'a>> {
155        let archive_map = unsafe {
156            Mmap::map(
157                File::open(rlib)
158                    .map_err(|e| ExtractBundledLibsError::OpenFile { rlib, error: Box::new(e) })?,
159            )
160            .map_err(|e| ExtractBundledLibsError::MmapFile { rlib, error: Box::new(e) })?
161        };
162        let archive = ArchiveFile::parse(&*archive_map)
163            .map_err(|e| ExtractBundledLibsError::ParseArchive { rlib, error: Box::new(e) })?;
164
165        for entry in archive.members() {
166            let entry = entry
167                .map_err(|e| ExtractBundledLibsError::ReadEntry { rlib, error: Box::new(e) })?;
168            let data = entry
169                .data(&*archive_map)
170                .map_err(|e| ExtractBundledLibsError::ArchiveMember { rlib, error: Box::new(e) })?;
171            let name = std::str::from_utf8(entry.name())
172                .map_err(|e| ExtractBundledLibsError::ConvertName { rlib, error: Box::new(e) })?;
173            if !bundled_lib_file_names.contains(&Symbol::intern(name)) {
174                continue; // We need to extract only native libraries.
175            }
176            let data = search_for_section(rlib, data, ".bundled_lib").map_err(|e| {
177                ExtractBundledLibsError::ExtractSection { rlib, error: Box::<dyn Error>::from(e) }
178            })?;
179            std::fs::write(&outdir.join(&name), data)
180                .map_err(|e| ExtractBundledLibsError::WriteFile { rlib, error: Box::new(e) })?;
181        }
182        Ok(())
183    }
184}
185
186fn create_mingw_dll_import_lib(
187    sess: &Session,
188    lib_name: &str,
189    items: Vec<ImportLibraryItem>,
190    output_path: &Path,
191) {
192    let def_file_path = output_path.with_extension("def");
193
194    let def_file_content = format!(
195        "EXPORTS\n{}",
196        items
197            .into_iter()
198            .map(|ImportLibraryItem { name, ordinal, .. }| {
199                match ordinal {
200                    Some(n) => format!("{name} @{n} NONAME"),
201                    None => name,
202                }
203            })
204            .collect::<Vec<String>>()
205            .join("\n")
206    );
207
208    match std::fs::write(&def_file_path, def_file_content) {
209        Ok(_) => {}
210        Err(e) => {
211            sess.dcx().emit_fatal(ErrorWritingDEFFile { error: e });
212        }
213    };
214
215    // --no-leading-underscore: For the `import_name_type` feature to work, we need to be
216    // able to control the *exact* spelling of each of the symbols that are being imported:
217    // hence we don't want `dlltool` adding leading underscores automatically.
218    let dlltool = find_binutils_dlltool(sess);
219    let temp_prefix = {
220        let mut path = PathBuf::from(&output_path);
221        path.pop();
222        path.push(lib_name);
223        path
224    };
225    // dlltool target architecture args from:
226    // https://github.com/llvm/llvm-project-release-prs/blob/llvmorg-15.0.6/llvm/lib/ToolDrivers/llvm-dlltool/DlltoolDriver.cpp#L69
227    let (dlltool_target_arch, dlltool_target_bitness) = match &sess.target.arch {
228        Arch::X86_64 => ("i386:x86-64", "--64"),
229        Arch::X86 => ("i386", "--32"),
230        Arch::AArch64 => ("arm64", "--64"),
231        Arch::Arm => ("arm", "--32"),
232        arch => panic!("unsupported arch {arch}"),
233    };
234    let mut dlltool_cmd = std::process::Command::new(&dlltool);
235    dlltool_cmd
236        .arg("-d")
237        .arg(def_file_path)
238        .arg("-D")
239        .arg(lib_name)
240        .arg("-l")
241        .arg(&output_path)
242        .arg("-m")
243        .arg(dlltool_target_arch)
244        .arg("-f")
245        .arg(dlltool_target_bitness)
246        .arg("--no-leading-underscore")
247        .arg("--temp-prefix")
248        .arg(temp_prefix);
249
250    match dlltool_cmd.output() {
251        Err(e) => {
252            sess.dcx().emit_fatal(ErrorCallingDllTool {
253                dlltool_path: dlltool.to_string_lossy(),
254                error: e,
255            });
256        }
257        // dlltool returns '0' on failure, so check for error output instead.
258        Ok(output) if !output.stderr.is_empty() => {
259            sess.dcx().emit_fatal(DlltoolFailImportLibrary {
260                dlltool_path: dlltool.to_string_lossy(),
261                dlltool_args: dlltool_cmd
262                    .get_args()
263                    .map(|arg| arg.to_string_lossy())
264                    .collect::<Vec<_>>()
265                    .join(" "),
266                stdout: String::from_utf8_lossy(&output.stdout),
267                stderr: String::from_utf8_lossy(&output.stderr),
268            })
269        }
270        _ => {}
271    }
272}
273
274fn find_binutils_dlltool(sess: &Session) -> OsString {
275    assert!(sess.target.options.is_like_windows && !sess.target.options.is_like_msvc);
276    if let Some(dlltool_path) = &sess.opts.cg.dlltool {
277        return dlltool_path.clone().into_os_string();
278    }
279
280    let tool_name: OsString = if sess.host.options.is_like_windows {
281        // If we're compiling on Windows, always use "dlltool.exe".
282        "dlltool.exe"
283    } else {
284        // On other platforms, use the architecture-specific name.
285        match sess.target.arch {
286            Arch::X86_64 => "x86_64-w64-mingw32-dlltool",
287            Arch::X86 => "i686-w64-mingw32-dlltool",
288            Arch::AArch64 => "aarch64-w64-mingw32-dlltool",
289
290            // For non-standard architectures (e.g., aarch32) fallback to "dlltool".
291            _ => "dlltool",
292        }
293    }
294    .into();
295
296    // NOTE: it's not clear how useful it is to explicitly search PATH.
297    for dir in env::split_paths(&env::var_os("PATH").unwrap_or_default()) {
298        let full_path = dir.join(&tool_name);
299        if full_path.is_file() {
300            return full_path.into_os_string();
301        }
302    }
303
304    // The user didn't specify the location of the dlltool binary, and we weren't able
305    // to find the appropriate one on the PATH. Just return the name of the tool
306    // and let the invocation fail with a hopefully useful error message.
307    tool_name
308}
309
310pub trait ArchiveBuilder {
311    fn add_file(&mut self, path: &Path);
312
313    fn add_archive(
314        &mut self,
315        archive: &Path,
316        skip: Box<dyn FnMut(&str) -> bool + 'static>,
317    ) -> io::Result<()>;
318
319    fn build(self: Box<Self>, output: &Path) -> bool;
320}
321
322pub struct ArArchiveBuilderBuilder;
323
324impl ArchiveBuilderBuilder for ArArchiveBuilderBuilder {
325    fn new_archive_builder<'a>(&self, sess: &'a Session) -> Box<dyn ArchiveBuilder + 'a> {
326        Box::new(ArArchiveBuilder::new(sess, &DEFAULT_OBJECT_READER))
327    }
328}
329
330#[must_use = "must call build() to finish building the archive"]
331pub struct ArArchiveBuilder<'a> {
332    sess: &'a Session,
333    object_reader: &'static ObjectReader,
334
335    src_archives: Vec<(PathBuf, Mmap)>,
336    // Don't use an `HashMap` here, as the order is important. `lib.rmeta` needs
337    // to be at the end of an archive in some cases for linkers to not get confused.
338    entries: Vec<(Vec<u8>, ArchiveEntry)>,
339}
340
341#[derive(Debug)]
342enum ArchiveEntry {
343    FromArchive { archive_index: usize, file_range: (u64, u64) },
344    File(PathBuf),
345}
346
347impl<'a> ArArchiveBuilder<'a> {
348    pub fn new(sess: &'a Session, object_reader: &'static ObjectReader) -> ArArchiveBuilder<'a> {
349        ArArchiveBuilder { sess, object_reader, src_archives: vec![], entries: vec![] }
350    }
351}
352
353fn try_filter_fat_archs(
354    archs: &[impl FatArch],
355    target_arch: object::Architecture,
356    archive_path: &Path,
357    archive_map_data: &[u8],
358) -> io::Result<Option<PathBuf>> {
359    let desired = match archs.iter().find(|a| a.architecture() == target_arch) {
360        Some(a) => a,
361        None => return Ok(None),
362    };
363
364    let (mut new_f, extracted_path) = tempfile::Builder::new()
365        .suffix(archive_path.file_name().unwrap())
366        .tempfile()?
367        .keep()
368        .unwrap();
369
370    new_f.write_all(
371        desired.data(archive_map_data).map_err(|e| io::Error::new(io::ErrorKind::Other, e))?,
372    )?;
373
374    Ok(Some(extracted_path))
375}
376
377pub fn try_extract_macho_fat_archive(
378    sess: &Session,
379    archive_path: &Path,
380) -> io::Result<Option<PathBuf>> {
381    let archive_map = unsafe { Mmap::map(File::open(&archive_path)?)? };
382    let target_arch = match sess.target.arch {
383        Arch::AArch64 => object::Architecture::Aarch64,
384        Arch::X86_64 => object::Architecture::X86_64,
385        _ => return Ok(None),
386    };
387
388    if let Ok(h) = object::read::macho::MachOFatFile32::parse(&*archive_map) {
389        let archs = h.arches();
390        try_filter_fat_archs(archs, target_arch, archive_path, &*archive_map)
391    } else if let Ok(h) = object::read::macho::MachOFatFile64::parse(&*archive_map) {
392        let archs = h.arches();
393        try_filter_fat_archs(archs, target_arch, archive_path, &*archive_map)
394    } else {
395        // Not a FatHeader at all, just return None.
396        Ok(None)
397    }
398}
399
400impl<'a> ArchiveBuilder for ArArchiveBuilder<'a> {
401    fn add_archive(
402        &mut self,
403        archive_path: &Path,
404        mut skip: Box<dyn FnMut(&str) -> bool + 'static>,
405    ) -> io::Result<()> {
406        let mut archive_path = archive_path.to_path_buf();
407        if self.sess.target.llvm_target.contains("-apple-macosx")
408            && let Some(new_archive_path) = try_extract_macho_fat_archive(self.sess, &archive_path)?
409        {
410            archive_path = new_archive_path
411        }
412
413        if self.src_archives.iter().any(|archive| archive.0 == archive_path) {
414            return Ok(());
415        }
416
417        let archive_map = unsafe { Mmap::map(File::open(&archive_path)?)? };
418        let archive = ArchiveFile::parse(&*archive_map)
419            .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
420        let archive_index = self.src_archives.len();
421
422        for entry in archive.members() {
423            let entry = entry.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
424            let file_name = String::from_utf8(entry.name().to_vec())
425                .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
426            if !skip(&file_name) {
427                if entry.is_thin() {
428                    let member_path = archive_path.parent().unwrap().join(Path::new(&file_name));
429                    self.entries.push((file_name.into_bytes(), ArchiveEntry::File(member_path)));
430                } else {
431                    self.entries.push((
432                        file_name.into_bytes(),
433                        ArchiveEntry::FromArchive { archive_index, file_range: entry.file_range() },
434                    ));
435                }
436            }
437        }
438
439        self.src_archives.push((archive_path, archive_map));
440        Ok(())
441    }
442
443    /// Adds an arbitrary file to this archive
444    fn add_file(&mut self, file: &Path) {
445        self.entries.push((
446            file.file_name().unwrap().to_str().unwrap().to_string().into_bytes(),
447            ArchiveEntry::File(file.to_owned()),
448        ));
449    }
450
451    /// Combine the provided files, rlibs, and native libraries into a single
452    /// `Archive`.
453    fn build(self: Box<Self>, output: &Path) -> bool {
454        let sess = self.sess;
455        match self.build_inner(output) {
456            Ok(any_members) => any_members,
457            Err(error) => {
458                sess.dcx().emit_fatal(ArchiveBuildFailure { path: output.to_owned(), error })
459            }
460        }
461    }
462}
463
464impl<'a> ArArchiveBuilder<'a> {
465    fn build_inner(self, output: &Path) -> io::Result<bool> {
466        let archive_kind = match &*self.sess.target.archive_format {
467            "gnu" => ArchiveKind::Gnu,
468            "bsd" => ArchiveKind::Bsd,
469            "darwin" => ArchiveKind::Darwin,
470            "coff" => ArchiveKind::Coff,
471            "aix_big" => ArchiveKind::AixBig,
472            kind => {
473                self.sess.dcx().emit_fatal(UnknownArchiveKind { kind });
474            }
475        };
476
477        let mut entries = Vec::new();
478
479        for (entry_name, entry) in self.entries {
480            let data =
481                match entry {
482                    ArchiveEntry::FromArchive { archive_index, file_range } => {
483                        let src_archive = &self.src_archives[archive_index];
484
485                        let data = &src_archive.1
486                            [file_range.0 as usize..file_range.0 as usize + file_range.1 as usize];
487
488                        Box::new(data) as Box<dyn AsRef<[u8]>>
489                    }
490                    ArchiveEntry::File(file) => unsafe {
491                        Box::new(
492                            Mmap::map(File::open(file).map_err(|err| {
493                                io_error_context("failed to open object file", err)
494                            })?)
495                            .map_err(|err| io_error_context("failed to map object file", err))?,
496                        ) as Box<dyn AsRef<[u8]>>
497                    },
498                };
499
500            entries.push(NewArchiveMember {
501                buf: data,
502                object_reader: self.object_reader,
503                member_name: String::from_utf8(entry_name).unwrap(),
504                mtime: 0,
505                uid: 0,
506                gid: 0,
507                perms: 0o644,
508            })
509        }
510
511        // Write to a temporary file first before atomically renaming to the final name.
512        // This prevents programs (including rustc) from attempting to read a partial archive.
513        // It also enables writing an archive with the same filename as a dependency on Windows as
514        // required by a test.
515        // The tempfile crate currently uses 0o600 as mode for the temporary files and directories
516        // it creates. We need it to be the default mode for back compat reasons however. (See
517        // #107495) To handle this we are telling tempfile to create a temporary directory instead
518        // and then inside this directory create a file using File::create.
519        let archive_tmpdir = TempDirBuilder::new()
520            .suffix(".temp-archive")
521            .tempdir_in(output.parent().unwrap_or_else(|| Path::new("")))
522            .map_err(|err| {
523                io_error_context("couldn't create a directory for the temp file", err)
524            })?;
525        let archive_tmpfile_path = archive_tmpdir.path().join("tmp.a");
526        let archive_tmpfile = File::create_new(&archive_tmpfile_path)
527            .map_err(|err| io_error_context("couldn't create the temp file", err))?;
528
529        let mut archive_tmpfile = BufWriter::new(archive_tmpfile);
530        write_archive_to_stream(
531            &mut archive_tmpfile,
532            &entries,
533            archive_kind,
534            false,
535            /* is_ec = */ Some(self.sess.target.arch == Arch::Arm64EC),
536        )?;
537        archive_tmpfile.flush()?;
538        drop(archive_tmpfile);
539
540        let any_entries = !entries.is_empty();
541        drop(entries);
542        // Drop src_archives to unmap all input archives, which is necessary if we want to write the
543        // output archive to the same location as an input archive on Windows.
544        drop(self.src_archives);
545
546        fs::rename(archive_tmpfile_path, output)
547            .map_err(|err| io_error_context("failed to rename archive file", err))?;
548        archive_tmpdir
549            .close()
550            .map_err(|err| io_error_context("failed to remove temporary directory", err))?;
551
552        Ok(any_entries)
553    }
554}
555
556fn io_error_context(context: &str, err: io::Error) -> io::Error {
557    io::Error::new(io::ErrorKind::Other, format!("{context}: {err}"))
558}