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, 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;
121use rustc_session::{Session, StableCrateId};
122use rustc_span::Symbol;
123use tracing::debug;
124
125use crate::errors;
126
127#[cfg(test)]
128mod tests;
129
130const LOCK_FILE_EXT: &str = ".lock";
131const DEP_GRAPH_FILENAME: &str = "dep-graph.bin";
132const STAGING_DEP_GRAPH_FILENAME: &str = "dep-graph.part.bin";
133const WORK_PRODUCTS_FILENAME: &str = "work-products.bin";
134const QUERY_CACHE_FILENAME: &str = "query-cache.bin";
135
136const INT_ENCODE_BASE: usize = base_n::CASE_INSENSITIVE;
141
142pub(crate) fn dep_graph_path(sess: &Session) -> PathBuf {
144 in_incr_comp_dir_sess(sess, DEP_GRAPH_FILENAME)
145}
146
147pub(crate) fn staging_dep_graph_path(sess: &Session) -> PathBuf {
152 in_incr_comp_dir_sess(sess, STAGING_DEP_GRAPH_FILENAME)
153}
154
155pub(crate) fn work_products_path(sess: &Session) -> PathBuf {
156 in_incr_comp_dir_sess(sess, WORK_PRODUCTS_FILENAME)
157}
158
159pub(crate) fn query_cache_path(sess: &Session) -> PathBuf {
161 in_incr_comp_dir_sess(sess, QUERY_CACHE_FILENAME)
162}
163
164fn lock_file_path(session_dir: &Path) -> PathBuf {
166 let crate_dir = session_dir.parent().unwrap();
167
168 let directory_name = session_dir
169 .file_name()
170 .unwrap()
171 .to_str()
172 .expect("malformed session dir name: contains non-Unicode characters");
173
174 let dash_indices: Vec<_> = directory_name.match_indices('-').map(|(idx, _)| idx).collect();
175 if dash_indices.len() != 3 {
176 bug!(
177 "Encountered incremental compilation session directory with \
178 malformed name: {}",
179 session_dir.display()
180 )
181 }
182
183 crate_dir.join(&directory_name[0..dash_indices[2]]).with_extension(&LOCK_FILE_EXT[1..])
184}
185
186pub fn in_incr_comp_dir_sess(sess: &Session, file_name: &str) -> PathBuf {
189 in_incr_comp_dir(&sess.incr_comp_session_dir(), file_name)
190}
191
192pub fn in_incr_comp_dir(incr_comp_session_dir: &Path, file_name: &str) -> PathBuf {
197 incr_comp_session_dir.join(file_name)
198}
199
200pub(crate) fn prepare_session_directory(sess: &Session, crate_name: Symbol) {
216 if sess.opts.incremental.is_none() {
217 return;
218 }
219
220 let _timer = sess.timer("incr_comp_prepare_session_directory");
221
222 debug!("prepare_session_directory");
223
224 let crate_dir = crate_path(sess, crate_name);
226 debug!("crate-dir: {}", crate_dir.display());
227 create_dir(sess, &crate_dir, "crate");
228
229 let crate_dir = match try_canonicalize(&crate_dir) {
234 Ok(v) => v,
235 Err(err) => {
236 sess.dcx().emit_fatal(errors::CanonicalizePath { path: crate_dir, err });
237 }
238 };
239
240 let mut source_directories_already_tried = FxHashSet::default();
241
242 loop {
243 let session_dir = generate_session_dir_path(&crate_dir);
247 debug!("session-dir: {}", session_dir.display());
248
249 let (directory_lock, lock_file_path) = lock_directory(sess, &session_dir);
252
253 create_dir(sess, &session_dir, "session");
256
257 let source_directory = find_source_directory(&crate_dir, &source_directories_already_tried);
260
261 let Some(source_directory) = source_directory else {
262 debug!(
264 "no source directory found. Continuing with empty session \
265 directory."
266 );
267
268 sess.init_incr_comp_session(session_dir, directory_lock);
269 return;
270 };
271
272 debug!("attempting to copy data from source: {}", source_directory.display());
273
274 if let Ok(allows_links) = copy_files(sess, &session_dir, &source_directory) {
276 debug!("successfully copied data from: {}", source_directory.display());
277
278 if !allows_links {
279 sess.dcx().emit_warn(errors::HardLinkFailed { path: &session_dir });
280 }
281
282 sess.init_incr_comp_session(session_dir, directory_lock);
283 return;
284 } else {
285 debug!("copying failed - trying next directory");
286
287 source_directories_already_tried.insert(source_directory);
290
291 if let Err(err) = safe_remove_dir_all(&session_dir) {
294 sess.dcx().emit_warn(errors::DeletePartial { path: &session_dir, err });
295 }
296
297 delete_session_dir_lock_file(sess, &lock_file_path);
298 drop(directory_lock);
299 }
300 }
301}
302
303pub fn finalize_session_directory(sess: &Session, svh: Option<Svh>) {
308 if sess.opts.incremental.is_none() {
309 return;
310 }
311 let svh = svh.unwrap();
313
314 let _timer = sess.timer("incr_comp_finalize_session_directory");
315
316 let incr_comp_session_dir: PathBuf = sess.incr_comp_session_dir().clone();
317
318 if sess.dcx().has_errors_or_delayed_bugs().is_some() {
319 debug!(
323 "finalize_session_directory() - invalidating session directory: {}",
324 incr_comp_session_dir.display()
325 );
326
327 if let Err(err) = safe_remove_dir_all(&*incr_comp_session_dir) {
328 sess.dcx().emit_warn(errors::DeleteFull { path: &incr_comp_session_dir, err });
329 }
330
331 let lock_file_path = lock_file_path(&*incr_comp_session_dir);
332 delete_session_dir_lock_file(sess, &lock_file_path);
333 sess.mark_incr_comp_session_as_invalid();
334 }
335
336 debug!("finalize_session_directory() - session directory: {}", incr_comp_session_dir.display());
337
338 let mut sub_dir_name = incr_comp_session_dir
339 .file_name()
340 .unwrap()
341 .to_str()
342 .expect("malformed session dir name: contains non-Unicode characters")
343 .to_string();
344
345 sub_dir_name.truncate(sub_dir_name.len() - "working".len());
347 assert!(sub_dir_name.ends_with('-'), "{:?}", sub_dir_name);
349 assert!(sub_dir_name.as_bytes().iter().filter(|b| **b == b'-').count() == 3);
350
351 sub_dir_name.push_str(&svh.as_u128().to_base_fixed_len(CASE_INSENSITIVE));
353
354 let new_path = incr_comp_session_dir.parent().unwrap().join(&*sub_dir_name);
356 debug!("finalize_session_directory() - new path: {}", new_path.display());
357
358 match rename_path_with_retry(&*incr_comp_session_dir, &new_path, 3) {
359 Ok(_) => {
360 debug!("finalize_session_directory() - directory renamed successfully");
361
362 sess.finalize_incr_comp_session(new_path);
364 }
365 Err(e) => {
366 sess.dcx().emit_warn(errors::Finalize { path: &incr_comp_session_dir, err: e });
368
369 debug!("finalize_session_directory() - error, marking as invalid");
370 sess.mark_incr_comp_session_as_invalid();
372 }
373 }
374
375 let _ = garbage_collect_session_directories(sess);
376}
377
378pub(crate) fn delete_all_session_dir_contents(sess: &Session) -> io::Result<()> {
379 let sess_dir_iterator = sess.incr_comp_session_dir().read_dir()?;
380 for entry in sess_dir_iterator {
381 let entry = entry?;
382 safe_remove_file(&entry.path())?
383 }
384 Ok(())
385}
386
387fn copy_files(sess: &Session, target_dir: &Path, source_dir: &Path) -> Result<bool, ()> {
388 let lock_file_path = lock_file_path(source_dir);
391
392 let Ok(_lock) = flock::Lock::new(
394 &lock_file_path,
395 false, false, false,
398 ) else {
399 return Err(());
401 };
402
403 let Ok(source_dir_iterator) = source_dir.read_dir() else {
404 return Err(());
405 };
406
407 let mut files_linked = 0;
408 let mut files_copied = 0;
409
410 for entry in source_dir_iterator {
411 match entry {
412 Ok(entry) => {
413 let file_name = entry.file_name();
414
415 let target_file_path = target_dir.join(file_name);
416 let source_path = entry.path();
417
418 debug!("copying into session dir: {}", source_path.display());
419 match link_or_copy(source_path, target_file_path) {
420 Ok(LinkOrCopy::Link) => files_linked += 1,
421 Ok(LinkOrCopy::Copy) => files_copied += 1,
422 Err(_) => return Err(()),
423 }
424 }
425 Err(_) => return Err(()),
426 }
427 }
428
429 if sess.opts.unstable_opts.incremental_info {
430 eprintln!(
431 "[incremental] session directory: \
432 {files_linked} files hard-linked"
433 );
434 eprintln!(
435 "[incremental] session directory: \
436 {files_copied} files copied"
437 );
438 }
439
440 Ok(files_linked > 0 || files_copied == 0)
441}
442
443fn generate_session_dir_path(crate_dir: &Path) -> PathBuf {
446 let timestamp = timestamp_to_string(SystemTime::now());
447 debug!("generate_session_dir_path: timestamp = {}", timestamp);
448 let random_number = rng().next_u32();
449 debug!("generate_session_dir_path: random_number = {}", random_number);
450
451 let (zeroes, timestamp) = timestamp.split_at(3);
453 assert_eq!(zeroes, "000");
454 let directory_name =
455 format!("s-{}-{}-working", timestamp, random_number.to_base_fixed_len(CASE_INSENSITIVE));
456 debug!("generate_session_dir_path: directory_name = {}", directory_name);
457 let directory_path = crate_dir.join(directory_name);
458 debug!("generate_session_dir_path: directory_path = {}", directory_path.display());
459 directory_path
460}
461
462fn create_dir(sess: &Session, path: &Path, dir_tag: &str) {
463 match std_fs::create_dir_all(path) {
464 Ok(()) => {
465 debug!("{} directory created successfully", dir_tag);
466 }
467 Err(err) => sess.dcx().emit_fatal(errors::CreateIncrCompDir { tag: dir_tag, path, err }),
468 }
469}
470
471fn lock_directory(sess: &Session, session_dir: &Path) -> (flock::Lock, PathBuf) {
473 let lock_file_path = lock_file_path(session_dir);
474 debug!("lock_directory() - lock_file: {}", lock_file_path.display());
475
476 match flock::Lock::new(
477 &lock_file_path,
478 false, true, true,
481 ) {
482 Ok(lock) => (lock, lock_file_path),
484 Err(lock_err) => {
485 let is_unsupported_lock = flock::Lock::error_unsupported(&lock_err);
486 sess.dcx().emit_fatal(errors::CreateLock {
487 lock_err,
488 session_dir,
489 is_unsupported_lock,
490 is_cargo: rustc_session::utils::was_invoked_from_cargo(),
491 });
492 }
493 }
494}
495
496fn delete_session_dir_lock_file(sess: &Session, lock_file_path: &Path) {
497 if let Err(err) = safe_remove_file(lock_file_path) {
498 sess.dcx().emit_warn(errors::DeleteLock { path: lock_file_path, err });
499 }
500}
501
502fn find_source_directory(
505 crate_dir: &Path,
506 source_directories_already_tried: &FxHashSet<PathBuf>,
507) -> Option<PathBuf> {
508 let iter = crate_dir
509 .read_dir()
510 .unwrap() .filter_map(|e| e.ok().map(|e| e.path()));
512
513 find_source_directory_in_iter(iter, source_directories_already_tried)
514}
515
516fn find_source_directory_in_iter<I>(
517 iter: I,
518 source_directories_already_tried: &FxHashSet<PathBuf>,
519) -> Option<PathBuf>
520where
521 I: Iterator<Item = PathBuf>,
522{
523 let mut best_candidate = (UNIX_EPOCH, None);
524
525 for session_dir in iter {
526 debug!("find_source_directory_in_iter - inspecting `{}`", session_dir.display());
527
528 let Some(directory_name) = session_dir.file_name().unwrap().to_str() else {
529 debug!("find_source_directory_in_iter - ignoring");
530 continue;
531 };
532
533 if source_directories_already_tried.contains(&session_dir)
534 || !is_session_directory(&directory_name)
535 || !is_finalized(&directory_name)
536 {
537 debug!("find_source_directory_in_iter - ignoring");
538 continue;
539 }
540
541 let timestamp = match extract_timestamp_from_session_dir(&directory_name) {
542 Ok(timestamp) => timestamp,
543 Err(e) => {
544 debug!("unexpected incr-comp session dir: {}: {}", session_dir.display(), e);
545 continue;
546 }
547 };
548
549 if timestamp > best_candidate.0 {
550 best_candidate = (timestamp, Some(session_dir.clone()));
551 }
552 }
553
554 best_candidate.1
555}
556
557fn is_finalized(directory_name: &str) -> bool {
558 !directory_name.ends_with("-working")
559}
560
561fn is_session_directory(directory_name: &str) -> bool {
562 directory_name.starts_with("s-") && !directory_name.ends_with(LOCK_FILE_EXT)
563}
564
565fn is_session_directory_lock_file(file_name: &str) -> bool {
566 file_name.starts_with("s-") && file_name.ends_with(LOCK_FILE_EXT)
567}
568
569fn extract_timestamp_from_session_dir(directory_name: &str) -> Result<SystemTime, &'static str> {
570 if !is_session_directory(directory_name) {
571 return Err("not a directory");
572 }
573
574 let dash_indices: Vec<_> = directory_name.match_indices('-').map(|(idx, _)| idx).collect();
575 if dash_indices.len() != 3 {
576 return Err("not three dashes in name");
577 }
578
579 string_to_timestamp(&directory_name[dash_indices[0] + 1..dash_indices[1]])
580}
581
582fn timestamp_to_string(timestamp: SystemTime) -> BaseNString {
583 let duration = timestamp.duration_since(UNIX_EPOCH).unwrap();
584 let micros: u64 = duration.as_micros().try_into().unwrap();
585 micros.to_base_fixed_len(CASE_INSENSITIVE)
586}
587
588fn string_to_timestamp(s: &str) -> Result<SystemTime, &'static str> {
589 let micros_since_unix_epoch = match u64::from_str_radix(s, INT_ENCODE_BASE as u32) {
590 Ok(micros) => micros,
591 Err(_) => return Err("timestamp not an int"),
592 };
593
594 let duration = Duration::from_micros(micros_since_unix_epoch);
595 Ok(UNIX_EPOCH + duration)
596}
597
598fn crate_path(sess: &Session, crate_name: Symbol) -> PathBuf {
599 let incr_dir = sess.opts.incremental.as_ref().unwrap().clone();
600
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}