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 if !dry_run {
405 sweep_staging(&units_dir(store_root, &parity));
406 }
407
408 let rows: Vec<(Unit, ArtifactInfo)> = fingerprint::read_units(build_root)?
409 .into_iter()
410 .map(|unit| {
411 let arts = fingerprint::artifacts_for(build_root, &unit);
412 (unit, arts)
413 })
414 .collect();
415
416 let newest_activity = rows.iter().filter_map(|(_, arts)| arts.newest).max();
419 if let (Some(newest), Some(installed)) = (newest_activity, toolchain_install_time()) {
420 if newest < installed {
421 report.note = Some(format!(
422 "the shared cache predates the installed toolchain ({}) — build once, then harvest",
423 tc.version_line()
424 ));
425 return Ok(report);
426 }
427 }
428
429 let active_gen = rows
433 .iter()
434 .filter(|(unit, _)| crate::who::rustc_of(unit) != "unknown")
435 .max_by_key(|(_, arts)| arts.newest)
436 .map(|(unit, _)| crate::who::rustc_of(unit))
437 .unwrap_or_else(|| "unknown".into());
438
439 let mut manifest = load_manifest(store_root, &parity)?.unwrap_or_else(|| Manifest {
440 parity_key: parity.clone(),
441 toolchain: tc.version_line(),
442 created: now_secs(),
443 updated: 0,
444 units: Vec::new(),
445 });
446
447 struct GroupPlan {
449 unit: Unit,
450 files: Vec<(PathBuf, String, String, String)>,
451 memo: String,
452 hash: String,
453 base: u64,
454 }
455 let mut plans: Vec<GroupPlan> = Vec::new();
456
457 for (unit, arts) in &rows {
458 if let Some(query) = unit_filter {
459 if !unit_name_matches(&unit.dir_name, query) {
460 continue;
461 }
462 }
463 let cohort = crate::who::rustc_of(unit);
464 if cohort != "unknown" && active_gen != "unknown" && cohort != active_gen {
465 report.skipped_stale += 1;
466 continue;
467 }
468
469 let unit_dir = build_root
470 .join(&unit.profile)
471 .join(".fingerprint")
472 .join(&unit.dir_name);
473 let profile_dir = build_root.join(&unit.profile);
474 let memo = unit.memo_file.clone();
475 let mut files: Vec<(PathBuf, String, String, String)> = Vec::new();
476 for entry in fs::read_dir(&unit_dir)
483 .with_context(|| format!("reading {}", unit_dir.display()))?
484 {
485 let entry = entry?;
486 if !entry.file_type()?.is_file() {
487 continue;
488 }
489 let name = entry.file_name().to_string_lossy().into_owned();
490 if is_volatile(&name) {
491 continue;
492 }
493 let role = if name.starts_with("dep-") {
494 "dep-info"
495 } else if name.ends_with(".json") {
496 "fingerprint"
497 } else if fs::read_to_string(entry.path())
498 .map(|text| crate::fingerprint::looks_like_hash(text.trim()))
499 .unwrap_or(false)
500 {
501 "memo"
502 } else {
503 "extra"
505 };
506 files.push((
507 entry.path(),
508 name.clone(),
509 format!(".fingerprint/{}/{name}", unit.dir_name),
510 role.into(),
511 ));
512 }
513
514 let mut artifact_count = 0;
517 for (path, _, _) in &arts.paths {
518 let name = path
519 .file_name()
520 .map(|name| name.to_string_lossy().into_owned())
521 .unwrap_or_default();
522 if is_volatile(&name) {
523 continue;
524 }
525 let Ok(rel) = path.strip_prefix(&profile_dir) else {
526 continue;
527 };
528 let rel = rel.to_string_lossy().replace('\\', "/");
529 if rel.starts_with("deps/") && rel.ends_with(".d") {
530 continue;
531 }
532 let role = if rel.starts_with("build/") {
533 "build-tree"
534 } else {
535 "artifact"
536 };
537 files.push((path.clone(), rel.clone(), rel, role.into()));
538 artifact_count += 1;
539 }
540 if artifact_count == 0 {
541 report.skipped_incomplete += 1;
542 continue;
543 }
544
545 plans.push(GroupPlan {
546 base: group_stamp(&files)?,
547 memo,
548 hash: unit.memo_hash.clone().unwrap_or_default(),
549 files,
550 unit: unit.clone(),
551 });
552 }
553
554 let stored_stamps: std::collections::HashMap<String, u64> = manifest
563 .units
564 .iter()
565 .map(|entry| (entry.hash.clone(), entry.stamp))
566 .collect();
567 let index: std::collections::HashMap<&str, usize> = plans
568 .iter()
569 .enumerate()
570 .map(|(position, plan)| (plan.hash.as_str(), position))
571 .collect();
572 let mut plan_deps: Vec<Vec<usize>> = Vec::with_capacity(plans.len());
573 let mut stamps: Vec<u64> = Vec::with_capacity(plans.len());
574 for plan in &plans {
575 let mut deps = Vec::new();
576 let mut stamp = plan.base;
577 for dep in plan
578 .unit
579 .json
580 .as_ref()
581 .map(|json| json.deps.as_slice())
582 .unwrap_or(&[])
583 {
584 let Some(hash) = dep.memo_hash() else { continue };
585 if let Some(&at) = index.get(hash.as_str()) {
586 deps.push(at);
587 } else if let Some(known) = stored_stamps.get(hash.as_str()) {
588 stamp = stamp.max(*known);
589 }
590 }
591 plan_deps.push(deps);
592 stamps.push(stamp);
593 }
594 let mut order: Vec<usize> = Vec::with_capacity(plans.len());
597 let mut settled = vec![false; plans.len()];
598 loop {
599 let mut progressed = false;
600 for at in 0..plans.len() {
601 if settled[at] || !plan_deps[at].iter().all(|&dep| settled[dep]) {
602 continue;
603 }
604 settled[at] = true;
605 order.push(at);
606 progressed = true;
607 }
608 if !progressed {
609 break;
610 }
611 }
612 for at in 0..plans.len() {
613 if !settled[at] {
614 order.push(at);
615 }
616 }
617 for &at in &order {
618 for dep in &plan_deps[at] {
619 stamps[at] = stamps[at].max(stamps[*dep]);
620 }
621 }
622
623 for (at, plan) in plans.iter().enumerate() {
625 let unit = &plan.unit;
626 let files = &plan.files;
627 let stamp = stamps[at];
628 let group_dir = units_dir(store_root, &parity).join(&unit.dir_name);
629
630 let mut new_entries: Vec<(&str, &str)> = files
633 .iter()
634 .map(|(_, path, _, role)| (path.as_str(), role.as_str()))
635 .collect();
636 new_entries.sort_unstable();
637 let already = manifest
638 .units
639 .iter()
640 .find(|entry| entry.name == unit.dir_name)
641 .map(|entry| {
642 let mut stored: Vec<(&str, &str)> = entry
643 .files
644 .iter()
645 .map(|file| (file.path.as_str(), file.role.as_str()))
646 .collect();
647 stored.sort_unstable();
648 entry.hash == plan.hash
649 && entry.profile == unit.profile
650 && entry.stamp == stamp
651 && stored == new_entries
652 && entry
653 .files
654 .iter()
655 .all(|file| group_dir.join(&file.path).is_file())
656 })
657 .unwrap_or(false);
658 if already {
659 report.unchanged += 1;
660 continue;
661 }
662
663 let bytes: u64 = files
664 .iter()
665 .filter_map(|(src, _, _, _)| fs::metadata(src).ok())
666 .map(|meta| meta.len())
667 .sum();
668 if dry_run {
669 report.stored += 1;
670 report.files += files.len();
671 report.bytes += bytes;
672 continue;
673 }
674
675 let mut entries = Vec::with_capacity(files.len());
676 store_group(store_root, &parity, &unit.dir_name, stamp, files, &mut entries)
677 .with_context(|| format!("storing unit {}", unit.dir_name))?;
678
679 let entry = UnitEntry {
680 name: unit.dir_name.clone(),
681 profile: unit.profile.clone(),
682 memo: plan.memo.clone(),
683 hash: plan.hash.clone(),
684 stamp,
685 files: entries,
686 };
687 match manifest
688 .units
689 .iter_mut()
690 .find(|existing| existing.name == entry.name)
691 {
692 Some(slot) => {
693 *slot = entry;
694 report.refreshed += 1;
695 }
696 None => {
697 manifest.units.push(entry);
698 report.stored += 1;
699 }
700 }
701 report.files += files.len();
702 report.bytes += bytes;
703 }
704
705 if !dry_run {
706 manifest.units.sort_by(|a, b| {
707 (a.profile.as_str(), a.name.as_str()).cmp(&(b.profile.as_str(), b.name.as_str()))
708 });
709 manifest.updated = now_secs();
710 save_manifest(store_root, &parity, &manifest)?;
711 }
712 Ok(report)
713}
714
715fn store_group(
719 store_root: &Path,
720 parity: &str,
721 name: &str,
722 stamp: u64,
723 files: &[(PathBuf, String, String, String)],
724 entries: &mut Vec<FileEntry>,
725) -> Result<()> {
726 let units = units_dir(store_root, parity);
727 let final_dir = units.join(name);
728 let tmp = units.join(format!(".{name}.tmp-{}", std::process::id()));
729 if tmp.is_dir() {
730 crate::util::force_remove_dir(&tmp)?;
731 }
732 fs::create_dir_all(&tmp)?;
733
734 if let Err(err) = stage_group(&tmp, files, stamp, entries) {
735 let _ = crate::util::force_remove_dir(&tmp);
738 return Err(err);
739 }
740
741 let old = units.join(format!(".{name}.old-{}", std::process::id()));
742 if final_dir.is_dir() {
743 fs::rename(&final_dir, &old)?;
744 }
745 fs::rename(&tmp, &final_dir)?;
746 if old.is_dir() {
747 let _ = fs::remove_dir_all(&old);
748 }
749 Ok(())
750}
751
752fn stage_group(
761 tmp: &Path,
762 files: &[(PathBuf, String, String, String)],
763 stamp: u64,
764 entries: &mut Vec<FileEntry>,
765) -> Result<()> {
766 for (src, path, dest, role) in files {
767 let dst = tmp.join(path);
768 if let Some(parent) = dst.parent() {
769 fs::create_dir_all(parent)?;
770 }
771 fs::copy(src, &dst)
772 .with_context(|| format!("copying {} -> {}", src.display(), dst.display()))?;
773 make_writable(&dst).with_context(|| format!("making {} writable", dst.display()))?;
774 set_stamp(&dst, stamp)?;
775 make_read_only(&dst)?;
776 let size = fs::metadata(&dst).map(|meta| meta.len()).unwrap_or(0);
777 entries.push(FileEntry {
778 path: path.clone(),
779 dest: dest.clone(),
780 size,
781 role: role.clone(),
782 });
783 }
784 Ok(())
785}
786
787fn sweep_staging(units: &Path) {
791 let Ok(entries) = fs::read_dir(units) else {
792 return;
793 };
794 let now = SystemTime::now();
795 for entry in entries.flatten() {
796 let name = entry.file_name();
797 let name = name.to_string_lossy();
798 if !(name.contains(".tmp-") || name.contains(".old-")) {
799 continue;
800 }
801 let stale = entry
802 .metadata()
803 .and_then(|meta| meta.modified())
804 .ok()
805 .and_then(|modified| now.duration_since(modified).ok())
806 .is_some_and(|age| age > Duration::from_secs(600));
807 if stale {
808 let _ = crate::util::force_remove_dir(&entry.path());
809 }
810 }
811}
812
813fn group_stamp(files: &[(PathBuf, String, String, String)]) -> Result<u64> {
818 let mut newest: Option<SystemTime> = None;
819 let mut metadata: Option<SystemTime> = None;
820 for (src, path, _, _) in files {
821 let modified = fs::metadata(src)
822 .and_then(|meta| meta.modified())
823 .with_context(|| format!("reading mtime of {}", src.display()))?;
824 if newest.map_or(true, |current| modified > current) {
825 newest = Some(modified);
826 }
827 if path.ends_with(".rmeta") && metadata.map_or(true, |current| modified > current) {
828 metadata = Some(modified);
829 }
830 }
831 Ok(match metadata.or(newest) {
837 Some(time) => ceil_secs(time),
838 None => now_secs(),
839 })
840}
841
842fn ceil_secs(time: SystemTime) -> u64 {
844 match time.duration_since(UNIX_EPOCH) {
845 Ok(duration) => {
846 let secs = duration.as_secs();
847 if duration.subsec_nanos() > 0 { secs + 1 } else { secs }
848 }
849 Err(_) => 0,
850 }
851}
852
853fn now_secs() -> u64 {
854 SystemTime::now()
855 .duration_since(UNIX_EPOCH)
856 .map(|duration| duration.as_secs())
857 .unwrap_or(0)
858}
859
860fn set_stamp(path: &Path, stamp: u64) -> Result<()> {
861 let time = UNIX_EPOCH + Duration::from_secs(stamp);
862 let file = fs::File::open(path)
863 .with_context(|| format!("opening {} to stamp it", path.display()))?;
864 file.set_modified(time)
865 .with_context(|| format!("stamping {}", path.display()))?;
866 Ok(())
867}
868
869fn copy_private(src: &Path, dst: &Path, stamp: u64) -> std::io::Result<()> {
874 fs::copy(src, dst)?;
875 make_writable(dst)?;
876 let _ = set_stamp(dst, stamp);
877 Ok(())
878}
879
880#[cfg(unix)]
882fn make_writable(path: &Path) -> std::io::Result<()> {
883 use std::os::unix::fs::PermissionsExt;
884 let mut perms = fs::metadata(path)?.permissions();
885 perms.set_mode(0o644);
886 fs::set_permissions(path, perms)
887}
888
889#[cfg(not(unix))]
890fn make_writable(path: &Path) -> std::io::Result<()> {
891 let mut perms = fs::metadata(path)?.permissions();
892 perms.set_readonly(false);
893 fs::set_permissions(path, perms)
894}
895
896fn make_read_only(path: &Path) -> Result<()> {
900 #[cfg(unix)]
901 {
902 use std::os::unix::fs::PermissionsExt;
903 fs::set_permissions(path, fs::Permissions::from_mode(0o444))?;
904 }
905 #[cfg(not(unix))]
906 {
907 let _ = path;
908 }
909 Ok(())
910}
911
912fn is_volatile(name: &str) -> bool {
916 name == "invoked.timestamp" || name.contains(".kache")
917}
918
919#[derive(Debug, Default)]
923pub struct RestoreReport {
924 pub parity: String,
925 pub target: PathBuf,
926 pub units_linked: usize,
927 pub units_already: usize,
928 pub files_linked: usize,
929 pub files_copied: usize,
930 pub files_existing: usize,
931 pub bytes: u64,
932 pub errors: usize,
933 pub note: Option<String>,
934}
935
936pub fn restore(
947 view_root: &Path,
948 store_root: &Path,
949 unit_filter: Option<&str>,
950 dry_run: bool,
951) -> Result<RestoreReport> {
952 let parity = parity_key()?;
953 let mut report = RestoreReport {
954 parity: parity.clone(),
955 target: view_root.to_path_buf(),
956 ..Default::default()
957 };
958 let Some(manifest) = load_manifest(store_root, &parity)? else {
959 report.note = Some(
960 "the vault holds no units for this generation yet — run `epicenter harvest` after a build"
961 .into(),
962 );
963 return Ok(report);
964 };
965
966 let units = units_dir(store_root, &parity);
967 for entry in &manifest.units {
968 if let Some(query) = unit_filter {
969 if !unit_name_matches(&entry.name, query) {
970 continue;
971 }
972 }
973 let fp_dir = view_root
974 .join(&entry.profile)
975 .join(".fingerprint")
976 .join(&entry.name);
977 if fs::read_to_string(fp_dir.join(&entry.memo))
978 .map(|text| text.trim() == entry.hash)
979 .unwrap_or(false)
980 {
981 report.units_already += 1;
982 continue;
983 }
984
985 let mut unit_touched = false;
986 for file in &entry.files {
987 let dst = view_root.join(&entry.profile).join(&file.dest);
988 if dst.exists() {
989 report.files_existing += 1;
990 continue;
991 }
992 let shared = file.path.starts_with("deps/") && !file.path.ends_with(".rmeta");
997 if dry_run {
998 unit_touched = true;
999 if shared {
1000 report.files_linked += 1;
1001 } else {
1002 report.files_copied += 1;
1003 }
1004 report.bytes += file.size;
1005 continue;
1006 }
1007 let src = units.join(&entry.name).join(&file.path);
1008 if let Some(parent) = dst.parent() {
1009 if let Err(err) = fs::create_dir_all(parent) {
1010 report.errors += 1;
1011 eprintln!("restore: creating {}: {err}", parent.display());
1012 continue;
1013 }
1014 }
1015 let linked = shared && fs::hard_link(&src, &dst).is_ok();
1016 let result = if linked {
1017 Ok(())
1018 } else {
1019 copy_private(&src, &dst, entry.stamp)
1020 };
1021 match result {
1022 Ok(()) => {
1023 if linked {
1024 report.files_linked += 1;
1025 } else {
1026 report.files_copied += 1;
1027 }
1028 unit_touched = true;
1029 }
1030 Err(err) => {
1031 report.errors += 1;
1032 eprintln!("restore: writing {}: {err}", dst.display());
1033 continue;
1034 }
1035 }
1036 report.bytes += file.size;
1037 }
1038 if unit_touched {
1039 report.units_linked += 1;
1040 }
1041 }
1042 Ok(report)
1043}
1044
1045#[derive(Debug, Default)]
1049pub struct VerifyReport {
1050 pub parity: String,
1051 pub units: usize,
1052 pub files: usize,
1053 pub bytes: u64,
1054 pub problems: Vec<String>,
1056 pub warnings: Vec<String>,
1058 pub ordering: Vec<String>,
1062}
1063
1064impl VerifyReport {
1065 pub fn is_clean(&self) -> bool {
1066 self.problems.is_empty()
1067 }
1068}
1069
1070pub fn verify(store_root: &Path) -> Result<Vec<VerifyReport>> {
1074 let mut reports = Vec::new();
1075 for parity in generation_names(store_root)? {
1076 let mut report = VerifyReport {
1077 parity: parity.clone(),
1078 ..Default::default()
1079 };
1080 let Some(manifest) = load_manifest(store_root, &parity)? else {
1081 report.problems.push(format!(
1082 "generation {parity} has no readable manifest.json — it cannot be audited"
1083 ));
1084 reports.push(report);
1085 continue;
1086 };
1087 let units = units_dir(store_root, &parity);
1088 for entry in &manifest.units {
1089 for file in &entry.files {
1090 let path = units.join(&entry.name).join(&file.path);
1091 let meta = match fs::metadata(&path) {
1092 Ok(meta) => meta,
1093 Err(_) => {
1094 report
1095 .problems
1096 .push(format!("missing: {}", display_join(&path, units.as_path())));
1097 continue;
1098 }
1099 };
1100 report.files += 1;
1101 report.bytes += meta.len();
1102 if meta.len() != file.size {
1103 report.problems.push(format!(
1104 "size drift: {} (recorded {}, found {})",
1105 display_join(&path, &units),
1106 file.size,
1107 meta.len()
1108 ));
1109 }
1110 let found_stamp = meta.modified().map(ceil_secs).unwrap_or(0);
1111 if found_stamp != entry.stamp {
1112 report.problems.push(format!(
1113 "stamp drift: {} (recorded {}, found {})",
1114 display_join(&path, &units),
1115 entry.stamp,
1116 found_stamp
1117 ));
1118 }
1119 if file.role == "memo" {
1120 match fs::read_to_string(&path) {
1121 Ok(text) if file.path == entry.memo && text.trim() != entry.hash => {
1122 report.problems.push(format!(
1123 "hash drift: {} (recorded {}, found {})",
1124 display_join(&path, &units),
1125 entry.hash,
1126 text.trim()
1127 ));
1128 }
1129 Ok(text)
1130 if file.path != entry.memo
1131 && !crate::fingerprint::looks_like_hash(text.trim()) =>
1132 {
1133 report.problems.push(format!(
1134 "malformed memo file: {} ({:?})",
1135 display_join(&path, &units),
1136 text.trim()
1137 ));
1138 }
1139 Ok(_) => {}
1140 Err(err) => report.problems.push(format!(
1141 "unreadable memo file {}: {err}",
1142 display_join(&path, &units)
1143 )),
1144 }
1145 }
1146 }
1147 }
1148 let by_hash: std::collections::HashMap<&str, usize> = manifest
1155 .units
1156 .iter()
1157 .enumerate()
1158 .map(|(index, entry)| (entry.hash.as_str(), index))
1159 .collect();
1160 for entry in &manifest.units {
1161 let json = entry
1162 .files
1163 .iter()
1164 .find(|file| file.path.ends_with(".json"))
1165 .and_then(|file| {
1166 fs::read_to_string(units.join(&entry.name).join(&file.path)).ok()
1167 })
1168 .and_then(|text| crate::fingerprint::parse_fingerprint(&text));
1169 let Some(json) = json else { continue };
1170 for dep in &json.deps {
1171 let Some(dep_index) = dep
1172 .memo_hash()
1173 .and_then(|hash| by_hash.get(hash.as_str()))
1174 .copied()
1175 else {
1176 continue;
1177 };
1178 let dep_entry = &manifest.units[dep_index];
1179 if dep_entry.stamp > entry.stamp {
1180 report.ordering.push(format!(
1181 "{} is older than its dependency {} ({} older) — cargo recompiles it after a restore",
1182 entry.name,
1183 dep_entry.name,
1184 crate::util::human_duration(Duration::from_secs(dep_entry.stamp - entry.stamp))
1185 ));
1186 }
1187 }
1188 }
1189 report.units = manifest.units.len();
1190
1191 if units.is_dir() {
1193 let known: std::collections::BTreeSet<&str> =
1194 manifest.units.iter().map(|unit| unit.name.as_str()).collect();
1195 for entry in fs::read_dir(&units)? {
1196 let entry = entry?;
1197 if !entry.file_type()?.is_dir() {
1198 continue;
1199 }
1200 let name = entry.file_name().to_string_lossy().into_owned();
1201 if name.starts_with('.') {
1202 continue; }
1204 if !known.contains(name.as_str()) {
1205 report
1206 .warnings
1207 .push(format!("unrecorded unit group: units/{name}"));
1208 }
1209 }
1210 }
1211 reports.push(report);
1212 }
1213 Ok(reports)
1214}
1215
1216fn display_join(path: &Path, base: &Path) -> String {
1218 path.strip_prefix(base)
1219 .unwrap_or(path)
1220 .to_string_lossy()
1221 .replace('\\', "/")
1222}
1223
1224#[derive(Debug, Default)]
1228pub struct GenerationInfo {
1229 pub parity: String,
1230 pub active: bool,
1232 pub units: usize,
1233 pub files: usize,
1234 pub bytes: u64,
1236 pub linked: usize,
1238 pub created: Option<u64>,
1239 pub updated: Option<u64>,
1240}
1241
1242pub fn generations(store_root: &Path) -> Result<Vec<GenerationInfo>> {
1244 let active = parity_key().ok();
1245 let mut out = Vec::new();
1246 for parity in generation_names(store_root)? {
1247 let mut info = GenerationInfo {
1248 parity: parity.clone(),
1249 active: active.as_deref() == Some(parity.as_str()),
1250 ..Default::default()
1251 };
1252 if let Some(manifest) = load_manifest(store_root, &parity)? {
1253 info.units = manifest.units.len();
1254 info.created = Some(manifest.created);
1255 info.updated = Some(manifest.updated);
1256 }
1257 let units = units_dir(store_root, &parity);
1258 if units.is_dir() {
1259 for entry in WalkDir::new(&units).min_depth(1) {
1260 let entry = entry?;
1261 if !entry.file_type().is_file() {
1262 continue;
1263 }
1264 let meta = entry.metadata()?;
1265 info.files += 1;
1266 info.bytes += meta.len();
1267 if link_count(&meta) > 1 {
1268 info.linked += 1;
1269 }
1270 }
1271 }
1272 out.push(info);
1273 }
1274 Ok(out)
1275}
1276
1277pub(crate) fn link_count(meta: &fs::Metadata) -> u64 {
1279 #[cfg(unix)]
1280 {
1281 use std::os::unix::fs::MetadataExt;
1282 meta.nlink()
1283 }
1284 #[cfg(not(unix))]
1285 {
1286 let _ = meta;
1287 1
1288 }
1289}
1290
1291fn unit_name_matches(name: &str, query: &str) -> bool {
1293 name.contains(query)
1294}
1295
1296#[cfg(test)]
1297mod tests {
1298 use super::*;
1299
1300 fn synthetic_build_dir(root: &Path) -> Result<()> {
1305 let jsons = r#"{"rustc":12019306335353385202,"features":"[]","declared_features":"[]","deps":[]}"#;
1306
1307 let lib = root.join("debug/.fingerprint/serde-102a33f055a8d229");
1308 fs::create_dir_all(&lib)?;
1309 fs::write(lib.join("lib-serde"), "5d82713a7f2d05e2")?;
1310 fs::write(lib.join("lib-serde.json"), jsons)?;
1311 fs::write(lib.join("dep-lib-serde"), [0u8; 24])?;
1312 fs::write(lib.join("invoked.timestamp"), "")?;
1313 fs::create_dir_all(root.join("debug/deps"))?;
1314 fs::write(
1315 root.join("debug/deps/libserde-102a33f055a8d229.rlib"),
1316 [7u8; 64],
1317 )?;
1318 fs::write(
1319 root.join("debug/deps/libserde-102a33f055a8d229.rmeta"),
1320 [6u8; 48],
1321 )?;
1322 fs::write(
1323 root.join("debug/deps/serde-102a33f055a8d229.d"),
1324 b"side file",
1325 )?;
1326 fs::write(
1327 root.join("debug/deps/libserde-102a33f055a8d229.kache-real"),
1328 b"wrapper",
1329 )?;
1330
1331 let compile = root.join("debug/.fingerprint/x-1122334455667788");
1332 fs::create_dir_all(&compile)?;
1333 fs::write(
1334 compile.join("build-script-build-script-build"),
1335 "aaaabbbbccccdddd",
1336 )?;
1337 fs::write(
1338 compile.join("build-script-build-script-build.json"),
1339 jsons,
1340 )?;
1341 fs::write(
1342 compile.join("dep-build-script-build-script-build"),
1343 [0u8; 8],
1344 )?;
1345 let compile_anchor = root.join("debug/build/x-1122334455667788");
1346 fs::create_dir_all(&compile_anchor)?;
1347 fs::write(
1348 compile_anchor.join("build_script_build-1122334455667788"),
1349 [3u8; 32],
1350 )?;
1351 fs::write(
1352 compile_anchor.join("build_script_build-1122334455667788.d"),
1353 b"dep",
1354 )?;
1355 fs::write(
1356 compile_anchor.join("build_script_build-1122334455667788.kache-real"),
1357 b"wrapper",
1358 )?;
1359 fs::write(compile_anchor.join("invoked.timestamp"), "")?;
1360
1361 let run = root.join("debug/.fingerprint/x-aabbccddeeff0011");
1363 fs::create_dir_all(&run)?;
1364 fs::write(
1365 run.join("run-build-script-build-script-build"),
1366 "eeeeffff00001111",
1367 )?;
1368 fs::write(
1369 run.join("run-build-script-build-script-build.json"),
1370 jsons,
1371 )?;
1372 let run_anchor = root.join("debug/build/x-aabbccddeeff0011");
1373 fs::create_dir_all(&run_anchor)?;
1374 fs::write(
1375 run_anchor.join("output"),
1376 b"cargo:rerun-if-changed=build.rs",
1377 )?;
1378 fs::write(run_anchor.join("stderr"), b"")?;
1379 fs::write(run_anchor.join("invoked.timestamp"), "")?;
1380 Ok(())
1381 }
1382
1383 fn synthetic_husk(root: &Path) -> Result<()> {
1385 let unit = root.join("debug/.fingerprint/husk-aaaabbbbccccdddd");
1386 fs::create_dir_all(&unit)?;
1387 fs::write(unit.join("lib-husk"), "0000111122223333")?;
1388 Ok(())
1389 }
1390
1391 #[test]
1392 fn parity_key_is_stable_and_shaped() -> Result<()> {
1393 let first = parity_key()?;
1394 let second = parity_key()?;
1395 assert_eq!(first, second);
1396 let tc = toolchain()?;
1397 assert!(first.starts_with(&format!("{}-{}", tc.release, tc.commit)));
1398 assert!(first.ends_with(&flags_digest()));
1399 Ok(())
1400 }
1401
1402 #[test]
1403 fn harvest_stores_groups_with_one_canonical_stamp() -> Result<()> {
1404 let build = tempfile::tempdir()?;
1405 let store = tempfile::tempdir()?;
1406 synthetic_build_dir(build.path())?;
1407
1408 let report = harvest(build.path(), store.path(), None, false)?;
1409 assert_eq!(report.stored, 3, "lib + build-script compile + run");
1410 assert_eq!(report.skipped_incomplete, 0);
1411 assert!(report.files >= 10, "got {}", report.files);
1412 assert!(report.bytes > 0);
1413
1414 let parity = parity_key()?;
1415 let manifest = load_manifest(store.path(), &parity)?.expect("manifest written");
1416 assert_eq!(manifest.units.len(), 3);
1417 let lib_entry = manifest
1418 .units
1419 .iter()
1420 .find(|unit| unit.name == "serde-102a33f055a8d229")
1421 .expect("lib unit recorded");
1422 assert_eq!(lib_entry.hash, "5d82713a7f2d05e2");
1423 assert_eq!(lib_entry.profile, "debug");
1424
1425 let group = units_dir(store.path(), &parity).join(&lib_entry.name);
1427 for file in &lib_entry.files {
1428 let meta = fs::metadata(group.join(&file.path))?;
1429 assert_eq!(ceil_secs(meta.modified()?), lib_entry.stamp, "{}", file.path);
1430 assert_eq!(meta.len(), file.size);
1431 }
1432
1433 assert!(!group.join("invoked.timestamp").exists());
1435 for file in &lib_entry.files {
1436 assert!(!file.path.contains("kache"), "{}", file.path);
1437 assert!(!file.path.ends_with(".d"), "{}", file.path);
1438 }
1439
1440 #[cfg(unix)]
1441 {
1442 use std::os::unix::fs::PermissionsExt;
1443 let meta = fs::metadata(group.join(&lib_entry.files[0].path))?;
1444 assert_eq!(meta.permissions().mode() & 0o777, 0o444, "store is read-only");
1445 }
1446 Ok(())
1447 }
1448
1449 #[test]
1450 fn harvest_is_idempotent_and_skips_incomplete_units() -> Result<()> {
1451 let build = tempfile::tempdir()?;
1452 let store = tempfile::tempdir()?;
1453 synthetic_build_dir(build.path())?;
1454 synthetic_husk(build.path())?;
1455
1456 let first = harvest(build.path(), store.path(), None, false)?;
1457 assert_eq!(first.stored, 3);
1458 assert_eq!(first.skipped_incomplete, 1, "husk without artifacts");
1459
1460 let second = harvest(build.path(), store.path(), None, false)?;
1461 assert_eq!(second.stored, 0);
1462 assert_eq!(second.unchanged, 3);
1463 assert_eq!(second.skipped_incomplete, 1);
1464 Ok(())
1465 }
1466
1467 #[test]
1468 fn harvest_refreshes_when_the_file_set_grows() -> Result<()> {
1469 let build = tempfile::tempdir()?;
1470 let store = tempfile::tempdir()?;
1471 synthetic_build_dir(build.path())?;
1472 harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1473
1474 fs::write(
1475 build
1476 .path()
1477 .join("debug/deps/libserde-102a33f055a8d229.so"),
1478 [1u8; 16],
1479 )?;
1480 let report = harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1481 assert_eq!(report.refreshed, 1);
1482 assert_eq!(report.stored, 0);
1483
1484 let manifest = load_manifest(store.path(), &parity_key()?)?.expect("manifest");
1485 let paths: Vec<&str> = manifest.units[0]
1486 .files
1487 .iter()
1488 .map(|file| file.path.as_str())
1489 .collect();
1490 assert!(
1491 paths.contains(&"deps/libserde-102a33f055a8d229.so"),
1492 "{paths:?}"
1493 );
1494 Ok(())
1495 }
1496
1497 #[test]
1498 fn harvest_stores_all_memos_of_shared_dirs() -> Result<()> {
1499 let build = tempfile::tempdir()?;
1500 let store = tempfile::tempdir()?;
1501 synthetic_build_dir(build.path())?;
1502 let lib = build.path().join("debug/.fingerprint/serde-102a33f055a8d229");
1503 fs::write(lib.join("bin-serde"), "1111222233334444")?;
1504 fs::write(lib.join("dep-bin-serde"), [0u8; 8])?;
1505
1506 let report = harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1507 assert_eq!(report.stored, 1);
1508 let manifest = load_manifest(store.path(), &parity_key()?)?.expect("manifest");
1509 let entry = &manifest.units[0];
1510 let paths: Vec<&str> = entry.files.iter().map(|file| file.path.as_str()).collect();
1511 assert!(paths.contains(&"bin-serde"), "{paths:?}");
1512 assert!(paths.contains(&"dep-bin-serde"), "{paths:?}");
1513
1514 let reports = verify(store.path())?;
1515 assert!(reports[0].is_clean(), "{:?}", reports[0].problems);
1516 Ok(())
1517 }
1518
1519 #[test]
1520 fn harvest_dry_run_writes_nothing() -> Result<()> {
1521 let build = tempfile::tempdir()?;
1522 let store = tempfile::tempdir()?;
1523 synthetic_build_dir(build.path())?;
1524
1525 let report = harvest(build.path(), store.path(), None, true)?;
1526 assert_eq!(report.stored, 3);
1527 assert_eq!(generation_names(store.path())?.len(), 0, "no generation");
1528 Ok(())
1529 }
1530
1531 #[test]
1532 fn harvest_unit_filter_selects_one_group() -> Result<()> {
1533 let build = tempfile::tempdir()?;
1534 let store = tempfile::tempdir()?;
1535 synthetic_build_dir(build.path())?;
1536
1537 let report = harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1538 assert_eq!(report.stored, 1);
1539 let manifest = load_manifest(store.path(), &parity_key()?)?.expect("manifest");
1540 assert_eq!(manifest.units.len(), 1);
1541 assert_eq!(manifest.units[0].name, "serde-102a33f055a8d229");
1542 Ok(())
1543 }
1544
1545 #[test]
1546 fn restore_links_hardlinks_and_never_overwrites() -> Result<()> {
1547 let build = tempfile::tempdir()?;
1548 let store = tempfile::tempdir()?;
1549 let view = tempfile::tempdir()?;
1550 synthetic_build_dir(build.path())?;
1551 harvest(build.path(), store.path(), None, false)?;
1552
1553 let occupied = view.path().join("debug/deps/libserde-102a33f055a8d229.rlib");
1555 fs::create_dir_all(occupied.parent().unwrap())?;
1556 fs::write(&occupied, b"live artifact")?;
1557
1558 let report = restore(view.path(), store.path(), None, false)?;
1559 assert_eq!(report.units_linked, 3);
1560 assert_eq!(report.files_existing, 1, "the live artifact was skipped");
1561 assert_eq!(fs::read(&occupied)?, b"live artifact");
1562 assert_eq!(report.files_linked, 0, "no other deps/ artifact");
1566 assert!(report.files_copied >= 12, "got {}", report.files_copied);
1567
1568 let memo = view
1570 .path()
1571 .join("debug/.fingerprint/serde-102a33f055a8d229/lib-serde");
1572 assert_eq!(fs::read_to_string(&memo)?.trim(), "5d82713a7f2d05e2");
1573 assert!(
1574 view.path()
1575 .join("debug/build/x-aabbccddeeff0011/output")
1576 .is_file()
1577 );
1578
1579 #[cfg(unix)]
1583 {
1584 use std::os::unix::fs::MetadataExt;
1585 use std::os::unix::fs::PermissionsExt;
1586 let parity = parity_key()?;
1587 let stored =
1588 units_dir(store.path(), &parity).join("serde-102a33f055a8d229/lib-serde");
1589 let view_meta = fs::metadata(&memo)?;
1590 assert_ne!(view_meta.ino(), fs::metadata(&stored)?.ino(), "state not shared");
1591 assert_eq!(view_meta.nlink(), 1, "state owns its inode");
1592 assert_ne!(view_meta.permissions().mode() & 0o200, 0, "state writable");
1593 }
1594
1595 let again = restore(view.path(), store.path(), None, false)?;
1597 assert_eq!(again.units_already, 3);
1598 assert_eq!(again.files_linked, 0);
1599 assert_eq!(again.files_copied, 0);
1600 Ok(())
1601 }
1602
1603 #[test]
1604 fn restore_into_missing_profile_dirs_creates_them() -> Result<()> {
1605 let build = tempfile::tempdir()?;
1606 let store = tempfile::tempdir()?;
1607 let view = tempfile::tempdir()?;
1608 synthetic_build_dir(build.path())?;
1609 harvest(build.path(), store.path(), None, false)?;
1610
1611 let report = restore(view.path(), store.path(), None, false)?;
1612 assert_eq!(report.errors, 0);
1613 assert!(view.path().join("debug/.fingerprint").is_dir());
1614 Ok(())
1615 }
1616
1617 #[test]
1618 fn restore_without_a_generation_reports_fail_open() -> Result<()> {
1619 let view = tempfile::tempdir()?;
1620 let store = tempfile::tempdir()?;
1621 let report = restore(view.path(), store.path(), None, false)?;
1622 assert!(report.note.is_some());
1623 assert_eq!(report.files_linked, 0);
1624 Ok(())
1625 }
1626
1627 #[test]
1628 fn verify_passes_clean_and_flags_drift() -> Result<()> {
1629 let build = tempfile::tempdir()?;
1630 let store = tempfile::tempdir()?;
1631 synthetic_build_dir(build.path())?;
1632 harvest(build.path(), store.path(), None, false)?;
1633
1634 let reports = verify(store.path())?;
1635 assert_eq!(reports.len(), 1);
1636 assert!(reports[0].is_clean(), "{:?}", reports[0].problems);
1637 assert_eq!(reports[0].units, 3);
1638
1639 let parity = parity_key()?;
1641 let victim =
1642 units_dir(store.path(), &parity).join("serde-102a33f055a8d229/dep-lib-serde");
1643 #[cfg(unix)]
1644 {
1645 use std::os::unix::fs::PermissionsExt;
1646 fs::set_permissions(&victim, fs::Permissions::from_mode(0o644))?;
1647 }
1648 fs::write(&victim, [9u8; 32])?;
1649
1650 let reports = verify(store.path())?;
1651 assert!(!reports[0].is_clean());
1652 assert!(
1653 reports[0]
1654 .problems
1655 .iter()
1656 .any(|line| line.contains("size drift")),
1657 "{:?}",
1658 reports[0].problems
1659 );
1660 Ok(())
1661 }
1662
1663 #[test]
1664 fn verify_reports_unrecorded_unit_groups() -> Result<()> {
1665 let build = tempfile::tempdir()?;
1666 let store = tempfile::tempdir()?;
1667 synthetic_build_dir(build.path())?;
1668 harvest(build.path(), store.path(), None, false)?;
1669
1670 let stray = units_dir(store.path(), &parity_key()?).join("stray-0000000000000000");
1671 fs::create_dir_all(stray.join("deps"))?;
1672 fs::write(stray.join("deps/x.rlib"), [0u8; 4])?;
1673
1674 let reports = verify(store.path())?;
1675 assert!(
1676 reports[0]
1677 .warnings
1678 .iter()
1679 .any(|line| line.contains("stray-0000000000000000")),
1680 "{:?}",
1681 reports[0].warnings
1682 );
1683 Ok(())
1684 }
1685
1686 #[test]
1687 fn generations_summary_counts_store_files() -> Result<()> {
1688 let build = tempfile::tempdir()?;
1689 let store = tempfile::tempdir()?;
1690 synthetic_build_dir(build.path())?;
1691 harvest(build.path(), store.path(), None, false)?;
1692
1693 let generations = generations(store.path())?;
1694 assert_eq!(generations.len(), 1);
1695 assert!(generations[0].active, "current toolchain generation");
1696 assert_eq!(generations[0].units, 3);
1697 assert!(generations[0].files >= 10);
1698 assert!(generations[0].linked <= generations[0].files);
1699 Ok(())
1700 }
1701
1702 #[test]
1703 fn cached_diagnostics_are_stored_as_extra_and_verify_clean() -> Result<()> {
1704 let build = tempfile::tempdir()?;
1705 let store = tempfile::tempdir()?;
1706 synthetic_build_dir(build.path())?;
1707 fs::write(
1709 build
1710 .path()
1711 .join("debug/.fingerprint/serde-102a33f055a8d229/output-lib-serde"),
1712 r#"{"$message_type":"diagnostic","message":"warning: unused"}"#,
1713 )?;
1714
1715 harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1716 let manifest = load_manifest(store.path(), &parity_key()?)?.expect("manifest");
1717 let output = manifest.units[0]
1718 .files
1719 .iter()
1720 .find(|file| file.path == "output-lib-serde")
1721 .expect("cached diagnostics are stored");
1722 assert_eq!(output.role, "extra");
1723
1724 let reports = verify(store.path())?;
1725 assert!(reports[0].is_clean(), "{:?}", reports[0].problems);
1726 Ok(())
1727 }
1728
1729 #[test]
1730 fn harvest_refreshes_when_a_role_drifted() -> Result<()> {
1731 let build = tempfile::tempdir()?;
1732 let store = tempfile::tempdir()?;
1733 synthetic_build_dir(build.path())?;
1734 harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1735
1736 let parity = parity_key()?;
1739 let mut manifest = load_manifest(store.path(), &parity)?.expect("manifest");
1740 let memo_index = manifest.units[0]
1741 .files
1742 .iter()
1743 .position(|file| file.path == manifest.units[0].memo)
1744 .expect("memo recorded");
1745 manifest.units[0].files[memo_index].role = "extra".to_string();
1746 save_manifest(store.path(), &parity, &manifest)?;
1747
1748 let report = harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1749 assert_eq!(report.refreshed, 1, "role drift forces a refresh");
1750 let manifest = load_manifest(store.path(), &parity)?.expect("manifest");
1751 let role = &manifest.units[0].files[memo_index].role;
1752 assert_eq!(role, "memo", "roles are rewritten");
1753 Ok(())
1754 }
1755
1756 #[test]
1757 fn restore_shares_artifacts_but_copies_state_writable() -> Result<()> {
1758 let build = tempfile::tempdir()?;
1759 let store = tempfile::tempdir()?;
1760 synthetic_build_dir(build.path())?;
1761 harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1762
1763 let view = tempfile::tempdir()?;
1764 restore(view.path(), store.path(), Some("102a33f0"), false)?;
1765
1766 let state = view
1767 .path()
1768 .join("debug/.fingerprint/serde-102a33f055a8d229/lib-serde");
1769 let artifact = view
1770 .path()
1771 .join("debug/deps/libserde-102a33f055a8d229.rlib");
1772 let metadata = view
1773 .path()
1774 .join("debug/deps/libserde-102a33f055a8d229.rmeta");
1775 assert!(state.is_file() && artifact.is_file() && metadata.is_file());
1776
1777 let parity = parity_key()?;
1778 let manifest = load_manifest(store.path(), &parity)?.expect("manifest");
1779 let entry = manifest
1780 .units
1781 .iter()
1782 .find(|entry| entry.name == "serde-102a33f055a8d229")
1783 .expect("stored unit");
1784 assert_eq!(ceil_secs(fs::metadata(&state)?.modified()?), entry.stamp);
1785
1786 fs::write(&state, "ffffffffffffffff")?;
1789 let stored = units_dir(store.path(), &parity)
1790 .join("serde-102a33f055a8d229")
1791 .join("lib-serde");
1792 assert_eq!(fs::read_to_string(&stored)?, "5d82713a7f2d05e2");
1793
1794 #[cfg(unix)]
1795 {
1796 use std::os::unix::fs::MetadataExt;
1797 assert!(fs::metadata(&artifact)?.nlink() >= 2, "artifact shares the store inode");
1798 assert_eq!(fs::metadata(&state)?.nlink(), 1, "state is a private copy");
1799 assert_eq!(
1800 fs::metadata(&metadata)?.nlink(),
1801 1,
1802 ".rmeta is a private copy (rustc opens it for write)"
1803 );
1804 }
1805 Ok(())
1806 }
1807
1808 fn synthetic_edge_dir(root: &Path, dep_at: u64, unit_at: u64) -> Result<()> {
1812 fs::create_dir_all(root.join("debug/deps"))?;
1813 let core = root.join("debug/.fingerprint/serde_core-b468be6fa8c1f936");
1814 fs::create_dir_all(&core)?;
1815 fs::write(core.join("lib-serde_core"), "71a9f323efa5ae82")?;
1816 fs::write(core.join("dep-lib-serde_core"), [0u8; 8])?;
1817 let core_rlib = root.join("debug/deps/libserde_core-b468be6fa8c1f936.rlib");
1818 fs::write(&core_rlib, [1u8; 32])?;
1819
1820 let unit = root.join("debug/.fingerprint/serde-746d173581f1a314");
1821 fs::create_dir_all(&unit)?;
1822 fs::write(unit.join("lib-serde"), "5d82713a7f2d05e2")?;
1823 fs::write(
1824 unit.join("lib-serde.json"),
1825 r#"{"rustc":5244445300240561903,"features":"[]","declared_features":"[]","deps":[[11029742160753049355,"serde_core",false,9416646317397223793]]}"#,
1826 )?;
1827 fs::write(unit.join("dep-lib-serde"), [0u8; 8])?;
1828 let unit_rlib = root.join("debug/deps/libserde-746d173581f1a314.rlib");
1829 fs::write(&unit_rlib, [1u8; 32])?;
1830
1831 set_stamp(&core.join("lib-serde_core"), dep_at)?;
1832 set_stamp(&core.join("dep-lib-serde_core"), dep_at)?;
1833 set_stamp(&core_rlib, dep_at)?;
1834 set_stamp(&unit.join("lib-serde"), unit_at)?;
1835 set_stamp(&unit.join("lib-serde.json"), unit_at)?;
1836 set_stamp(&unit.join("dep-lib-serde"), unit_at)?;
1837 set_stamp(&unit_rlib, unit_at)?;
1838 Ok(())
1839 }
1840
1841 #[test]
1842 fn harvest_clamps_a_dependent_to_its_dependency() -> Result<()> {
1843 let build = tempfile::tempdir()?;
1844 let store = tempfile::tempdir()?;
1845 let now = now_secs();
1846 synthetic_edge_dir(build.path(), now, now - 3600)?;
1849 harvest(build.path(), store.path(), None, false)?;
1850
1851 let manifest = load_manifest(store.path(), &parity_key()?)?.expect("manifest");
1852 let dependent = manifest
1853 .units
1854 .iter()
1855 .find(|entry| entry.name == "serde-746d173581f1a314")
1856 .expect("dependent stored");
1857 let dependency = manifest
1858 .units
1859 .iter()
1860 .find(|entry| entry.name == "serde_core-b468be6fa8c1f936")
1861 .expect("dependency stored");
1862 assert!(
1863 dependent.stamp >= dependency.stamp,
1864 "clamped: {} >= {}",
1865 dependent.stamp,
1866 dependency.stamp
1867 );
1868
1869 let reports = verify(store.path())?;
1870 assert!(reports[0].is_clean(), "{:?}", reports[0].problems);
1871 assert!(reports[0].ordering.is_empty(), "{:?}", reports[0].ordering);
1872 Ok(())
1873 }
1874
1875 #[test]
1876 fn harvest_stamps_groups_by_the_metadata_output() -> Result<()> {
1877 let build = tempfile::tempdir()?;
1878 let store = tempfile::tempdir()?;
1879 synthetic_build_dir(build.path())?;
1880 let now = now_secs();
1881 set_stamp(
1884 &build.path().join("debug/deps/libserde-102a33f055a8d229.rmeta"),
1885 now - 30,
1886 )?;
1887 set_stamp(
1888 &build.path().join("debug/deps/libserde-102a33f055a8d229.rlib"),
1889 now,
1890 )?;
1891 harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1892
1893 let manifest = load_manifest(store.path(), &parity_key()?)?.expect("manifest");
1894 assert_eq!(manifest.units[0].stamp, now - 30);
1895 Ok(())
1896 }
1897
1898 #[test]
1899 fn harvest_stores_read_only_sources() -> Result<()> {
1900 let build = tempfile::tempdir()?;
1901 let store = tempfile::tempdir()?;
1902 synthetic_build_dir(build.path())?;
1903 let source = build
1907 .path()
1908 .join("debug/.fingerprint/serde-102a33f055a8d229/dep-lib-serde");
1909 let mut perms = fs::metadata(&source)?.permissions();
1910 perms.set_readonly(true);
1911 fs::set_permissions(&source, perms)?;
1912
1913 let report = harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1914 assert_eq!(report.stored, 1);
1915
1916 let parity = parity_key()?;
1917 let manifest = load_manifest(store.path(), &parity)?.expect("manifest");
1918 let stored = units_dir(store.path(), &parity)
1919 .join(&manifest.units[0].name)
1920 .join("dep-lib-serde");
1921 assert!(stored.is_file(), "the state file is stored");
1922 assert_eq!(
1923 ceil_secs(fs::metadata(&stored)?.modified()?),
1924 manifest.units[0].stamp,
1925 "the stored copy carries the canonical stamp"
1926 );
1927 Ok(())
1928 }
1929
1930 #[cfg(unix)]
1931 #[test]
1932 fn a_failed_store_drops_its_staging_dir() -> Result<()> {
1933 use std::os::unix::fs::MetadataExt;
1934
1935 let build = tempfile::tempdir()?;
1936 let store = tempfile::tempdir()?;
1937 synthetic_build_dir(build.path())?;
1938 if fs::metadata(build.path())?.uid() == 0 {
1939 return Ok(()); }
1941 let artifact = build
1942 .path()
1943 .join("debug/deps/libserde-102a33f055a8d229.rlib");
1944 {
1945 use std::os::unix::fs::PermissionsExt;
1946 fs::set_permissions(&artifact, fs::Permissions::from_mode(0o000))?;
1947 }
1948
1949 let result = harvest(build.path(), store.path(), Some("102a33f0"), false);
1950 assert!(result.is_err(), "an unreadable artifact aborts the store");
1951
1952 let units = units_dir(store.path(), &parity_key()?);
1953 if units.is_dir() {
1954 for entry in fs::read_dir(&units)? {
1955 let name = entry?.file_name().to_string_lossy().into_owned();
1956 assert!(!name.contains(".tmp-"), "staging leftover: {name}");
1957 }
1958 }
1959 Ok(())
1960 }
1961
1962 #[cfg(unix)]
1963 #[test]
1964 fn harvest_sweeps_stale_staging_dirs() -> Result<()> {
1965 use std::os::unix::fs::PermissionsExt;
1966
1967 let build = tempfile::tempdir()?;
1968 let store = tempfile::tempdir()?;
1969 synthetic_build_dir(build.path())?;
1970 let units = units_dir(store.path(), &parity_key()?);
1971
1972 let stale = units.join(".serde-102a33f055a8d229.tmp-99999");
1974 fs::create_dir_all(&stale)?;
1975 let leftover = stale.join("dep-lib-serde");
1976 fs::write(&leftover, b"x")?;
1977 fs::set_permissions(&leftover, fs::Permissions::from_mode(0o444))?;
1978 set_stamp(&stale, now_secs() - 7200)?;
1979
1980 let fresh = units.join(".serde-102a33f055a8d229.tmp-11111");
1982 fs::create_dir_all(&fresh)?;
1983
1984 harvest(build.path(), store.path(), Some("102a33f0"), false)?;
1985 assert!(!stale.exists(), "stale staging dir swept");
1986 assert!(fresh.exists(), "fresh staging dir left alone");
1987 Ok(())
1988 }
1989
1990 #[test]
1991 fn verify_flags_an_inverted_store_stamp() -> Result<()> {
1992 let build = tempfile::tempdir()?;
1993 let store = tempfile::tempdir()?;
1994 let now = now_secs();
1995 synthetic_edge_dir(build.path(), now - 3600, now)?;
1996 harvest(build.path(), store.path(), None, false)?;
1997
1998 let parity = parity_key()?;
2001 let mut manifest = load_manifest(store.path(), &parity)?.expect("manifest");
2002 let units = units_dir(store.path(), &parity);
2003 let entry = manifest
2004 .units
2005 .iter_mut()
2006 .find(|entry| entry.name == "serde-746d173581f1a314")
2007 .expect("dependent stored");
2008 entry.stamp = now - 7200;
2009 for file in &entry.files {
2010 set_stamp(&units.join(&entry.name).join(&file.path), now - 7200)?;
2011 }
2012 save_manifest(store.path(), &parity, &manifest)?;
2013
2014 let reports = verify(store.path())?;
2015 assert!(reports[0].is_clean(), "{:?}", reports[0].problems);
2016 assert_eq!(reports[0].ordering.len(), 1, "{:?}", reports[0].ordering);
2017 assert!(reports[0].ordering[0].contains("serde-746d173581f1a314"));
2018 assert!(reports[0].ordering[0].contains("serde_core-b468be6fa8c1f936"));
2019 Ok(())
2020 }
2021
2022 #[test]
2023 fn verify_ordering_is_quiet_when_units_are_current() -> Result<()> {
2024 let build = tempfile::tempdir()?;
2025 let store = tempfile::tempdir()?;
2026 let now = now_secs();
2027 synthetic_edge_dir(build.path(), now - 3600, now)?;
2028 harvest(build.path(), store.path(), None, false)?;
2029
2030 let reports = verify(store.path())?;
2031 assert!(reports[0].is_clean(), "{:?}", reports[0].problems);
2032 assert!(reports[0].ordering.is_empty(), "{:?}", reports[0].ordering);
2033 Ok(())
2034 }
2035}