1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq)]
30#[non_exhaustive]
31pub enum ArchiveFormat {
32 TarZst,
34}
35
36impl ArchiveFormat {
37 pub const SUPPORTED_FORMATS: &'static [(&'static str, Self)] = &[(".tar.zst", Self::TarZst)];
39
40 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#[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 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 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 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 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 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 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 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 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 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 self.append_path_recursive(
392 ArchiveStep::BuildScriptOutDirs,
393 &src_path,
394 &rel_path,
395 RecursionDepth::Finite(1),
396 false,
397 callback,
398 )?;
399
400 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 for (linked_path, requested_by) in &self.binary_list.rust_build_meta.linked_paths {
422 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 if !src_path.exists() {
433 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 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 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 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 true,
486 callback,
487 )?;
488 }
489 }
490
491 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 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 let encoder = self
513 .builder
514 .into_inner()
515 .map_err(ArchiveCreateError::OutputArchiveIo)?;
516 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 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 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 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 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 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 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 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 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 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#[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}