nextest_runner/reuse_build/
mod.rs1use 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};
23
24mod archive_reporter;
25mod archiver;
26mod unarchiver;
27
28pub use archive_reporter::*;
29pub use archiver::*;
30pub use unarchiver::*;
31
32pub const CARGO_METADATA_FILE_NAME: &str = "target/nextest/cargo-metadata.json";
34
35pub const BINARIES_METADATA_FILE_NAME: &str = "target/nextest/binaries-metadata.json";
37
38pub const LIBDIRS_BASE_DIR: &str = "target/nextest/libdirs";
40
41#[derive(Debug, Default)]
43pub struct ReuseBuildInfo {
44 pub cargo_metadata: Option<MetadataWithRemap<ReusedCargoMetadata>>,
46
47 pub binaries_metadata: Option<MetadataWithRemap<ReusedBinaryList>>,
49
50 pub libdir_mapper: LibdirMapper,
52
53 _temp_dir: Option<Utf8TempDir>,
55}
56
57impl ReuseBuildInfo {
58 pub fn new(
60 cargo_metadata: Option<MetadataWithRemap<ReusedCargoMetadata>>,
61 binaries_metadata: Option<MetadataWithRemap<ReusedBinaryList>>,
62 ) -> Self {
64 Self {
65 cargo_metadata,
66 binaries_metadata,
67 libdir_mapper: LibdirMapper::default(),
68 _temp_dir: None,
69 }
70 }
71
72 pub fn extract_archive<F>(
74 archive_file: &Utf8Path,
75 format: ArchiveFormat,
76 dest: ExtractDestination,
77 callback: F,
78 workspace_remap: Option<&Utf8Path>,
79 ) -> Result<Self, ArchiveExtractError>
80 where
81 F: for<'e> FnMut(ArchiveEvent<'e>) -> io::Result<()>,
82 {
83 let mut file = fs::File::open(archive_file)
84 .map_err(|err| ArchiveExtractError::Read(ArchiveReadError::Io(err)))?;
85
86 let mut unarchiver = Unarchiver::new(&mut file, format);
87 let ExtractInfo {
88 dest_dir,
89 temp_dir,
90 binary_list,
91 cargo_metadata_json,
92 graph,
93 libdir_mapper,
94 } = unarchiver.extract(dest, callback)?;
95
96 let cargo_metadata = MetadataWithRemap {
97 metadata: ReusedCargoMetadata::new((cargo_metadata_json, graph)),
98 remap: workspace_remap.map(|p| p.to_owned()),
99 };
100 let binaries_metadata = MetadataWithRemap {
101 metadata: ReusedBinaryList::new(binary_list),
102 remap: Some(dest_dir.join("target")),
103 };
104
105 Ok(Self {
106 cargo_metadata: Some(cargo_metadata),
107 binaries_metadata: Some(binaries_metadata),
108 libdir_mapper,
109 _temp_dir: temp_dir,
110 })
111 }
112
113 pub fn cargo_metadata(&self) -> Option<&ReusedCargoMetadata> {
115 self.cargo_metadata.as_ref().map(|m| &m.metadata)
116 }
117
118 pub fn binaries_metadata(&self) -> Option<&ReusedBinaryList> {
120 self.binaries_metadata.as_ref().map(|m| &m.metadata)
121 }
122
123 #[inline]
125 pub fn is_active(&self) -> bool {
126 self.cargo_metadata.is_some() || self.binaries_metadata.is_some()
127 }
128
129 pub fn workspace_remap(&self) -> Option<&Utf8Path> {
131 self.cargo_metadata
132 .as_ref()
133 .and_then(|m| m.remap.as_deref())
134 }
135
136 pub fn target_dir_remap(&self) -> Option<&Utf8Path> {
138 self.binaries_metadata
139 .as_ref()
140 .and_then(|m| m.remap.as_deref())
141 }
142}
143
144#[derive(Clone, Debug)]
146pub struct MetadataWithRemap<T> {
147 pub metadata: T,
149
150 pub remap: Option<Utf8PathBuf>,
152}
153
154pub trait MetadataKind: Clone + fmt::Debug {
156 type MetadataType: Sized;
158
159 fn new(metadata: Self::MetadataType) -> Self;
161
162 fn materialize(path: &Utf8Path) -> Result<Self, MetadataMaterializeError>;
164}
165
166#[derive(Clone, Debug)]
168pub struct ReusedBinaryList {
169 pub binary_list: Arc<BinaryList>,
171}
172
173impl MetadataKind for ReusedBinaryList {
174 type MetadataType = BinaryList;
175
176 fn new(binary_list: Self::MetadataType) -> Self {
177 Self {
178 binary_list: Arc::new(binary_list),
179 }
180 }
181
182 fn materialize(path: &Utf8Path) -> Result<Self, MetadataMaterializeError> {
183 let contents =
189 fs::read_to_string(path).map_err(|error| MetadataMaterializeError::Read {
190 path: path.to_owned(),
191 error,
192 })?;
193
194 let summary: BinaryListSummary = serde_json::from_str(&contents).map_err(|error| {
195 MetadataMaterializeError::Deserialize {
196 path: path.to_owned(),
197 error,
198 }
199 })?;
200
201 let binary_list = BinaryList::from_summary(summary).map_err(|error| {
202 MetadataMaterializeError::RustBuildMeta {
203 path: path.to_owned(),
204 error,
205 }
206 })?;
207
208 Ok(Self::new(binary_list))
209 }
210}
211
212#[derive(Clone, Debug)]
214pub struct ReusedCargoMetadata {
215 pub json: Arc<String>,
217
218 pub graph: Arc<PackageGraph>,
220}
221
222impl MetadataKind for ReusedCargoMetadata {
223 type MetadataType = (String, PackageGraph);
224
225 fn new((json, graph): Self::MetadataType) -> Self {
226 Self {
227 json: Arc::new(json),
228 graph: Arc::new(graph),
229 }
230 }
231
232 fn materialize(path: &Utf8Path) -> Result<Self, MetadataMaterializeError> {
233 let json =
235 std::fs::read_to_string(path).map_err(|error| MetadataMaterializeError::Read {
236 path: path.to_owned(),
237 error,
238 })?;
239 let graph = PackageGraph::from_json(&json).map_err(|error| {
240 MetadataMaterializeError::PackageGraphConstruct {
241 path: path.to_owned(),
242 error,
243 }
244 })?;
245
246 Ok(Self::new((json, graph)))
247 }
248}
249
250#[derive(Clone, Debug, Default)]
255pub struct PathMapper {
256 workspace: Option<(Utf8PathBuf, Utf8PathBuf)>,
257 target_dir: Option<(Utf8PathBuf, Utf8PathBuf)>,
258 libdir_mapper: LibdirMapper,
259}
260
261impl PathMapper {
262 pub fn new(
264 orig_workspace_root: impl Into<Utf8PathBuf>,
265 workspace_remap: Option<&Utf8Path>,
266 orig_target_dir: impl Into<Utf8PathBuf>,
267 target_dir_remap: Option<&Utf8Path>,
268 libdir_mapper: LibdirMapper,
269 ) -> Result<Self, PathMapperConstructError> {
270 let workspace_root = workspace_remap
271 .map(|root| Self::canonicalize_dir(root, PathMapperConstructKind::WorkspaceRoot))
272 .transpose()?;
273 let target_dir = target_dir_remap
274 .map(|dir| Self::canonicalize_dir(dir, PathMapperConstructKind::TargetDir))
275 .transpose()?;
276
277 Ok(Self {
278 workspace: workspace_root.map(|w| (orig_workspace_root.into(), w)),
279 target_dir: target_dir.map(|d| (orig_target_dir.into(), d)),
280 libdir_mapper,
281 })
282 }
283
284 pub fn noop() -> Self {
286 Self {
287 workspace: None,
288 target_dir: None,
289 libdir_mapper: LibdirMapper::default(),
290 }
291 }
292
293 pub fn libdir_mapper(&self) -> &LibdirMapper {
295 &self.libdir_mapper
296 }
297
298 fn canonicalize_dir(
299 input: &Utf8Path,
300 kind: PathMapperConstructKind,
301 ) -> Result<Utf8PathBuf, PathMapperConstructError> {
302 let canonicalized_path =
303 input
304 .canonicalize()
305 .map_err(|err| PathMapperConstructError::Canonicalization {
306 kind,
307 input: input.into(),
308 err,
309 })?;
310 let canonicalized_path: Utf8PathBuf =
311 canonicalized_path
312 .try_into()
313 .map_err(|err| PathMapperConstructError::NonUtf8Path {
314 kind,
315 input: input.into(),
316 err,
317 })?;
318 if !canonicalized_path.is_dir() {
319 return Err(PathMapperConstructError::NotADirectory {
320 kind,
321 input: input.into(),
322 canonicalized_path,
323 });
324 }
325
326 Ok(canonicalized_path)
327 }
328
329 pub(super) fn new_target_dir(&self) -> Option<&Utf8Path> {
330 self.target_dir.as_ref().map(|(_, new)| new.as_path())
331 }
332
333 pub(crate) fn map_cwd(&self, path: Utf8PathBuf) -> Utf8PathBuf {
334 match &self.workspace {
335 Some((from, to)) => match path.strip_prefix(from) {
336 Ok(p) => to.join(p),
337 Err(_) => path,
338 },
339 None => path,
340 }
341 }
342
343 pub(crate) fn map_binary(&self, path: Utf8PathBuf) -> Utf8PathBuf {
344 match &self.target_dir {
345 Some((from, to)) => match path.strip_prefix(from) {
346 Ok(p) => to.join(p),
347 Err(_) => path,
348 },
349 None => path,
350 }
351 }
352}
353
354#[derive(Clone, Debug, Default)]
358pub struct LibdirMapper {
359 pub(crate) host: PlatformLibdirMapper,
361
362 pub(crate) target: PlatformLibdirMapper,
364}
365
366#[derive(Clone, Debug, Default)]
370pub(crate) enum PlatformLibdirMapper {
371 Path(Utf8PathBuf),
372 Unavailable,
373 #[default]
374 NotRequested,
375}
376
377impl PlatformLibdirMapper {
378 pub(crate) fn map(&self, original: &PlatformLibdir) -> PlatformLibdir {
379 match self {
380 PlatformLibdirMapper::Path(new) => {
381 PlatformLibdir::Available(new.clone())
385 }
386 PlatformLibdirMapper::Unavailable => {
387 PlatformLibdir::Unavailable(PlatformLibdirUnavailable::NOT_IN_ARCHIVE)
390 }
391 PlatformLibdirMapper::NotRequested => original.clone(),
392 }
393 }
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399
400 #[test]
402 fn test_path_mapper_relative() {
403 let current_dir: Utf8PathBuf = std::env::current_dir()
404 .expect("current dir obtained")
405 .try_into()
406 .expect("current dir is valid UTF-8");
407
408 let temp_workspace_root = Utf8TempDir::new().expect("new temp dir created");
409 let workspace_root_path: Utf8PathBuf = temp_workspace_root
410 .path()
411 .canonicalize()
413 .expect("workspace root canonicalized correctly")
414 .try_into()
415 .expect("workspace root is valid UTF-8");
416 let rel_workspace_root = pathdiff::diff_utf8_paths(&workspace_root_path, ¤t_dir)
417 .expect("abs to abs diff is non-None");
418
419 let temp_target_dir = Utf8TempDir::new().expect("new temp dir created");
420 let target_dir_path: Utf8PathBuf = temp_target_dir
421 .path()
422 .canonicalize()
423 .expect("target dir canonicalized correctly")
424 .try_into()
425 .expect("target dir is valid UTF-8");
426 let rel_target_dir = pathdiff::diff_utf8_paths(&target_dir_path, ¤t_dir)
427 .expect("abs to abs diff is non-None");
428
429 let orig_workspace_root = Utf8Path::new(env!("CARGO_MANIFEST_DIR"));
431 let orig_target_dir = orig_workspace_root.join("target");
432
433 let path_mapper = PathMapper::new(
434 orig_workspace_root,
435 Some(&rel_workspace_root),
436 &orig_target_dir,
437 Some(&rel_target_dir),
438 LibdirMapper::default(),
439 )
440 .expect("remapped paths exist");
441
442 assert_eq!(
443 path_mapper.map_cwd(orig_workspace_root.join("foobar")),
444 workspace_root_path.join("foobar")
445 );
446 assert_eq!(
447 path_mapper.map_binary(orig_target_dir.join("foobar")),
448 target_dir_path.join("foobar")
449 );
450 }
451}