nextest_runner/reuse_build/
archiver.rs

1// Copyright (c) The nextest Contributors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4use super::{ArchiveCounts, ArchiveEvent, BINARIES_METADATA_FILE_NAME, CARGO_METADATA_FILE_NAME};
5use crate::{
6    config::{
7        core::{EvaluatableProfile, get_num_cpus},
8        elements::{ArchiveConfig, ArchiveIncludeOnMissing, RecursionDepth},
9    },
10    errors::{ArchiveCreateError, UnknownArchiveFormat},
11    helpers::{convert_rel_path_to_forward_slash, rel_path_join},
12    list::{BinaryList, OutputFormat, SerializableFormat},
13    redact::Redactor,
14    reuse_build::{LIBDIRS_BASE_DIR, PathMapper},
15};
16use atomicwrites::{AtomicFile, OverwriteBehavior};
17use camino::{Utf8Path, Utf8PathBuf};
18use core::fmt;
19use guppy::{PackageId, graph::PackageGraph};
20use std::{
21    collections::HashSet,
22    fs,
23    io::{self, BufWriter, Write},
24    time::{Instant, SystemTime},
25};
26use tracing::{debug, trace, warn};
27use zstd::Encoder;
28
29/// Archive format.
30#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31#[non_exhaustive]
32pub enum ArchiveFormat {
33    /// A Zstandard-compressed tarball.
34    TarZst,
35}
36
37impl ArchiveFormat {
38    /// The list of supported formats as a list of (file extension, format) pairs.
39    pub const SUPPORTED_FORMATS: &'static [(&'static str, Self)] = &[(".tar.zst", Self::TarZst)];
40
41    /// Automatically detects an archive format from a given file name, and returns an error if the
42    /// detection failed.
43    pub fn autodetect(archive_file: &Utf8Path) -> Result<Self, UnknownArchiveFormat> {
44        let file_name = archive_file.file_name().unwrap_or("");
45        for (extension, format) in Self::SUPPORTED_FORMATS {
46            if file_name.ends_with(extension) {
47                return Ok(*format);
48            }
49        }
50
51        Err(UnknownArchiveFormat {
52            file_name: file_name.to_owned(),
53        })
54    }
55}
56
57/// Archives test binaries along with metadata to the given file.
58///
59/// The output file is a Zstandard-compressed tarball (`.tar.zst`).
60#[expect(clippy::too_many_arguments)]
61pub fn archive_to_file<'a, F>(
62    profile: EvaluatableProfile<'a>,
63    binary_list: &'a BinaryList,
64    cargo_metadata: &'a str,
65    graph: &'a PackageGraph,
66    path_mapper: &'a PathMapper,
67    format: ArchiveFormat,
68    zstd_level: i32,
69    output_file: &'a Utf8Path,
70    mut callback: F,
71    redactor: Redactor,
72) -> Result<(), ArchiveCreateError>
73where
74    F: for<'b> FnMut(ArchiveEvent<'b>) -> io::Result<()>,
75{
76    let config = profile.archive_config();
77
78    let start_time = Instant::now();
79
80    let file = AtomicFile::new(output_file, OverwriteBehavior::AllowOverwrite);
81    let file_count = file
82        .write(|file| {
83            // Tests require the standard library in two cases:
84            // * proc-macro tests (host)
85            // * tests compiled with -C prefer-dynamic (target)
86            //
87            // We only care about libstd -- empirically, other libraries in the path aren't
88            // required.
89            let (host_stdlib, host_stdlib_err) = if let Some(libdir) = binary_list
90                .rust_build_meta
91                .build_platforms
92                .host
93                .libdir
94                .as_path()
95            {
96                split_result(find_std(libdir))
97            } else {
98                (None, None)
99            };
100
101            let (target_stdlib, target_stdlib_err) =
102                if let Some(target) = &binary_list.rust_build_meta.build_platforms.target {
103                    if let Some(libdir) = target.libdir.as_path() {
104                        split_result(find_std(libdir))
105                    } else {
106                        (None, None)
107                    }
108                } else {
109                    (None, None)
110                };
111
112            let stdlib_count = host_stdlib.is_some() as usize + target_stdlib.is_some() as usize;
113
114            let archiver = Archiver::new(
115                config,
116                binary_list,
117                cargo_metadata,
118                graph,
119                path_mapper,
120                host_stdlib,
121                target_stdlib,
122                format,
123                zstd_level,
124                file,
125                redactor,
126            )?;
127
128            let test_binary_count = binary_list.rust_binaries.len();
129            let non_test_binary_count = binary_list.rust_build_meta.non_test_binaries.len();
130            let build_script_out_dir_count =
131                binary_list.rust_build_meta.build_script_out_dirs.len();
132            let linked_path_count = binary_list.rust_build_meta.linked_paths.len();
133            let extra_path_count = config.include.len();
134
135            let counts = ArchiveCounts {
136                test_binary_count,
137                non_test_binary_count,
138                build_script_out_dir_count,
139                linked_path_count,
140                extra_path_count,
141                stdlib_count,
142            };
143
144            callback(ArchiveEvent::ArchiveStarted {
145                counts,
146                output_file,
147            })
148            .map_err(ArchiveCreateError::ReporterIo)?;
149
150            // Was there an error finding the standard library?
151            if let Some(err) = host_stdlib_err {
152                callback(ArchiveEvent::StdlibPathError {
153                    error: &err.to_string(),
154                })
155                .map_err(ArchiveCreateError::ReporterIo)?;
156            }
157            if let Some(err) = target_stdlib_err {
158                callback(ArchiveEvent::StdlibPathError {
159                    error: &err.to_string(),
160                })
161                .map_err(ArchiveCreateError::ReporterIo)?;
162            }
163
164            let (_, file_count) = archiver.archive(&mut callback)?;
165            Ok(file_count)
166        })
167        .map_err(|err| match err {
168            atomicwrites::Error::Internal(err) => ArchiveCreateError::OutputArchiveIo(err),
169            atomicwrites::Error::User(err) => err,
170        })?;
171
172    let elapsed = start_time.elapsed();
173
174    callback(ArchiveEvent::Archived {
175        file_count,
176        output_file,
177        elapsed,
178    })
179    .map_err(ArchiveCreateError::ReporterIo)?;
180
181    Ok(())
182}
183
184struct Archiver<'a, W: Write> {
185    binary_list: &'a BinaryList,
186    cargo_metadata: &'a str,
187    graph: &'a PackageGraph,
188    path_mapper: &'a PathMapper,
189    host_stdlib: Option<Utf8PathBuf>,
190    target_stdlib: Option<Utf8PathBuf>,
191    builder: tar::Builder<Encoder<'static, BufWriter<W>>>,
192    unix_timestamp: u64,
193    added_files: HashSet<Utf8PathBuf>,
194    config: &'a ArchiveConfig,
195    redactor: Redactor,
196}
197
198impl<'a, W: Write> Archiver<'a, W> {
199    #[expect(clippy::too_many_arguments)]
200    fn new(
201        config: &'a ArchiveConfig,
202        binary_list: &'a BinaryList,
203        cargo_metadata: &'a str,
204        graph: &'a PackageGraph,
205        path_mapper: &'a PathMapper,
206        host_stdlib: Option<Utf8PathBuf>,
207        target_stdlib: Option<Utf8PathBuf>,
208        format: ArchiveFormat,
209        compression_level: i32,
210        writer: W,
211        redactor: Redactor,
212    ) -> Result<Self, ArchiveCreateError> {
213        let buf_writer = BufWriter::new(writer);
214        let builder = match format {
215            ArchiveFormat::TarZst => {
216                let mut encoder = zstd::Encoder::new(buf_writer, compression_level)
217                    .map_err(ArchiveCreateError::OutputArchiveIo)?;
218                encoder
219                    .include_checksum(true)
220                    .map_err(ArchiveCreateError::OutputArchiveIo)?;
221                if let Err(err) = encoder.multithread(get_num_cpus() as u32) {
222                    tracing::warn!(
223                        ?err,
224                        "libzstd compiled without multithreading, defaulting to single-thread"
225                    );
226                }
227                tar::Builder::new(encoder)
228            }
229        };
230
231        let unix_timestamp = SystemTime::now()
232            .duration_since(SystemTime::UNIX_EPOCH)
233            .expect("current time should be after 1970-01-01")
234            .as_secs();
235
236        Ok(Self {
237            binary_list,
238            cargo_metadata,
239            graph,
240            path_mapper,
241            host_stdlib,
242            target_stdlib,
243            builder,
244            unix_timestamp,
245            added_files: HashSet::new(),
246            config,
247            redactor,
248        })
249    }
250
251    fn archive<F>(mut self, callback: &mut F) -> Result<(W, usize), ArchiveCreateError>
252    where
253        F: for<'b> FnMut(ArchiveEvent<'b>) -> io::Result<()>,
254    {
255        // Add the binaries metadata first so that while unarchiving, reports are instant.
256        let binaries_metadata = self
257            .binary_list
258            .to_string(OutputFormat::Serializable(SerializableFormat::JsonPretty))
259            .map_err(ArchiveCreateError::CreateBinaryList)?;
260
261        self.append_from_memory(BINARIES_METADATA_FILE_NAME, &binaries_metadata)?;
262
263        self.append_from_memory(CARGO_METADATA_FILE_NAME, self.cargo_metadata)?;
264
265        let target_dir = &self.binary_list.rust_build_meta.target_directory;
266
267        fn filter_map_err<T>(result: io::Result<()>) -> Option<Result<T, ArchiveCreateError>> {
268            match result {
269                Ok(()) => None,
270                Err(err) => Some(Err(ArchiveCreateError::ReporterIo(err))),
271            }
272        }
273
274        // Check that all archive.include paths exist.
275        let archive_include_paths = self
276            .config
277            .include
278            .iter()
279            .filter_map(|include| {
280                let src_path = include.join_path(target_dir);
281                let src_path = self.path_mapper.map_binary(src_path);
282
283                match src_path.symlink_metadata() {
284                    Ok(metadata) => {
285                        if metadata.is_dir() {
286                            if include.depth().is_zero() {
287                                // A directory with depth 0 will not be archived, so warn on that.
288                                filter_map_err(callback(ArchiveEvent::DirectoryAtDepthZero {
289                                    path: &src_path,
290                                }))
291                            } else {
292                                Some(Ok((include, src_path)))
293                            }
294                        } else if metadata.is_file() || metadata.is_symlink() {
295                            Some(Ok((include, src_path)))
296                        } else {
297                            filter_map_err(callback(ArchiveEvent::UnknownFileType {
298                                step: ArchiveStep::ExtraPaths,
299                                path: &src_path,
300                            }))
301                        }
302                    }
303                    Err(error) => {
304                        if error.kind() == io::ErrorKind::NotFound {
305                            match include.on_missing() {
306                                ArchiveIncludeOnMissing::Error => {
307                                    // TODO: accumulate errors rather than failing on the first one
308                                    Some(Err(ArchiveCreateError::MissingExtraPath {
309                                        path: src_path.to_owned(),
310                                        redactor: self.redactor.clone(),
311                                    }))
312                                }
313                                ArchiveIncludeOnMissing::Warn => {
314                                    filter_map_err(callback(ArchiveEvent::ExtraPathMissing {
315                                        path: &src_path,
316                                        warn: true,
317                                    }))
318                                }
319                                ArchiveIncludeOnMissing::Ignore => {
320                                    filter_map_err(callback(ArchiveEvent::ExtraPathMissing {
321                                        path: &src_path,
322                                        warn: false,
323                                    }))
324                                }
325                            }
326                        } else {
327                            Some(Err(ArchiveCreateError::InputFileRead {
328                                step: ArchiveStep::ExtraPaths,
329                                path: src_path.to_owned(),
330                                is_dir: None,
331                                error,
332                            }))
333                        }
334                    }
335                }
336            })
337            .collect::<Result<Vec<_>, ArchiveCreateError>>()?;
338
339        // Write all discovered binaries into the archive.
340        for binary in &self.binary_list.rust_binaries {
341            let rel_path = binary
342                .path
343                .strip_prefix(target_dir)
344                .expect("binary paths must be within target directory");
345            // The target directory might not be called "target", so strip all of it then add
346            // "target" to the beginning.
347            let rel_path = Utf8Path::new("target").join(rel_path);
348            let rel_path = convert_rel_path_to_forward_slash(&rel_path);
349
350            self.append_file(ArchiveStep::TestBinaries, &binary.path, &rel_path)?;
351        }
352        for non_test_binary in self
353            .binary_list
354            .rust_build_meta
355            .non_test_binaries
356            .iter()
357            .flat_map(|(_, binaries)| binaries)
358        {
359            let src_path = self
360                .binary_list
361                .rust_build_meta
362                .target_directory
363                .join(&non_test_binary.path);
364            let src_path = self.path_mapper.map_binary(src_path);
365
366            let rel_path = Utf8Path::new("target").join(&non_test_binary.path);
367            let rel_path = convert_rel_path_to_forward_slash(&rel_path);
368
369            self.append_file(ArchiveStep::NonTestBinaries, &src_path, &rel_path)?;
370        }
371
372        // Write build script output directories to the archive.
373        for build_script_out_dir in self
374            .binary_list
375            .rust_build_meta
376            .build_script_out_dirs
377            .values()
378        {
379            let src_path = self
380                .binary_list
381                .rust_build_meta
382                .target_directory
383                .join(build_script_out_dir);
384            let src_path = self.path_mapper.map_binary(src_path);
385
386            let rel_path = Utf8Path::new("target").join(build_script_out_dir);
387            let rel_path = convert_rel_path_to_forward_slash(&rel_path);
388
389            // XXX: For now, we only archive one level of build script output directories as a
390            // conservative solution. If necessary, we may have to either broaden this by default or
391            // add configuration for this. Archiving too much can cause unnecessary slowdowns.
392            self.append_path_recursive(
393                ArchiveStep::BuildScriptOutDirs,
394                &src_path,
395                &rel_path,
396                RecursionDepth::Finite(1),
397                false,
398                callback,
399            )?;
400
401            // Archive build script output in order to set environment variables from there
402            let Some(out_dir_parent) = build_script_out_dir.parent() else {
403                warn!(
404                    "could not determine parent directory of output directory {build_script_out_dir}"
405                );
406                continue;
407            };
408            let out_file_path = out_dir_parent.join("output");
409            let src_path = self
410                .binary_list
411                .rust_build_meta
412                .target_directory
413                .join(&out_file_path);
414
415            let rel_path = Utf8Path::new("target").join(out_file_path);
416            let rel_path = convert_rel_path_to_forward_slash(&rel_path);
417
418            self.append_file(ArchiveStep::BuildScriptOutDirs, &src_path, &rel_path)?;
419        }
420
421        // Write linked paths to the archive.
422        for (linked_path, requested_by) in &self.binary_list.rust_build_meta.linked_paths {
423            // Linked paths are relative, e.g. debug/foo/bar. We need to prepend the target
424            // directory.
425            let src_path = self
426                .binary_list
427                .rust_build_meta
428                .target_directory
429                .join(linked_path);
430            let src_path = self.path_mapper.map_binary(src_path);
431
432            // Some crates produce linked paths that don't exist. This is a bug in those libraries.
433            if !src_path.exists() {
434                // Map each requested_by to its package name and version.
435                let mut requested_by: Vec<_> = requested_by
436                    .iter()
437                    .map(|package_id| {
438                        self.graph
439                            .metadata(&PackageId::new(package_id.clone()))
440                            .map_or_else(
441                                |_| {
442                                    // If a package ID is not found in the graph, it's strange but not
443                                    // fatal -- just use the ID.
444                                    package_id.to_owned()
445                                },
446                                |metadata| format!("{} v{}", metadata.name(), metadata.version()),
447                            )
448                    })
449                    .collect();
450                requested_by.sort_unstable();
451
452                callback(ArchiveEvent::LinkedPathNotFound {
453                    path: &src_path,
454                    requested_by: &requested_by,
455                })
456                .map_err(ArchiveCreateError::ReporterIo)?;
457                continue;
458            }
459
460            let rel_path = Utf8Path::new("target").join(linked_path);
461            let rel_path = convert_rel_path_to_forward_slash(&rel_path);
462            // Since LD_LIBRARY_PATH etc aren't recursive, we only need to add the top-level files
463            // from linked paths.
464            self.append_path_recursive(
465                ArchiveStep::LinkedPaths,
466                &src_path,
467                &rel_path,
468                RecursionDepth::Finite(1),
469                false,
470                callback,
471            )?;
472        }
473
474        // Also include extra paths.
475        for (include, src_path) in archive_include_paths {
476            let rel_path = include.join_path(Utf8Path::new("target"));
477            let rel_path = convert_rel_path_to_forward_slash(&rel_path);
478
479            if src_path.exists() {
480                self.append_path_recursive(
481                    ArchiveStep::ExtraPaths,
482                    &src_path,
483                    &rel_path,
484                    include.depth(),
485                    // Warn if the implicit depth limit for these paths is in use.
486                    true,
487                    callback,
488                )?;
489            }
490        }
491
492        // Add the standard libraries to the archive if available.
493        if let Some(host_stdlib) = self.host_stdlib.clone() {
494            let rel_path = Utf8Path::new(LIBDIRS_BASE_DIR)
495                .join("host")
496                .join(host_stdlib.file_name().unwrap());
497            let rel_path = convert_rel_path_to_forward_slash(&rel_path);
498
499            self.append_file(ArchiveStep::ExtraPaths, &host_stdlib, &rel_path)?;
500        }
501        if let Some(target_stdlib) = self.target_stdlib.clone() {
502            // Use libdir/target/0 as the path to the target standard library, to support multiple
503            // targets in the future.
504            let rel_path = Utf8Path::new(LIBDIRS_BASE_DIR)
505                .join("target/0")
506                .join(target_stdlib.file_name().unwrap());
507            let rel_path = convert_rel_path_to_forward_slash(&rel_path);
508
509            self.append_file(ArchiveStep::ExtraPaths, &target_stdlib, &rel_path)?;
510        }
511
512        // Finish writing the archive.
513        let encoder = self
514            .builder
515            .into_inner()
516            .map_err(ArchiveCreateError::OutputArchiveIo)?;
517        // Finish writing the zstd stream.
518        let buf_writer = encoder
519            .finish()
520            .map_err(ArchiveCreateError::OutputArchiveIo)?;
521        let writer = buf_writer
522            .into_inner()
523            .map_err(|err| ArchiveCreateError::OutputArchiveIo(err.into_error()))?;
524
525        Ok((writer, self.added_files.len()))
526    }
527
528    // ---
529    // Helper methods
530    // ---
531
532    fn append_from_memory(&mut self, name: &str, contents: &str) -> Result<(), ArchiveCreateError> {
533        let mut header = tar::Header::new_gnu();
534        header.set_size(contents.len() as u64);
535        header.set_mtime(self.unix_timestamp);
536        header.set_mode(0o664);
537        header.set_cksum();
538
539        self.builder
540            .append_data(&mut header, name, io::Cursor::new(contents))
541            .map_err(ArchiveCreateError::OutputArchiveIo)?;
542        // We always prioritize appending files from memory over files on disk, so don't check
543        // membership in added_files before adding the file to the archive.
544        self.added_files.insert(name.into());
545        Ok(())
546    }
547
548    fn append_path_recursive<F>(
549        &mut self,
550        step: ArchiveStep,
551        src_path: &Utf8Path,
552        rel_path: &Utf8Path,
553        limit: RecursionDepth,
554        warn_on_exceed_depth: bool,
555        callback: &mut F,
556    ) -> Result<(), ArchiveCreateError>
557    where
558        F: for<'b> FnMut(ArchiveEvent<'b>) -> io::Result<()>,
559    {
560        // Within the loop, the metadata will be part of the directory entry.
561        let metadata =
562            fs::symlink_metadata(src_path).map_err(|error| ArchiveCreateError::InputFileRead {
563                step,
564                path: src_path.to_owned(),
565                is_dir: None,
566                error,
567            })?;
568
569        // Use an explicit stack to avoid the unlikely but possible situation of a stack overflow.
570        let mut stack = vec![(limit, src_path.to_owned(), rel_path.to_owned(), metadata)];
571
572        while let Some((depth, src_path, rel_path, metadata)) = stack.pop() {
573            trace!(
574                target: "nextest-runner",
575                "processing `{src_path}` with metadata {metadata:?} \
576                 (depth: {depth})",
577            );
578
579            if metadata.is_dir() {
580                // Check the recursion limit.
581                if depth.is_zero() {
582                    callback(ArchiveEvent::RecursionDepthExceeded {
583                        step,
584                        path: &src_path,
585                        limit: limit.unwrap_finite(),
586                        warn: warn_on_exceed_depth,
587                    })
588                    .map_err(ArchiveCreateError::ReporterIo)?;
589                    continue;
590                }
591
592                // Iterate over this directory.
593                debug!(
594                    target: "nextest-runner",
595                    "recursing into `{}`",
596                    src_path
597                );
598                let entries = src_path.read_dir_utf8().map_err(|error| {
599                    ArchiveCreateError::InputFileRead {
600                        step,
601                        path: src_path.to_owned(),
602                        is_dir: Some(true),
603                        error,
604                    }
605                })?;
606                for entry in entries {
607                    let entry = entry.map_err(|error| ArchiveCreateError::DirEntryRead {
608                        path: src_path.to_owned(),
609                        error,
610                    })?;
611                    let metadata =
612                        entry
613                            .metadata()
614                            .map_err(|error| ArchiveCreateError::InputFileRead {
615                                step,
616                                path: entry.path().to_owned(),
617                                is_dir: None,
618                                error,
619                            })?;
620                    let entry_rel_path = rel_path_join(&rel_path, entry.file_name().as_ref());
621                    stack.push((
622                        depth.decrement(),
623                        entry.into_path(),
624                        entry_rel_path,
625                        metadata,
626                    ));
627                }
628            } else if metadata.is_file() || metadata.is_symlink() {
629                self.append_file(step, &src_path, &rel_path)?;
630            } else {
631                // Don't archive other kinds of files.
632                callback(ArchiveEvent::UnknownFileType {
633                    step,
634                    path: &src_path,
635                })
636                .map_err(ArchiveCreateError::ReporterIo)?;
637            }
638        }
639
640        Ok(())
641    }
642
643    fn append_file(
644        &mut self,
645        step: ArchiveStep,
646        src: &Utf8Path,
647        dest: &Utf8Path,
648    ) -> Result<(), ArchiveCreateError> {
649        // Check added_files to ensure we aren't adding duplicate files.
650        if !self.added_files.contains(dest) {
651            debug!(
652                target: "nextest-runner",
653                "adding `{src}` to archive as `{dest}`",
654            );
655            self.builder
656                .append_path_with_name(src, dest)
657                .map_err(|error| ArchiveCreateError::InputFileRead {
658                    step,
659                    path: src.to_owned(),
660                    is_dir: Some(false),
661                    error,
662                })?;
663            self.added_files.insert(dest.into());
664        }
665        Ok(())
666    }
667}
668
669fn find_std(libdir: &Utf8Path) -> io::Result<Utf8PathBuf> {
670    for path in libdir.read_dir_utf8()? {
671        let path = path?;
672        // As of Rust 1.78, std is of the form:
673        //
674        //   libstd-<hash>.so (non-macOS Unix)
675        //   libstd-<hash>.dylib (macOS)
676        //   std-<hash>.dll (Windows)
677        let file_name = path.file_name();
678        let is_unix = file_name.starts_with("libstd-")
679            && (file_name.ends_with(".so") || file_name.ends_with(".dylib"));
680        let is_windows = file_name.starts_with("std-") && file_name.ends_with(".dll");
681
682        if is_unix || is_windows {
683            return Ok(path.into_path());
684        }
685    }
686
687    Err(io::Error::other(
688        "could not find the Rust standard library in the libdir",
689    ))
690}
691
692fn split_result<T, E>(result: Result<T, E>) -> (Option<T>, Option<E>) {
693    match result {
694        Ok(v) => (Some(v), None),
695        Err(e) => (None, Some(e)),
696    }
697}
698
699/// The part of the archive process that is currently in progress.
700///
701/// This is used for better warnings and errors.
702#[derive(Clone, Copy, Debug)]
703pub enum ArchiveStep {
704    /// Test binaries are being archived.
705    TestBinaries,
706
707    /// Non-test binaries are being archived.
708    NonTestBinaries,
709
710    /// Build script output directories are being archived.
711    BuildScriptOutDirs,
712
713    /// Linked paths are being archived.
714    LinkedPaths,
715
716    /// Extra paths are being archived.
717    ExtraPaths,
718
719    /// The standard library is being archived.
720    Stdlib,
721}
722
723impl fmt::Display for ArchiveStep {
724    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
725        match self {
726            Self::TestBinaries => write!(f, "test binaries"),
727            Self::NonTestBinaries => write!(f, "non-test binaries"),
728            Self::BuildScriptOutDirs => write!(f, "build script output directories"),
729            Self::LinkedPaths => write!(f, "linked paths"),
730            Self::ExtraPaths => write!(f, "extra paths"),
731            Self::Stdlib => write!(f, "standard library"),
732        }
733    }
734}
735
736#[cfg(test)]
737mod tests {
738    use super::*;
739
740    #[test]
741    fn test_archive_format_autodetect() {
742        assert_eq!(
743            ArchiveFormat::autodetect("foo.tar.zst".as_ref()).unwrap(),
744            ArchiveFormat::TarZst,
745        );
746        assert_eq!(
747            ArchiveFormat::autodetect("foo/bar.tar.zst".as_ref()).unwrap(),
748            ArchiveFormat::TarZst,
749        );
750        ArchiveFormat::autodetect("foo".as_ref()).unwrap_err();
751        ArchiveFormat::autodetect("/".as_ref()).unwrap_err();
752    }
753}