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            if let Some(new_archive_path) = try_extract_macho_fat_archive(self.sess, &archive_path)?
394            {
395                archive_path = new_archive_path
396            }
397        }
398
399        if self.src_archives.iter().any(|archive| archive.0 == archive_path) {
400            return Ok(());
401        }
402
403        let archive_map = unsafe { Mmap::map(File::open(&archive_path)?)? };
404        let archive = ArchiveFile::parse(&*archive_map)
405            .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
406        let archive_index = self.src_archives.len();
407
408        for entry in archive.members() {
409            let entry = entry.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
410            let file_name = String::from_utf8(entry.name().to_vec())
411                .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
412            if !skip(&file_name) {
413                if entry.is_thin() {
414                    let member_path = archive_path.parent().unwrap().join(Path::new(&file_name));
415                    self.entries.push((file_name.into_bytes(), ArchiveEntry::File(member_path)));
416                } else {
417                    self.entries.push((
418                        file_name.into_bytes(),
419                        ArchiveEntry::FromArchive { archive_index, file_range: entry.file_range() },
420                    ));
421                }
422            }
423        }
424
425        self.src_archives.push((archive_path, archive_map));
426        Ok(())
427    }
428
429    /// Adds an arbitrary file to this archive
430    fn add_file(&mut self, file: &Path) {
431        self.entries.push((
432            file.file_name().unwrap().to_str().unwrap().to_string().into_bytes(),
433            ArchiveEntry::File(file.to_owned()),
434        ));
435    }
436
437    /// Combine the provided files, rlibs, and native libraries into a single
438    /// `Archive`.
439    fn build(self: Box<Self>, output: &Path) -> bool {
440        let sess = self.sess;
441        match self.build_inner(output) {
442            Ok(any_members) => any_members,
443            Err(error) => {
444                sess.dcx().emit_fatal(ArchiveBuildFailure { path: output.to_owned(), error })
445            }
446        }
447    }
448}
449
450impl<'a> ArArchiveBuilder<'a> {
451    fn build_inner(self, output: &Path) -> io::Result<bool> {
452        let archive_kind = match &*self.sess.target.archive_format {
453            "gnu" => ArchiveKind::Gnu,
454            "bsd" => ArchiveKind::Bsd,
455            "darwin" => ArchiveKind::Darwin,
456            "coff" => ArchiveKind::Coff,
457            "aix_big" => ArchiveKind::AixBig,
458            kind => {
459                self.sess.dcx().emit_fatal(UnknownArchiveKind { kind });
460            }
461        };
462
463        let mut entries = Vec::new();
464
465        for (entry_name, entry) in self.entries {
466            let data =
467                match entry {
468                    ArchiveEntry::FromArchive { archive_index, file_range } => {
469                        let src_archive = &self.src_archives[archive_index];
470
471                        let data = &src_archive.1
472                            [file_range.0 as usize..file_range.0 as usize + file_range.1 as usize];
473
474                        Box::new(data) as Box<dyn AsRef<[u8]>>
475                    }
476                    ArchiveEntry::File(file) => unsafe {
477                        Box::new(
478                            Mmap::map(File::open(file).map_err(|err| {
479                                io_error_context("failed to open object file", err)
480                            })?)
481                            .map_err(|err| io_error_context("failed to map object file", err))?,
482                        ) as Box<dyn AsRef<[u8]>>
483                    },
484                };
485
486            entries.push(NewArchiveMember {
487                buf: data,
488                object_reader: self.object_reader,
489                member_name: String::from_utf8(entry_name).unwrap(),
490                mtime: 0,
491                uid: 0,
492                gid: 0,
493                perms: 0o644,
494            })
495        }
496
497        // Write to a temporary file first before atomically renaming to the final name.
498        // This prevents programs (including rustc) from attempting to read a partial archive.
499        // It also enables writing an archive with the same filename as a dependency on Windows as
500        // required by a test.
501        // The tempfile crate currently uses 0o600 as mode for the temporary files and directories
502        // it creates. We need it to be the default mode for back compat reasons however. (See
503        // #107495) To handle this we are telling tempfile to create a temporary directory instead
504        // and then inside this directory create a file using File::create.
505        let archive_tmpdir = TempFileBuilder::new()
506            .suffix(".temp-archive")
507            .tempdir_in(output.parent().unwrap_or_else(|| Path::new("")))
508            .map_err(|err| {
509                io_error_context("couldn't create a directory for the temp file", err)
510            })?;
511        let archive_tmpfile_path = archive_tmpdir.path().join("tmp.a");
512        let archive_tmpfile = File::create_new(&archive_tmpfile_path)
513            .map_err(|err| io_error_context("couldn't create the temp file", err))?;
514
515        let mut archive_tmpfile = BufWriter::new(archive_tmpfile);
516        write_archive_to_stream(
517            &mut archive_tmpfile,
518            &entries,
519            archive_kind,
520            false,
521            /* is_ec = */ self.sess.target.arch == "arm64ec",
522        )?;
523        archive_tmpfile.flush()?;
524        drop(archive_tmpfile);
525
526        let any_entries = !entries.is_empty();
527        drop(entries);
528        // Drop src_archives to unmap all input archives, which is necessary if we want to write the
529        // output archive to the same location as an input archive on Windows.
530        drop(self.src_archives);
531
532        fs::rename(archive_tmpfile_path, output)
533            .map_err(|err| io_error_context("failed to rename archive file", err))?;
534        archive_tmpdir
535            .close()
536            .map_err(|err| io_error_context("failed to remove temporary directory", err))?;
537
538        Ok(any_entries)
539    }
540}
541
542fn io_error_context(context: &str, err: io::Error) -> io::Error {
543    io::Error::new(io::ErrorKind::Other, format!("{context}: {err}"))
544}