1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
31#[non_exhaustive]
32pub enum ArchiveFormat {
33 TarZst,
35}
36
37impl ArchiveFormat {
38 pub const SUPPORTED_FORMATS: &'static [(&'static str, Self)] = &[(".tar.zst", Self::TarZst)];
40
41 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#[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 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 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 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 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 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 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 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 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 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 self.append_path_recursive(
393 ArchiveStep::BuildScriptOutDirs,
394 &src_path,
395 &rel_path,
396 RecursionDepth::Finite(1),
397 false,
398 callback,
399 )?;
400
401 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 for (linked_path, requested_by) in &self.binary_list.rust_build_meta.linked_paths {
423 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 if !src_path.exists() {
434 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 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 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 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 true,
487 callback,
488 )?;
489 }
490 }
491
492 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 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 let encoder = self
514 .builder
515 .into_inner()
516 .map_err(ArchiveCreateError::OutputArchiveIo)?;
517 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 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 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 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 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 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 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 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 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 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#[derive(Clone, Copy, Debug)]
703pub enum ArchiveStep {
704 TestBinaries,
706
707 NonTestBinaries,
709
710 BuildScriptOutDirs,
712
713 LinkedPaths,
715
716 ExtraPaths,
718
719 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}