use rustc_data_structures::fx::{FxHashMap, FxHashSet};
use rustc_data_structures::svh::Svh;
use rustc_data_structures::{base_n, flock};
use rustc_fs_util::{link_or_copy, LinkOrCopy};
use rustc_session::{CrateDisambiguator, Session};
use std::fs as std_fs;
use std::io;
use std::mem;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use rand::{thread_rng, RngCore};
#[cfg(test)]
mod tests;
const LOCK_FILE_EXT: &str = ".lock";
const DEP_GRAPH_FILENAME: &str = "dep-graph.bin";
const WORK_PRODUCTS_FILENAME: &str = "work-products.bin";
const QUERY_CACHE_FILENAME: &str = "query-cache.bin";
const INT_ENCODE_BASE: usize = base_n::CASE_INSENSITIVE;
pub fn dep_graph_path(sess: &Session) -> PathBuf {
in_incr_comp_dir_sess(sess, DEP_GRAPH_FILENAME)
}
pub fn dep_graph_path_from(incr_comp_session_dir: &Path) -> PathBuf {
in_incr_comp_dir(incr_comp_session_dir, DEP_GRAPH_FILENAME)
}
pub fn work_products_path(sess: &Session) -> PathBuf {
in_incr_comp_dir_sess(sess, WORK_PRODUCTS_FILENAME)
}
pub fn query_cache_path(sess: &Session) -> PathBuf {
in_incr_comp_dir_sess(sess, QUERY_CACHE_FILENAME)
}
pub fn lock_file_path(session_dir: &Path) -> PathBuf {
let crate_dir = session_dir.parent().unwrap();
let directory_name = session_dir.file_name().unwrap().to_string_lossy();
assert_no_characters_lost(&directory_name);
let dash_indices: Vec<_> = directory_name.match_indices('-').map(|(idx, _)| idx).collect();
if dash_indices.len() != 3 {
bug!(
"Encountered incremental compilation session directory with \
malformed name: {}",
session_dir.display()
)
}
crate_dir.join(&directory_name[0..dash_indices[2]]).with_extension(&LOCK_FILE_EXT[1..])
}
pub fn in_incr_comp_dir_sess(sess: &Session, file_name: &str) -> PathBuf {
in_incr_comp_dir(&sess.incr_comp_session_dir(), file_name)
}
pub fn in_incr_comp_dir(incr_comp_session_dir: &Path, file_name: &str) -> PathBuf {
incr_comp_session_dir.join(file_name)
}
pub fn prepare_session_directory(
sess: &Session,
crate_name: &str,
crate_disambiguator: CrateDisambiguator,
) {
if sess.opts.incremental.is_none() {
return;
}
let _timer = sess.timer("incr_comp_prepare_session_directory");
debug!("prepare_session_directory");
let crate_dir = crate_path(sess, crate_name, crate_disambiguator);
debug!("crate-dir: {}", crate_dir.display());
if create_dir(sess, &crate_dir, "crate").is_err() {
return;
}
let crate_dir = match crate_dir.canonicalize() {
Ok(v) => v,
Err(err) => {
sess.err(&format!(
"incremental compilation: error canonicalizing path `{}`: {}",
crate_dir.display(),
err
));
return;
}
};
let mut source_directories_already_tried = FxHashSet::default();
loop {
let session_dir = generate_session_dir_path(&crate_dir);
debug!("session-dir: {}", session_dir.display());
let (directory_lock, lock_file_path) = match lock_directory(sess, &session_dir) {
Ok(e) => e,
Err(_) => return,
};
if create_dir(sess, &session_dir, "session").is_err() {
return;
}
let source_directory = find_source_directory(&crate_dir, &source_directories_already_tried);
let source_directory = if let Some(dir) = source_directory {
dir
} else {
debug!(
"no source directory found. Continuing with empty session \
directory."
);
sess.init_incr_comp_session(session_dir, directory_lock, false);
return;
};
debug!("attempting to copy data from source: {}", source_directory.display());
if let Ok(allows_links) = copy_files(sess, &session_dir, &source_directory) {
debug!("successfully copied data from: {}", source_directory.display());
if !allows_links {
sess.warn(&format!(
"Hard linking files in the incremental \
compilation cache failed. Copying files \
instead. Consider moving the cache \
directory to a file system which supports \
hard linking in session dir `{}`",
session_dir.display()
));
}
sess.init_incr_comp_session(session_dir, directory_lock, true);
return;
} else {
debug!("copying failed - trying next directory");
source_directories_already_tried.insert(source_directory);
if let Err(err) = safe_remove_dir_all(&session_dir) {
sess.warn(&format!(
"Failed to delete partly initialized \
session dir `{}`: {}",
session_dir.display(),
err
));
}
delete_session_dir_lock_file(sess, &lock_file_path);
mem::drop(directory_lock);
}
}
}
pub fn finalize_session_directory(sess: &Session, svh: Svh) {
if sess.opts.incremental.is_none() {
return;
}
let _timer = sess.timer("incr_comp_finalize_session_directory");
let incr_comp_session_dir: PathBuf = sess.incr_comp_session_dir().clone();
if sess.has_errors_or_delayed_span_bugs() {
debug!(
"finalize_session_directory() - invalidating session directory: {}",
incr_comp_session_dir.display()
);
if let Err(err) = safe_remove_dir_all(&*incr_comp_session_dir) {
sess.warn(&format!(
"Error deleting incremental compilation \
session directory `{}`: {}",
incr_comp_session_dir.display(),
err
));
}
let lock_file_path = lock_file_path(&*incr_comp_session_dir);
delete_session_dir_lock_file(sess, &lock_file_path);
sess.mark_incr_comp_session_as_invalid();
}
debug!("finalize_session_directory() - session directory: {}", incr_comp_session_dir.display());
let old_sub_dir_name = incr_comp_session_dir.file_name().unwrap().to_string_lossy();
assert_no_characters_lost(&old_sub_dir_name);
let dash_indices: Vec<_> = old_sub_dir_name.match_indices('-').map(|(idx, _)| idx).collect();
if dash_indices.len() != 3 {
bug!(
"Encountered incremental compilation session directory with \
malformed name: {}",
incr_comp_session_dir.display()
)
}
let mut new_sub_dir_name = String::from(&old_sub_dir_name[..=dash_indices[2]]);
base_n::push_str(svh.as_u64() as u128, INT_ENCODE_BASE, &mut new_sub_dir_name);
let new_path = incr_comp_session_dir.parent().unwrap().join(new_sub_dir_name);
debug!("finalize_session_directory() - new path: {}", new_path.display());
match std_fs::rename(&*incr_comp_session_dir, &new_path) {
Ok(_) => {
debug!("finalize_session_directory() - directory renamed successfully");
sess.finalize_incr_comp_session(new_path);
}
Err(e) => {
sess.warn(&format!(
"Error finalizing incremental compilation \
session directory `{}`: {}",
incr_comp_session_dir.display(),
e
));
debug!("finalize_session_directory() - error, marking as invalid");
sess.mark_incr_comp_session_as_invalid();
}
}
let _ = garbage_collect_session_directories(sess);
}
pub fn delete_all_session_dir_contents(sess: &Session) -> io::Result<()> {
let sess_dir_iterator = sess.incr_comp_session_dir().read_dir()?;
for entry in sess_dir_iterator {
let entry = entry?;
safe_remove_file(&entry.path())?
}
Ok(())
}
fn copy_files(sess: &Session, target_dir: &Path, source_dir: &Path) -> Result<bool, ()> {
let lock_file_path = lock_file_path(source_dir);
let _lock = if let Ok(lock) = flock::Lock::new(
&lock_file_path,
false,
false,
false,
) {
lock
} else {
return Err(());
};
let source_dir_iterator = match source_dir.read_dir() {
Ok(it) => it,
Err(_) => return Err(()),
};
let mut files_linked = 0;
let mut files_copied = 0;
for entry in source_dir_iterator {
match entry {
Ok(entry) => {
let file_name = entry.file_name();
let target_file_path = target_dir.join(file_name);
let source_path = entry.path();
debug!("copying into session dir: {}", source_path.display());
match link_or_copy(source_path, target_file_path) {
Ok(LinkOrCopy::Link) => files_linked += 1,
Ok(LinkOrCopy::Copy) => files_copied += 1,
Err(_) => return Err(()),
}
}
Err(_) => return Err(()),
}
}
if sess.opts.debugging_opts.incremental_info {
println!(
"[incremental] session directory: \
{} files hard-linked",
files_linked
);
println!(
"[incremental] session directory: \
{} files copied",
files_copied
);
}
Ok(files_linked > 0 || files_copied == 0)
}
fn generate_session_dir_path(crate_dir: &Path) -> PathBuf {
let timestamp = timestamp_to_string(SystemTime::now());
debug!("generate_session_dir_path: timestamp = {}", timestamp);
let random_number = thread_rng().next_u32();
debug!("generate_session_dir_path: random_number = {}", random_number);
let directory_name = format!(
"s-{}-{}-working",
timestamp,
base_n::encode(random_number as u128, INT_ENCODE_BASE)
);
debug!("generate_session_dir_path: directory_name = {}", directory_name);
let directory_path = crate_dir.join(directory_name);
debug!("generate_session_dir_path: directory_path = {}", directory_path.display());
directory_path
}
fn create_dir(sess: &Session, path: &Path, dir_tag: &str) -> Result<(), ()> {
match std_fs::create_dir_all(path) {
Ok(()) => {
debug!("{} directory created successfully", dir_tag);
Ok(())
}
Err(err) => {
sess.err(&format!(
"Could not create incremental compilation {} \
directory `{}`: {}",
dir_tag,
path.display(),
err
));
Err(())
}
}
}
fn lock_directory(sess: &Session, session_dir: &Path) -> Result<(flock::Lock, PathBuf), ()> {
let lock_file_path = lock_file_path(session_dir);
debug!("lock_directory() - lock_file: {}", lock_file_path.display());
match flock::Lock::new(
&lock_file_path,
false,
true,
true,
) {
Ok(lock) => Ok((lock, lock_file_path)),
Err(err) => {
sess.err(&format!(
"incremental compilation: could not create \
session directory lock file: {}",
err
));
Err(())
}
}
}
fn delete_session_dir_lock_file(sess: &Session, lock_file_path: &Path) {
if let Err(err) = safe_remove_file(&lock_file_path) {
sess.warn(&format!(
"Error deleting lock file for incremental \
compilation session directory `{}`: {}",
lock_file_path.display(),
err
));
}
}
fn find_source_directory(
crate_dir: &Path,
source_directories_already_tried: &FxHashSet<PathBuf>,
) -> Option<PathBuf> {
let iter = crate_dir
.read_dir()
.unwrap()
.filter_map(|e| e.ok().map(|e| e.path()));
find_source_directory_in_iter(iter, source_directories_already_tried)
}
fn find_source_directory_in_iter<I>(
iter: I,
source_directories_already_tried: &FxHashSet<PathBuf>,
) -> Option<PathBuf>
where
I: Iterator<Item = PathBuf>,
{
let mut best_candidate = (UNIX_EPOCH, None);
for session_dir in iter {
debug!("find_source_directory_in_iter - inspecting `{}`", session_dir.display());
let directory_name = session_dir.file_name().unwrap().to_string_lossy();
assert_no_characters_lost(&directory_name);
if source_directories_already_tried.contains(&session_dir)
|| !is_session_directory(&directory_name)
|| !is_finalized(&directory_name)
{
debug!("find_source_directory_in_iter - ignoring");
continue;
}
let timestamp = extract_timestamp_from_session_dir(&directory_name).unwrap_or_else(|_| {
bug!("unexpected incr-comp session dir: {}", session_dir.display())
});
if timestamp > best_candidate.0 {
best_candidate = (timestamp, Some(session_dir.clone()));
}
}
best_candidate.1
}
fn is_finalized(directory_name: &str) -> bool {
!directory_name.ends_with("-working")
}
fn is_session_directory(directory_name: &str) -> bool {
directory_name.starts_with("s-") && !directory_name.ends_with(LOCK_FILE_EXT)
}
fn is_session_directory_lock_file(file_name: &str) -> bool {
file_name.starts_with("s-") && file_name.ends_with(LOCK_FILE_EXT)
}
fn extract_timestamp_from_session_dir(directory_name: &str) -> Result<SystemTime, ()> {
if !is_session_directory(directory_name) {
return Err(());
}
let dash_indices: Vec<_> = directory_name.match_indices('-').map(|(idx, _)| idx).collect();
if dash_indices.len() != 3 {
return Err(());
}
string_to_timestamp(&directory_name[dash_indices[0] + 1..dash_indices[1]])
}
fn timestamp_to_string(timestamp: SystemTime) -> String {
let duration = timestamp.duration_since(UNIX_EPOCH).unwrap();
let micros = duration.as_secs() * 1_000_000 + (duration.subsec_nanos() as u64) / 1000;
base_n::encode(micros as u128, INT_ENCODE_BASE)
}
fn string_to_timestamp(s: &str) -> Result<SystemTime, ()> {
let micros_since_unix_epoch = u64::from_str_radix(s, INT_ENCODE_BASE as u32);
if micros_since_unix_epoch.is_err() {
return Err(());
}
let micros_since_unix_epoch = micros_since_unix_epoch.unwrap();
let duration = Duration::new(
micros_since_unix_epoch / 1_000_000,
1000 * (micros_since_unix_epoch % 1_000_000) as u32,
);
Ok(UNIX_EPOCH + duration)
}
fn crate_path(
sess: &Session,
crate_name: &str,
crate_disambiguator: CrateDisambiguator,
) -> PathBuf {
let incr_dir = sess.opts.incremental.as_ref().unwrap().clone();
let crate_disambiguator = crate_disambiguator.to_fingerprint().to_smaller_hash();
let crate_disambiguator = base_n::encode(crate_disambiguator as u128, INT_ENCODE_BASE);
let crate_name = format!("{}-{}", crate_name, crate_disambiguator);
incr_dir.join(crate_name)
}
fn assert_no_characters_lost(s: &str) {
if s.contains('\u{FFFD}') {
bug!("Could not losslessly convert '{}'.", s)
}
}
fn is_old_enough_to_be_collected(timestamp: SystemTime) -> bool {
timestamp < SystemTime::now() - Duration::from_secs(10)
}
pub fn garbage_collect_session_directories(sess: &Session) -> io::Result<()> {
debug!("garbage_collect_session_directories() - begin");
let session_directory = sess.incr_comp_session_dir();
debug!(
"garbage_collect_session_directories() - session directory: {}",
session_directory.display()
);
let crate_directory = session_directory.parent().unwrap();
debug!(
"garbage_collect_session_directories() - crate directory: {}",
crate_directory.display()
);
let mut session_directories = FxHashSet::default();
let mut lock_files = FxHashSet::default();
for dir_entry in crate_directory.read_dir()? {
let dir_entry = match dir_entry {
Ok(dir_entry) => dir_entry,
_ => {
continue;
}
};
let entry_name = dir_entry.file_name();
let entry_name = entry_name.to_string_lossy();
if is_session_directory_lock_file(&entry_name) {
assert_no_characters_lost(&entry_name);
lock_files.insert(entry_name.into_owned());
} else if is_session_directory(&entry_name) {
assert_no_characters_lost(&entry_name);
session_directories.insert(entry_name.into_owned());
} else {
}
}
let lock_file_to_session_dir: FxHashMap<String, Option<String>> = lock_files
.into_iter()
.map(|lock_file_name| {
assert!(lock_file_name.ends_with(LOCK_FILE_EXT));
let dir_prefix_end = lock_file_name.len() - LOCK_FILE_EXT.len();
let session_dir = {
let dir_prefix = &lock_file_name[0..dir_prefix_end];
session_directories.iter().find(|dir_name| dir_name.starts_with(dir_prefix))
};
(lock_file_name, session_dir.map(String::clone))
})
.collect();
for (lock_file_name, directory_name) in &lock_file_to_session_dir {
if directory_name.is_none() {
let timestamp = match extract_timestamp_from_session_dir(lock_file_name) {
Ok(timestamp) => timestamp,
Err(()) => {
debug!(
"found lock-file with malformed timestamp: {}",
crate_directory.join(&lock_file_name).display()
);
continue;
}
};
let lock_file_path = crate_directory.join(&**lock_file_name);
if is_old_enough_to_be_collected(timestamp) {
debug!(
"garbage_collect_session_directories() - deleting \
garbage lock file: {}",
lock_file_path.display()
);
delete_session_dir_lock_file(sess, &lock_file_path);
} else {
debug!(
"garbage_collect_session_directories() - lock file with \
no session dir not old enough to be collected: {}",
lock_file_path.display()
);
}
}
}
let lock_file_to_session_dir: FxHashMap<String, String> = lock_file_to_session_dir
.into_iter()
.filter_map(|(lock_file_name, directory_name)| directory_name.map(|n| (lock_file_name, n)))
.collect();
for directory_name in session_directories {
if !lock_file_to_session_dir.values().any(|dir| *dir == directory_name) {
let path = crate_directory.join(directory_name);
if let Err(err) = safe_remove_dir_all(&path) {
sess.warn(&format!(
"Failed to garbage collect invalid incremental \
compilation session directory `{}`: {}",
path.display(),
err
));
}
}
}
let mut deletion_candidates = vec![];
for (lock_file_name, directory_name) in &lock_file_to_session_dir {
debug!("garbage_collect_session_directories() - inspecting: {}", directory_name);
let timestamp = match extract_timestamp_from_session_dir(directory_name) {
Ok(timestamp) => timestamp,
Err(()) => {
debug!(
"found session-dir with malformed timestamp: {}",
crate_directory.join(directory_name).display()
);
continue;
}
};
if is_finalized(directory_name) {
let lock_file_path = crate_directory.join(lock_file_name);
match flock::Lock::new(
&lock_file_path,
false,
false,
true,
) {
Ok(lock) => {
debug!(
"garbage_collect_session_directories() - \
successfully acquired lock"
);
debug!(
"garbage_collect_session_directories() - adding \
deletion candidate: {}",
directory_name
);
deletion_candidates.push((
timestamp,
crate_directory.join(directory_name),
Some(lock),
));
}
Err(_) => {
debug!(
"garbage_collect_session_directories() - \
not collecting, still in use"
);
}
}
} else if is_old_enough_to_be_collected(timestamp) {
let lock_file_path = crate_directory.join(lock_file_name);
match flock::Lock::new(
&lock_file_path,
false,
false,
true,
) {
Ok(lock) => {
debug!(
"garbage_collect_session_directories() - \
successfully acquired lock"
);
delete_old(sess, &crate_directory.join(directory_name));
mem::drop(lock);
}
Err(_) => {
debug!(
"garbage_collect_session_directories() - \
not collecting, still in use"
);
}
}
} else {
debug!(
"garbage_collect_session_directories() - not finalized, not \
old enough"
);
}
}
for (path, lock) in all_except_most_recent(deletion_candidates) {
debug!("garbage_collect_session_directories() - deleting `{}`", path.display());
if let Err(err) = safe_remove_dir_all(&path) {
sess.warn(&format!(
"Failed to garbage collect finalized incremental \
compilation session directory `{}`: {}",
path.display(),
err
));
} else {
delete_session_dir_lock_file(sess, &lock_file_path(&path));
}
mem::drop(lock);
}
Ok(())
}
fn delete_old(sess: &Session, path: &Path) {
debug!("garbage_collect_session_directories() - deleting `{}`", path.display());
if let Err(err) = safe_remove_dir_all(&path) {
sess.warn(&format!(
"Failed to garbage collect incremental compilation session directory `{}`: {}",
path.display(),
err
));
} else {
delete_session_dir_lock_file(sess, &lock_file_path(&path));
}
}
fn all_except_most_recent(
deletion_candidates: Vec<(SystemTime, PathBuf, Option<flock::Lock>)>,
) -> FxHashMap<PathBuf, Option<flock::Lock>> {
let most_recent = deletion_candidates.iter().map(|&(timestamp, ..)| timestamp).max();
if let Some(most_recent) = most_recent {
deletion_candidates
.into_iter()
.filter(|&(timestamp, ..)| timestamp != most_recent)
.map(|(_, path, lock)| (path, lock))
.collect()
} else {
FxHashMap::default()
}
}
fn safe_remove_dir_all(p: &Path) -> io::Result<()> {
if p.exists() {
let canonicalized = p.canonicalize()?;
std_fs::remove_dir_all(canonicalized)
} else {
Ok(())
}
}
fn safe_remove_file(p: &Path) -> io::Result<()> {
if p.exists() {
let canonicalized = p.canonicalize()?;
match std_fs::remove_file(canonicalized) {
Err(ref err) if err.kind() == io::ErrorKind::NotFound => Ok(()),
result => result,
}
} else {
Ok(())
}
}