1use std::fs;
32use std::path::{Path, PathBuf};
33use std::process::Command;
34use std::time::{Duration, SystemTime, UNIX_EPOCH};
35
36use anyhow::{Context, Result};
37use serde::{Deserialize, Serialize};
38use walkdir::WalkDir;
39
40use crate::fingerprint::{self, ArtifactInfo, Unit};
41
42const PARITY_SEED: &str = "epicenter-parity-v1";
44
45#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct Toolchain {
50 pub release: String,
52 pub commit: String,
54 pub host: String,
56}
57
58impl Toolchain {
59 pub fn version_line(&self) -> String {
61 format!("rustc {} ({})", self.release, self.commit)
62 }
63}
64
65pub fn toolchain() -> Result<Toolchain> {
67 let output = Command::new("rustc")
68 .arg("-vV")
69 .output()
70 .context("running `rustc -vV`")?;
71 if !output.status.success() {
72 anyhow::bail!("`rustc -vV` failed — is a toolchain installed?");
73 }
74 let text = String::from_utf8_lossy(&output.stdout);
75
76 let mut release = "unknown".to_string();
77 let mut commit = "unknown".to_string();
78 let mut host = "unknown".to_string();
79 for line in text.lines() {
80 if let Some(rest) = line.strip_prefix("rustc ") {
81 let mut parts = rest.split_whitespace();
82 if let Some(version) = parts.next() {
83 release = version.to_string();
84 }
85 if let Some(commit_part) = parts.next() {
87 let value = commit_part.trim_start_matches('(');
88 if !value.is_empty() {
89 commit = value.to_string();
90 }
91 }
92 } else if let Some(value) = line.strip_prefix("host: ") {
93 host = value.trim().to_string();
94 }
95 }
96 Ok(Toolchain {
97 release,
98 commit,
99 host,
100 })
101}
102
103pub fn toolchain_install_time() -> Option<SystemTime> {
107 let output = Command::new("rustc")
108 .args(["--print", "sysroot"])
109 .output()
110 .ok()?;
111 if !output.status.success() {
112 return None;
113 }
114 let sysroot = PathBuf::from(String::from_utf8_lossy(&output.stdout).trim());
115 let rustc = sysroot
116 .join("bin")
117 .join(if cfg!(windows) { "rustc.exe" } else { "rustc" });
118 fs::metadata(&rustc)
119 .and_then(|meta| meta.modified())
120 .ok()
121 .or_else(|| fs::metadata(&sysroot).and_then(|meta| meta.modified()).ok())
122}
123
124pub fn parity_key() -> Result<String> {
129 let tc = toolchain()?;
130 Ok(format!(
131 "{}-{}-{}-{}",
132 tc.release,
133 tc.commit,
134 tc.host,
135 flags_digest()
136 ))
137}
138
139fn flags_digest() -> String {
142 let mut input = String::from(PARITY_SEED);
143 for name in [
144 "RUSTFLAGS",
145 "RUSTDOCFLAGS",
146 "CARGO_BUILD_RUSTFLAGS",
147 "CARGO_BUILD_RUSTDOCFLAGS",
148 "CARGO_BUILD_TARGET",
149 ] {
150 input.push('\0');
151 input.push_str(name);
152 input.push('=');
153 input.push_str(&std::env::var(name).unwrap_or_default());
154 }
155 if let Ok(path) = crate::config::cargo_config_path() {
156 if let Ok(text) = fs::read_to_string(path) {
157 for key in ["rustflags", "rustdocflags", "target"] {
158 if let Some(value) = crate::config::read_table_key(&text, "build", key) {
159 input.push('\0');
160 input.push_str(key);
161 input.push('=');
162 input.push_str(&value);
163 }
164 }
165 }
166 }
167 format!("{:08x}", (fnv1a64(input.as_bytes()) >> 32) as u32)
168}
169
170fn fnv1a64(bytes: &[u8]) -> u64 {
171 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
172 for byte in bytes {
173 hash ^= *byte as u64;
174 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
175 }
176 hash
177}
178
179pub fn store_root(build_root: &Path) -> PathBuf {
184 build_root
185 .parent()
186 .unwrap_or(build_root)
187 .join("store")
188}
189
190pub fn units_dir(store_root: &Path, parity: &str) -> PathBuf {
192 store_root.join(parity).join("units")
193}
194
195fn manifest_path(store_root: &Path, parity: &str) -> PathBuf {
196 store_root.join(parity).join("manifest.json")
197}
198
199#[derive(Debug, Clone, Default, Serialize, Deserialize)]
203pub struct Manifest {
204 pub parity_key: String,
205 pub toolchain: String,
206 pub created: u64,
208 pub updated: u64,
210 pub units: Vec<UnitEntry>,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize)]
215pub struct UnitEntry {
216 pub name: String,
218 pub profile: String,
219 pub memo: String,
221 pub hash: String,
223 pub stamp: u64,
225 pub files: Vec<FileEntry>,
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct FileEntry {
231 pub path: String,
233 pub dest: String,
235 pub size: u64,
236 pub role: String,
238}
239
240fn load_manifest(store_root: &Path, parity: &str) -> Result<Option<Manifest>> {
241 let path = manifest_path(store_root, parity);
242 if !path.is_file() {
243 return Ok(None);
244 }
245 let text = fs::read_to_string(&path)
246 .with_context(|| format!("reading {}", path.display()))?;
247 let manifest = serde_json::from_str(&text)
248 .with_context(|| format!("parsing {}", path.display()))?;
249 Ok(Some(manifest))
250}
251
252fn save_manifest(store_root: &Path, parity: &str, manifest: &Manifest) -> Result<()> {
253 let dir = store_root.join(parity);
254 fs::create_dir_all(&dir)?;
255 let what = serde_json::to_string_pretty(manifest)?;
256 let tmp = dir.join(format!("manifest.json.tmp-{}", std::process::id()));
257 fs::write(&tmp, what + "\n")?;
258 fs::rename(&tmp, manifest_path(store_root, parity))?;
259 Ok(())
260}
261
262pub fn generation_names(store_root: &Path) -> Result<Vec<String>> {
264 if !store_root.is_dir() {
265 return Ok(Vec::new());
266 }
267 let mut names = Vec::new();
268 for entry in fs::read_dir(store_root)
269 .with_context(|| format!("reading {}", store_root.display()))?
270 {
271 let entry = entry?;
272 if !entry.file_type()?.is_dir() {
273 continue;
274 }
275 let name = entry.file_name().to_string_lossy().into_owned();
276 if !name.starts_with('.') {
277 names.push(name);
278 }
279 }
280 names.sort();
281 Ok(names)
282}
283
284#[derive(Debug, Clone)]
288pub struct StoredUnit {
289 pub generation: String,
291 pub entry: UnitEntry,
293 pub dir: PathBuf,
295}
296
297pub fn stored_units(store_root: &Path) -> Result<Vec<StoredUnit>> {
299 let mut out = Vec::new();
300 for generation in generation_names(store_root)? {
301 let Some(manifest) = load_manifest(store_root, &generation)? else {
302 continue;
303 };
304 let units = units_dir(store_root, &generation);
305 for entry in manifest.units {
306 let dir = units.join(&entry.name);
307 out.push(StoredUnit {
308 generation: generation.clone(),
309 entry,
310 dir,
311 });
312 }
313 }
314 Ok(out)
315}
316
317pub fn stored_unit_rustc(unit: &StoredUnit) -> Option<String> {
319 let text = fs::read_to_string(unit.dir.join(format!("{}.json", unit.entry.memo))).ok()?;
320 let value: serde_json::Value = serde_json::from_str(&text).ok()?;
321 match value.get("rustc") {
322 Some(serde_json::Value::Number(number)) => Some(number.to_string()),
323 Some(serde_json::Value::String(text)) => Some(text.clone()),
324 _ => None,
325 }
326}
327
328pub fn drop_units(store_root: &Path, generation: &str, names: &[String]) -> Result<usize> {
332 let Some(mut manifest) = load_manifest(store_root, generation)? else {
333 return Ok(0);
334 };
335 let units = units_dir(store_root, generation);
336 let mut dropped = 0;
337 manifest.units.retain(|entry| {
338 if !names.contains(&entry.name) {
339 return true;
340 }
341 match fs::remove_dir_all(units.join(&entry.name)) {
342 Ok(()) => {
343 dropped += 1;
344 false
345 }
346 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
347 dropped += 1;
348 false
349 }
350 Err(err) => {
351 eprintln!("could not remove {}: {err}", units.join(&entry.name).display());
352 true
353 }
354 }
355 });
356 if manifest.units.is_empty() {
357 let _ = fs::remove_dir_all(store_root.join(generation));
358 } else if dropped > 0 {
359 manifest.updated = now_secs();
360 save_manifest(store_root, generation, &manifest)?;
361 }
362 Ok(dropped)
363}
364
365#[derive(Debug, Default)]
369pub struct HarvestReport {
370 pub parity: String,
371 pub toolchain: String,
372 pub stored: usize,
373 pub refreshed: usize,
374 pub unchanged: usize,
375 pub skipped_stale: usize,
376 pub skipped_incomplete: usize,
377 pub files: usize,
378 pub bytes: u64,
379 pub note: Option<String>,
380}
381
382pub fn harvest(
389 build_root: &Path,
390 store_root: &Path,
391 unit_filter: Option<&str>,
392 dry_run: bool,
393) -> Result<HarvestReport> {
394 let tc = toolchain()?;
395 let parity = parity_key()?;
396 let mut report = HarvestReport {
397 parity: parity.clone(),
398 toolchain: tc.version_line(),
399 ..Default::default()
400 };
401
402 let rows: Vec<(Unit, ArtifactInfo)> = fingerprint::read_units(build_root)?
403 .into_iter()
404 .map(|unit| {
405 let arts = fingerprint::artifacts_for(build_root, &unit);
406 (unit, arts)
407 })
408 .collect();
409
410 let newest_activity = rows.iter().filter_map(|(_, arts)| arts.newest).max();
413 if let (Some(newest), Some(installed)) = (newest_activity, toolchain_install_time()) {
414 if newest < installed {
415 report.note = Some(format!(
416 "the shared cache predates the installed toolchain ({}) — build once, then harvest",
417 tc.version_line()
418 ));
419 return Ok(report);
420 }
421 }
422
423 let active_gen = rows
427 .iter()
428 .filter(|(unit, _)| crate::who::rustc_of(unit) != "unknown")
429 .max_by_key(|(_, arts)| arts.newest)
430 .map(|(unit, _)| crate::who::rustc_of(unit))
431 .unwrap_or_else(|| "unknown".into());
432
433 let mut manifest = load_manifest(store_root, &parity)?.unwrap_or_else(|| Manifest {
434 parity_key: parity.clone(),
435 toolchain: tc.version_line(),
436 created: now_secs(),
437 updated: 0,
438 units: Vec::new(),
439 });
440
441 struct GroupPlan {
443 unit: Unit,
444 files: Vec<(PathBuf, String, String, String)>,
445 memo: String,
446 hash: String,
447 base: u64,
448 }
449 let mut plans: Vec<GroupPlan> = Vec::new();
450
451 for (unit, arts) in &rows {
452 if let Some(query) = unit_filter {
453 if !unit_name_matches(&unit.dir_name, query) {
454 continue;
455 }
456 }
457 let cohort = crate::who::rustc_of(unit);
458 if cohort != "unknown" && active_gen != "unknown" && cohort != active_gen {
459 report.skipped_stale += 1;
460 continue;
461 }
462
463 let unit_dir = build_root
464 .join(&unit.profile)
465 .join(".fingerprint")
466 .join(&unit.dir_name);
467 let profile_dir = build_root.join(&unit.profile);
468 let memo = unit.memo_file.clone();
469 let mut files: Vec<(PathBuf, String, String, String)> = Vec::new();
470 for entry in fs::read_dir(&unit_dir)
477 .with_context(|| format!("reading {}", unit_dir.display()))?
478 {
479 let entry = entry?;
480 if !entry.file_type()?.is_file() {
481 continue;
482 }
483 let name = entry.file_name().to_string_lossy().into_owned();
484 if is_volatile(&name) {
485 continue;
486 }
487 let role = if name.starts_with("dep-") {
488 "dep-info"
489 } else if name.ends_with(".json") {
490 "fingerprint"
491 } else if fs::read_to_string(entry.path())
492 .map(|text| crate::fingerprint::looks_like_hash(text.trim()))
493 .unwrap_or(false)
494 {
495 "memo"
496 } else {
497 "extra"
499 };
500 files.push((
501 entry.path(),
502 name.clone(),
503 format!(".fingerprint/{}/{name}", unit.dir_name),
504 role.into(),
505 ));
506 }
507
508 let mut artifact_count = 0;
511 for (path, _, _) in &arts.paths {
512 let name = path
513 .file_name()
514 .map(|name| name.to_string_lossy().into_owned())
515 .unwrap_or_default();
516 if is_volatile(&name) {
517 continue;
518 }
519 let Ok(rel) = path.strip_prefix(&profile_dir) else {
520 continue;
521 };
522 let rel = rel.to_string_lossy().replace('\\', "/");
523 if rel.starts_with("deps/") && rel.ends_with(".d") {
524 continue;
525 }
526 let role = if rel.starts_with("build/") {
527 "build-tree"
528 } else {
529 "artifact"
530 };
531 files.push((path.clone(), rel.clone(), rel, role.into()));
532 artifact_count += 1;
533 }
534 if artifact_count == 0 {
535 report.skipped_incomplete += 1;
536 continue;
537 }
538
539 plans.push(GroupPlan {
540 base: group_stamp(&files)?,
541 memo,
542 hash: unit.memo_hash.clone().unwrap_or_default(),
543 files,
544 unit: unit.clone(),
545 });
546 }
547
548 let stored_stamps: std::collections::HashMap<String, u64> = manifest
557 .units
558 .iter()
559 .map(|entry| (entry.hash.clone(), entry.stamp))
560 .collect();
561 let index: std::collections::HashMap<&str, usize> = plans
562 .iter()
563 .enumerate()
564 .map(|(position, plan)| (plan.hash.as_str(), position))
565 .collect();
566 let mut plan_deps: Vec<Vec<usize>> = Vec::with_capacity(plans.len());
567 let mut stamps: Vec<u64> = Vec::with_capacity(plans.len());
568 for plan in &plans {
569 let mut deps = Vec::new();
570 let mut stamp = plan.base;
571 for dep in plan
572 .unit
573 .json
574 .as_ref()
575 .map(|json| json.deps.as_slice())
576 .unwrap_or(&[])
577 {
578 let Some(hash) = dep.memo_hash() else { continue };
579 if let Some(&at) = index.get(hash.as_str()) {
580 deps.push(at);
581 } else if let Some(known) = stored_stamps.get(hash.as_str()) {
582 stamp = stamp.max(*known);
583 }
584 }
585 plan_deps.push(deps);
586 stamps.push(stamp);
587 }
588 let mut order: Vec<usize> = Vec::with_capacity(plans.len());
591 let mut settled = vec![false; plans.len()];
592 loop {
593 let mut progressed = false;
594 for at in 0..plans.len() {
595 if settled[at] || !plan_deps[at].iter().all(|&dep| settled[dep]) {
596 continue;
597 }
598 settled[at] = true;
599 order.push(at);
600 progressed = true;
601 }
602 if !progressed {
603 break;
604 }
605 }
606 for at in 0..plans.len() {
607 if !settled[at] {
608 order.push(at);
609 }
610 }
611 for &at in &order {
612 for dep in &plan_deps[at] {
613 stamps[at] = stamps[at].max(stamps[*dep]);
614 }
615 }
616
617 for (at, plan) in plans.iter().enumerate() {
619 let unit = &plan.unit;
620 let files = &plan.files;
621 let stamp = stamps[at];
622 let group_dir = units_dir(store_root, &parity).join(&unit.dir_name);
623
624 let mut new_entries: Vec<(&str, &str)> = files
627 .iter()
628 .map(|(_, path, _, role)| (path.as_str(), role.as_str()))
629 .collect();
630 new_entries.sort_unstable();
631 let already = manifest
632 .units
633 .iter()
634 .find(|entry| entry.name == unit.dir_name)
635 .map(|entry| {
636 let mut stored: Vec<(&str, &str)> = entry
637 .files
638 .iter()
639 .map(|file| (file.path.as_str(), file.role.as_str()))
640 .collect();
641 stored.sort_unstable();
642 entry.hash == plan.hash
643 && entry.profile == unit.profile
644 && entry.stamp == stamp
645 && stored == new_entries
646 && entry
647 .files
648 .iter()
649 .all(|file| group_dir.join(&file.path).is_file())
650 })
651 .unwrap_or(false);
652 if already {
653 report.unchanged += 1;
654 continue;
655 }
656
657 let bytes: u64 = files
658 .iter()
659 .filter_map(|(src, _, _, _)| fs::metadata(src).ok())
660 .map(|meta| meta.len())
661 .sum();
662 if dry_run {
663 report.stored += 1;
664 report.files += files.len();
665 report.bytes += bytes;
666 continue;
667 }
668
669 let mut entries = Vec::with_capacity(files.len());
670 store_group(store_root, &parity, &unit.dir_name, stamp, files, &mut entries)
671 .with_context(|| format!("storing unit {}", unit.dir_name))?;
672
673 let entry = UnitEntry {
674 name: unit.dir_name.clone(),
675 profile: unit.profile.clone(),
676 memo: plan.memo.clone(),
677 hash: plan.hash.clone(),
678 stamp,
679 files: entries,
680 };
681 match manifest
682 .units
683 .iter_mut()
684 .find(|existing| existing.name == entry.name)
685 {
686 Some(slot) => {
687 *slot = entry;
688 report.refreshed += 1;
689 }
690 None => {
691 manifest.units.push(entry);
692 report.stored += 1;
693 }
694 }
695 report.files += files.len();
696 report.bytes += bytes;
697 }
698
699 if !dry_run {
700 manifest.units.sort_by(|a, b| {
701 (a.profile.as_str(), a.name.as_str()).cmp(&(b.profile.as_str(), b.name.as_str()))
702 });
703 manifest.updated = now_secs();
704 save_manifest(store_root, &parity, &manifest)?;
705 }
706 Ok(report)
707}
708
709fn store_group(
713 store_root: &Path,
714 parity: &str,
715 name: &str,
716 stamp: u64,
717 files: &[(PathBuf, String, String, String)],
718 entries: &mut Vec<FileEntry>,
719) -> Result<()> {
720 let units = units_dir(store_root, parity);
721 let final_dir = units.join(name);
722 let tmp = units.join(format!(".{name}.tmp-{}", std::process::id()));
723 if tmp.is_dir() {
724 fs::remove_dir_all(&tmp)?;
725 }
726 fs::create_dir_all(&tmp)?;
727
728 for (src, path, dest, role) in files {
729 let dst = tmp.join(path);
730 if let Some(parent) = dst.parent() {
731 fs::create_dir_all(parent)?;
732 }
733 fs::copy(src, &dst)
734 .with_context(|| format!("copying {} -> {}", src.display(), dst.display()))?;
735 set_stamp(&dst, stamp)?;
736 make_read_only(&dst)?;
737 let size = fs::metadata(&dst).map(|meta| meta.len()).unwrap_or(0);
738 entries.push(FileEntry {
739 path: path.clone(),
740 dest: dest.clone(),
741 size,
742 role: role.clone(),
743 });
744 }
745
746 let old = units.join(format!(".{name}.old-{}", std::process::id()));
747 if final_dir.is_dir() {
748 fs::rename(&final_dir, &old)?;
749 }
750 fs::rename(&tmp, &final_dir)?;
751 if old.is_dir() {
752 let _ = fs::remove_dir_all(&old);
753 }
754 Ok(())
755}
756
757fn group_stamp(files: &[(PathBuf, String, String, String)]) -> Result<u64> {
762 let mut newest: Option<SystemTime> = None;
763 let mut metadata: Option<SystemTime> = None;
764 for (src, path, _, _) in files {
765 let modified = fs::metadata(src)
766 .and_then(|meta| meta.modified())
767 .with_context(|| format!("reading mtime of {}", src.display()))?;
768 if newest.map_or(true, |current| modified > current) {
769 newest = Some(modified);
770 }
771 if path.ends_with(".rmeta") && metadata.map_or(true, |current| modified > current) {
772 metadata = Some(modified);
773 }
774 }
775 Ok(match metadata.or(newest) {
781 Some(time) => ceil_secs(time),
782 None => now_secs(),
783 })
784}
785
786fn ceil_secs(time: SystemTime) -> u64 {
788 match time.duration_since(UNIX_EPOCH) {
789 Ok(duration) => {
790 let secs = duration.as_secs();
791 if duration.subsec_nanos() > 0 { secs + 1 } else { secs }
792 }
793 Err(_) => 0,
794 }
795}
796
797fn now_secs() -> u64 {
798 SystemTime::now()
799 .duration_since(UNIX_EPOCH)
800 .map(|duration| duration.as_secs())
801 .unwrap_or(0)
802}
803
804fn set_stamp(path: &Path, stamp: u64) -> Result<()> {
805 let time = UNIX_EPOCH + Duration::from_secs(stamp);
806 let file = fs::File::open(path)
807 .with_context(|| format!("opening {} to stamp it", path.display()))?;
808 file.set_modified(time)
809 .with_context(|| format!("stamping {}", path.display()))?;
810 Ok(())
811}
812
813fn copy_private(src: &Path, dst: &Path, stamp: u64) -> std::io::Result<()> {
818 fs::copy(src, dst)?;
819 make_writable(dst)?;
820 let _ = set_stamp(dst, stamp);
821 Ok(())
822}
823
824#[cfg(unix)]
826fn make_writable(path: &Path) -> std::io::Result<()> {
827 use std::os::unix::fs::PermissionsExt;
828 let mut perms = fs::metadata(path)?.permissions();
829 perms.set_mode(0o644);
830 fs::set_permissions(path, perms)
831}
832
833#[cfg(not(unix))]
834fn make_writable(path: &Path) -> std::io::Result<()> {
835 let mut perms = fs::metadata(path)?.permissions();
836 perms.set_readonly(false);
837 fs::set_permissions(path, perms)
838}
839
840fn make_read_only(path: &Path) -> Result<()> {
844 #[cfg(unix)]
845 {
846 use std::os::unix::fs::PermissionsExt;
847 fs::set_permissions(path, fs::Permissions::from_mode(0o444))?;
848 }
849 #[cfg(not(unix))]
850 {
851 let _ = path;
852 }
853 Ok(())
854}
855
856fn is_volatile(name: &str) -> bool {
860 name == "invoked.timestamp" || name.contains(".kache")
861}
862
863#[derive(Debug, Default)]
867pub struct RestoreReport {
868 pub parity: String,
869 pub target: PathBuf,
870 pub units_linked: usize,
871 pub units_already: usize,
872 pub files_linked: usize,
873 pub files_copied: usize,
874 pub files_existing: usize,
875 pub bytes: u64,
876 pub errors: usize,
877 pub note: Option<String>,
878}
879
880pub fn restore(
891 view_root: &Path,
892 store_root: &Path,
893 unit_filter: Option<&str>,
894 dry_run: bool,
895) -> Result<RestoreReport> {
896 let parity = parity_key()?;
897 let mut report = RestoreReport {
898 parity: parity.clone(),
899 target: view_root.to_path_buf(),
900 ..Default::default()
901 };
902 let Some(manifest) = load_manifest(store_root, &parity)? else {
903 report.note = Some(
904 "the vault holds no units for this generation yet — run `epicenter harvest` after a build"
905 .into(),
906 );
907 return Ok(report);
908 };
909
910 let units = units_dir(store_root, &parity);
911 for entry in &manifest.units {
912 if let Some(query) = unit_filter {
913 if !unit_name_matches(&entry.name, query) {
914 continue;
915 }
916 }
917 let fp_dir = view_root
918 .join(&entry.profile)
919 .join(".fingerprint")
920 .join(&entry.name);
921 if fs::read_to_string(fp_dir.join(&entry.memo))
922 .map(|text| text.trim() == entry.hash)
923 .unwrap_or(false)
924 {
925 report.units_already += 1;
926 continue;
927 }
928
929 let mut unit_touched = false;
930 for file in &entry.files {
931 let dst = view_root.join(&entry.profile).join(&file.dest);
932 if dst.exists() {
933 report.files_existing += 1;
934 continue;
935 }
936 let shared = file.path.starts_with("deps/") && !file.path.ends_with(".rmeta");
941 if dry_run {
942 unit_touched = true;
943 if shared {
944 report.files_linked += 1;
945 } else {
946 report.files_copied += 1;
947 }
948 report.bytes += file.size;
949 continue;
950 }
951 let src = units.join(&entry.name).join(&file.path);
952 if let Some(parent) = dst.parent() {
953 if let Err(err) = fs::create_dir_all(parent) {
954 report.errors += 1;
955 eprintln!("restore: creating {}: {err}", parent.display());
956 continue;
957 }
958 }
959 let linked = shared && fs::hard_link(&src, &dst).is_ok();
960 let result = if linked {
961 Ok(())
962 } else {
963 copy_private(&src, &dst, entry.stamp)
964 };
965 match result {
966 Ok(()) => {
967 if linked {
968 report.files_linked += 1;
969 } else {
970 report.files_copied += 1;
971 }
972 unit_touched = true;
973 }
974 Err(err) => {
975 report.errors += 1;
976 eprintln!("restore: writing {}: {err}", dst.display());
977 continue;
978 }
979 }
980 report.bytes += file.size;
981 }
982 if unit_touched {
983 report.units_linked += 1;
984 }
985 }
986 Ok(report)
987}
988
989#[derive(Debug, Default)]
993pub struct VerifyReport {
994 pub parity: String,
995 pub units: usize,
996 pub files: usize,
997 pub bytes: u64,
998 pub problems: Vec<String>,
1000 pub warnings: Vec<String>,
1002 pub ordering: Vec<String>,
1006}
1007
1008impl VerifyReport {
1009 pub fn is_clean(&self) -> bool {
1010 self.problems.is_empty()
1011 }
1012}
1013
1014pub fn verify(store_root: &Path) -> Result<Vec<VerifyReport>> {
1018 let mut reports = Vec::new();
1019 for parity in generation_names(store_root)? {
1020 let mut report = VerifyReport {
1021 parity: parity.clone(),
1022 ..Default::default()
1023 };
1024 let Some(manifest) = load_manifest(store_root, &parity)? else {
1025 report.problems.push(format!(
1026 "generation {parity} has no readable manifest.json — it cannot be audited"
1027 ));
1028 reports.push(report);
1029 continue;
1030 };
1031 let units = units_dir(store_root, &parity);
1032 for entry in &manifest.units {
1033 for file in &entry.files {
1034 let path = units.join(&entry.name).join(&file.path);
1035 let meta = match fs::metadata(&path) {
1036 Ok(meta) => meta,
1037 Err(_) => {
1038 report
1039 .problems
1040 .push(format!("missing: {}", display_join(&path, units.as_path())));
1041 continue;
1042 }
1043 };
1044 report.files += 1;
1045 report.bytes += meta.len();
1046 if meta.len() != file.size {
1047 report.problems.push(format!(
1048 "size drift: {} (recorded {}, found {})",
1049 display_join(&path, &units),
1050 file.size,
1051 meta.len()
1052 ));
1053 }
1054 let found_stamp = meta.modified().map(ceil_secs).unwrap_or(0);
1055 if found_stamp != entry.stamp {
1056 report.problems.push(format!(
1057 "stamp drift: {} (recorded {}, found {})",
1058 display_join(&path, &units),
1059 entry.stamp,
1060 found_stamp
1061 ));
1062 }
1063 if file.role == "memo" {
1064 match fs::read_to_string(&path) {
1065 Ok(text) if file.path == entry.memo && text.trim() != entry.hash => {
1066 report.problems.push(format!(
1067 "hash drift: {} (recorded {}, found {})",
1068 display_join(&path, &units),
1069 entry.hash,
1070 text.trim()
1071 ));
1072 }
1073 Ok(text)
1074 if file.path != entry.memo
1075 && !crate::fingerprint::looks_like_hash(text.trim()) =>
1076 {
1077 report.problems.push(format!(
1078 "malformed memo file: {} ({:?})",
1079 display_join(&path, &units),
1080 text.trim()
1081 ));
1082 }
1083 Ok(_) => {}
1084 Err(err) => report.problems.push(format!(
1085 "unreadable memo file {}: {err}",
1086 display_join(&path, &units)
1087 )),
1088 }
1089 }
1090 }
1091 }
1092 let by_hash: std::collections::HashMap<&str, usize> = manifest
1099 .units
1100 .iter()
1101 .enumerate()
1102 .map(|(index, entry)| (entry.hash.as_str(), index))
1103 .collect();
1104 for entry in &manifest.units {
1105 let json = entry
1106 .files
1107 .iter()
1108 .find(|file| file.path.ends_with(".json"))
1109 .and_then(|file| {
1110 fs::read_to_string(units.join(&entry.name).join(&file.path)).ok()
1111 })
1112 .and_then(|text| crate::fingerprint::parse_fingerprint(&text));
1113 let Some(json) = json else { continue };
1114 for dep in &json.deps {
1115 let Some(dep_index) = dep
1116 .memo_hash()
1117 .and_then(|hash| by_hash.get(hash.as_str()))
1118 .copied()
1119 else {
1120 continue;
1121 };
1122 let dep_entry = &manifest.units[dep_index];
1123 if dep_entry.stamp > entry.stamp {
1124 report.ordering.push(format!(
1125 "{} is older than its dependency {} ({} older) — cargo recompiles it after a restore",
1126 entry.name,
1127 dep_entry.name,
1128 crate::util::human_duration(Duration::from_secs(dep_entry.stamp - entry.stamp))
1129 ));
1130 }
1131 }
1132 }
1133 report.units = manifest.units.len();
1134
1135 if units.is_dir() {
1137 let known: std::collections::BTreeSet<&str> =
1138 manifest.units.iter().map(|unit| unit.name.as_str()).collect();
1139 for entry in fs::read_dir(&units)? {
1140 let entry = entry?;
1141 if !entry.file_type()?.is_dir() {
1142 continue;
1143 }
1144 let name = entry.file_name().to_string_lossy().into_owned();
1145 if name.starts_with('.') {
1146 continue; }
1148 if !known.contains(name.as_str()) {
1149 report
1150 .warnings
1151 .push(format!("unrecorded unit group: units/{name}"));
1152 }
1153 }
1154 }
1155 reports.push(report);
1156 }
1157 Ok(reports)
1158}
1159
1160fn display_join(path: &Path, base: &Path) -> String {
1162 path.strip_prefix(base)
1163 .unwrap_or(path)
1164 .to_string_lossy()
1165 .replace('\\', "/")
1166}
1167
1168#[derive(Debug, Default)]
1172pub struct GenerationInfo {
1173 pub parity: String,
1174 pub active: bool,
1176 pub units: usize,
1177 pub files: usize,
1178 pub bytes: u64,
1180 pub linked: usize,
1182 pub created: Option<u64>,
1183 pub updated: Option<u64>,
1184}
1185
1186pub fn generations(store_root: &Path) -> Result<Vec<GenerationInfo>> {
1188 let active = parity_key().ok();
1189 let mut out = Vec::new();
1190 for parity in generation_names(store_root)? {
1191 let mut info = GenerationInfo {
1192 parity: parity.clone(),
1193 active: active.as_deref() == Some(parity.as_str()),
1194 ..Default::default()
1195 };
1196 if let Some(manifest) = load_manifest(store_root, &parity)? {
1197 info.units = manifest.units.len();
1198 info.created = Some(manifest.created);
1199 info.updated = Some(manifest.updated);
1200 }
1201 let units = units_dir(store_root, &parity);
1202 if units.is_dir() {
1203 for entry in WalkDir::new(&units).min_depth(1) {
1204 let entry = entry?;
1205 if !entry.file_type().is_file() {
1206 continue;
1207 }
1208 let meta = entry.metadata()?;
1209 info.files += 1;
1210 info.bytes += meta.len();
1211 if link_count(&meta) > 1 {
1212 info.linked += 1;
1213 }
1214 }
1215 }
1216 out.push(info);
1217 }
1218 Ok(out)
1219}
1220
1221pub(crate) fn link_count(meta: &fs::Metadata) -> u64 {
1223 #[cfg(unix)]
1224 {
1225 use std::os::unix::fs::MetadataExt;
1226 meta.nlink()
1227 }
1228 #[cfg(not(unix))]
1229 {
1230 let _ = meta;
1231 1
1232 }
1233}
1234
1235fn unit_name_matches(name: &str, query: &str) -> bool {
1237 name.contains(query)
1238}
1239
1240#[cfg(test)]
1241mod tests {
1242 use super::*;
1243
1244 fn synthetic_build_dir(root: &Path) -> Result<()> {
1249 let jsons = r#"{"rustc":12019306335353385202,"features":"[]","declared_features":"[]","deps":[]}"#;
1250
1251 let lib = root.join("debug/.fingerprint/serde-102a33f055a8d229");
1252 fs::create_dir_all(&lib)?;
1253 fs::write(lib.join("lib-serde"), "5d82713a7f2d05e2")?;
1254 fs::write(lib.join("lib-serde.json"), jsons)?;
1255 fs::write(lib.join("dep-lib-serde"), [0u8; 24])?;
1256 fs::write(lib.join("invoked.timestamp"), "")?;
1257 fs::create_dir_all(root.join("debug/deps"))?;
1258 fs::write(
1259 root.join("debug/deps/libserde-102a33f055a8d229.rlib"),
1260 [7u8; 64],
1261 )?;
1262 fs::write(
1263 root.join("debug/deps/libserde-102a33f055a8d229.rmeta"),
1264 [6u8; 48],
1265 )?;
1266 fs::write(
1267 root.join("debug/deps/serde-102a33f055a8d229.d"),
1268 b"side file",
1269 )?;
1270 fs::write(
1271 root.join("debug/deps/libserde-102a33f055a8d229.kache-real"),
1272 b"wrapper",
1273 )?;
1274
1275 let compile = root.join("debug/.fingerprint/x-1122334455667788");
1276 fs::create_dir_all(&compile)?;
1277 fs::write(
1278 compile.join("build-script-build-script-build"),
1279 "aaaabbbbccccdddd",
1280 )?;
1281 fs::write(
1282 compile.join("build-script-build-script-build.json"),
1283 jsons,
1284 )?;
1285 fs::write(
1286 compile.join("dep-build-script-build-script-build"),
1287 [0u8; 8],
1288 )?;
1289 let compile_anchor = root.join("debug/build/x-1122334455667788");
1290 fs::create_dir_all(&compile_anchor)?;
1291 fs::write(
1292 compile_anchor.join("build_script_build-1122334455667788"),
1293 [3u8; 32],
1294 )?;
1295 fs::write(
1296 compile_anchor.join("build_script_build-1122334455667788.d"),
1297 b"dep",
1298 )?;
1299 fs::write(
1300 compile_anchor.join("build_script_build-1122334455667788.kache-real"),
1301 b"wrapper",
1302 )?;
1303 fs::write(compile_anchor.join("invoked.timestamp"), "")?;
1304
1305 let run = root.join("debug/.fingerprint/x-aabbccddeeff0011");
1307 fs::create_dir_all(&run)?;
1308 fs::write(
1309 run.join("run-build-script-build-script-build"),
1310 "eeeeffff00001111",
1311 )?;
1312 fs::write(
1313 run.join("run-build-script-build-script-build.json"),
1314 jsons,
1315 )?;
1316 let run_anchor = root.join("debug/build/x-aabbccddeeff0011");
1317 fs::create_dir_all(&run_anchor)?;
1318 fs::write(
1319 run_anchor.join("output"),
1320 b"cargo:rerun-if-changed=build.rs",
1321 )?;
1322 fs::write(run_anchor.join("stderr"), b"")?;
1323 fs::write(run_anchor.join("invoked.timestamp"), "")?;
1324 Ok(())
1325 }
1326
1327 fn synthetic_husk(root: &Path) -> Result<()> {
1329 let unit = root.join("debug/.fingerprint/husk-aaaabbbbccccdddd");
1330 fs::create_dir_all(&unit)?;
1331 fs::write(unit.join("lib-husk"), "0000111122223333")?;
1332 Ok(())
1333 }
1334
1335 #[test]
1336 fn parity_key_is_stable_and_shaped() -> Result<()> {
1337 let first = parity_key()?;
1338 let second = parity_key()?;
1339 assert_eq!(first, second);
1340 let tc = toolchain()?;
1341 assert!(first.starts_with(&format!("{}-{}", tc.release, tc.commit)));
1342 assert!(first.ends_with(&flags_digest()));
1343 Ok(())
1344 }
1345
1346 #[test]
1347 fn harvest_stores_groups_with_one_canonical_stamp() -> Result<()> {
1348 let build = tempfile::tempdir()?;
1349 let store = tempfile::tempdir()?;
1350 synthetic_build_dir(build.path())?;
1351
1352 let report = harvest(build.path(), store.path(), None, false)?;
1353 assert_eq!(report.stored, 3, "lib + build-script compile + run");
1354 assert_eq!(report.skipped_incomplete, 0);
1355 assert!(report.files >= 10, "got {}", report.files);
1356 assert!(report.bytes > 0);
1357
1358 let parity = parity_key()?;
1359 let manifest = load_manifest(store.path(), &parity)?.expect("manifest written");
1360 assert_eq!(manifest.units.len(), 3);
1361 let lib_entry = manifest
1362 .units
1363 .iter()
1364 .find(|unit| unit.name == "serde-102a33f055a8d229")
1365 .expect("lib unit recorded");
1366 assert_eq!(lib_entry.hash, "5d82713a7f2d05e2");
1367 assert_eq!(lib_entry.profile, "debug");
1368
1369 let group = units_dir(store.path(), &parity).join(&lib_entry.name);
1371 for file in &lib_entry.files {
1372 let meta = fs::metadata(group.join(&file.path))?;
1373 assert_eq!(ceil_secs(meta.modified()?), lib_entry.stamp, "{}", file.path);
1374 assert_eq!(meta.len(), file.size);
1375 }
1376
1377 assert!(!group.join("invoked.timestamp").exists());
1379 for file in &lib_entry.files {
1380 assert!(!file.path.contains("kache"), "{}", file.path);
1381 assert!(!file.path.ends_with(".d"), "{}", file.path);
1382 }
1383
1384 #[cfg(unix)]
1385 {
1386 use std::os::unix::fs::PermissionsExt;
1387 let meta = fs::metadata(group.join(&lib_entry.files[0].path))?;
1388 assert_eq!(meta.permissions().mode() & 0o777, 0o444, "store is read-only");
1389 }
1390 Ok(())
1391 }
1392
1393 #[test]
1394 fn harvest_is_idempotent_and_skips_incomplete_units() -> Result<()> {
1395 let build = tempfile::tempdir()?;
1396 let store = tempfile::tempdir()?;
1397 synthetic_build_dir(build.path())?;
1398 synthetic_husk(build.path())?;
1399
1400 let first = harvest(build.path(), store.path(), None, false)?;
1401 assert_eq!(first.stored, 3);
1402 assert_eq!(first.skipped_incomplete, 1, "husk without artifacts");
1403
1404 let second = harvest(build.path(), store.path(), None, false)?;
1405 assert_eq!(second.stored, 0);
1406 assert_eq!(second.unchanged, 3);
1407 assert_eq!(second.skipped_incomplete, 1);
1408 Ok(())
1409 }
1410
1411 #[test]
1412 fn harvest_refreshes_when_the_file_set_grows() -> Result<()> {
1413 let build = tempfile::tempdir()?;
1414 let store = tempfile::tempdir()?;
1415 synthetic_build_dir(build.path())?;
1416 harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1417
1418 fs::write(
1419 build
1420 .path()
1421 .join("debug/deps/libserde-102a33f055a8d229.so"),
1422 [1u8; 16],
1423 )?;
1424 let report = harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1425 assert_eq!(report.refreshed, 1);
1426 assert_eq!(report.stored, 0);
1427
1428 let manifest = load_manifest(store.path(), &parity_key()?)?.expect("manifest");
1429 let paths: Vec<&str> = manifest.units[0]
1430 .files
1431 .iter()
1432 .map(|file| file.path.as_str())
1433 .collect();
1434 assert!(
1435 paths.contains(&"deps/libserde-102a33f055a8d229.so"),
1436 "{paths:?}"
1437 );
1438 Ok(())
1439 }
1440
1441 #[test]
1442 fn harvest_stores_all_memos_of_shared_dirs() -> Result<()> {
1443 let build = tempfile::tempdir()?;
1444 let store = tempfile::tempdir()?;
1445 synthetic_build_dir(build.path())?;
1446 let lib = build.path().join("debug/.fingerprint/serde-102a33f055a8d229");
1447 fs::write(lib.join("bin-serde"), "1111222233334444")?;
1448 fs::write(lib.join("dep-bin-serde"), [0u8; 8])?;
1449
1450 let report = harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1451 assert_eq!(report.stored, 1);
1452 let manifest = load_manifest(store.path(), &parity_key()?)?.expect("manifest");
1453 let entry = &manifest.units[0];
1454 let paths: Vec<&str> = entry.files.iter().map(|file| file.path.as_str()).collect();
1455 assert!(paths.contains(&"bin-serde"), "{paths:?}");
1456 assert!(paths.contains(&"dep-bin-serde"), "{paths:?}");
1457
1458 let reports = verify(store.path())?;
1459 assert!(reports[0].is_clean(), "{:?}", reports[0].problems);
1460 Ok(())
1461 }
1462
1463 #[test]
1464 fn harvest_dry_run_writes_nothing() -> Result<()> {
1465 let build = tempfile::tempdir()?;
1466 let store = tempfile::tempdir()?;
1467 synthetic_build_dir(build.path())?;
1468
1469 let report = harvest(build.path(), store.path(), None, true)?;
1470 assert_eq!(report.stored, 3);
1471 assert_eq!(generation_names(store.path())?.len(), 0, "no generation");
1472 Ok(())
1473 }
1474
1475 #[test]
1476 fn harvest_unit_filter_selects_one_group() -> Result<()> {
1477 let build = tempfile::tempdir()?;
1478 let store = tempfile::tempdir()?;
1479 synthetic_build_dir(build.path())?;
1480
1481 let report = harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1482 assert_eq!(report.stored, 1);
1483 let manifest = load_manifest(store.path(), &parity_key()?)?.expect("manifest");
1484 assert_eq!(manifest.units.len(), 1);
1485 assert_eq!(manifest.units[0].name, "serde-102a33f055a8d229");
1486 Ok(())
1487 }
1488
1489 #[test]
1490 fn restore_links_hardlinks_and_never_overwrites() -> Result<()> {
1491 let build = tempfile::tempdir()?;
1492 let store = tempfile::tempdir()?;
1493 let view = tempfile::tempdir()?;
1494 synthetic_build_dir(build.path())?;
1495 harvest(build.path(), store.path(), None, false)?;
1496
1497 let occupied = view.path().join("debug/deps/libserde-102a33f055a8d229.rlib");
1499 fs::create_dir_all(occupied.parent().unwrap())?;
1500 fs::write(&occupied, b"live artifact")?;
1501
1502 let report = restore(view.path(), store.path(), None, false)?;
1503 assert_eq!(report.units_linked, 3);
1504 assert_eq!(report.files_existing, 1, "the live artifact was skipped");
1505 assert_eq!(fs::read(&occupied)?, b"live artifact");
1506 assert_eq!(report.files_linked, 0, "no other deps/ artifact");
1510 assert!(report.files_copied >= 12, "got {}", report.files_copied);
1511
1512 let memo = view
1514 .path()
1515 .join("debug/.fingerprint/serde-102a33f055a8d229/lib-serde");
1516 assert_eq!(fs::read_to_string(&memo)?.trim(), "5d82713a7f2d05e2");
1517 assert!(
1518 view.path()
1519 .join("debug/build/x-aabbccddeeff0011/output")
1520 .is_file()
1521 );
1522
1523 #[cfg(unix)]
1527 {
1528 use std::os::unix::fs::MetadataExt;
1529 use std::os::unix::fs::PermissionsExt;
1530 let parity = parity_key()?;
1531 let stored =
1532 units_dir(store.path(), &parity).join("serde-102a33f055a8d229/lib-serde");
1533 let view_meta = fs::metadata(&memo)?;
1534 assert_ne!(view_meta.ino(), fs::metadata(&stored)?.ino(), "state not shared");
1535 assert_eq!(view_meta.nlink(), 1, "state owns its inode");
1536 assert_ne!(view_meta.permissions().mode() & 0o200, 0, "state writable");
1537 }
1538
1539 let again = restore(view.path(), store.path(), None, false)?;
1541 assert_eq!(again.units_already, 3);
1542 assert_eq!(again.files_linked, 0);
1543 assert_eq!(again.files_copied, 0);
1544 Ok(())
1545 }
1546
1547 #[test]
1548 fn restore_into_missing_profile_dirs_creates_them() -> Result<()> {
1549 let build = tempfile::tempdir()?;
1550 let store = tempfile::tempdir()?;
1551 let view = tempfile::tempdir()?;
1552 synthetic_build_dir(build.path())?;
1553 harvest(build.path(), store.path(), None, false)?;
1554
1555 let report = restore(view.path(), store.path(), None, false)?;
1556 assert_eq!(report.errors, 0);
1557 assert!(view.path().join("debug/.fingerprint").is_dir());
1558 Ok(())
1559 }
1560
1561 #[test]
1562 fn restore_without_a_generation_reports_fail_open() -> Result<()> {
1563 let view = tempfile::tempdir()?;
1564 let store = tempfile::tempdir()?;
1565 let report = restore(view.path(), store.path(), None, false)?;
1566 assert!(report.note.is_some());
1567 assert_eq!(report.files_linked, 0);
1568 Ok(())
1569 }
1570
1571 #[test]
1572 fn verify_passes_clean_and_flags_drift() -> Result<()> {
1573 let build = tempfile::tempdir()?;
1574 let store = tempfile::tempdir()?;
1575 synthetic_build_dir(build.path())?;
1576 harvest(build.path(), store.path(), None, false)?;
1577
1578 let reports = verify(store.path())?;
1579 assert_eq!(reports.len(), 1);
1580 assert!(reports[0].is_clean(), "{:?}", reports[0].problems);
1581 assert_eq!(reports[0].units, 3);
1582
1583 let parity = parity_key()?;
1585 let victim =
1586 units_dir(store.path(), &parity).join("serde-102a33f055a8d229/dep-lib-serde");
1587 #[cfg(unix)]
1588 {
1589 use std::os::unix::fs::PermissionsExt;
1590 fs::set_permissions(&victim, fs::Permissions::from_mode(0o644))?;
1591 }
1592 fs::write(&victim, [9u8; 32])?;
1593
1594 let reports = verify(store.path())?;
1595 assert!(!reports[0].is_clean());
1596 assert!(
1597 reports[0]
1598 .problems
1599 .iter()
1600 .any(|line| line.contains("size drift")),
1601 "{:?}",
1602 reports[0].problems
1603 );
1604 Ok(())
1605 }
1606
1607 #[test]
1608 fn verify_reports_unrecorded_unit_groups() -> Result<()> {
1609 let build = tempfile::tempdir()?;
1610 let store = tempfile::tempdir()?;
1611 synthetic_build_dir(build.path())?;
1612 harvest(build.path(), store.path(), None, false)?;
1613
1614 let stray = units_dir(store.path(), &parity_key()?).join("stray-0000000000000000");
1615 fs::create_dir_all(stray.join("deps"))?;
1616 fs::write(stray.join("deps/x.rlib"), [0u8; 4])?;
1617
1618 let reports = verify(store.path())?;
1619 assert!(
1620 reports[0]
1621 .warnings
1622 .iter()
1623 .any(|line| line.contains("stray-0000000000000000")),
1624 "{:?}",
1625 reports[0].warnings
1626 );
1627 Ok(())
1628 }
1629
1630 #[test]
1631 fn generations_summary_counts_store_files() -> Result<()> {
1632 let build = tempfile::tempdir()?;
1633 let store = tempfile::tempdir()?;
1634 synthetic_build_dir(build.path())?;
1635 harvest(build.path(), store.path(), None, false)?;
1636
1637 let generations = generations(store.path())?;
1638 assert_eq!(generations.len(), 1);
1639 assert!(generations[0].active, "current toolchain generation");
1640 assert_eq!(generations[0].units, 3);
1641 assert!(generations[0].files >= 10);
1642 assert!(generations[0].linked <= generations[0].files);
1643 Ok(())
1644 }
1645
1646 #[test]
1647 fn cached_diagnostics_are_stored_as_extra_and_verify_clean() -> Result<()> {
1648 let build = tempfile::tempdir()?;
1649 let store = tempfile::tempdir()?;
1650 synthetic_build_dir(build.path())?;
1651 fs::write(
1653 build
1654 .path()
1655 .join("debug/.fingerprint/serde-102a33f055a8d229/output-lib-serde"),
1656 r#"{"$message_type":"diagnostic","message":"warning: unused"}"#,
1657 )?;
1658
1659 harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1660 let manifest = load_manifest(store.path(), &parity_key()?)?.expect("manifest");
1661 let output = manifest.units[0]
1662 .files
1663 .iter()
1664 .find(|file| file.path == "output-lib-serde")
1665 .expect("cached diagnostics are stored");
1666 assert_eq!(output.role, "extra");
1667
1668 let reports = verify(store.path())?;
1669 assert!(reports[0].is_clean(), "{:?}", reports[0].problems);
1670 Ok(())
1671 }
1672
1673 #[test]
1674 fn harvest_refreshes_when_a_role_drifted() -> Result<()> {
1675 let build = tempfile::tempdir()?;
1676 let store = tempfile::tempdir()?;
1677 synthetic_build_dir(build.path())?;
1678 harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1679
1680 let parity = parity_key()?;
1683 let mut manifest = load_manifest(store.path(), &parity)?.expect("manifest");
1684 let memo_index = manifest.units[0]
1685 .files
1686 .iter()
1687 .position(|file| file.path == manifest.units[0].memo)
1688 .expect("memo recorded");
1689 manifest.units[0].files[memo_index].role = "extra".to_string();
1690 save_manifest(store.path(), &parity, &manifest)?;
1691
1692 let report = harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1693 assert_eq!(report.refreshed, 1, "role drift forces a refresh");
1694 let manifest = load_manifest(store.path(), &parity)?.expect("manifest");
1695 let role = &manifest.units[0].files[memo_index].role;
1696 assert_eq!(role, "memo", "roles are rewritten");
1697 Ok(())
1698 }
1699
1700 #[test]
1701 fn restore_shares_artifacts_but_copies_state_writable() -> Result<()> {
1702 let build = tempfile::tempdir()?;
1703 let store = tempfile::tempdir()?;
1704 synthetic_build_dir(build.path())?;
1705 harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1706
1707 let view = tempfile::tempdir()?;
1708 restore(view.path(), store.path(), Some("102a33f0"), false)?;
1709
1710 let state = view
1711 .path()
1712 .join("debug/.fingerprint/serde-102a33f055a8d229/lib-serde");
1713 let artifact = view
1714 .path()
1715 .join("debug/deps/libserde-102a33f055a8d229.rlib");
1716 let metadata = view
1717 .path()
1718 .join("debug/deps/libserde-102a33f055a8d229.rmeta");
1719 assert!(state.is_file() && artifact.is_file() && metadata.is_file());
1720
1721 let parity = parity_key()?;
1722 let manifest = load_manifest(store.path(), &parity)?.expect("manifest");
1723 let entry = manifest
1724 .units
1725 .iter()
1726 .find(|entry| entry.name == "serde-102a33f055a8d229")
1727 .expect("stored unit");
1728 assert_eq!(ceil_secs(fs::metadata(&state)?.modified()?), entry.stamp);
1729
1730 fs::write(&state, "ffffffffffffffff")?;
1733 let stored = units_dir(store.path(), &parity)
1734 .join("serde-102a33f055a8d229")
1735 .join("lib-serde");
1736 assert_eq!(fs::read_to_string(&stored)?, "5d82713a7f2d05e2");
1737
1738 #[cfg(unix)]
1739 {
1740 use std::os::unix::fs::MetadataExt;
1741 assert!(fs::metadata(&artifact)?.nlink() >= 2, "artifact shares the store inode");
1742 assert_eq!(fs::metadata(&state)?.nlink(), 1, "state is a private copy");
1743 assert_eq!(
1744 fs::metadata(&metadata)?.nlink(),
1745 1,
1746 ".rmeta is a private copy (rustc opens it for write)"
1747 );
1748 }
1749 Ok(())
1750 }
1751
1752 fn synthetic_edge_dir(root: &Path, dep_at: u64, unit_at: u64) -> Result<()> {
1756 fs::create_dir_all(root.join("debug/deps"))?;
1757 let core = root.join("debug/.fingerprint/serde_core-b468be6fa8c1f936");
1758 fs::create_dir_all(&core)?;
1759 fs::write(core.join("lib-serde_core"), "71a9f323efa5ae82")?;
1760 fs::write(core.join("dep-lib-serde_core"), [0u8; 8])?;
1761 let core_rlib = root.join("debug/deps/libserde_core-b468be6fa8c1f936.rlib");
1762 fs::write(&core_rlib, [1u8; 32])?;
1763
1764 let unit = root.join("debug/.fingerprint/serde-746d173581f1a314");
1765 fs::create_dir_all(&unit)?;
1766 fs::write(unit.join("lib-serde"), "5d82713a7f2d05e2")?;
1767 fs::write(
1768 unit.join("lib-serde.json"),
1769 r#"{"rustc":5244445300240561903,"features":"[]","declared_features":"[]","deps":[[11029742160753049355,"serde_core",false,9416646317397223793]]}"#,
1770 )?;
1771 fs::write(unit.join("dep-lib-serde"), [0u8; 8])?;
1772 let unit_rlib = root.join("debug/deps/libserde-746d173581f1a314.rlib");
1773 fs::write(&unit_rlib, [1u8; 32])?;
1774
1775 set_stamp(&core.join("lib-serde_core"), dep_at)?;
1776 set_stamp(&core.join("dep-lib-serde_core"), dep_at)?;
1777 set_stamp(&core_rlib, dep_at)?;
1778 set_stamp(&unit.join("lib-serde"), unit_at)?;
1779 set_stamp(&unit.join("lib-serde.json"), unit_at)?;
1780 set_stamp(&unit.join("dep-lib-serde"), unit_at)?;
1781 set_stamp(&unit_rlib, unit_at)?;
1782 Ok(())
1783 }
1784
1785 #[test]
1786 fn harvest_clamps_a_dependent_to_its_dependency() -> Result<()> {
1787 let build = tempfile::tempdir()?;
1788 let store = tempfile::tempdir()?;
1789 let now = now_secs();
1790 synthetic_edge_dir(build.path(), now, now - 3600)?;
1793 harvest(build.path(), store.path(), None, false)?;
1794
1795 let manifest = load_manifest(store.path(), &parity_key()?)?.expect("manifest");
1796 let dependent = manifest
1797 .units
1798 .iter()
1799 .find(|entry| entry.name == "serde-746d173581f1a314")
1800 .expect("dependent stored");
1801 let dependency = manifest
1802 .units
1803 .iter()
1804 .find(|entry| entry.name == "serde_core-b468be6fa8c1f936")
1805 .expect("dependency stored");
1806 assert!(
1807 dependent.stamp >= dependency.stamp,
1808 "clamped: {} >= {}",
1809 dependent.stamp,
1810 dependency.stamp
1811 );
1812
1813 let reports = verify(store.path())?;
1814 assert!(reports[0].is_clean(), "{:?}", reports[0].problems);
1815 assert!(reports[0].ordering.is_empty(), "{:?}", reports[0].ordering);
1816 Ok(())
1817 }
1818
1819 #[test]
1820 fn harvest_stamps_groups_by_the_metadata_output() -> Result<()> {
1821 let build = tempfile::tempdir()?;
1822 let store = tempfile::tempdir()?;
1823 synthetic_build_dir(build.path())?;
1824 let now = now_secs();
1825 set_stamp(
1828 &build.path().join("debug/deps/libserde-102a33f055a8d229.rmeta"),
1829 now - 30,
1830 )?;
1831 set_stamp(
1832 &build.path().join("debug/deps/libserde-102a33f055a8d229.rlib"),
1833 now,
1834 )?;
1835 harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1836
1837 let manifest = load_manifest(store.path(), &parity_key()?)?.expect("manifest");
1838 assert_eq!(manifest.units[0].stamp, now - 30);
1839 Ok(())
1840 }
1841
1842 #[test]
1843 fn verify_flags_an_inverted_store_stamp() -> Result<()> {
1844 let build = tempfile::tempdir()?;
1845 let store = tempfile::tempdir()?;
1846 let now = now_secs();
1847 synthetic_edge_dir(build.path(), now - 3600, now)?;
1848 harvest(build.path(), store.path(), None, false)?;
1849
1850 let parity = parity_key()?;
1853 let mut manifest = load_manifest(store.path(), &parity)?.expect("manifest");
1854 let units = units_dir(store.path(), &parity);
1855 let entry = manifest
1856 .units
1857 .iter_mut()
1858 .find(|entry| entry.name == "serde-746d173581f1a314")
1859 .expect("dependent stored");
1860 entry.stamp = now - 7200;
1861 for file in &entry.files {
1862 set_stamp(&units.join(&entry.name).join(&file.path), now - 7200)?;
1863 }
1864 save_manifest(store.path(), &parity, &manifest)?;
1865
1866 let reports = verify(store.path())?;
1867 assert!(reports[0].is_clean(), "{:?}", reports[0].problems);
1868 assert_eq!(reports[0].ordering.len(), 1, "{:?}", reports[0].ordering);
1869 assert!(reports[0].ordering[0].contains("serde-746d173581f1a314"));
1870 assert!(reports[0].ordering[0].contains("serde_core-b468be6fa8c1f936"));
1871 Ok(())
1872 }
1873
1874 #[test]
1875 fn verify_ordering_is_quiet_when_units_are_current() -> Result<()> {
1876 let build = tempfile::tempdir()?;
1877 let store = tempfile::tempdir()?;
1878 let now = now_secs();
1879 synthetic_edge_dir(build.path(), now - 3600, now)?;
1880 harvest(build.path(), store.path(), None, false)?;
1881
1882 let reports = verify(store.path())?;
1883 assert!(reports[0].is_clean(), "{:?}", reports[0].problems);
1884 assert!(reports[0].ordering.is_empty(), "{:?}", reports[0].ordering);
1885 Ok(())
1886 }
1887}