diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index 6f982c71..b708c582 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 00000000..fd25f88b --- /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 00000000..8fa01582 --- /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 00000000..619757ce --- /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 00000000..94656643 --- /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 00000000..619757ce --- /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 ac56bcf0..36c5de93 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 1b819823..279b392c 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.