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