1use crate::{
11 errors::{
12 ArchiveExtractError, ArchiveReadError, MetadataMaterializeError, PathMapperConstructError,
13 PathMapperConstructKind,
14 },
15 list::BinaryList,
16 platform::PlatformLibdir,
17};
18use camino::{Utf8Path, Utf8PathBuf};
19use camino_tempfile::Utf8TempDir;
20use guppy::graph::PackageGraph;
21use nextest_metadata::{BinaryListSummary, PlatformLibdirUnavailable};
22use std::{fmt, fs, io, sync::Arc};
23use tracing::debug;
24
25mod archive_reporter;
26mod archiver;
27mod unarchiver;
28
29pub use archive_reporter::*;
30pub use archiver::*;
31pub use unarchiver::*;
32
33pub const CARGO_METADATA_FILE_NAME: &str = "target/nextest/cargo-metadata.json";
35
36pub const BINARIES_METADATA_FILE_NAME: &str = "target/nextest/binaries-metadata.json";
38
39pub const LIBDIRS_BASE_DIR: &str = "target/nextest/libdirs";
41
42#[derive(Debug, Default)]
44pub struct ReuseBuildInfo {
45 pub cargo_metadata: Option<MetadataWithRemap<ReusedCargoMetadata>>,
47
48 pub binaries_metadata: Option<MetadataWithRemap<ReusedBinaryList>>,
50
51 pub build_dir_remap: Option<Utf8PathBuf>,
57
58 pub libdir_mapper: LibdirMapper,
60
61 _temp_dir: Option<Utf8TempDir>,
63}
64
65impl ReuseBuildInfo {
66 pub fn new(
68 cargo_metadata: Option<MetadataWithRemap<ReusedCargoMetadata>>,
69 binaries_metadata: Option<MetadataWithRemap<ReusedBinaryList>>,
70 build_dir_remap: Option<Utf8PathBuf>,
71 ) -> Self {
73 Self {
74 cargo_metadata,
75 binaries_metadata,
76 build_dir_remap,
77 libdir_mapper: LibdirMapper::default(),
78 _temp_dir: None,
79 }
80 }
81
82 pub fn extract_archive<F>(
84 archive_file: &Utf8Path,
85 format: ArchiveFormat,
86 dest: ExtractDestination,
87 callback: F,
88 workspace_remap: Option<&Utf8Path>,
89 ) -> Result<Self, ArchiveExtractError>
90 where
91 F: for<'e> FnMut(ArchiveEvent<'e>) -> io::Result<()>,
92 {
93 let mut file = fs::File::open(archive_file)
94 .map_err(|err| ArchiveExtractError::Read(ArchiveReadError::Io(err)))?;
95
96 let mut unarchiver = Unarchiver::new(&mut file, format);
97 let ExtractInfo {
98 dest_dir,
99 temp_dir,
100 binary_list,
101 cargo_metadata_json,
102 graph,
103 libdir_mapper,
104 } = unarchiver.extract(dest, callback)?;
105
106 let cargo_metadata = MetadataWithRemap {
107 metadata: ReusedCargoMetadata::new((cargo_metadata_json, graph)),
108 remap: workspace_remap.map(|p| p.to_owned()),
109 };
110 let binaries_metadata = MetadataWithRemap {
111 metadata: ReusedBinaryList::new(binary_list),
112 remap: Some(dest_dir.join("target")),
113 };
114
115 Ok(Self {
116 cargo_metadata: Some(cargo_metadata),
117 binaries_metadata: Some(binaries_metadata),
118 build_dir_remap: None,
121 libdir_mapper,
122 _temp_dir: temp_dir,
123 })
124 }
125
126 pub fn cargo_metadata(&self) -> Option<&ReusedCargoMetadata> {
128 self.cargo_metadata.as_ref().map(|m| &m.metadata)
129 }
130
131 pub fn binaries_metadata(&self) -> Option<&ReusedBinaryList> {
133 self.binaries_metadata.as_ref().map(|m| &m.metadata)
134 }
135
136 #[inline]
138 pub fn is_active(&self) -> bool {
139 self.cargo_metadata.is_some() || self.binaries_metadata.is_some()
140 }
141
142 pub fn workspace_remap(&self) -> Option<&Utf8Path> {
144 self.cargo_metadata
145 .as_ref()
146 .and_then(|m| m.remap.as_deref())
147 }
148
149 pub fn target_dir_remap(&self) -> Option<&Utf8Path> {
151 self.binaries_metadata
152 .as_ref()
153 .and_then(|m| m.remap.as_deref())
154 }
155
156 pub fn build_dir_remap(&self) -> Option<&Utf8Path> {
158 self.build_dir_remap.as_deref()
159 }
160}
161
162#[derive(Clone, Debug)]
164pub struct MetadataWithRemap<T> {
165 pub metadata: T,
167
168 pub remap: Option<Utf8PathBuf>,
170}
171
172pub trait MetadataKind: Clone + fmt::Debug {
174 type MetadataType: Sized;
176
177 fn new(metadata: Self::MetadataType) -> Self;
179
180 fn materialize(path: &Utf8Path) -> Result<Self, MetadataMaterializeError>;
182}
183
184#[derive(Clone, Debug)]
186pub struct ReusedBinaryList {
187 pub binary_list: Arc<BinaryList>,
189}
190
191impl MetadataKind for ReusedBinaryList {
192 type MetadataType = BinaryList;
193
194 fn new(binary_list: Self::MetadataType) -> Self {
195 Self {
196 binary_list: Arc::new(binary_list),
197 }
198 }
199
200 fn materialize(path: &Utf8Path) -> Result<Self, MetadataMaterializeError> {
201 let contents =
207 fs::read_to_string(path).map_err(|error| MetadataMaterializeError::Read {
208 path: path.to_owned(),
209 error,
210 })?;
211
212 let summary: BinaryListSummary = serde_json::from_str(&contents).map_err(|error| {
213 MetadataMaterializeError::Deserialize {
214 path: path.to_owned(),
215 error,
216 }
217 })?;
218
219 let binary_list = BinaryList::from_summary(summary).map_err(|error| {
220 MetadataMaterializeError::RustBuildMeta {
221 path: path.to_owned(),
222 error,
223 }
224 })?;
225
226 Ok(Self::new(binary_list))
227 }
228}
229
230#[derive(Clone, Debug)]
232pub struct ReusedCargoMetadata {
233 pub json: Arc<String>,
235
236 pub graph: Arc<PackageGraph>,
238}
239
240impl MetadataKind for ReusedCargoMetadata {
241 type MetadataType = (String, PackageGraph);
242
243 fn new((json, graph): Self::MetadataType) -> Self {
244 Self {
245 json: Arc::new(json),
246 graph: Arc::new(graph),
247 }
248 }
249
250 fn materialize(path: &Utf8Path) -> Result<Self, MetadataMaterializeError> {
251 let json =
253 std::fs::read_to_string(path).map_err(|error| MetadataMaterializeError::Read {
254 path: path.to_owned(),
255 error,
256 })?;
257 let graph = PackageGraph::from_json(&json).map_err(|error| {
258 MetadataMaterializeError::PackageGraphConstruct {
259 path: path.to_owned(),
260 error,
261 }
262 })?;
263
264 Ok(Self::new((json, graph)))
265 }
266}
267
268#[derive(Clone, Debug, Default)]
273pub struct PathMapper {
274 workspace: Option<(Utf8PathBuf, Utf8PathBuf)>,
275 target_dir: Option<(Utf8PathBuf, Utf8PathBuf)>,
276 build_dir: Option<(Utf8PathBuf, Utf8PathBuf)>,
277 libdir_mapper: LibdirMapper,
278}
279
280impl PathMapper {
281 pub fn new(
283 orig_workspace_root: impl Into<Utf8PathBuf>,
284 workspace_remap: Option<&Utf8Path>,
285 orig_target_dir: impl Into<Utf8PathBuf>,
286 target_dir_remap: Option<&Utf8Path>,
287 orig_build_dir: impl Into<Utf8PathBuf>,
288 build_dir_remap: Option<&Utf8Path>,
289 libdir_mapper: LibdirMapper,
290 ) -> Result<Self, PathMapperConstructError> {
291 let workspace_root = workspace_remap
292 .map(|root| Self::canonicalize_dir(root, PathMapperConstructKind::WorkspaceRoot))
293 .transpose()?;
294 let target_dir = target_dir_remap
295 .map(|dir| Self::canonicalize_dir(dir, PathMapperConstructKind::TargetDir))
296 .transpose()?;
297 let build_dir = build_dir_remap
298 .map(|dir| Self::canonicalize_dir(dir, PathMapperConstructKind::BuildDir))
299 .transpose()?;
300
301 Ok(Self {
302 workspace: workspace_root.map(|w| (orig_workspace_root.into(), w)),
303 target_dir: target_dir.map(|d| (orig_target_dir.into(), d)),
304 build_dir: build_dir.map(|d| (orig_build_dir.into(), d)),
305 libdir_mapper,
306 })
307 }
308
309 pub fn noop() -> Self {
311 Self {
312 workspace: None,
313 target_dir: None,
314 build_dir: None,
315 libdir_mapper: LibdirMapper::default(),
316 }
317 }
318
319 pub fn libdir_mapper(&self) -> &LibdirMapper {
321 &self.libdir_mapper
322 }
323
324 fn canonicalize_dir(
325 input: &Utf8Path,
326 kind: PathMapperConstructKind,
327 ) -> Result<Utf8PathBuf, PathMapperConstructError> {
328 let canonicalized_path =
329 input
330 .canonicalize()
331 .map_err(|err| PathMapperConstructError::Canonicalization {
332 kind,
333 input: input.into(),
334 err,
335 })?;
336 let canonicalized_path: Utf8PathBuf =
337 canonicalized_path
338 .try_into()
339 .map_err(|err| PathMapperConstructError::NonUtf8Path {
340 kind,
341 input: input.into(),
342 err,
343 })?;
344 if !canonicalized_path.is_dir() {
345 return Err(PathMapperConstructError::NotADirectory {
346 kind,
347 input: input.into(),
348 canonicalized_path,
349 });
350 }
351
352 Ok(os_imp::strip_verbatim(canonicalized_path))
353 }
354
355 pub fn new_workspace_root(&self) -> Option<&Utf8Path> {
358 self.workspace.as_ref().map(|(_, new)| new.as_path())
359 }
360
361 pub(super) fn new_target_dir(&self) -> Option<&Utf8Path> {
362 self.target_dir.as_ref().map(|(_, new)| new.as_path())
363 }
364
365 pub(super) fn new_build_dir(&self) -> Option<&Utf8Path> {
366 self.build_dir.as_ref().map(|(_, new)| new.as_path())
367 }
368
369 pub(crate) fn map_cwd(&self, path: Utf8PathBuf) -> Utf8PathBuf {
370 Self::remap_path(self.workspace.as_ref(), path)
371 }
372
373 pub(crate) fn map_target_path(&self, path: Utf8PathBuf) -> Utf8PathBuf {
378 Self::remap_path(self.target_dir.as_ref(), path)
379 }
380
381 pub(crate) fn map_build_path(&self, path: Utf8PathBuf) -> Utf8PathBuf {
386 Self::remap_path(self.build_dir.as_ref(), path)
387 }
388
389 fn remap_path(mapping: Option<&(Utf8PathBuf, Utf8PathBuf)>, path: Utf8PathBuf) -> Utf8PathBuf {
390 match mapping {
391 Some((from, to)) => match path.strip_prefix(from) {
392 Ok(p) if !p.as_str().is_empty() => to.join(p),
393 Ok(_) => to.clone(),
394 Err(_) => {
395 debug!(
396 target: "nextest-runner::reuse_build",
397 "path `{path}` does not start with remap prefix `{from}`, \
398 returning unchanged",
399 );
400 path
401 }
402 },
403 None => path,
404 }
405 }
406}
407
408#[derive(Clone, Debug, Default)]
412pub struct LibdirMapper {
413 pub(crate) host: PlatformLibdirMapper,
415
416 pub(crate) target: PlatformLibdirMapper,
418}
419
420#[derive(Clone, Debug, Default)]
424pub(crate) enum PlatformLibdirMapper {
425 Path(Utf8PathBuf),
426 Unavailable,
427 #[default]
428 NotRequested,
429}
430
431impl PlatformLibdirMapper {
432 pub(crate) fn map(&self, original: &PlatformLibdir) -> PlatformLibdir {
433 match self {
434 PlatformLibdirMapper::Path(new) => {
435 PlatformLibdir::Available(new.clone())
439 }
440 PlatformLibdirMapper::Unavailable => {
441 PlatformLibdir::Unavailable(PlatformLibdirUnavailable::NOT_IN_ARCHIVE)
444 }
445 PlatformLibdirMapper::NotRequested => original.clone(),
446 }
447 }
448}
449
450#[cfg(windows)]
451mod os_imp {
452 use camino::Utf8PathBuf;
453 use std::ptr;
454 use windows_sys::Win32::Storage::FileSystem::GetFullPathNameW;
455
456 pub(super) fn strip_verbatim(path: Utf8PathBuf) -> Utf8PathBuf {
458 let path_str = String::from(path);
459 if path_str.starts_with(r"\\?\UNC") {
460 path_str.into()
462 } else if path_str.starts_with(r"\\?\") {
463 const START_LEN: usize = r"\\?\".len();
464
465 let is_absolute_exact = {
466 let mut v = path_str[START_LEN..].encode_utf16().collect::<Vec<u16>>();
467 v.push(0);
469 is_absolute_exact(&v)
470 };
471
472 if is_absolute_exact {
473 path_str[START_LEN..].into()
474 } else {
475 path_str.into()
476 }
477 } else {
478 path_str.into()
480 }
481 }
482
483 fn is_absolute_exact(path: &[u16]) -> bool {
494 if path.is_empty() || path.len() > u32::MAX as usize || path.last() != Some(&0) {
502 return false;
503 }
504 let buffer_len = path.len();
507 let mut new_path = Vec::with_capacity(buffer_len);
508 let result = unsafe {
509 GetFullPathNameW(
510 path.as_ptr(),
511 new_path.capacity() as u32,
512 new_path.as_mut_ptr(),
513 ptr::null_mut(),
514 )
515 };
516 if result == 0 || result as usize != buffer_len - 1 {
518 false
519 } else {
520 unsafe {
522 new_path.set_len((result as usize) + 1);
523 }
524 path == new_path
525 }
526 }
527}
528
529#[cfg(unix)]
530mod os_imp {
531 use camino::Utf8PathBuf;
532
533 pub(super) fn strip_verbatim(path: Utf8PathBuf) -> Utf8PathBuf {
534 path
537 }
538}
539
540#[cfg(test)]
541mod tests {
542 use super::*;
543 use crate::{list::RustBuildMeta, platform::BuildPlatforms};
544
545 #[test]
547 fn test_path_mapper_relative() {
548 let current_dir: Utf8PathBuf = std::env::current_dir()
549 .expect("current dir obtained")
550 .try_into()
551 .expect("current dir is valid UTF-8");
552
553 let temp_workspace_root = Utf8TempDir::new().expect("new temp dir created");
554 let workspace_root_path: Utf8PathBuf = os_imp::strip_verbatim(
555 temp_workspace_root
556 .path()
557 .canonicalize()
559 .expect("workspace root canonicalized correctly")
560 .try_into()
561 .expect("workspace root is valid UTF-8"),
562 );
563 let rel_workspace_root = pathdiff::diff_utf8_paths(&workspace_root_path, ¤t_dir)
564 .expect("abs to abs diff is non-None");
565
566 let temp_target_dir = Utf8TempDir::new().expect("new temp dir created");
567 let target_dir_path: Utf8PathBuf = os_imp::strip_verbatim(
568 temp_target_dir
569 .path()
570 .canonicalize()
571 .expect("target dir canonicalized correctly")
572 .try_into()
573 .expect("target dir is valid UTF-8"),
574 );
575 let rel_target_dir = pathdiff::diff_utf8_paths(&target_dir_path, ¤t_dir)
576 .expect("abs to abs diff is non-None");
577
578 let temp_build_dir = Utf8TempDir::new().expect("new temp dir created");
579 let build_dir_path: Utf8PathBuf = os_imp::strip_verbatim(
580 temp_build_dir
581 .path()
582 .canonicalize()
583 .expect("build dir canonicalized correctly")
584 .try_into()
585 .expect("build dir is valid UTF-8"),
586 );
587 let rel_build_dir = pathdiff::diff_utf8_paths(&build_dir_path, ¤t_dir)
588 .expect("abs to abs diff is non-None");
589
590 let orig_workspace_root = Utf8PathBuf::from(
592 std::env::var("NEXTEST_WORKSPACE_ROOT")
593 .expect("NEXTEST_WORKSPACE_ROOT is set (running under cargo nextest run)"),
594 );
595 let orig_target_dir = orig_workspace_root.join("target");
596 let orig_build_dir = orig_workspace_root.join("build");
597
598 let path_mapper = PathMapper::new(
599 orig_workspace_root.as_path(),
600 Some(&rel_workspace_root),
601 &orig_target_dir,
602 Some(&rel_target_dir),
603 &orig_build_dir,
604 Some(&rel_build_dir),
605 LibdirMapper::default(),
606 )
607 .expect("remapped paths exist");
608
609 assert_eq!(
610 path_mapper.map_cwd(orig_workspace_root.join("foobar")),
611 workspace_root_path.join("foobar")
612 );
613 assert_eq!(
614 path_mapper.map_target_path(orig_target_dir.join("foobar")),
615 target_dir_path.join("foobar")
616 );
617 assert_eq!(
618 path_mapper.map_build_path(orig_build_dir.join("foobar")),
619 build_dir_path.join("foobar")
620 );
621 }
622
623 #[test]
626 fn test_path_mapper_separate_build_dir() {
627 let temp_target_dir = Utf8TempDir::new().expect("new temp dir created");
628 let target_dir_path: Utf8PathBuf = os_imp::strip_verbatim(
629 temp_target_dir
630 .path()
631 .canonicalize()
632 .expect("target dir canonicalized correctly")
633 .try_into()
634 .expect("target dir is valid UTF-8"),
635 );
636
637 let temp_build_dir = Utf8TempDir::new().expect("new temp dir created");
638 let build_dir_path: Utf8PathBuf = os_imp::strip_verbatim(
639 temp_build_dir
640 .path()
641 .canonicalize()
642 .expect("build dir canonicalized correctly")
643 .try_into()
644 .expect("build dir is valid UTF-8"),
645 );
646
647 let orig_workspace_root = Utf8PathBuf::from(
648 std::env::var("NEXTEST_WORKSPACE_ROOT")
649 .expect("NEXTEST_WORKSPACE_ROOT is set (running under cargo nextest run)"),
650 );
651 let orig_target_dir = orig_workspace_root.join("target");
652 let orig_build_dir = orig_workspace_root.join("build");
653
654 let path_mapper = PathMapper::new(
655 orig_workspace_root.as_path(),
656 None,
657 &orig_target_dir,
658 Some(target_dir_path.as_path()),
659 &orig_build_dir,
660 Some(build_dir_path.as_path()),
661 LibdirMapper::default(),
662 )
663 .expect("remapped paths exist");
664
665 assert_eq!(
667 path_mapper.map_target_path(orig_target_dir.join("debug/mybin")),
668 target_dir_path.join("debug/mybin"),
669 );
670
671 assert_eq!(
673 path_mapper.map_build_path(orig_build_dir.join("debug/deps/test-abc123")),
674 build_dir_path.join("debug/deps/test-abc123"),
675 );
676
677 let unrelated = Utf8PathBuf::from("/some/other/path");
679 assert_eq!(path_mapper.map_target_path(unrelated.clone()), unrelated,);
680 assert_eq!(path_mapper.map_build_path(unrelated.clone()), unrelated,);
681 }
682
683 #[test]
686 fn test_map_paths_build_dir_fallback() {
687 let temp_target_dir = Utf8TempDir::new().expect("new temp dir created");
688 let target_dir_path: Utf8PathBuf = os_imp::strip_verbatim(
689 temp_target_dir
690 .path()
691 .canonicalize()
692 .expect("target dir canonicalized correctly")
693 .try_into()
694 .expect("target dir is valid UTF-8"),
695 );
696
697 let orig_workspace_root = Utf8PathBuf::from(
698 std::env::var("NEXTEST_WORKSPACE_ROOT")
699 .expect("NEXTEST_WORKSPACE_ROOT is set (running under cargo nextest run)"),
700 );
701 let orig_target_dir = orig_workspace_root.join("target");
702 let orig_build_dir = orig_workspace_root.join("build");
703
704 let path_mapper = PathMapper::new(
706 orig_workspace_root.as_path(),
707 None,
708 &orig_target_dir,
709 Some(target_dir_path.as_path()),
710 &orig_build_dir,
711 None,
712 LibdirMapper::default(),
713 )
714 .expect("remapped paths exist");
715
716 assert_eq!(path_mapper.new_build_dir(), None);
719 assert_eq!(
720 path_mapper.new_target_dir(),
721 Some(target_dir_path.as_path()),
722 );
723
724 let build_platforms = BuildPlatforms::new_with_no_target()
728 .expect("creating BuildPlatforms without target triple succeeds");
729 let meta = RustBuildMeta::new(&orig_target_dir, &orig_build_dir, build_platforms);
730 let mapped = meta.map_paths(&path_mapper);
731
732 assert_eq!(mapped.target_directory, target_dir_path);
733 assert_eq!(
734 mapped.build_directory, target_dir_path,
735 "build_directory should fall back to the target_dir remap",
736 );
737 }
738}