diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index b7d05496a..ff4025dd2 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -1063,24 +1063,31 @@ def sdist(self, directory: Path) -> pathlib.Path: with tarfile.open(meson_dist_path, 'r:gz') as meson_dist, mesonpy._util.create_targz(sdist_path) as sdist: for member in meson_dist.getmembers(): + # Record the original member name. The symbolic link + # resolution loop will make ``member`` point to the link + # target, but it needs to be archived under the original name. + # Symbolic link target resolution must be relative to + # ``member.name``, which therefore cannot be updated in the + # symbolic link resolution loop. + name = member.name + # Recursively resolve symbolic links. The source distribution # archive format specification allows for symbolic links as # long as the target path does not include a '..' component. # This makes symbolic links support unusable in most cases, # therefore include the symbolic link targets as regular files # in all cases. + visited = set() while member.issym(): - name = member.name + # Detect symbolic link chains resulting in a cycle. + if member.name in visited: + warnings.warn( + f'symbolic link resulting in a cycle ignored: {name}', stacklevel=1) + break + visited.add(member.name) target = posixpath.normpath(posixpath.join(posixpath.dirname(member.name), member.linkname)) try: - # This can be implemented using the .replace() method - # in Python 3.12 and later. The .replace() method was - # added as part of PEP 706 and back-ported to Python - # 3.9 and later in patch releases, thus it cannot be - # relied upon until the minimum supported Python - # version is 3.12. - member = copy.copy(meson_dist.getmember(target)) - member.name = name + member = meson_dist.getmember(target) except KeyError: warnings.warn( 'symbolic link with absolute path target, pointing outside the ' @@ -1089,13 +1096,14 @@ def sdist(self, directory: Path) -> pathlib.Path: if member.isdir(): warnings.warn( f'symbolic link pointing to a directory ignored: {name}', stacklevel=1) - - # Copy `member` before starting to modify it - member = copy.copy(member) + break if member.isfile(): file = meson_dist.extractfile(member.name) + # Copy ``member`` before starting to modify it + member = copy.copy(member) + # Reset pax extended header. The tar archive member may be # using pax headers to store some file metadata. The pax # headers are not reset when the metadata is modified and @@ -1113,7 +1121,7 @@ def sdist(self, directory: Path) -> pathlib.Path: member.pax_headers = {} # Rewrite the path to match the sdist distribution name. - stem = member.name.split('/', 1)[1] + stem = name.split('/', 1)[1] member.name = '/'.join((dist_name, stem)) if stem == 'pyproject.toml': diff --git a/tests/packages/symlinks/ccc.py b/tests/packages/symlinks/ccc.py new file mode 120000 index 000000000..caf3b0e3d --- /dev/null +++ b/tests/packages/symlinks/ccc.py @@ -0,0 +1 @@ +submodule/bbb.py \ No newline at end of file diff --git a/tests/packages/symlinks/cycle b/tests/packages/symlinks/cycle new file mode 120000 index 000000000..191028156 --- /dev/null +++ b/tests/packages/symlinks/cycle @@ -0,0 +1 @@ +foo \ No newline at end of file diff --git a/tests/packages/symlinks/foo b/tests/packages/symlinks/foo new file mode 120000 index 000000000..b9d47ed8c --- /dev/null +++ b/tests/packages/symlinks/foo @@ -0,0 +1 @@ +cycle \ No newline at end of file diff --git a/tests/test_sdist.py b/tests/test_sdist.py index 70a3e357e..9048a4e23 100644 --- a/tests/test_sdist.py +++ b/tests/test_sdist.py @@ -237,6 +237,7 @@ def test_symlinks(tmp_path, sdist_symlinks): 'symlinks-1.0.0/meson.build', 'symlinks-1.0.0/pyproject.toml', 'symlinks-1.0.0/__init__.py', + 'symlinks-1.0.0/ccc.py', 'symlinks-1.0.0/submodule/__init__.py', 'symlinks-1.0.0/submodule/aaa.py', 'symlinks-1.0.0/submodule/bbb.py',