From 930c808ff2c159a9a6750e4d75d36ddbc10b2d43 Mon Sep 17 00:00:00 2001 From: Karthik Nadig Date: Thu, 15 Jan 2026 17:54:19 -0800 Subject: [PATCH] Fix Linux cache invalidation by making creation time optional in file path representation --- crates/pet-python-utils/src/cache.rs | 24 ++++++++++++++++-------- crates/pet-python-utils/src/fs_cache.rs | 19 +++++++++++++++---- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/crates/pet-python-utils/src/cache.rs b/crates/pet-python-utils/src/cache.rs index e0575a86..5729ec2e 100644 --- a/crates/pet-python-utils/src/cache.rs +++ b/crates/pet-python-utils/src/cache.rs @@ -98,7 +98,11 @@ impl CacheImpl { } } -type FilePathWithMTimeCTime = (PathBuf, SystemTime, SystemTime); +/// Represents a file path with its modification time and optional creation time. +/// Creation time (ctime) is optional because many Linux filesystems (ext4, etc.) +/// don't support file creation time, causing metadata.created() to return Err. +/// See: https://github.com/microsoft/python-environment-tools/issues/223 +type FilePathWithMTimeCTime = (PathBuf, SystemTime, Option); struct CacheEntryImpl { cache_directory: Option, @@ -120,9 +124,13 @@ impl CacheEntryImpl { // Check if any of the exes have changed since we last cached this. for symlink_info in self.symlinks.lock().unwrap().iter() { if let Ok(metadata) = symlink_info.0.metadata() { - if metadata.modified().ok() != Some(symlink_info.1) - || metadata.created().ok() != Some(symlink_info.2) - { + let mtime_changed = metadata.modified().ok() != Some(symlink_info.1); + // Only check ctime if we have it stored (may be None on Linux) + let ctime_changed = match symlink_info.2 { + Some(stored_ctime) => metadata.created().ok() != Some(stored_ctime), + None => false, // Can't check ctime if we don't have it + }; + if mtime_changed || ctime_changed { trace!( "Symlink {:?} has changed since we last cached it. original mtime & ctime {:?}, {:?}, current mtime & ctime {:?}, {:?}", symlink_info.0, @@ -168,10 +176,10 @@ impl CacheEntry for CacheEntryImpl { let mut symlinks = vec![]; for symlink in environment.symlinks.clone().unwrap_or_default().iter() { if let Ok(metadata) = symlink.metadata() { - // We only care if we have the information - if let (Some(modified), Some(created)) = - (metadata.modified().ok(), metadata.created().ok()) - { + // We require mtime, but ctime is optional (not available on all Linux filesystems) + // See: https://github.com/microsoft/python-environment-tools/issues/223 + if let Ok(modified) = metadata.modified() { + let created = metadata.created().ok(); // May be None on Linux symlinks.push((symlink.clone(), modified, created)); } } diff --git a/crates/pet-python-utils/src/fs_cache.rs b/crates/pet-python-utils/src/fs_cache.rs index ece5a3b5..cf93fee5 100644 --- a/crates/pet-python-utils/src/fs_cache.rs +++ b/crates/pet-python-utils/src/fs_cache.rs @@ -14,7 +14,11 @@ use std::{ use crate::env::ResolvedPythonEnv; -type FilePathWithMTimeCTime = (PathBuf, SystemTime, SystemTime); +/// Represents a file path with its modification time and optional creation time. +/// Creation time (ctime) is optional because many Linux filesystems (ext4, etc.) +/// don't support file creation time, causing metadata.created() to return Err. +/// See: https://github.com/microsoft/python-environment-tools/issues/223 +type FilePathWithMTimeCTime = (PathBuf, SystemTime, Option); #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -24,7 +28,9 @@ struct CacheEntry { } pub fn generate_cache_file(cache_directory: &Path, executable: &PathBuf) -> PathBuf { - cache_directory.join(format!("{}.3.json", generate_hash(executable))) + // Version 4: Changed ctime from required to optional for Linux compatibility + // See: https://github.com/microsoft/python-environment-tools/issues/223 + cache_directory.join(format!("{}.4.json", generate_hash(executable))) } pub fn delete_cache_file(cache_directory: &Path, executable: &PathBuf) { @@ -61,8 +67,13 @@ pub fn get_cache_from_file( // Check if any of the exes have changed since we last cached them. let cache_is_valid = cache.symlinks.iter().all(|symlink| { if let Ok(metadata) = symlink.0.metadata() { - metadata.modified().ok() == Some(symlink.1) - && metadata.created().ok() == Some(symlink.2) + let mtime_valid = metadata.modified().ok() == Some(symlink.1); + // Only check ctime if we have it stored (may be None on Linux) + let ctime_valid = match symlink.2 { + Some(stored_ctime) => metadata.created().ok() == Some(stored_ctime), + None => true, // Can't check ctime if we don't have it + }; + mtime_valid && ctime_valid } else { // File may have been deleted. false