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