@@ -6,14 +6,17 @@ use log::{trace, warn};
66use std:: {
77 collections:: { hash_map:: Entry , HashMap , HashSet } ,
88 io,
9- path:: PathBuf ,
9+ path:: { Path , PathBuf } ,
1010 sync:: { Arc , Mutex } ,
1111 time:: SystemTime ,
1212} ;
1313
1414use crate :: {
1515 env:: ResolvedPythonEnv ,
16- fs_cache:: { delete_cache_file, get_cache_from_file, store_cache_in_file} ,
16+ fs_cache:: {
17+ delete_cache_file, executable_cache_key, executable_cache_key_from, get_cache_from_file,
18+ store_cache_in_file,
19+ } ,
1720} ;
1821
1922lazy_static ! {
@@ -22,6 +25,10 @@ lazy_static! {
2225
2326pub trait CacheEntry : Send + Sync {
2427 fn get ( & self ) -> Option < ResolvedPythonEnv > ;
28+ fn get_for_executable ( & self , executable : & Path ) -> Option < ResolvedPythonEnv > {
29+ self . get ( )
30+ . map ( |environment| add_executable_alias ( environment, executable) )
31+ }
2532 fn store ( & self , environment : ResolvedPythonEnv ) ;
2633 fn track_symlinks ( & self , symlinks : Vec < PathBuf > ) ;
2734}
@@ -102,6 +109,7 @@ impl CacheImpl {
102109 }
103110 }
104111 fn create_cache ( & self , executable : PathBuf ) -> LockableCacheEntry {
112+ let cache_key = executable_cache_key ( & executable) ;
105113 let cache_directory = self
106114 . cache_dir
107115 . lock ( )
@@ -111,11 +119,11 @@ impl CacheImpl {
111119 . locks
112120 . lock ( )
113121 . expect ( "locks mutex poisoned" )
114- . entry ( executable . clone ( ) )
122+ . entry ( cache_key . clone ( ) )
115123 {
116124 Entry :: Occupied ( lock) => lock. get ( ) . clone ( ) ,
117125 Entry :: Vacant ( lock) => {
118- let cache = Box :: new ( CacheEntryImpl :: create ( cache_directory. clone ( ) , executable ) )
126+ let cache = Box :: new ( CacheEntryImpl :: create ( cache_directory. clone ( ) , cache_key ) )
119127 as Box < dyn CacheEntry + ' static > ;
120128 lock. insert ( Arc :: new ( Mutex :: new ( cache) ) ) . clone ( )
121129 }
@@ -129,6 +137,83 @@ impl CacheImpl {
129137/// See: https://github.com/microsoft/python-environment-tools/issues/223
130138type FilePathWithMTimeCTime = ( PathBuf , SystemTime , Option < SystemTime > ) ;
131139
140+ fn current_dir_for_aliases ( aliases : & [ PathBuf ] ) -> Option < PathBuf > {
141+ aliases
142+ . iter ( )
143+ . any ( |alias| alias. is_relative ( ) )
144+ . then ( std:: env:: current_dir)
145+ . transpose ( )
146+ . ok ( )
147+ . flatten ( )
148+ }
149+
150+ fn add_executable_alias (
151+ mut environment : ResolvedPythonEnv ,
152+ executable : & Path ,
153+ ) -> ResolvedPythonEnv {
154+ let aliases = environment. symlinks . get_or_insert_with ( Vec :: new) ;
155+ if !aliases. iter ( ) . any ( |alias| alias == executable) {
156+ aliases. push ( executable. to_path_buf ( ) ) ;
157+ aliases. sort ( ) ;
158+ aliases. dedup ( ) ;
159+ }
160+ environment
161+ }
162+
163+ fn current_dir_for_cached_aliases (
164+ environment : & ResolvedPythonEnv ,
165+ executable : & Path ,
166+ ) -> Option < PathBuf > {
167+ current_dir_for_cached_aliases_with ( environment, executable, std:: env:: current_dir)
168+ }
169+
170+ fn current_dir_for_cached_aliases_with (
171+ environment : & ResolvedPythonEnv ,
172+ executable : & Path ,
173+ current_dir : impl FnOnce ( ) -> io:: Result < PathBuf > ,
174+ ) -> Option < PathBuf > {
175+ environment
176+ . symlinks
177+ . as_ref ( )
178+ . is_some_and ( |aliases| {
179+ aliases
180+ . iter ( )
181+ . any ( |alias| alias. is_relative ( ) && alias != executable)
182+ } )
183+ . then ( current_dir)
184+ . transpose ( )
185+ . ok ( )
186+ . flatten ( )
187+ }
188+
189+ fn bind_validated_executable_alias (
190+ mut environment : ResolvedPythonEnv ,
191+ executable : & Path ,
192+ tracked_aliases : & [ FilePathWithMTimeCTime ] ,
193+ current_dir : Option < & Path > ,
194+ ) -> ResolvedPythonEnv {
195+ let aliases = environment. symlinks . get_or_insert_with ( Vec :: new) ;
196+ aliases. retain ( |alias| {
197+ if alias == executable {
198+ return true ;
199+ }
200+ if tracked_aliases. iter ( ) . any ( |tracked| tracked. 0 == * alias) {
201+ return true ;
202+ }
203+ if alias. is_relative ( ) && current_dir. is_none ( ) {
204+ return false ;
205+ }
206+ let key = executable_cache_key_from ( alias, current_dir) ;
207+ tracked_aliases. iter ( ) . any ( |tracked| tracked. 0 == key)
208+ } ) ;
209+ if !aliases. iter ( ) . any ( |alias| alias == executable) {
210+ aliases. push ( executable. to_path_buf ( ) ) ;
211+ }
212+ aliases. sort ( ) ;
213+ aliases. dedup ( ) ;
214+ environment
215+ }
216+
132217struct CacheEntryImpl {
133218 cache_directory : Option < PathBuf > ,
134219 executable : PathBuf ,
@@ -146,37 +231,35 @@ impl CacheEntryImpl {
146231 }
147232 }
148233 pub fn verify_in_memory_cache ( & self ) {
149- // Check if any of the exes have changed since we last cached this.
150- for symlink_info in self
234+ let cache_is_valid = self
151235 . symlinks
152236 . lock ( )
153237 . expect ( "symlinks mutex poisoned" )
154238 . iter ( )
155- {
156- if let Ok ( metadata) = symlink_info. 0 . metadata ( ) {
157- let mtime_changed = metadata. modified ( ) . ok ( ) != Some ( symlink_info. 1 ) ;
158- // Only check ctime if we have it stored (may be None on Linux)
159- let ctime_changed = match symlink_info. 2 {
160- Some ( stored_ctime) => metadata. created ( ) . ok ( ) != Some ( stored_ctime) ,
161- None => false , // Can't check ctime if we don't have it
162- } ;
163- if mtime_changed || ctime_changed {
164- trace ! (
165- "Symlink {:?} has changed since we last cached it. original mtime & ctime {:?}, {:?}, current mtime & ctime {:?}, {:?}" ,
166- symlink_info. 0 ,
167- symlink_info. 1 ,
168- symlink_info. 2 ,
169- metadata. modified( ) . ok( ) ,
170- metadata. created( ) . ok( )
171- ) ;
172- self . envoronment
173- . lock ( )
174- . expect ( "envoronment mutex poisoned" )
175- . take ( ) ;
176- if let Some ( cache_directory) = & self . cache_directory {
177- delete_cache_file ( cache_directory, & self . executable ) ;
178- }
239+ . all ( |symlink_info| {
240+ if let Ok ( metadata) = symlink_info. 0 . metadata ( ) {
241+ let mtime_changed = metadata. modified ( ) . ok ( ) != Some ( symlink_info. 1 ) ;
242+ let ctime_changed = match symlink_info. 2 {
243+ Some ( stored_ctime) => metadata. created ( ) . ok ( ) != Some ( stored_ctime) ,
244+ None => false ,
245+ } ;
246+ !mtime_changed && !ctime_changed
247+ } else {
248+ false
179249 }
250+ } ) ;
251+
252+ if !cache_is_valid {
253+ trace ! (
254+ "Tracked executable changed or disappeared for {:?}" ,
255+ self . executable
256+ ) ;
257+ self . envoronment
258+ . lock ( )
259+ . expect ( "envoronment mutex poisoned" )
260+ . take ( ) ;
261+ if let Some ( cache_directory) = & self . cache_directory {
262+ delete_cache_file ( cache_directory, & self . executable ) ;
180263 }
181264 }
182265 }
@@ -213,16 +296,31 @@ impl CacheEntry for CacheEntryImpl {
213296 }
214297 }
215298
299+ fn get_for_executable ( & self , executable : & Path ) -> Option < ResolvedPythonEnv > {
300+ let environment = self . get ( ) ?;
301+ let current_dir = current_dir_for_cached_aliases ( & environment, executable) ;
302+ let tracked_aliases = self . symlinks . lock ( ) . expect ( "symlinks mutex poisoned" ) ;
303+ Some ( bind_validated_executable_alias (
304+ environment,
305+ executable,
306+ & tracked_aliases,
307+ current_dir. as_deref ( ) ,
308+ ) )
309+ }
310+
216311 fn store ( & self , environment : ResolvedPythonEnv ) {
217312 // Get hold of the mtimes and ctimes of the symlinks.
313+ let aliases = environment. symlinks . clone ( ) . unwrap_or_default ( ) ;
314+ let current_dir = current_dir_for_aliases ( & aliases) ;
218315 let mut symlinks = vec ! [ ] ;
219- for symlink in environment. symlinks . clone ( ) . unwrap_or_default ( ) . iter ( ) {
316+ for alias in & aliases {
317+ let symlink = executable_cache_key_from ( alias, current_dir. as_deref ( ) ) ;
220318 if let Ok ( metadata) = symlink. metadata ( ) {
221319 // We require mtime, but ctime is optional (not available on all Linux filesystems)
222320 // See: https://github.com/microsoft/python-environment-tools/issues/223
223321 if let Ok ( modified) = metadata. modified ( ) {
224322 let created = metadata. created ( ) . ok ( ) ; // May be None on Linux
225- symlinks. push ( ( symlink. clone ( ) , modified, created) ) ;
323+ symlinks. push ( ( symlink, modified, created) ) ;
226324 }
227325 }
228326 }
@@ -259,8 +357,12 @@ impl CacheEntry for CacheEntryImpl {
259357 . iter ( )
260358 . map ( |x| x. 0 . clone ( ) )
261359 . collect ( ) ;
262-
263- if symlinks. iter ( ) . all ( |x| known_symlinks. contains ( x) ) {
360+ let current_dir = current_dir_for_aliases ( & symlinks) ;
361+ if symlinks
362+ . iter ( )
363+ . map ( |alias| executable_cache_key_from ( alias, current_dir. as_deref ( ) ) )
364+ . all ( |key| known_symlinks. contains ( & key) )
365+ {
264366 return ;
265367 }
266368
@@ -283,3 +385,142 @@ impl CacheEntry for CacheEntryImpl {
283385 }
284386 }
285387}
388+
389+ #[ cfg( test) ]
390+ mod tests {
391+ use super :: * ;
392+ use std:: sync:: atomic:: { AtomicUsize , Ordering } ;
393+ use tempfile:: tempdir_in;
394+
395+ fn environment ( executable : PathBuf , aliases : Vec < PathBuf > ) -> ResolvedPythonEnv {
396+ ResolvedPythonEnv {
397+ executable,
398+ prefix : PathBuf :: from ( "prefix" ) ,
399+ version : "3.12.0" . to_string ( ) ,
400+ is64_bit : true ,
401+ symlinks : Some ( aliases) ,
402+ }
403+ }
404+
405+ fn aliases ( ) -> ( tempfile:: TempDir , PathBuf , PathBuf ) {
406+ let current_dir = std:: env:: current_dir ( ) . unwrap ( ) ;
407+ let temp_dir = tempdir_in ( & current_dir) . unwrap ( ) ;
408+ let absolute = temp_dir. path ( ) . join ( "python" ) ;
409+ std:: fs:: write ( & absolute, "python" ) . unwrap ( ) ;
410+ let relative = absolute. strip_prefix ( & current_dir) . unwrap ( ) . to_path_buf ( ) ;
411+ ( temp_dir, relative, absolute)
412+ }
413+
414+ #[ test]
415+ fn relative_and_absolute_aliases_share_in_memory_entry ( ) {
416+ let ( _temp_dir, relative, absolute) = aliases ( ) ;
417+ let cache = CacheImpl :: new ( None ) ;
418+
419+ let relative_entry = cache. create_cache ( relative) ;
420+ let absolute_entry = cache. create_cache ( absolute) ;
421+
422+ assert ! ( Arc :: ptr_eq( & relative_entry, & absolute_entry) ) ;
423+ }
424+
425+ #[ test]
426+ fn cache_hit_preserves_canonical_executable_and_current_aliases ( ) {
427+ let ( temp_dir, relative, absolute) = aliases ( ) ;
428+ let canonical = temp_dir. path ( ) . join ( "canonical-python" ) ;
429+ std:: fs:: write ( & canonical, "python" ) . unwrap ( ) ;
430+ let cache = CacheImpl :: new ( None ) ;
431+ let entry = cache. create_cache ( relative. clone ( ) ) ;
432+ let entry = entry. lock ( ) . unwrap ( ) ;
433+ entry. store ( environment (
434+ canonical. clone ( ) ,
435+ vec ! [ relative. clone( ) , absolute. clone( ) , canonical. clone( ) ] ,
436+ ) ) ;
437+
438+ let relative_hit = entry. get_for_executable ( & relative) . unwrap ( ) ;
439+ assert_eq ! ( relative_hit. executable, canonical) ;
440+
441+ let absolute_hit = entry. get_for_executable ( & absolute) . unwrap ( ) ;
442+ assert_eq ! ( absolute_hit. executable, canonical) ;
443+ let hit_aliases = absolute_hit. symlinks . unwrap ( ) ;
444+ assert ! ( hit_aliases. contains( & relative) ) ;
445+ assert ! ( hit_aliases. contains( & absolute) ) ;
446+ assert ! ( hit_aliases. contains( & canonical) ) ;
447+ }
448+
449+ #[ test]
450+ fn disk_cache_reuses_relative_entry_for_absolute_alias ( ) {
451+ let ( temp_dir, relative, absolute) = aliases ( ) ;
452+ let canonical = temp_dir. path ( ) . join ( "canonical-python" ) ;
453+ std:: fs:: write ( & canonical, "python" ) . unwrap ( ) ;
454+ let cache_directory = temp_dir. path ( ) . join ( "cache" ) ;
455+ {
456+ let cache = CacheImpl :: new ( Some ( cache_directory. clone ( ) ) ) ;
457+ let entry = cache. create_cache ( relative. clone ( ) ) ;
458+ entry. lock ( ) . unwrap ( ) . store ( environment (
459+ canonical. clone ( ) ,
460+ vec ! [ relative. clone( ) , absolute. clone( ) , canonical. clone( ) ] ,
461+ ) ) ;
462+ }
463+
464+ let cache = CacheImpl :: new ( Some ( cache_directory) ) ;
465+ let entry = cache. create_cache ( absolute. clone ( ) ) ;
466+ let hit = entry. lock ( ) . unwrap ( ) . get_for_executable ( & absolute) . unwrap ( ) ;
467+
468+ assert_eq ! ( hit. executable, canonical) ;
469+ let hit_aliases = hit. symlinks . unwrap ( ) ;
470+ assert ! ( hit_aliases. contains( & relative) ) ;
471+ assert ! ( hit_aliases. contains( & absolute) ) ;
472+ assert ! ( hit_aliases. contains( & canonical) ) ;
473+ }
474+
475+ #[ test]
476+ fn stale_relative_alias_from_another_working_directory_is_dropped ( ) {
477+ let ( temp_dir, relative, absolute) = aliases ( ) ;
478+ let metadata = absolute. metadata ( ) . unwrap ( ) ;
479+ let tracked_aliases = vec ! [ (
480+ absolute. clone( ) ,
481+ metadata. modified( ) . unwrap( ) ,
482+ metadata. created( ) . ok( ) ,
483+ ) ] ;
484+ let stale_working_directory = temp_dir. path ( ) . join ( "another-workspace" ) ;
485+
486+ let hit = bind_validated_executable_alias (
487+ environment ( absolute. clone ( ) , vec ! [ relative. clone( ) , absolute. clone( ) ] ) ,
488+ & absolute,
489+ & tracked_aliases,
490+ Some ( & stale_working_directory) ,
491+ ) ;
492+
493+ let hit_aliases = hit. symlinks . unwrap ( ) ;
494+ assert ! ( !hit_aliases. contains( & relative) ) ;
495+ assert ! ( hit_aliases. contains( & absolute) ) ;
496+ }
497+
498+ #[ test]
499+ fn absolute_cache_hit_does_not_query_current_directory ( ) {
500+ let ( _temp_dir, _relative, absolute) = aliases ( ) ;
501+ let current_dir_calls = AtomicUsize :: new ( 0 ) ;
502+ let environment = environment ( absolute. clone ( ) , vec ! [ absolute. clone( ) ] ) ;
503+
504+ let current_dir = current_dir_for_cached_aliases_with ( & environment, & absolute, || {
505+ current_dir_calls. fetch_add ( 1 , Ordering :: Relaxed ) ;
506+ std:: env:: current_dir ( )
507+ } ) ;
508+
509+ assert ! ( current_dir. is_none( ) ) ;
510+ assert_eq ! ( current_dir_calls. load( Ordering :: Relaxed ) , 0 ) ;
511+ }
512+
513+ #[ test]
514+ fn missing_tracked_executable_invalidates_in_memory_entry ( ) {
515+ let ( temp_dir, _relative, absolute) = aliases ( ) ;
516+ let cache = CacheImpl :: new ( Some ( temp_dir. path ( ) . join ( "cache" ) ) ) ;
517+ let entry = cache. create_cache ( absolute. clone ( ) ) ;
518+ let entry = entry. lock ( ) . unwrap ( ) ;
519+ entry. store ( environment ( absolute. clone ( ) , vec ! [ absolute. clone( ) ] ) ) ;
520+ assert ! ( entry. get( ) . is_some( ) ) ;
521+
522+ std:: fs:: remove_file ( & absolute) . unwrap ( ) ;
523+
524+ assert ! ( entry. get( ) . is_none( ) ) ;
525+ }
526+ }
0 commit comments