Skip to main content

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