diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 6d1c6b5b5e6347..139c134b089ae7 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -59,4 +59,5 @@ jobs: cache: pip cache-dependency-path: Tools/requirements-dev.txt - run: pip install -r Tools/requirements-dev.txt + - run: python3 Misc/mypy/make_symlinks.py - run: mypy --config-file ${{ matrix.target }}/mypy.ini diff --git a/Makefile.pre.in b/Makefile.pre.in index e10c78d6403472..d65d5bddef652e 100644 --- a/Makefile.pre.in +++ b/Makefile.pre.in @@ -3206,6 +3206,7 @@ distclean: clobber docclean Modules/ld_so_aix Modules/python.exp Misc/python.pc \ Misc/python-embed.pc Misc/python-config.sh -rm -f python*-gdb.py + -find Misc/mypy -type l -delete # Issue #28258: set LC_ALL to avoid issues with Estonian locale. # Expansion is performed here by shell (spawned by make) itself before # arguments are passed to find. So LC_ALL=C must be set as a separate diff --git a/Misc/mypy/.gitignore b/Misc/mypy/.gitignore new file mode 100644 index 00000000000000..5b2c526787e984 --- /dev/null +++ b/Misc/mypy/.gitignore @@ -0,0 +1,5 @@ +# This list is also used in make_symlinks.py, only put actual concrete paths below +# (no wildcards!). + +_colorize.py +_pyrepl diff --git a/Misc/mypy/README.md b/Misc/mypy/README.md index 05eda6c0b82f0a..99846426e30579 100644 --- a/Misc/mypy/README.md +++ b/Misc/mypy/README.md @@ -11,6 +11,17 @@ like `types`, `typing`, and `collections.abc`. So instead, we set `mypy_path` to include this directory, which only links modules and packages we know are safe to be -type-checked themselves and used as dependencies. +type-checked themselves and used as dependencies. See +`Lib/_pyrepl/mypy.ini` for an example. -See `Lib/_pyrepl/mypy.ini` for an example. \ No newline at end of file +At the same time, we don't want symlinks to be checked into the +repository as they would end up being shipped as part of the source +tarballs, which can create compatibility issues with unpacking on +operating systems without symlink support. Additionally, those symlinks +would have to be part of the SBOM, which we don't want either. + +Instead, this directory ships with a `make_symlinks.py` script, which +creates the symlinks when they are needed. This happens automatically +on GitHub Actions. You will have to run it manually for a local +type-checking workflow. Note that `make distclean` removes the symlinks +to ensure that the produced distribution is clean. diff --git a/Misc/mypy/_colorize.py b/Misc/mypy/_colorize.py deleted file mode 120000 index 9b7304769ec30b..00000000000000 --- a/Misc/mypy/_colorize.py +++ /dev/null @@ -1 +0,0 @@ -../../Lib/_colorize.py \ No newline at end of file diff --git a/Misc/mypy/_pyrepl b/Misc/mypy/_pyrepl deleted file mode 120000 index bd7b69909663b6..00000000000000 --- a/Misc/mypy/_pyrepl +++ /dev/null @@ -1 +0,0 @@ -../../Lib/_pyrepl \ No newline at end of file diff --git a/Misc/mypy/make_symlinks.py b/Misc/mypy/make_symlinks.py new file mode 100755 index 00000000000000..ed895d0024dd65 --- /dev/null +++ b/Misc/mypy/make_symlinks.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import os +from pathlib import Path + +CURRENT_DIR = Path(__file__).parent +MISC_DIR = CURRENT_DIR.parent +REPO_ROOT = MISC_DIR.parent +LIB_DIR = REPO_ROOT / "Lib" + +parser = argparse.ArgumentParser(prog="make_symlinks.py") +parser.add_argument( + "--dry-run", + action="store_true", + help="Don't actually symlink or delete anything", +) +parser.add_argument( + "--force", + action="store_true", + help="Delete destination paths if they exist", +) + +args = parser.parse_args() + +for link in (CURRENT_DIR / ".gitignore").read_text().splitlines(): + link = link.strip() + if not link or link.startswith('#'): + continue + + src = LIB_DIR / link + dst = CURRENT_DIR / link + src_at_root = src.relative_to(REPO_ROOT) + dst_at_root = dst.relative_to(REPO_ROOT) + if args.force: + if dst.exists(): + if args.dry_run: + print(f"{dst_at_root} already exists, would delete") + else: + print(f"{dst_at_root} already exists, deleting") + dst.unlink() + elif ( + dst.is_symlink() + and src.resolve(strict=True) == dst.resolve(strict=True) + ): + print(f"{dst_at_root} already exists, skipping") + continue + + # we specifically want relative path links with .. + src_rel = os.path.relpath(src, CURRENT_DIR) + action = "symlinking" if not args.dry_run else "would symlink" + print(f"{action} {src_at_root} at {dst_at_root}") + if not args.dry_run: + os.symlink(src_rel, dst)