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