rustc_incremental/persist/
fs.rs
1use std::fs as std_fs;
107use std::io::{self, ErrorKind};
108use std::path::{Path, PathBuf};
109use std::time::{Duration, SystemTime, UNIX_EPOCH};
110
111use rand::{RngCore, thread_rng};
112use rustc_data_structures::base_n::{BaseNString, CASE_INSENSITIVE, ToBaseN};
113use rustc_data_structures::fx::{FxHashSet, FxIndexSet};
114use rustc_data_structures::svh::Svh;
115use rustc_data_structures::unord::{UnordMap, UnordSet};
116use rustc_data_structures::{base_n, flock};
117use rustc_fs_util::{LinkOrCopy, link_or_copy, try_canonicalize};
118use rustc_middle::bug;
119use rustc_session::config::CrateType;
120use rustc_session::output::{collect_crate_types, find_crate_name};
121use rustc_session::{Session, StableCrateId};
122use tracing::debug;
123
124use crate::errors;
125
126#[cfg(test)]
127mod tests;
128
129const LOCK_FILE_EXT: &str = ".lock";
130const DEP_GRAPH_FILENAME: &str = "dep-graph.bin";
131const STAGING_DEP_GRAPH_FILENAME: &str = "dep-graph.part.bin";
132const WORK_PRODUCTS_FILENAME: &str = "work-products.bin";
133const QUERY_CACHE_FILENAME: &str = "query-cache.bin";
134
135const INT_ENCODE_BASE: usize = base_n::CASE_INSENSITIVE;
140
141pub(crate) fn dep_graph_path(sess: &Session) -> PathBuf {
143 in_incr_comp_dir_sess(sess, DEP_GRAPH_FILENAME)
144}
145
146pub(crate) fn staging_dep_graph_path(sess: &Session) -> PathBuf {
151 in_incr_comp_dir_sess(sess, STAGING_DEP_GRAPH_FILENAME)
152}
153
154pub(crate) fn work_products_path(sess: &Session) -> PathBuf {
155 in_incr_comp_dir_sess(sess, WORK_PRODUCTS_FILENAME)
156}
157
158pub(crate) fn query_cache_path(sess: &Session) -> PathBuf {
160 in_incr_comp_dir_sess(sess, QUERY_CACHE_FILENAME)
161}
162
163fn lock_file_path(session_dir: &Path) -> PathBuf {
165 let crate_dir = session_dir.parent().unwrap();
166
167 let directory_name = session_dir
168 .file_name()
169 .unwrap()
170 .to_str()
171 .expect("malformed session dir name: contains non-Unicode characters");
172
173 let dash_indices: Vec<_> = directory_name.match_indices('-').map(|(idx, _)| idx).collect();
174 if dash_indices.len() != 3 {
175 bug!(
176 "Encountered incremental compilation session directory with \
177 malformed name: {}",
178 session_dir.display()
179 )
180 }
181
182 crate_dir.join(&directory_name[0..dash_indices[2]]).with_extension(&LOCK_FILE_EXT[1..])
183}
184
185pub fn in_incr_comp_dir_sess(sess: &Session, file_name: &str) -> PathBuf {
188 in_incr_comp_dir(&sess.incr_comp_session_dir(), file_name)
189}
190
191pub fn in_incr_comp_dir(incr_comp_session_dir: &Path, file_name: &str) -> PathBuf {
196 incr_comp_session_dir.join(file_name)
197}
198
199pub(crate) fn prepare_session_directory(sess: &Session) {
215 if sess.opts.incremental.is_none() {
216 return;
217 }
218
219 let _timer = sess.timer("incr_comp_prepare_session_directory");
220
221 debug!("prepare_session_directory");
222
223 let crate_dir = crate_path(sess);
225 debug!("crate-dir: {}", crate_dir.display());
226 create_dir(sess, &crate_dir, "crate");
227
228 let crate_dir = match try_canonicalize(&crate_dir) {
233 Ok(v) => v,
234 Err(err) => {
235 sess.dcx().emit_fatal(errors::CanonicalizePath { path: crate_dir, err });
236 }
237 };
238
239 let mut source_directories_already_tried = FxHashSet::default();
240
241 loop {
242 let session_dir = generate_session_dir_path(&crate_dir);
246 debug!("session-dir: {}", session_dir.display());
247
248 let (directory_lock, lock_file_path) = lock_directory(sess, &session_dir);
251
252 create_dir(sess, &session_dir, "session");
255
256 let source_directory = find_source_directory(&crate_dir, &source_directories_already_tried);
259
260 let Some(source_directory) = source_directory else {
261 debug!(
263 "no source directory found. Continuing with empty session \
264 directory."
265 );
266
267 sess.init_incr_comp_session(session_dir, directory_lock);
268 return;
269 };
270
271 debug!("attempting to copy data from source: {}", source_directory.display());
272
273 if let Ok(allows_links) = copy_files(sess, &session_dir, &source_directory) {
275 debug!("successfully copied data from: {}", source_directory.display());
276
277 if !allows_links {
278 sess.dcx().emit_warn(errors::HardLinkFailed { path: &session_dir });
279 }
280
281 sess.init_incr_comp_session(session_dir, directory_lock);
282 return;
283 } else {
284 debug!("copying failed - trying next directory");
285
286 source_directories_already_tried.insert(source_directory);
289
290 if let Err(err) = safe_remove_dir_all(&session_dir) {
293 sess.dcx().emit_warn(errors::DeletePartial { path: &session_dir, err });
294 }
295
296 delete_session_dir_lock_file(sess, &lock_file_path);
297 drop(directory_lock);
298 }
299 }
300}
301
302pub fn finalize_session_directory(sess: &Session, svh: Option<Svh>) {
307 if sess.opts.incremental.is_none() {
308 return;
309 }
310 let svh = svh.unwrap();
312
313 let _timer = sess.timer("incr_comp_finalize_session_directory");
314
315 let incr_comp_session_dir: PathBuf = sess.incr_comp_session_dir().clone();
316
317 if sess.dcx().has_errors_or_delayed_bugs().is_some() {
318 debug!(
322 "finalize_session_directory() - invalidating session directory: {}",
323 incr_comp_session_dir.display()
324 );
325
326 if let Err(err) = safe_remove_dir_all(&*incr_comp_session_dir) {
327 sess.dcx().emit_warn(errors::DeleteFull { path: &incr_comp_session_dir, err });
328 }
329
330 let lock_file_path = lock_file_path(&*incr_comp_session_dir);
331 delete_session_dir_lock_file(sess, &lock_file_path);
332 sess.mark_incr_comp_session_as_invalid();
333 }
334
335 debug!("finalize_session_directory() - session directory: {}", incr_comp_session_dir.display());
336
337 let mut sub_dir_name = incr_comp_session_dir
338 .file_name()
339 .unwrap()
340 .to_str()
341 .expect("malformed session dir name: contains non-Unicode characters")
342 .to_string();
343
344 sub_dir_name.truncate(sub_dir_name.len() - "working".len());
346 assert!(sub_dir_name.ends_with('-'), "{:?}", sub_dir_name);
348 assert!(sub_dir_name.as_bytes().iter().filter(|b| **b == b'-').count() == 3);
349
350 sub_dir_name.push_str(&svh.as_u128().to_base_fixed_len(CASE_INSENSITIVE));
352
353 let new_path = incr_comp_session_dir.parent().unwrap().join(&*sub_dir_name);
355 debug!("finalize_session_directory() - new path: {}", new_path.display());
356
357 match rename_path_with_retry(&*incr_comp_session_dir, &new_path, 3) {
358 Ok(_) => {
359 debug!("finalize_session_directory() - directory renamed successfully");
360
361 sess.finalize_incr_comp_session(new_path);
363 }
364 Err(e) => {
365 sess.dcx().emit_warn(errors::Finalize { path: &incr_comp_session_dir, err: e });
367
368 debug!("finalize_session_directory() - error, marking as invalid");
369 sess.mark_incr_comp_session_as_invalid();
371 }
372 }
373
374 let _ = garbage_collect_session_directories(sess);
375}
376
377pub(crate) fn delete_all_session_dir_contents(sess: &Session) -> io::Result<()> {
378 let sess_dir_iterator = sess.incr_comp_session_dir().read_dir()?;
379 for entry in sess_dir_iterator {
380 let entry = entry?;
381 safe_remove_file(&entry.path())?
382 }
383 Ok(())
384}
385
386fn copy_files(sess: &Session, target_dir: &Path, source_dir: &Path) -> Result<bool, ()> {
387 let lock_file_path = lock_file_path(source_dir);
390
391 let Ok(_lock) = flock::Lock::new(
393 &lock_file_path,
394 false, false, false,
397 ) else {
398 return Err(());
400 };
401
402 let Ok(source_dir_iterator) = source_dir.read_dir() else {
403 return Err(());
404 };
405
406 let mut files_linked = 0;
407 let mut files_copied = 0;
408
409 for entry in source_dir_iterator {
410 match entry {
411 Ok(entry) => {
412 let file_name = entry.file_name();
413
414 let target_file_path = target_dir.join(file_name);
415 let source_path = entry.path();
416
417 debug!("copying into session dir: {}", source_path.display());
418 match link_or_copy(source_path, target_file_path) {
419 Ok(LinkOrCopy::Link) => files_linked += 1,
420 Ok(LinkOrCopy::Copy) => files_copied += 1,
421 Err(_) => return Err(()),
422 }
423 }
424 Err(_) => return Err(()),
425 }
426 }
427
428 if sess.opts.unstable_opts.incremental_info {
429 eprintln!(
430 "[incremental] session directory: \
431 {files_linked} files hard-linked"
432 );
433 eprintln!(
434 "[incremental] session directory: \
435 {files_copied} files copied"
436 );
437 }
438
439 Ok(files_linked > 0 || files_copied == 0)
440}
441
442fn generate_session_dir_path(crate_dir: &Path) -> PathBuf {
445 let timestamp = timestamp_to_string(SystemTime::now());
446 debug!("generate_session_dir_path: timestamp = {}", timestamp);
447 let random_number = thread_rng().next_u32();
448 debug!("generate_session_dir_path: random_number = {}", random_number);
449
450 let (zeroes, timestamp) = timestamp.split_at(3);
452 assert_eq!(zeroes, "000");
453 let directory_name =
454 format!("s-{}-{}-working", timestamp, random_number.to_base_fixed_len(CASE_INSENSITIVE));
455 debug!("generate_session_dir_path: directory_name = {}", directory_name);
456 let directory_path = crate_dir.join(directory_name);
457 debug!("generate_session_dir_path: directory_path = {}", directory_path.display());
458 directory_path
459}
460
461fn create_dir(sess: &Session, path: &Path, dir_tag: &str) {
462 match std_fs::create_dir_all(path) {
463 Ok(()) => {
464 debug!("{} directory created successfully", dir_tag);
465 }
466 Err(err) => sess.dcx().emit_fatal(errors::CreateIncrCompDir { tag: dir_tag, path, err }),
467 }
468}
469
470fn lock_directory(sess: &Session, session_dir: &Path) -> (flock::Lock, PathBuf) {
472 let lock_file_path = lock_file_path(session_dir);
473 debug!("lock_directory() - lock_file: {}", lock_file_path.display());
474
475 match flock::Lock::new(
476 &lock_file_path,
477 false, true, true,
480 ) {
481 Ok(lock) => (lock, lock_file_path),
483 Err(lock_err) => {
484 let is_unsupported_lock = flock::Lock::error_unsupported(&lock_err);
485 sess.dcx().emit_fatal(errors::CreateLock {
486 lock_err,
487 session_dir,
488 is_unsupported_lock,
489 is_cargo: rustc_session::utils::was_invoked_from_cargo(),
490 });
491 }
492 }
493}
494
495fn delete_session_dir_lock_file(sess: &Session, lock_file_path: &Path) {
496 if let Err(err) = safe_remove_file(lock_file_path) {
497 sess.dcx().emit_warn(errors::DeleteLock { path: lock_file_path, err });
498 }
499}
500
501fn find_source_directory(
504 crate_dir: &Path,
505 source_directories_already_tried: &FxHashSet<PathBuf>,
506) -> Option<PathBuf> {
507 let iter = crate_dir
508 .read_dir()
509 .unwrap() .filter_map(|e| e.ok().map(|e| e.path()));
511
512 find_source_directory_in_iter(iter, source_directories_already_tried)
513}
514
515fn find_source_directory_in_iter<I>(
516 iter: I,
517 source_directories_already_tried: &FxHashSet<PathBuf>,
518) -> Option<PathBuf>
519where
520 I: Iterator<Item = PathBuf>,
521{
522 let mut best_candidate = (UNIX_EPOCH, None);
523
524 for session_dir in iter {
525 debug!("find_source_directory_in_iter - inspecting `{}`", session_dir.display());
526
527 let Some(directory_name) = session_dir.file_name().unwrap().to_str() else {
528 debug!("find_source_directory_in_iter - ignoring");
529 continue;
530 };
531
532 if source_directories_already_tried.contains(&session_dir)
533 || !is_session_directory(&directory_name)
534 || !is_finalized(&directory_name)
535 {
536 debug!("find_source_directory_in_iter - ignoring");
537 continue;
538 }
539
540 let timestamp = match extract_timestamp_from_session_dir(&directory_name) {
541 Ok(timestamp) => timestamp,
542 Err(e) => {
543 debug!("unexpected incr-comp session dir: {}: {}", session_dir.display(), e);
544 continue;
545 }
546 };
547
548 if timestamp > best_candidate.0 {
549 best_candidate = (timestamp, Some(session_dir.clone()));
550 }
551 }
552
553 best_candidate.1
554}
555
556fn is_finalized(directory_name: &str) -> bool {
557 !directory_name.ends_with("-working")
558}
559
560fn is_session_directory(directory_name: &str) -> bool {
561 directory_name.starts_with("s-") && !directory_name.ends_with(LOCK_FILE_EXT)
562}
563
564fn is_session_directory_lock_file(file_name: &str) -> bool {
565 file_name.starts_with("s-") && file_name.ends_with(LOCK_FILE_EXT)
566}
567
568fn extract_timestamp_from_session_dir(directory_name: &str) -> Result<SystemTime, &'static str> {
569 if !is_session_directory(directory_name) {
570 return Err("not a directory");
571 }
572
573 let dash_indices: Vec<_> = directory_name.match_indices('-').map(|(idx, _)| idx).collect();
574 if dash_indices.len() != 3 {
575 return Err("not three dashes in name");
576 }
577
578 string_to_timestamp(&directory_name[dash_indices[0] + 1..dash_indices[1]])
579}
580
581fn timestamp_to_string(timestamp: SystemTime) -> BaseNString {
582 let duration = timestamp.duration_since(UNIX_EPOCH).unwrap();
583 let micros: u64 = duration.as_micros().try_into().unwrap();
584 micros.to_base_fixed_len(CASE_INSENSITIVE)
585}
586
587fn string_to_timestamp(s: &str) -> Result<SystemTime, &'static str> {
588 let micros_since_unix_epoch = match u64::from_str_radix(s, INT_ENCODE_BASE as u32) {
589 Ok(micros) => micros,
590 Err(_) => return Err("timestamp not an int"),
591 };
592
593 let duration = Duration::from_micros(micros_since_unix_epoch);
594 Ok(UNIX_EPOCH + duration)
595}
596
597fn crate_path(sess: &Session) -> PathBuf {
598 let incr_dir = sess.opts.incremental.as_ref().unwrap().clone();
599
600 let crate_name = find_crate_name(sess, &[]);
601 let crate_types = collect_crate_types(sess, &[]);
602 let stable_crate_id = StableCrateId::new(
603 crate_name,
604 crate_types.contains(&CrateType::Executable),
605 sess.opts.cg.metadata.clone(),
606 sess.cfg_version,
607 );
608
609 let crate_name =
610 format!("{crate_name}-{}", stable_crate_id.as_u64().to_base_fixed_len(CASE_INSENSITIVE));
611 incr_dir.join(crate_name)
612}
613
614fn is_old_enough_to_be_collected(timestamp: SystemTime) -> bool {
615 timestamp < SystemTime::now() - Duration::from_secs(10)
616}
617
618pub(crate) fn garbage_collect_session_directories(sess: &Session) -> io::Result<()> {
620 debug!("garbage_collect_session_directories() - begin");
621
622 let session_directory = sess.incr_comp_session_dir();
623 debug!(
624 "garbage_collect_session_directories() - session directory: {}",
625 session_directory.display()
626 );
627
628 let crate_directory = session_directory.parent().unwrap();
629 debug!(
630 "garbage_collect_session_directories() - crate directory: {}",
631 crate_directory.display()
632 );
633
634 let mut session_directories = FxIndexSet::default();
637 let mut lock_files = UnordSet::default();
638
639 for dir_entry in crate_directory.read_dir()? {
640 let Ok(dir_entry) = dir_entry else {
641 continue;
643 };
644
645 let entry_name = dir_entry.file_name();
646 let Some(entry_name) = entry_name.to_str() else {
647 continue;
648 };
649
650 if is_session_directory_lock_file(&entry_name) {
651 lock_files.insert(entry_name.to_string());
652 } else if is_session_directory(&entry_name) {
653 session_directories.insert(entry_name.to_string());
654 } else {
655 }
657 }
658 session_directories.sort();
659
660 let lock_file_to_session_dir: UnordMap<String, Option<String>> = lock_files
662 .into_items()
663 .map(|lock_file_name| {
664 assert!(lock_file_name.ends_with(LOCK_FILE_EXT));
665 let dir_prefix_end = lock_file_name.len() - LOCK_FILE_EXT.len();
666 let session_dir = {
667 let dir_prefix = &lock_file_name[0..dir_prefix_end];
668 session_directories.iter().find(|dir_name| dir_name.starts_with(dir_prefix))
669 };
670 (lock_file_name, session_dir.map(String::clone))
671 })
672 .into();
673
674 for (lock_file_name, directory_name) in
677 lock_file_to_session_dir.items().into_sorted_stable_ord()
678 {
679 if directory_name.is_none() {
680 let Ok(timestamp) = extract_timestamp_from_session_dir(lock_file_name) else {
681 debug!(
682 "found lock-file with malformed timestamp: {}",
683 crate_directory.join(&lock_file_name).display()
684 );
685 continue;
687 };
688
689 let lock_file_path = crate_directory.join(&*lock_file_name);
690
691 if is_old_enough_to_be_collected(timestamp) {
692 debug!(
693 "garbage_collect_session_directories() - deleting \
694 garbage lock file: {}",
695 lock_file_path.display()
696 );
697 delete_session_dir_lock_file(sess, &lock_file_path);
698 } else {
699 debug!(
700 "garbage_collect_session_directories() - lock file with \
701 no session dir not old enough to be collected: {}",
702 lock_file_path.display()
703 );
704 }
705 }
706 }
707
708 let lock_file_to_session_dir: UnordMap<String, String> = lock_file_to_session_dir
710 .into_items()
711 .filter_map(|(lock_file_name, directory_name)| directory_name.map(|n| (lock_file_name, n)))
712 .into();
713
714 for directory_name in session_directories {
716 if !lock_file_to_session_dir.items().any(|(_, dir)| *dir == directory_name) {
717 let path = crate_directory.join(directory_name);
718 if let Err(err) = safe_remove_dir_all(&path) {
719 sess.dcx().emit_warn(errors::InvalidGcFailed { path: &path, err });
720 }
721 }
722 }
723
724 let deletion_candidates =
726 lock_file_to_session_dir.items().filter_map(|(lock_file_name, directory_name)| {
727 debug!("garbage_collect_session_directories() - inspecting: {}", directory_name);
728
729 let Ok(timestamp) = extract_timestamp_from_session_dir(directory_name) else {
730 debug!(
731 "found session-dir with malformed timestamp: {}",
732 crate_directory.join(directory_name).display()
733 );
734 return None;
736 };
737
738 if is_finalized(directory_name) {
739 let lock_file_path = crate_directory.join(lock_file_name);
740 match flock::Lock::new(
741 &lock_file_path,
742 false, false, true,
745 ) {
746 Ok(lock) => {
748 debug!(
749 "garbage_collect_session_directories() - \
750 successfully acquired lock"
751 );
752 debug!(
753 "garbage_collect_session_directories() - adding \
754 deletion candidate: {}",
755 directory_name
756 );
757
758 return Some((
760 (timestamp, crate_directory.join(directory_name)),
761 Some(lock),
762 ));
763 }
764 Err(_) => {
765 debug!(
766 "garbage_collect_session_directories() - \
767 not collecting, still in use"
768 );
769 }
770 }
771 } else if is_old_enough_to_be_collected(timestamp) {
772 let lock_file_path = crate_directory.join(lock_file_name);
784 match flock::Lock::new(
785 &lock_file_path,
786 false, false, true,
789 ) {
790 Ok(lock) => {
792 debug!(
793 "garbage_collect_session_directories() - \
794 successfully acquired lock"
795 );
796
797 delete_old(sess, &crate_directory.join(directory_name));
798
799 drop(lock);
802 }
803 Err(_) => {
804 debug!(
805 "garbage_collect_session_directories() - \
806 not collecting, still in use"
807 );
808 }
809 }
810 } else {
811 debug!(
812 "garbage_collect_session_directories() - not finalized, not \
813 old enough"
814 );
815 }
816 None
817 });
818 let deletion_candidates = deletion_candidates.into();
819
820 all_except_most_recent(deletion_candidates).into_items().all(|(path, lock)| {
822 debug!("garbage_collect_session_directories() - deleting `{}`", path.display());
823
824 if let Err(err) = safe_remove_dir_all(&path) {
825 sess.dcx().emit_warn(errors::FinalizedGcFailed { path: &path, err });
826 } else {
827 delete_session_dir_lock_file(sess, &lock_file_path(&path));
828 }
829
830 drop(lock);
833 true
834 });
835
836 Ok(())
837}
838
839fn delete_old(sess: &Session, path: &Path) {
840 debug!("garbage_collect_session_directories() - deleting `{}`", path.display());
841
842 if let Err(err) = safe_remove_dir_all(path) {
843 sess.dcx().emit_warn(errors::SessionGcFailed { path, err });
844 } else {
845 delete_session_dir_lock_file(sess, &lock_file_path(path));
846 }
847}
848
849fn all_except_most_recent(
850 deletion_candidates: UnordMap<(SystemTime, PathBuf), Option<flock::Lock>>,
851) -> UnordMap<PathBuf, Option<flock::Lock>> {
852 let most_recent = deletion_candidates.items().map(|(&(timestamp, _), _)| timestamp).max();
853
854 if let Some(most_recent) = most_recent {
855 deletion_candidates
856 .into_items()
857 .filter(|&((timestamp, _), _)| timestamp != most_recent)
858 .map(|((_, path), lock)| (path, lock))
859 .collect()
860 } else {
861 UnordMap::default()
862 }
863}
864
865fn safe_remove_dir_all(p: &Path) -> io::Result<()> {
872 let canonicalized = match try_canonicalize(p) {
873 Ok(canonicalized) => canonicalized,
874 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
875 Err(err) => return Err(err),
876 };
877
878 std_fs::remove_dir_all(canonicalized)
879}
880
881fn safe_remove_file(p: &Path) -> io::Result<()> {
882 let canonicalized = match try_canonicalize(p) {
883 Ok(canonicalized) => canonicalized,
884 Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(()),
885 Err(err) => return Err(err),
886 };
887
888 match std_fs::remove_file(canonicalized) {
889 Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(()),
890 result => result,
891 }
892}
893
894fn rename_path_with_retry(from: &Path, to: &Path, mut retries_left: usize) -> std::io::Result<()> {
899 loop {
900 match std_fs::rename(from, to) {
901 Ok(()) => return Ok(()),
902 Err(e) => {
903 if retries_left > 0 && e.kind() == ErrorKind::PermissionDenied {
904 std::thread::sleep(Duration::from_millis(50));
906 retries_left -= 1;
907 } else {
908 return Err(e);
909 }
910 }
911 }
912 }
913}