Skip redundant `.so` copies for extensions we generated. setuptools' copy_extensions_to_source rewrites every `.so` in the source tree on every build_ext, even when nothing changed. On macOS this invalidates AMFI's signature cache (~100 ms re-verification per `.so` on the next impor
()
| 455 | |
| 456 | |
| 457 | def _patch_setuptools_copy_extensions_to_source() -> None: |
| 458 | """Skip redundant `.so` copies for extensions we generated. |
| 459 | |
| 460 | setuptools' copy_extensions_to_source rewrites every `.so` in the |
| 461 | source tree on every build_ext, even when nothing changed. On macOS |
| 462 | this invalidates AMFI's signature cache (~100 ms re-verification per |
| 463 | `.so` on the next import), eating most of the separate=True |
| 464 | incremental speedup. |
| 465 | |
| 466 | The patch is global because copy_extensions_to_source runs during |
| 467 | setup()'s build_ext command, after mypycify() has already returned; |
| 468 | we can't scope a context manager around it. Instead the skip only |
| 469 | fires for extensions tagged by mypycify (via the marker attribute), |
| 470 | so other setuptools users in the same setup.py see the unmodified |
| 471 | upstream behavior, including stub writes. |
| 472 | """ |
| 473 | global _setuptools_patch_applied |
| 474 | if _setuptools_patch_applied: |
| 475 | return |
| 476 | _setuptools_patch_applied = True |
| 477 | |
| 478 | from setuptools.command.build_ext import build_ext as _build_ext |
| 479 | |
| 480 | original = _build_ext.copy_extensions_to_source |
| 481 | |
| 482 | def _files_match(a: str, b: str) -> bool: |
| 483 | try: |
| 484 | sa = os.stat(a) |
| 485 | sb = os.stat(b) |
| 486 | except OSError: |
| 487 | return False |
| 488 | # Compare size + whole-second mtime. distutils' copy_file |
| 489 | # propagates the source mtime, but macOS drops sub-second |
| 490 | # precision on write so the float values never match verbatim. |
| 491 | return sa.st_size == sb.st_size and int(sa.st_mtime) == int(sb.st_mtime) |
| 492 | |
| 493 | def patched(self: Any) -> None: |
| 494 | build_py = self.get_finalized_command("build_py") |
| 495 | |
| 496 | def is_redundant(ext: Any) -> bool: |
| 497 | if not getattr(ext, _MYPYC_EXTENSION_MARKER, False): |
| 498 | return False |
| 499 | inplace_file, regular_file = self._get_inplace_equivalent(build_py, ext) |
| 500 | return _files_match(regular_file, inplace_file) |
| 501 | |
| 502 | # Hide our already-fresh extensions from setuptools' loop and |
| 503 | # let it handle whatever's left. Delegating instead of |
| 504 | # reimplementing the body means future setuptools changes carry |
| 505 | # over for free. self.extensions is restored before we return |
| 506 | # so anything that inspects it later sees the original list. |
| 507 | saved = self.extensions |
| 508 | self.extensions = [ext for ext in saved if not is_redundant(ext)] |
| 509 | try: |
| 510 | original(self) |
| 511 | finally: |
| 512 | self.extensions = saved |
| 513 | |
| 514 | _build_ext.copy_extensions_to_source = patched # type: ignore[method-assign] |
no outgoing calls
no test coverage detected
searching dependent graphs…