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