From 9ac7f0a321c59f42bbf09fe81f133bb3185b7e08 Mon Sep 17 00:00:00 2001 From: Daniele Nicolodi Date: Sun, 30 Mar 2025 15:33:33 +0200 Subject: [PATCH] ENH: add support for symbolic links in package repository Symbolic links are included as regular files in the sdist archive. Only symbolic links pointing to files withing the archive are supported. --- mesonpy/__init__.py | 25 +++++++++++++++++++ tests/packages/symlinks/__init__.py | 3 +++ tests/packages/symlinks/baz.py | 1 + tests/packages/symlinks/meson.build | 16 ++++++++++++ tests/packages/symlinks/pyproject.toml | 7 ++++++ tests/packages/symlinks/qux.py | 1 + tests/packages/symlinks/submodule/__init__.py | 1 + tests/packages/symlinks/submodule/aaa.py | 6 +++++ tests/packages/symlinks/submodule/bbb.py | 1 + tests/test_sdist.py | 19 ++++++++++++++ 10 files changed, 80 insertions(+) create mode 100644 tests/packages/symlinks/__init__.py create mode 120000 tests/packages/symlinks/baz.py create mode 100644 tests/packages/symlinks/meson.build create mode 100644 tests/packages/symlinks/pyproject.toml create mode 120000 tests/packages/symlinks/qux.py create mode 120000 tests/packages/symlinks/submodule/__init__.py create mode 100644 tests/packages/symlinks/submodule/aaa.py create mode 120000 tests/packages/symlinks/submodule/bbb.py diff --git a/mesonpy/__init__.py b/mesonpy/__init__.py index 51c6f0fb7..99fb16cb1 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 @@ -23,6 +24,7 @@ import os import pathlib import platform +import posixpath import re import shutil import subprocess @@ -937,6 +939,29 @@ 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(): + # 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. + while member.issym(): + name = 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 + except KeyError: + # Symbolic link with absolute path target, pointing + # outside the archive, or dangling: ignore. + break + if member.isfile(): file = meson_dist.extractfile(member.name) diff --git a/tests/packages/symlinks/__init__.py b/tests/packages/symlinks/__init__.py new file mode 100644 index 000000000..443ef1738 --- /dev/null +++ b/tests/packages/symlinks/__init__.py @@ -0,0 +1,3 @@ +# SPDX-FileCopyrightText: 2025 The meson-python developers +# +# SPDX-License-Identifier: MIT diff --git a/tests/packages/symlinks/baz.py b/tests/packages/symlinks/baz.py new file mode 120000 index 000000000..fe023bf27 --- /dev/null +++ b/tests/packages/symlinks/baz.py @@ -0,0 +1 @@ +/__init__.py \ No newline at end of file diff --git a/tests/packages/symlinks/meson.build b/tests/packages/symlinks/meson.build new file mode 100644 index 000000000..a4cd913a3 --- /dev/null +++ b/tests/packages/symlinks/meson.build @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: 2025 The meson-python developers +# +# SPDX-License-Identifier: MIT + +project('symlinks', version: '1.0.0') + +py = import('python').find_installation() + +py.install_sources( + '__init__.py', + 'submodule/__init__.py', + 'submodule/aaa.py', + 'submodule/bbb.py', + subdir: 'symlinks', + preserve_path: true, +) diff --git a/tests/packages/symlinks/pyproject.toml b/tests/packages/symlinks/pyproject.toml new file mode 100644 index 000000000..0e67ab758 --- /dev/null +++ b/tests/packages/symlinks/pyproject.toml @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2021 The meson-python developers +# +# SPDX-License-Identifier: MIT + +[build-system] +build-backend = 'mesonpy' +requires = ['meson-python'] diff --git a/tests/packages/symlinks/qux.py b/tests/packages/symlinks/qux.py new file mode 120000 index 000000000..b5b7b4681 --- /dev/null +++ b/tests/packages/symlinks/qux.py @@ -0,0 +1 @@ +../__init__.py \ No newline at end of file diff --git a/tests/packages/symlinks/submodule/__init__.py b/tests/packages/symlinks/submodule/__init__.py new file mode 120000 index 000000000..b5b7b4681 --- /dev/null +++ b/tests/packages/symlinks/submodule/__init__.py @@ -0,0 +1 @@ +../__init__.py \ No newline at end of file diff --git a/tests/packages/symlinks/submodule/aaa.py b/tests/packages/symlinks/submodule/aaa.py new file mode 100644 index 000000000..d49091dbc --- /dev/null +++ b/tests/packages/symlinks/submodule/aaa.py @@ -0,0 +1,6 @@ +# SPDX-FileCopyrightText: 2025 The meson-python developers +# +# SPDX-License-Identifier: MIT + +def foo(): + return 42 diff --git a/tests/packages/symlinks/submodule/bbb.py b/tests/packages/symlinks/submodule/bbb.py new file mode 120000 index 000000000..1c9110e2d --- /dev/null +++ b/tests/packages/symlinks/submodule/bbb.py @@ -0,0 +1 @@ +aaa.py \ No newline at end of file diff --git a/tests/test_sdist.py b/tests/test_sdist.py index a98b27d10..599eb1181 100644 --- a/tests/test_sdist.py +++ b/tests/test_sdist.py @@ -216,3 +216,22 @@ def test_reproducible(package_pure, tmp_path): assert sdist_path_a == sdist_path_b assert tmp_path.joinpath('a', sdist_path_a).read_bytes() == tmp_path.joinpath('b', sdist_path_b).read_bytes() + + +def test_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()} + + assert names == { + 'symlinks-1.0.0/PKG-INFO', + 'symlinks-1.0.0/meson.build', + 'symlinks-1.0.0/pyproject.toml', + 'symlinks-1.0.0/__init__.py', + 'symlinks-1.0.0/submodule/__init__.py', + 'symlinks-1.0.0/submodule/aaa.py', + 'symlinks-1.0.0/submodule/bbb.py', + } + + # All the archive members have a valid mtime. + assert 0 not in mtimes