From 35d3a093ed471a2c82a9b82e3c9c5b1735db4c5f Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Fri, 13 Dec 2024 02:48:12 -0500 Subject: [PATCH] Preserve symlink contents in sdists Wheels do not support symlinks (as they are ZIP files), but tarballs do. For consistent results however, expand those symlinks to the files they reference. This is consistent with 0.16.0, and fixes this regression in 0.17.0. --- mesonpy/__init__.py | 12 ++++++++++ tests/packages/symlinks/meson.build | 12 ++++++++++ tests/packages/symlinks/pyproject.toml | 7 ++++++ tests/packages/symlinks/subdir/__init__.py | 3 +++ tests/packages/symlinks/subdir/symlink.py | 1 + tests/packages/symlinks/subdir/test.py | 3 +++ tests/test_sdist.py | 27 ++++++++++++++++++++++ tests/test_wheel.py | 17 ++++++++++++++ 8 files changed, 82 insertions(+) create mode 100644 tests/packages/symlinks/meson.build create mode 100644 tests/packages/symlinks/pyproject.toml create mode 100644 tests/packages/symlinks/subdir/__init__.py create mode 120000 tests/packages/symlinks/subdir/symlink.py create mode 100644 tests/packages/symlinks/subdir/test.py diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index 6f982c71d..b708c5828 100644 --- a/mesonpy/__init__.py +++ b/mesonpy/__init__.py @@ -14,6 +14,7 @@ import argparse import collections import contextlib +import copy import difflib import functools import importlib.machinery @@ -854,6 +855,17 @@ 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(): + # Wheels do not support links, though sdist tarballs could. For portability, reduce these to regular files. + if member.islnk() or member.issym(): + # Symlinks are relative to member directory, but hard links are relative to tarball root. + path = member.name.rsplit('/', 1)[0] + '/' if member.issym() else '' + orig = meson_dist.getmember(path + member.linkname) + member = copy.copy(member) + member.mode = orig.mode + member.mtime = orig.mtime + member.size = orig.size + member.type = tarfile.REGTYPE + if member.isfile(): file = meson_dist.extractfile(member.name) diff --git a/tests/packages/symlinks/meson.build b/tests/packages/symlinks/meson.build new file mode 100644 index 000000000..fd25f88b3 --- /dev/null +++ b/tests/packages/symlinks/meson.build @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2024 The meson-python developers +# +# SPDX-License-Identifier: MIT + +project('symlinks', version: '1.0.0') + +py = import('python').find_installation() + +install_subdir( + 'subdir', + install_dir: py.get_install_dir(pure: false), +) diff --git a/tests/packages/symlinks/pyproject.toml b/tests/packages/symlinks/pyproject.toml new file mode 100644 index 000000000..8fa01582c --- /dev/null +++ b/tests/packages/symlinks/pyproject.toml @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2024 The meson-python developers +# +# SPDX-License-Identifier: MIT + +[build-system] +build-backend = 'mesonpy' +requires = ['meson-python'] diff --git a/tests/packages/symlinks/subdir/__init__.py b/tests/packages/symlinks/subdir/__init__.py new file mode 100644 index 000000000..619757cee --- /dev/null +++ b/tests/packages/symlinks/subdir/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2024 The meson-python developers +# +# SPDX-License-Identifier: MIT diff --git a/tests/packages/symlinks/subdir/symlink.py b/tests/packages/symlinks/subdir/symlink.py new file mode 120000 index 000000000..946566431 --- /dev/null +++ b/tests/packages/symlinks/subdir/symlink.py @@ -0,0 +1 @@ +test.py \ No newline at end of file diff --git a/tests/packages/symlinks/subdir/test.py b/tests/packages/symlinks/subdir/test.py new file mode 100644 index 000000000..619757cee --- /dev/null +++ b/tests/packages/symlinks/subdir/test.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2024 The meson-python developers +# +# SPDX-License-Identifier: MIT diff --git a/tests/test_sdist.py b/tests/test_sdist.py index ac56bcf06..36c5de93a 100644 --- a/tests/test_sdist.py +++ b/tests/test_sdist.py @@ -126,6 +126,33 @@ def test_contents_subdirs(sdist_subdirs): assert 0 not in mtimes +def test_contents_symlinks(sdist_symlinks): + with tarfile.open(sdist_symlinks, 'r:gz') as sdist: + names = {member.name for member in sdist.getmembers()} + mtimes = {member.mtime for member in sdist.getmembers()} + + orig_info = sdist.getmember('symlinks-1.0.0/subdir/test.py') + symlink_info = sdist.getmember('symlinks-1.0.0/subdir/symlink.py') + assert orig_info.mode == symlink_info.mode + assert orig_info.mtime == symlink_info.mtime + assert orig_info.size == symlink_info.size + orig = sdist.extractfile('symlinks-1.0.0/subdir/test.py') + symlink = sdist.extractfile('symlinks-1.0.0/subdir/symlink.py') + assert orig.read() == symlink.read() + + assert names == { + 'symlinks-1.0.0/PKG-INFO', + 'symlinks-1.0.0/meson.build', + 'symlinks-1.0.0/pyproject.toml', + 'symlinks-1.0.0/subdir/__init__.py', + 'symlinks-1.0.0/subdir/test.py', + 'symlinks-1.0.0/subdir/symlink.py', + } + + # All the archive members have a valid mtime. + assert 0 not in mtimes + + def test_contents_unstaged(package_pure, tmp_path): new = textwrap.dedent(''' def bar(): diff --git a/tests/test_wheel.py b/tests/test_wheel.py index 707022483..ca4b5818d 100644 --- a/tests/test_wheel.py +++ b/tests/test_wheel.py @@ -339,6 +339,23 @@ def test_install_subdir(wheel_install_subdir): } +def test_install_symlink(wheel_symlinks): + artifact = wheel.wheelfile.WheelFile(wheel_symlinks) + # Handling of the exclude_files and exclude_directories requires + # Meson 1.1.0, see https://github.com/mesonbuild/meson/pull/11432. + # Run the test anyway to ensure that meson-python can produce a + # wheel also for older versions of Meson. + if MESON_VERSION >= (1, 1, 99): + assert wheel_contents(artifact) == { + 'symlinks-1.0.0.dist-info/METADATA', + 'symlinks-1.0.0.dist-info/RECORD', + 'symlinks-1.0.0.dist-info/WHEEL', + 'subdir/__init__.py', + 'subdir/test.py', + 'subdir/symlink.py', + } + + def test_vendored_meson(wheel_vendored_meson): # This test will error if the vendored meson.py wrapper script in # the test package isn't used.