@@ -13,7 +13,10 @@ use std::{
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 : & std:: path:: Path ) -> Option < ResolvedPythonEnv > {
29+ self . get ( )
30+ . map ( |environment| environment. for_executable_alias ( 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,16 @@ 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+
132150struct CacheEntryImpl {
133151 cache_directory : Option < PathBuf > ,
134152 executable : PathBuf ,
@@ -146,37 +164,35 @@ impl CacheEntryImpl {
146164 }
147165 }
148166 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
167+ let cache_is_valid = self
151168 . symlinks
152169 . lock ( )
153170 . expect ( "symlinks mutex poisoned" )
154171 . 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- }
172+ . all ( |symlink_info| {
173+ if let Ok ( metadata) = symlink_info. 0 . metadata ( ) {
174+ let mtime_changed = metadata. modified ( ) . ok ( ) != Some ( symlink_info. 1 ) ;
175+ let ctime_changed = match symlink_info. 2 {
176+ Some ( stored_ctime) => metadata. created ( ) . ok ( ) != Some ( stored_ctime) ,
177+ None => false ,
178+ } ;
179+ !mtime_changed && !ctime_changed
180+ } else {
181+ false
179182 }
183+ } ) ;
184+
185+ if !cache_is_valid {
186+ trace ! (
187+ "Tracked executable changed or disappeared for {:?}" ,
188+ self . executable
189+ ) ;
190+ self . envoronment
191+ . lock ( )
192+ . expect ( "envoronment mutex poisoned" )
193+ . take ( ) ;
194+ if let Some ( cache_directory) = & self . cache_directory {
195+ delete_cache_file ( cache_directory, & self . executable ) ;
180196 }
181197 }
182198 }
@@ -215,14 +231,17 @@ impl CacheEntry for CacheEntryImpl {
215231
216232 fn store ( & self , environment : ResolvedPythonEnv ) {
217233 // Get hold of the mtimes and ctimes of the symlinks.
234+ let aliases = environment. symlinks . clone ( ) . unwrap_or_default ( ) ;
235+ let current_dir = current_dir_for_aliases ( & aliases) ;
218236 let mut symlinks = vec ! [ ] ;
219- for symlink in environment. symlinks . clone ( ) . unwrap_or_default ( ) . iter ( ) {
237+ for alias in & aliases {
238+ let symlink = executable_cache_key_from ( alias, current_dir. as_deref ( ) ) ;
220239 if let Ok ( metadata) = symlink. metadata ( ) {
221240 // We require mtime, but ctime is optional (not available on all Linux filesystems)
222241 // See: https://github.com/microsoft/python-environment-tools/issues/223
223242 if let Ok ( modified) = metadata. modified ( ) {
224243 let created = metadata. created ( ) . ok ( ) ; // May be None on Linux
225- symlinks. push ( ( symlink. clone ( ) , modified, created) ) ;
244+ symlinks. push ( ( symlink, modified, created) ) ;
226245 }
227246 }
228247 }
@@ -259,8 +278,12 @@ impl CacheEntry for CacheEntryImpl {
259278 . iter ( )
260279 . map ( |x| x. 0 . clone ( ) )
261280 . collect ( ) ;
262-
263- if symlinks. iter ( ) . all ( |x| known_symlinks. contains ( x) ) {
281+ let current_dir = current_dir_for_aliases ( & symlinks) ;
282+ if symlinks
283+ . iter ( )
284+ . map ( |alias| executable_cache_key_from ( alias, current_dir. as_deref ( ) ) )
285+ . all ( |key| known_symlinks. contains ( & key) )
286+ {
264287 return ;
265288 }
266289
@@ -283,3 +306,97 @@ impl CacheEntry for CacheEntryImpl {
283306 }
284307 }
285308}
309+
310+ #[ cfg( test) ]
311+ mod tests {
312+ use super :: * ;
313+ use tempfile:: tempdir_in;
314+
315+ fn environment ( executable : PathBuf , aliases : Vec < PathBuf > ) -> ResolvedPythonEnv {
316+ ResolvedPythonEnv {
317+ executable,
318+ prefix : PathBuf :: from ( "prefix" ) ,
319+ version : "3.12.0" . to_string ( ) ,
320+ is64_bit : true ,
321+ symlinks : Some ( aliases) ,
322+ }
323+ }
324+
325+ fn aliases ( ) -> ( tempfile:: TempDir , PathBuf , PathBuf ) {
326+ let current_dir = std:: env:: current_dir ( ) . unwrap ( ) ;
327+ let temp_dir = tempdir_in ( & current_dir) . unwrap ( ) ;
328+ let absolute = temp_dir. path ( ) . join ( "python" ) ;
329+ std:: fs:: write ( & absolute, "python" ) . unwrap ( ) ;
330+ let relative = absolute. strip_prefix ( & current_dir) . unwrap ( ) . to_path_buf ( ) ;
331+ ( temp_dir, relative, absolute)
332+ }
333+
334+ #[ test]
335+ fn relative_and_absolute_aliases_share_in_memory_entry ( ) {
336+ let ( _temp_dir, relative, absolute) = aliases ( ) ;
337+ let cache = CacheImpl :: new ( None ) ;
338+
339+ let relative_entry = cache. create_cache ( relative) ;
340+ let absolute_entry = cache. create_cache ( absolute) ;
341+
342+ assert ! ( Arc :: ptr_eq( & relative_entry, & absolute_entry) ) ;
343+ }
344+
345+ #[ test]
346+ fn cache_hit_uses_current_alias_and_preserves_shorter_aliases ( ) {
347+ let ( _temp_dir, relative, absolute) = aliases ( ) ;
348+ let cache = CacheImpl :: new ( None ) ;
349+ let entry = cache. create_cache ( relative. clone ( ) ) ;
350+ let entry = entry. lock ( ) . unwrap ( ) ;
351+ entry. store ( environment (
352+ relative. clone ( ) ,
353+ vec ! [ relative. clone( ) , absolute. clone( ) ] ,
354+ ) ) ;
355+
356+ let relative_hit = entry. get_for_executable ( & relative) . unwrap ( ) ;
357+ assert_eq ! ( relative_hit. executable, relative) ;
358+
359+ let absolute_hit = entry. get_for_executable ( & absolute) . unwrap ( ) ;
360+ assert_eq ! ( absolute_hit. executable, absolute) ;
361+ let hit_aliases = absolute_hit. symlinks . unwrap ( ) ;
362+ assert ! ( hit_aliases. contains( & relative) ) ;
363+ assert ! ( hit_aliases. contains( & absolute) ) ;
364+ }
365+
366+ #[ test]
367+ fn disk_cache_reuses_relative_entry_for_absolute_alias ( ) {
368+ let ( temp_dir, relative, absolute) = aliases ( ) ;
369+ let cache_directory = temp_dir. path ( ) . join ( "cache" ) ;
370+ {
371+ let cache = CacheImpl :: new ( Some ( cache_directory. clone ( ) ) ) ;
372+ let entry = cache. create_cache ( relative. clone ( ) ) ;
373+ entry. lock ( ) . unwrap ( ) . store ( environment (
374+ relative. clone ( ) ,
375+ vec ! [ relative. clone( ) , absolute. clone( ) ] ,
376+ ) ) ;
377+ }
378+
379+ let cache = CacheImpl :: new ( Some ( cache_directory) ) ;
380+ let entry = cache. create_cache ( absolute. clone ( ) ) ;
381+ let hit = entry. lock ( ) . unwrap ( ) . get_for_executable ( & absolute) . unwrap ( ) ;
382+
383+ assert_eq ! ( hit. executable, absolute) ;
384+ let hit_aliases = hit. symlinks . unwrap ( ) ;
385+ assert ! ( hit_aliases. contains( & relative) ) ;
386+ assert ! ( hit_aliases. contains( & absolute) ) ;
387+ }
388+
389+ #[ test]
390+ fn missing_tracked_executable_invalidates_in_memory_entry ( ) {
391+ let ( temp_dir, _relative, absolute) = aliases ( ) ;
392+ let cache = CacheImpl :: new ( Some ( temp_dir. path ( ) . join ( "cache" ) ) ) ;
393+ let entry = cache. create_cache ( absolute. clone ( ) ) ;
394+ let entry = entry. lock ( ) . unwrap ( ) ;
395+ entry. store ( environment ( absolute. clone ( ) , vec ! [ absolute. clone( ) ] ) ) ;
396+ assert ! ( entry. get( ) . is_some( ) ) ;
397+
398+ std:: fs:: remove_file ( & absolute) . unwrap ( ) ;
399+
400+ assert ! ( entry. get( ) . is_none( ) ) ;
401+ }
402+ }
0 commit comments