@@ -76,7 +76,30 @@ fn hash_python_packages(root: &Path, hasher: &mut DefaultHasher) {
7676 . collect ( ) ;
7777 dirs. sort ( ) ;
7878 for dir in dirs {
79- hash_dir_if_exists ( & dir, hasher) ;
79+ hash_python_package_recursive ( & dir, hasher) ;
80+ }
81+ }
82+
83+ /// Recursively hash Python source files inside a package directory.
84+ /// Skips generated/cache directories at any depth so that bytecode and tool
85+ /// caches do not affect the structural cache key.
86+ fn hash_python_package_recursive ( dir : & Path , hasher : & mut DefaultHasher ) {
87+ let Ok ( mut entries) = std:: fs:: read_dir ( dir) else { return ; } ;
88+ let mut paths: Vec < _ > = entries. by_ref ( ) . flatten ( ) . map ( |e| e. path ( ) ) . collect ( ) ;
89+ paths. sort ( ) ;
90+ for path in paths {
91+ let name = path. file_name ( ) . and_then ( |n| n. to_str ( ) ) . unwrap_or ( "" ) ;
92+ if path. is_dir ( ) {
93+ if matches ! ( name, "__pycache__" | ".mypy_cache" | ".pytest_cache"
94+ | ".tox" | ".venv" | "venv" | "build" | "dist" )
95+ || name. starts_with ( '.' )
96+ {
97+ continue ;
98+ }
99+ hash_python_package_recursive ( & path, hasher) ;
100+ } else if path. extension ( ) . and_then ( |e| e. to_str ( ) ) == Some ( "py" ) {
101+ hash_file_content ( & path, hasher) ;
102+ }
80103 }
81104}
82105
@@ -397,24 +420,49 @@ mod tests {
397420
398421 #[ test]
399422 fn workspace_member_hash_is_isolated_from_sibling ( ) {
400- let member_a = tempdir ( ) . unwrap ( ) ;
401- let member_b = tempdir ( ) . unwrap ( ) ;
423+ // Use a realistic workspace layout: workspace/crates/a and workspace/crates/b
424+ let ws = tempdir ( ) . unwrap ( ) ;
425+ let crates = ws. path ( ) . join ( "crates" ) ;
426+ let member_a = crates. join ( "a" ) ;
427+ let member_b = crates. join ( "b" ) ;
428+ std:: fs:: create_dir_all ( & member_a) . unwrap ( ) ;
429+ std:: fs:: create_dir_all ( & member_b) . unwrap ( ) ;
402430
403- std:: fs:: write ( member_a. path ( ) . join ( "Cargo.toml" ) , b"[package]\n name='a'" ) . unwrap ( ) ;
404- std:: fs:: write ( member_b. path ( ) . join ( "Cargo.toml" ) , b"[package]\n name='b'" ) . unwrap ( ) ;
431+ std:: fs:: write ( member_a. join ( "Cargo.toml" ) , b"[package]\n name='a'" ) . unwrap ( ) ;
432+ std:: fs:: write ( member_b. join ( "Cargo.toml" ) , b"[package]\n name='b'" ) . unwrap ( ) ;
405433
406- let ha1 = compute_project_hash ( member_a. path ( ) , Language :: Rust ) ;
407- let hb1 = compute_project_hash ( member_b. path ( ) , Language :: Rust ) ;
434+ let ha1 = compute_project_hash ( & member_a, Language :: Rust ) ;
435+ let hb1 = compute_project_hash ( & member_b, Language :: Rust ) ;
408436
409437 // Modify member_b; member_a's hash must not change
410- std:: fs:: write ( member_b. path ( ) . join ( "Cargo.toml" ) , b"[package]\n name='b-changed'" ) . unwrap ( ) ;
411- let ha2 = compute_project_hash ( member_a. path ( ) , Language :: Rust ) ;
412- let hb2 = compute_project_hash ( member_b. path ( ) , Language :: Rust ) ;
438+ std:: fs:: write ( member_b. join ( "Cargo.toml" ) , b"[package]\n name='b-changed'" ) . unwrap ( ) ;
439+ let ha2 = compute_project_hash ( & member_a, Language :: Rust ) ;
440+ let hb2 = compute_project_hash ( & member_b, Language :: Rust ) ;
413441
414- assert_eq ! ( ha1, ha2, "member_a hash must not change when member_b changes" ) ;
442+ assert_eq ! ( ha1, ha2, "member_a hash must not change when sibling member_b changes" ) ;
415443 assert_ne ! ( hb1, hb2, "member_b hash must change after its own file changes" ) ;
416444 }
417445
446+ #[ test]
447+ fn python_pycache_dir_does_not_affect_hash ( ) {
448+ let dir = tempdir ( ) . unwrap ( ) ;
449+ std:: fs:: write ( dir. path ( ) . join ( "pyproject.toml" ) , b"[project]\n name='x'" ) . unwrap ( ) ;
450+
451+ let pkg = dir. path ( ) . join ( "mypackage" ) ;
452+ std:: fs:: create_dir ( & pkg) . unwrap ( ) ;
453+ std:: fs:: write ( pkg. join ( "__init__.py" ) , b"" ) . unwrap ( ) ;
454+ std:: fs:: write ( pkg. join ( "core.py" ) , b"def run(): pass" ) . unwrap ( ) ;
455+ let h1 = compute_project_hash ( dir. path ( ) , Language :: Python ) ;
456+
457+ // Add a __pycache__ directory with bytecode — must not change the hash
458+ let pycache = pkg. join ( "__pycache__" ) ;
459+ std:: fs:: create_dir ( & pycache) . unwrap ( ) ;
460+ std:: fs:: write ( pycache. join ( "core.cpython-311.pyc" ) , b"\x00 \x00 bytecode" ) . unwrap ( ) ;
461+ let h2 = compute_project_hash ( dir. path ( ) , Language :: Python ) ;
462+
463+ assert_eq ! ( h1, h2, "__pycache__ contents must not affect the Python structural cache hash" ) ;
464+ }
465+
418466 // ── save / load round-trip ────────────────────────────────────────────────
419467
420468 #[ test]
0 commit comments