diff --git a/scripts/build_wheel.py b/scripts/build_wheel.py index b984152c94cb..5d48337d5889 100755 --- a/scripts/build_wheel.py +++ b/scripts/build_wheel.py @@ -822,18 +822,47 @@ def symlink_remove_dst_tree(src, dst, dirs_exist_ok=True): # Helper function to resolve symlinks and copy actual content def copy_resolving_symlink(src_path, dst_path): - """Copy file or directory, resolving symlinks to copy actual content.""" + """Copy file or directory, resolving symlinks to copy actual content. + + Skips the copy when dst already exists and a stamp file confirms the + previous copy completed successfully, and the stamp is at least as new + as src. Using a stamp (rather than dst mtime) avoids two pitfalls: + - Directory mtime on Linux only reflects direct-child changes, not + deeply nested ones, so a modified source file may not update it. + - An interrupted copy leaves dst with a fresh mtime that would + incorrectly satisfy an mtime check, causing the next build to skip. + """ if src_path.is_symlink(): resolved_src = src_path.resolve() else: resolved_src = src_path if resolved_src.is_dir(): - if dst_path.exists(): + stamp = dst_path.parent / f".{dst_path.name}.stamp" + if dst_path.exists() and stamp.exists(): + if stamp.stat().st_mtime >= resolved_src.stat().st_mtime: + return # destination is up to date rmtree(dst_path) - # Use symlinks=False (default) to follow symlinks and copy actual content - # This ensures nested symlinks are also resolved - copytree(resolved_src, dst_path, symlinks=False) + elif dst_path.exists(): + rmtree( + dst_path) # dst exists but no stamp — interrupted copy + stamp.unlink(missing_ok=True) + # Shell out to cp -rL: dereferences symlinks like copytree(symlinks=False) + # but uses kernel-level copy primitives, which is significantly faster + # than Python's file-by-file copytree on network-mounted filesystems. + # Fall back to copytree if cp is unavailable (non-Linux or minimal containers). + cp_bin = shutil.which("cp") + if cp_bin is not None: + try: + run([cp_bin, "-rL", + str(resolved_src), + str(dst_path)], + check=True) + except FileNotFoundError: + copytree(resolved_src, dst_path, symlinks=False) + else: + copytree(resolved_src, dst_path, symlinks=False) + stamp.touch() else: if dst_path.is_dir(): dst_path = dst_path / src_path.name