1use std::collections::{HashMap, HashSet, VecDeque};
2use std::fs;
3use std::path::Path;
4use std::time::{Duration, SystemTime};
5
6use anyhow::{Context as _, Result};
7
8pub fn static_regex(pattern: &str) -> regex::Regex {
16 regex::Regex::new(pattern)
17 .unwrap_or_else(|e| panic!("invalid static regex literal `{}`: {}", pattern, e))
18}
19
20pub fn topological_sort(items: &[(impl AsRef<str>, impl AsRef<[String]>)]) -> Vec<String> {
34 let names: HashSet<&str> = items.iter().map(|(n, _)| n.as_ref()).collect();
35
36 let mut in_degree: HashMap<&str, usize> = items
37 .iter()
38 .map(|(n, deps)| {
39 let deg = deps
40 .as_ref()
41 .iter()
42 .filter(|d| names.contains(d.as_str()))
43 .count();
44 (n.as_ref(), deg)
45 })
46 .collect();
47
48 let mut edges: HashMap<&str, Vec<&str>> = HashMap::new();
50 for (n, deps) in items {
51 for dep in deps.as_ref() {
52 if names.contains(dep.as_str()) {
53 edges.entry(dep.as_str()).or_default().push(n.as_ref());
54 }
55 }
56 }
57
58 let mut queue: VecDeque<&str> = {
60 let mut v: Vec<&str> = in_degree
61 .iter()
62 .filter(|(_, d)| **d == 0)
63 .map(|(&n, _)| n)
64 .collect();
65 v.sort_unstable();
66 VecDeque::from(v)
67 };
68
69 let mut result = Vec::with_capacity(items.len());
70 while let Some(node) = queue.pop_front() {
71 result.push(node.to_string());
72 if let Some(dependents) = edges.get(node) {
73 let mut next: Vec<&str> = dependents
74 .iter()
75 .filter_map(|&dep| {
76 let deg = in_degree.get_mut(dep)?;
77 *deg -= 1;
78 if *deg == 0 { Some(dep) } else { None }
79 })
80 .collect();
81 next.sort_unstable();
82 for n in next {
83 queue.push_back(n);
84 }
85 }
86 }
87
88 if result.len() < items.len() {
90 let in_result: HashSet<String> = result.iter().cloned().collect();
91 for (n, _) in items {
92 if !in_result.contains(n.as_ref()) {
93 result.push(n.as_ref().to_string());
94 }
95 }
96 }
97
98 result
99}
100
101pub fn find_binary(name: &str) -> bool {
113 if name.contains('/') || name.contains('\\') {
114 return Path::new(name).exists();
115 }
116
117 let extensions: Vec<String> = if cfg!(windows) {
120 std::env::var("PATHEXT")
121 .unwrap_or_else(|_| ".COM;.EXE;.BAT;.CMD".to_string())
122 .split(';')
123 .filter(|e| !e.is_empty())
124 .map(|e| e.to_string())
125 .collect()
126 } else {
127 Vec::new()
128 };
129
130 if let Ok(path_var) = std::env::var("PATH") {
131 for dir in std::env::split_paths(&path_var) {
132 let candidate = dir.join(name);
133 if candidate.is_file() {
134 return true;
135 }
136 for ext in &extensions {
137 let with_ext = dir.join(format!("{}{}", name, ext));
138 if with_ext.is_file() {
139 return true;
140 }
141 }
142 }
143 }
144
145 false
146}
147
148pub fn parse_mod_timestamp(raw: &str) -> Result<SystemTime> {
162 if let Ok(epoch_secs) = raw.parse::<u64>() {
164 return Ok(SystemTime::UNIX_EPOCH + Duration::from_secs(epoch_secs));
165 }
166 if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(raw) {
168 let epoch_secs = dt.timestamp() as u64;
169 return Ok(SystemTime::UNIX_EPOCH + Duration::from_secs(epoch_secs));
170 }
171 if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(raw, "%Y-%m-%dT%H:%M:%S") {
173 let epoch_secs = dt.and_utc().timestamp() as u64;
174 return Ok(SystemTime::UNIX_EPOCH + Duration::from_secs(epoch_secs));
175 }
176 anyhow::bail!(
177 "mod_timestamp value '{raw}' is not a valid timestamp. \
178 Accepted formats: Unix epoch seconds (e.g. \"1704067200\") or \
179 RFC 3339 datetime (e.g. \"2024-01-01T00:00:00Z\")"
180 )
181}
182
183pub fn apply_mod_timestamp(dir: &Path, raw: &str, log: &crate::log::StageLogger) -> Result<()> {
193 let mtime = parse_mod_timestamp(raw)?;
194
195 let mut stack: Vec<std::path::PathBuf> = vec![dir.to_path_buf()];
196 while let Some(p) = stack.pop() {
197 for entry in
198 fs::read_dir(&p).with_context(|| format!("read staging dir {}", p.display()))?
199 {
200 let entry = entry?;
201 let path = entry.path();
202 let ft = entry.file_type()?;
203 if ft.is_dir() {
204 stack.push(path);
205 } else if ft.is_file() {
206 set_file_mtime(&path, mtime)?;
207 }
208 }
209 }
210
211 log.status(&format!("applied mod_timestamp={raw} to staging files"));
212 Ok(())
213}
214
215pub fn set_file_mtime(path: &Path, mtime: SystemTime) -> Result<()> {
217 let file = std::fs::OpenOptions::new()
218 .write(true)
219 .open(path)
220 .with_context(|| format!("open {} for mtime update", path.display()))?;
221 file.set_times(
222 std::fs::FileTimes::new()
223 .set_accessed(mtime)
224 .set_modified(mtime),
225 )
226 .with_context(|| format!("set mtime on {}", path.display()))?;
227 Ok(())
228}
229
230pub fn set_file_mtime_epoch(path: &Path, epoch_secs: i64) -> Result<()> {
235 let mtime = if epoch_secs >= 0 {
236 SystemTime::UNIX_EPOCH + Duration::from_secs(epoch_secs as u64)
237 } else {
238 SystemTime::UNIX_EPOCH - Duration::from_secs((-epoch_secs) as u64)
239 };
240 set_file_mtime(path, mtime)
241}
242
243pub fn pin_dir_mtimes_epoch(dir: &Path, epoch_secs: i64) -> Result<()> {
252 let mut stack: Vec<std::path::PathBuf> = vec![dir.to_path_buf()];
253 while let Some(p) = stack.pop() {
254 for entry in
255 fs::read_dir(&p).with_context(|| format!("read_dir {} for mtime pin", p.display()))?
256 {
257 let entry = entry?;
258 let path = entry.path();
259 let ft = entry.file_type()?;
260 if ft.is_dir() {
261 stack.push(path);
262 } else if ft.is_file() {
263 set_file_mtime_epoch(&path, epoch_secs)
264 .with_context(|| format!("pin mtime on {}", path.display()))?;
265 }
266 }
267 }
268 Ok(())
269}
270
271pub fn copy_dir_tree(src: &Path, dst: &Path) -> Result<()> {
283 fs::create_dir_all(dst).with_context(|| format!("create dir {}", dst.display()))?;
284 for entry in fs::read_dir(src).with_context(|| format!("read dir {}", src.display()))? {
285 let entry = entry.with_context(|| format!("read entry under {}", src.display()))?;
286 let from = entry.path();
287 let to = dst.join(entry.file_name());
288 let file_type = entry
291 .file_type()
292 .with_context(|| format!("stat {}", from.display()))?;
293 if file_type.is_symlink() {
294 #[cfg(unix)]
295 {
296 let target = fs::read_link(&from)
297 .with_context(|| format!("read symlink {}", from.display()))?;
298 std::os::unix::fs::symlink(&target, &to).with_context(|| {
299 format!("recreate symlink {} -> {}", to.display(), target.display())
300 })?;
301 }
302 #[cfg(not(unix))]
303 {
304 if from.is_dir() {
305 copy_dir_tree(&from, &to)?;
306 } else {
307 fs::copy(&from, &to)
308 .with_context(|| format!("copy {} to {}", from.display(), to.display()))?;
309 }
310 }
311 } else if file_type.is_dir() {
312 copy_dir_tree(&from, &to)?;
313 } else {
314 fs::copy(&from, &to)
315 .with_context(|| format!("copy {} to {}", from.display(), to.display()))?;
316 }
317 }
318 Ok(())
319}
320
321pub fn collect_replace_archives(
327 artifacts: &crate::artifact::ArtifactRegistry,
328 crate_name: &str,
329 target: Option<&str>,
330) -> Vec<std::path::PathBuf> {
331 artifacts
332 .by_kind_and_crate(crate::artifact::ArtifactKind::Archive, crate_name)
333 .iter()
334 .filter(|a| a.target.as_deref() == target)
335 .map(|a| a.path.clone())
336 .collect()
337}
338
339pub fn collect_if_replace(
346 replace: Option<bool>,
347 artifacts: &crate::artifact::ArtifactRegistry,
348 crate_name: &str,
349 target: Option<&str>,
350) -> Vec<std::path::PathBuf> {
351 if replace.unwrap_or(false) {
352 collect_replace_archives(artifacts, crate_name, target)
353 } else {
354 Vec::new()
355 }
356}
357
358pub fn normalize_path_separators(s: &str) -> String {
363 s.replace('\\', "/")
364}
365
366pub fn apply_minimal_env(command: &mut std::process::Command) {
377 const PASSTHROUGH: &[&str] = &[
378 "HOME",
379 "USER",
380 "USERPROFILE",
381 "TMPDIR",
382 "TMP",
383 "TEMP",
384 "PATH",
385 "LOCALAPPDATA",
386 ];
387 for key in PASSTHROUGH {
388 if let Ok(val) = std::env::var(key) {
389 command.env(key, val);
390 }
391 }
392}
393
394const CARGO_BUILD_INTERMEDIATE_DIRS: &[&str] = &["deps", "build", "incremental", ".fingerprint"];
404
405pub fn free_cargo_build_intermediates(
425 profile_dir: &Path,
426 log: &crate::log::StageLogger,
427) -> Vec<&'static str> {
428 let is_cargo_profile_dir = profile_dir
429 .file_name()
430 .and_then(|n| n.to_str())
431 .is_some_and(|n| n == "release" || n == "debug");
432 if !is_cargo_profile_dir {
433 log.verbose(&format!(
434 "refusing to free build intermediates under non-profile dir {}",
435 profile_dir.display()
436 ));
437 return Vec::new();
438 }
439 let mut freed = Vec::new();
440 for sub in CARGO_BUILD_INTERMEDIATE_DIRS {
441 let path = profile_dir.join(sub);
442 if !path.exists() {
443 continue;
444 }
445 match fs::remove_dir_all(&path) {
446 Ok(()) => freed.push(*sub),
447 Err(err) => log.verbose(&format!(
448 "could not free build intermediate {}: {err}",
449 path.display()
450 )),
451 }
452 }
453 freed
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459
460 #[test]
465 fn test_topo_sort_simple_chain() {
466 let items = vec![
467 ("c".to_string(), vec!["b".to_string()]),
468 ("b".to_string(), vec!["a".to_string()]),
469 ("a".to_string(), vec![]),
470 ];
471 let sorted = topological_sort(&items);
472 assert_eq!(sorted, vec!["a", "b", "c"]);
473 }
474
475 #[test]
476 fn test_topo_sort_no_deps() {
477 let items = vec![("b".to_string(), vec![]), ("a".to_string(), vec![])];
478 let sorted = topological_sort(&items);
480 assert_eq!(sorted, vec!["a", "b"]);
481 }
482
483 #[test]
484 fn test_topo_sort_ignores_external_deps() {
485 let items = vec![
486 (
487 "b".to_string(),
488 vec!["a".to_string(), "external".to_string()],
489 ),
490 ("a".to_string(), vec![]),
491 ];
492 let sorted = topological_sort(&items);
493 assert_eq!(sorted, vec!["a", "b"]);
494 }
495
496 #[test]
497 fn test_topo_sort_diamond() {
498 let items = vec![
499 ("d".to_string(), vec!["b".to_string(), "c".to_string()]),
500 ("b".to_string(), vec!["a".to_string()]),
501 ("c".to_string(), vec!["a".to_string()]),
502 ("a".to_string(), vec![]),
503 ];
504 let sorted = topological_sort(&items);
505 assert_eq!(sorted[0], "a");
507 assert_eq!(sorted[3], "d");
508 }
509
510 #[test]
511 fn test_topo_sort_cycle_appends_remaining() {
512 let items = vec![
513 ("a".to_string(), vec!["b".to_string()]),
514 ("b".to_string(), vec!["a".to_string()]),
515 ("c".to_string(), vec![]),
516 ];
517 let sorted = topological_sort(&items);
518 assert_eq!(sorted.len(), 3);
519 assert_eq!(sorted[0], "c");
521 }
522
523 #[test]
524 fn test_topo_sort_empty() {
525 let items: Vec<(String, Vec<String>)> = vec![];
526 let sorted = topological_sort(&items);
527 assert!(sorted.is_empty());
528 }
529
530 #[test]
535 fn test_find_binary_absolute_path_exists() {
536 if cfg!(windows) {
537 assert!(find_binary("C:\\Windows\\System32\\cmd.exe"));
539 } else {
540 assert!(find_binary("/usr/bin/env"));
542 }
543 }
544
545 #[test]
546 fn test_find_binary_absolute_path_does_not_exist() {
547 if cfg!(windows) {
548 assert!(!find_binary("C:\\nonexistent\\binary\\path.exe"));
549 } else {
550 assert!(!find_binary("/nonexistent/binary/path"));
551 }
552 }
553
554 #[test]
555 fn test_find_binary_bare_name_on_path() {
556 if cfg!(windows) {
557 assert!(find_binary("cmd.exe"));
560 } else {
561 assert!(find_binary("env"));
563 }
564 }
565
566 #[test]
567 fn test_find_binary_bare_name_not_on_path() {
568 assert!(!find_binary("nonexistent-binary-xyz-12345"));
569 }
570
571 #[test]
576 fn test_parse_mod_timestamp_epoch_integer() {
577 let t = parse_mod_timestamp("1704067200").unwrap();
578 let epoch = t.duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
579 assert_eq!(epoch, 1704067200);
580 }
581
582 #[test]
583 fn test_parse_mod_timestamp_rfc3339() {
584 let t = parse_mod_timestamp("2024-01-01T00:00:00Z").unwrap();
585 let epoch = t.duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
586 assert_eq!(epoch, 1704067200);
587 }
588
589 #[test]
590 fn test_parse_mod_timestamp_rfc3339_with_offset() {
591 let t = parse_mod_timestamp("2024-01-01T01:00:00+01:00").unwrap();
592 let epoch = t.duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
593 assert_eq!(epoch, 1704067200);
595 }
596
597 #[test]
598 fn test_parse_mod_timestamp_naive_datetime() {
599 let t = parse_mod_timestamp("2024-01-01T00:00:00").unwrap();
600 let epoch = t.duration_since(SystemTime::UNIX_EPOCH).unwrap().as_secs();
601 assert_eq!(epoch, 1704067200);
602 }
603
604 #[test]
605 fn test_parse_mod_timestamp_invalid() {
606 let err = parse_mod_timestamp("not-a-timestamp").unwrap_err();
607 let msg = err.to_string();
608 assert!(
609 msg.contains("not a valid timestamp"),
610 "unexpected error: {msg}"
611 );
612 assert!(
615 msg.contains("not-a-timestamp"),
616 "error must include the bad value, got: {msg}"
617 );
618 }
619
620 #[test]
621 fn test_parse_mod_timestamp_zero() {
622 let t = parse_mod_timestamp("0").unwrap();
623 assert_eq!(t, SystemTime::UNIX_EPOCH);
624 }
625
626 #[test]
631 fn test_set_file_mtime_sets_both_atime_and_mtime() {
632 let dir = tempfile::tempdir().unwrap();
633 let dir = dir.path();
634
635 let file_path = dir.join("test.txt");
636 std::fs::write(&file_path, "hello").unwrap();
637
638 let target = SystemTime::UNIX_EPOCH + Duration::from_secs(1704067200);
640 set_file_mtime(&file_path, target).unwrap();
641
642 let meta = std::fs::metadata(&file_path).unwrap();
643 let actual_mtime = meta.modified().unwrap();
644
645 let diff = if actual_mtime > target {
647 actual_mtime.duration_since(target).unwrap()
648 } else {
649 target.duration_since(actual_mtime).unwrap()
650 };
651 assert!(
652 diff.as_secs() <= 1,
653 "mtime should be within 1s of target, diff={:?}",
654 diff
655 );
656
657 let actual_atime = meta.accessed().unwrap();
659 let diff_a = if actual_atime > target {
660 actual_atime.duration_since(target).unwrap()
661 } else {
662 target.duration_since(actual_atime).unwrap()
663 };
664 assert!(
665 diff_a.as_secs() <= 1,
666 "atime should be within 1s of target, diff={:?}",
667 diff_a
668 );
669 }
670
671 #[test]
672 fn test_pin_dir_mtimes_epoch_recurses_into_subdirs() {
673 let dir = tempfile::tempdir().unwrap();
674 let dir = dir.path();
675 let sub = dir.join("nested");
676 std::fs::create_dir_all(&sub).unwrap();
677
678 let top = dir.join("top.txt");
679 let nested = sub.join("nested.txt");
680 std::fs::write(&top, "top").unwrap();
681 std::fs::write(&nested, "nested").unwrap();
682
683 let epoch: i64 = 1704067200;
684 pin_dir_mtimes_epoch(dir, epoch).unwrap();
685
686 let target = SystemTime::UNIX_EPOCH + Duration::from_secs(epoch as u64);
687 for path in [&top, &nested] {
688 let mtime = std::fs::metadata(path).unwrap().modified().unwrap();
689 assert_eq!(
690 mtime,
691 target,
692 "{}: mtime must equal the pinned epoch exactly",
693 path.display()
694 );
695 }
696 }
697
698 #[test]
699 fn test_set_file_mtime_nonexistent_file() {
700 let result = set_file_mtime(Path::new("/nonexistent/file.txt"), SystemTime::UNIX_EPOCH);
701 assert!(result.is_err());
702 }
703
704 #[test]
709 fn test_apply_mod_timestamp_sets_mtime_on_regular_files() {
710 let dir = tempfile::tempdir().unwrap();
711 let dir = dir.path();
712
713 std::fs::write(dir.join("a.txt"), "aaa").unwrap();
715 std::fs::write(dir.join("b.txt"), "bbb").unwrap();
716 std::fs::create_dir(dir.join("subdir")).unwrap();
717
718 let log = crate::log::StageLogger::new("test", crate::log::Verbosity::Quiet);
719 apply_mod_timestamp(dir, "1704067200", &log).unwrap();
720
721 let target = SystemTime::UNIX_EPOCH + Duration::from_secs(1704067200);
722 for name in &["a.txt", "b.txt"] {
723 let meta = std::fs::metadata(dir.join(name)).unwrap();
724 let mtime = meta.modified().unwrap();
725 let diff = if mtime > target {
726 mtime.duration_since(target).unwrap()
727 } else {
728 target.duration_since(mtime).unwrap()
729 };
730 assert!(
731 diff.as_secs() <= 1,
732 "{name}: mtime should be within 1s of target, diff={:?}",
733 diff
734 );
735 }
736 }
737
738 #[test]
739 fn test_apply_mod_timestamp_recurses_into_subdirs() {
740 let dir = tempfile::tempdir().unwrap();
741 let dir = dir.path();
742 let sub = dir.join("docs");
743 std::fs::create_dir_all(&sub).unwrap();
744
745 let top = dir.join("top.txt");
746 let nested = sub.join("README.txt");
747 std::fs::write(&top, "top").unwrap();
748 std::fs::write(&nested, "nested").unwrap();
749
750 let log = crate::log::StageLogger::new("test", crate::log::Verbosity::Quiet);
751 apply_mod_timestamp(dir, "1704067200", &log).unwrap();
752
753 let target = SystemTime::UNIX_EPOCH + Duration::from_secs(1704067200);
754 for path in [&top, &nested] {
755 let mtime = std::fs::metadata(path).unwrap().modified().unwrap();
756 let diff = if mtime > target {
757 mtime.duration_since(target).unwrap()
758 } else {
759 target.duration_since(mtime).unwrap()
760 };
761 assert!(
762 diff.as_secs() <= 1,
763 "{}: nested file must receive mod_timestamp, diff={:?}",
764 path.display(),
765 diff
766 );
767 }
768 }
769
770 #[test]
771 fn test_apply_mod_timestamp_invalid_timestamp_errors() {
772 let dir = tempfile::tempdir().unwrap();
773
774 let log = crate::log::StageLogger::new("test", crate::log::Verbosity::Quiet);
775 let result = apply_mod_timestamp(dir.path(), "not-valid", &log);
776 assert!(result.is_err());
777 }
778
779 fn mk_release_dir(root: &Path) -> std::path::PathBuf {
787 let profile = root
788 .join("target")
789 .join("x86_64-unknown-linux-gnu")
790 .join("release");
791 std::fs::create_dir_all(&profile).unwrap();
792 profile
793 }
794
795 #[test]
796 fn test_free_cargo_build_intermediates_removes_transient_keeps_binary() {
797 let tmp = tempfile::tempdir().unwrap();
798 let profile = mk_release_dir(tmp.path());
799
800 for sub in ["deps", "build", "incremental", ".fingerprint"] {
804 let d = profile.join(sub);
805 std::fs::create_dir_all(&d).unwrap();
806 std::fs::write(d.join("scratch.o"), "obj").unwrap();
807 }
808 std::fs::write(profile.join("myapp"), b"\x7fELF binary").unwrap();
809 std::fs::write(profile.join("myapp.d"), "depinfo").unwrap();
810
811 let log = crate::log::StageLogger::new("test", crate::log::Verbosity::Quiet);
812 let mut freed = free_cargo_build_intermediates(&profile, &log);
813 freed.sort_unstable();
814 assert_eq!(freed, vec![".fingerprint", "build", "deps", "incremental"]);
815
816 for sub in ["deps", "build", "incremental", ".fingerprint"] {
817 assert!(
818 !profile.join(sub).exists(),
819 "transient subdir {sub} should be removed"
820 );
821 }
822 assert!(profile.join("myapp").exists(), "binary must be retained");
823 assert_eq!(
824 std::fs::read(profile.join("myapp")).unwrap(),
825 b"\x7fELF binary"
826 );
827 assert!(
828 profile.join("myapp.d").exists(),
829 "sibling regular file must be retained"
830 );
831 }
832
833 #[test]
834 fn test_free_cargo_build_intermediates_missing_dirs_is_noop() {
835 let tmp = tempfile::tempdir().unwrap();
836 let profile = mk_release_dir(tmp.path());
837 std::fs::write(profile.join("myapp"), "bin").unwrap();
839
840 let log = crate::log::StageLogger::new("test", crate::log::Verbosity::Quiet);
841 let freed = free_cargo_build_intermediates(&profile, &log);
842 assert!(freed.is_empty(), "nothing to free when no subdirs exist");
843 assert!(profile.join("myapp").exists());
844 }
845
846 #[test]
847 fn test_free_cargo_build_intermediates_partial_subset() {
848 let tmp = tempfile::tempdir().unwrap();
849 let profile = mk_release_dir(tmp.path());
850 std::fs::create_dir_all(profile.join("deps")).unwrap();
853 std::fs::create_dir_all(profile.join("incremental")).unwrap();
854 std::fs::write(profile.join("myapp"), "bin").unwrap();
855
856 let log = crate::log::StageLogger::new("test", crate::log::Verbosity::Quiet);
857 let mut freed = free_cargo_build_intermediates(&profile, &log);
858 freed.sort_unstable();
859 assert_eq!(freed, vec!["deps", "incremental"]);
860 assert!(profile.join("myapp").exists());
861 }
862
863 #[test]
867 fn test_free_cargo_build_intermediates_non_profile_dir_is_noop() {
868 let tmp = tempfile::tempdir().unwrap();
869 let not_profile = tmp.path().join("target");
871 std::fs::create_dir_all(not_profile.join("deps")).unwrap();
872 std::fs::create_dir_all(not_profile.join("build")).unwrap();
873
874 let log = crate::log::StageLogger::new("test", crate::log::Verbosity::Quiet);
875 let freed = free_cargo_build_intermediates(¬_profile, &log);
876 assert!(
877 freed.is_empty(),
878 "non-profile dir must free nothing (guard)"
879 );
880 assert!(
881 not_profile.join("deps").exists() && not_profile.join("build").exists(),
882 "guard must leave a non-profile dir's contents untouched"
883 );
884 }
885}