diff --git a/.gitignore b/.gitignore index f25d073e24..9ad7e2dd67 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ include lib distribute.egg-info setuptools.egg-info +setuptools/tests/bdist_wheel_testdata/*/*.egg-info/ .coverage .eggs .tox diff --git a/conftest.py b/conftest.py index 328d45d351..99d020b733 100644 --- a/conftest.py +++ b/conftest.py @@ -39,6 +39,7 @@ def pytest_configure(config): 'pkg_resources/_vendor', 'setuptools/config/_validate_pyproject', 'setuptools/modified.py', + 'setuptools/tests/bdist_wheel_testdata', ] diff --git a/docs/build_meta.rst b/docs/build_meta.rst index aa4f190712..5cb383227e 100644 --- a/docs/build_meta.rst +++ b/docs/build_meta.rst @@ -60,10 +60,8 @@ being used to package your scripts and install from source). To use it with build-backend = "setuptools.build_meta" ``build_meta`` implements ``setuptools``' build system support. -The ``setuptools`` package implements the ``build_sdist`` -command and the ``wheel`` package implements the ``build_wheel`` -command; the latter is a dependency of the former -exposed via :pep:`517` hooks. +The ``setuptools`` package implements the ``build_sdist`` and +``build_wheel`` commands. Use ``setuptools``' :ref:`declarative config ` to specify the package information in ``setup.cfg``:: diff --git a/docs/userguide/quickstart.rst b/docs/userguide/quickstart.rst index a3f285f010..c4875d71fc 100644 --- a/docs/userguide/quickstart.rst +++ b/docs/userguide/quickstart.rst @@ -60,9 +60,9 @@ library will be used to actually do the packaging. Historically this documentation has unnecessarily listed ``wheel`` in the ``requires`` list, and many projects still do that. This is - not recommended. The backend automatically adds ``wheel`` dependency - when it is required, and listing it explicitly causes it to be - unnecessarily required for source distribution builds. + not recommended, as the backend no longer requires the ``wheel`` + package, and listing it explicitly causes it to be unnecessarily + required for source distribution builds. You should only include ``wheel`` in ``requires`` if you need to explicitly access it during build time (e.g. if your project needs a ``setup.py`` script that imports ``wheel``). diff --git a/mypy.ini b/mypy.ini index 231330d270..c5b13942c4 100644 --- a/mypy.ini +++ b/mypy.ini @@ -14,6 +14,7 @@ exclude = (?x)( | ^.+?/(_vendor|extern)/ # Vendored | ^setuptools/_distutils/ # Vendored | ^setuptools/config/_validate_pyproject/ # Auto-generated + | ^setuptools/tests/bdist_wheel_testdata/ # Duplicate module name ) # Ignoring attr-defined because setuptools wraps a lot of distutils classes, adding new attributes, diff --git a/newsfragments/1386.feature.rst b/newsfragments/1386.feature.rst new file mode 100644 index 0000000000..c8d50bc22e --- /dev/null +++ b/newsfragments/1386.feature.rst @@ -0,0 +1 @@ +Adopted the ``bdist_wheel`` command from the ``wheel`` project -- by :user:`agronholm` diff --git a/pyproject.toml b/pyproject.toml index 7f74060916..7e9e66df9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -98,6 +98,7 @@ certs = [] alias = "setuptools.command.alias:alias" bdist_egg = "setuptools.command.bdist_egg:bdist_egg" bdist_rpm = "setuptools.command.bdist_rpm:bdist_rpm" +bdist_wheel = "setuptools.command.bdist_wheel:bdist_wheel" build = "setuptools.command.build:build" build_clib = "setuptools.command.build_clib:build_clib" build_ext = "setuptools.command.build_ext:build_ext" diff --git a/setuptools/_vendor/vendored.txt b/setuptools/_vendor/vendored.txt index 7255f98aee..c981dde807 100644 --- a/setuptools/_vendor/vendored.txt +++ b/setuptools/_vendor/vendored.txt @@ -9,3 +9,4 @@ zipp==3.7.0 tomli==2.0.1 # required for jaraco.context on older Pythons backports.tarfile +wheel==0.43.0 diff --git a/setuptools/_vendor/wheel-0.43.0.dist-info/INSTALLER b/setuptools/_vendor/wheel-0.43.0.dist-info/INSTALLER new file mode 100644 index 0000000000..a1b589e38a --- /dev/null +++ b/setuptools/_vendor/wheel-0.43.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/setuptools/_vendor/wheel-0.43.0.dist-info/LICENSE.txt b/setuptools/_vendor/wheel-0.43.0.dist-info/LICENSE.txt new file mode 100644 index 0000000000..a31470f14c --- /dev/null +++ b/setuptools/_vendor/wheel-0.43.0.dist-info/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2012 Daniel Holth and contributors + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/setuptools/_vendor/wheel-0.43.0.dist-info/METADATA b/setuptools/_vendor/wheel-0.43.0.dist-info/METADATA new file mode 100644 index 0000000000..e3722c00b9 --- /dev/null +++ b/setuptools/_vendor/wheel-0.43.0.dist-info/METADATA @@ -0,0 +1,61 @@ +Metadata-Version: 2.1 +Name: wheel +Version: 0.43.0 +Summary: A built-package format for Python +Keywords: wheel,packaging +Author-email: Daniel Holth +Maintainer-email: Alex Grönholm +Requires-Python: >=3.8 +Description-Content-Type: text/x-rst +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Topic :: System :: Archiving :: Packaging +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Requires-Dist: pytest >= 6.0.0 ; extra == "test" +Requires-Dist: setuptools >= 65 ; extra == "test" +Project-URL: Changelog, https://wheel.readthedocs.io/en/stable/news.html +Project-URL: Documentation, https://wheel.readthedocs.io/ +Project-URL: Issue Tracker, https://github.com/pypa/wheel/issues +Project-URL: Source, https://github.com/pypa/wheel +Provides-Extra: test + +wheel +===== + +This library is the reference implementation of the Python wheel packaging +standard, as defined in `PEP 427`_. + +It has two different roles: + +#. A setuptools_ extension for building wheels that provides the + ``bdist_wheel`` setuptools command +#. A command line tool for working with wheel files + +It should be noted that wheel is **not** intended to be used as a library, and +as such there is no stable, public API. + +.. _PEP 427: https://www.python.org/dev/peps/pep-0427/ +.. _setuptools: https://pypi.org/project/setuptools/ + +Documentation +------------- + +The documentation_ can be found on Read The Docs. + +.. _documentation: https://wheel.readthedocs.io/ + +Code of Conduct +--------------- + +Everyone interacting in the wheel project's codebases, issue trackers, chat +rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_. + +.. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md + diff --git a/setuptools/_vendor/wheel-0.43.0.dist-info/RECORD b/setuptools/_vendor/wheel-0.43.0.dist-info/RECORD new file mode 100644 index 0000000000..786fe55190 --- /dev/null +++ b/setuptools/_vendor/wheel-0.43.0.dist-info/RECORD @@ -0,0 +1,63 @@ +../../bin/wheel,sha256=Y73OywJ5gxOkyLS7G4Z9CS6Pb63oCt-LMViLs-ygeGE,245 +wheel-0.43.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +wheel-0.43.0.dist-info/LICENSE.txt,sha256=MMI2GGeRCPPo6h0qZYx8pBe9_IkcmO8aifpP8MmChlQ,1107 +wheel-0.43.0.dist-info/METADATA,sha256=WbrCKwClnT5WCKVrjPjvxDgxo2tyeS7kOJyc1GaceEE,2153 +wheel-0.43.0.dist-info/RECORD,, +wheel-0.43.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +wheel-0.43.0.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81 +wheel-0.43.0.dist-info/entry_points.txt,sha256=rTY1BbkPHhkGMm4Q3F0pIzJBzW2kMxoG1oriffvGdA0,104 +wheel/__init__.py,sha256=D6jhH00eMzbgrXGAeOwVfD5i-lCAMMycuG1L0useDlo,59 +wheel/__main__.py,sha256=NkMUnuTCGcOkgY0IBLgBCVC_BGGcWORx2K8jYGS12UE,455 +wheel/__pycache__/__init__.cpython-312.pyc,, +wheel/__pycache__/__main__.cpython-312.pyc,, +wheel/__pycache__/_setuptools_logging.cpython-312.pyc,, +wheel/__pycache__/bdist_wheel.cpython-312.pyc,, +wheel/__pycache__/macosx_libfile.cpython-312.pyc,, +wheel/__pycache__/metadata.cpython-312.pyc,, +wheel/__pycache__/util.cpython-312.pyc,, +wheel/__pycache__/wheelfile.cpython-312.pyc,, +wheel/_setuptools_logging.py,sha256=NoCnjJ4DFEZ45Eo-2BdXLsWJCwGkait1tp_17paleVw,746 +wheel/bdist_wheel.py,sha256=OKJyp9E831zJrxoRfmM9AgOjByG1CB-pzF5kXQFmaKk,20938 +wheel/cli/__init__.py,sha256=eBNhnPwWTtdKAJHy77lvz7gOQ5Eu3GavGugXxhSsn-U,4264 +wheel/cli/__pycache__/__init__.cpython-312.pyc,, +wheel/cli/__pycache__/convert.cpython-312.pyc,, +wheel/cli/__pycache__/pack.cpython-312.pyc,, +wheel/cli/__pycache__/tags.cpython-312.pyc,, +wheel/cli/__pycache__/unpack.cpython-312.pyc,, +wheel/cli/convert.py,sha256=qJcpYGKqdfw1P6BelgN1Hn_suNgM6bvyEWFlZeuSWx0,9439 +wheel/cli/pack.py,sha256=CAFcHdBVulvsHYJlndKVO7KMI9JqBTZz5ii0PKxxCOs,3103 +wheel/cli/tags.py,sha256=lHw-LaWrkS5Jy_qWcw-6pSjeNM6yAjDnqKI3E5JTTCU,4760 +wheel/cli/unpack.py,sha256=Y_J7ynxPSoFFTT7H0fMgbBlVErwyDGcObgme5MBuz58,1021 +wheel/macosx_libfile.py,sha256=HnW6OPdN993psStvwl49xtx2kw7hoVbe6nvwmf8WsKI,16103 +wheel/metadata.py,sha256=q-xCCqSAK7HzyZxK9A6_HAWmhqS1oB4BFw1-rHQxBiQ,5884 +wheel/util.py,sha256=e0jpnsbbM9QhaaMSyap-_ZgUxcxwpyLDk6RHcrduPLg,621 +wheel/vendored/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +wheel/vendored/__pycache__/__init__.cpython-312.pyc,, +wheel/vendored/packaging/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +wheel/vendored/packaging/__pycache__/__init__.cpython-312.pyc,, +wheel/vendored/packaging/__pycache__/_elffile.cpython-312.pyc,, +wheel/vendored/packaging/__pycache__/_manylinux.cpython-312.pyc,, +wheel/vendored/packaging/__pycache__/_musllinux.cpython-312.pyc,, +wheel/vendored/packaging/__pycache__/_parser.cpython-312.pyc,, +wheel/vendored/packaging/__pycache__/_structures.cpython-312.pyc,, +wheel/vendored/packaging/__pycache__/_tokenizer.cpython-312.pyc,, +wheel/vendored/packaging/__pycache__/markers.cpython-312.pyc,, +wheel/vendored/packaging/__pycache__/requirements.cpython-312.pyc,, +wheel/vendored/packaging/__pycache__/specifiers.cpython-312.pyc,, +wheel/vendored/packaging/__pycache__/tags.cpython-312.pyc,, +wheel/vendored/packaging/__pycache__/utils.cpython-312.pyc,, +wheel/vendored/packaging/__pycache__/version.cpython-312.pyc,, +wheel/vendored/packaging/_elffile.py,sha256=hbmK8OD6Z7fY6hwinHEUcD1by7czkGiNYu7ShnFEk2k,3266 +wheel/vendored/packaging/_manylinux.py,sha256=P7sdR5_7XBY09LVYYPhHmydMJIIwPXWsh4olk74Uuj4,9588 +wheel/vendored/packaging/_musllinux.py,sha256=z1s8To2hQ0vpn_d-O2i5qxGwEK8WmGlLt3d_26V7NeY,2674 +wheel/vendored/packaging/_parser.py,sha256=4tT4emSl2qTaU7VTQE1Xa9o1jMPCsBezsYBxyNMUN-s,10347 +wheel/vendored/packaging/_structures.py,sha256=q3eVNmbWJGG_S0Dit_S3Ao8qQqz_5PYTXFAKBZe5yr4,1431 +wheel/vendored/packaging/_tokenizer.py,sha256=alCtbwXhOFAmFGZ6BQ-wCTSFoRAJ2z-ysIf7__MTJ_k,5292 +wheel/vendored/packaging/markers.py,sha256=_TSPI1BhJYO7Bp9AzTmHQxIqHEVXaTjmDh9G-w8qzPA,8232 +wheel/vendored/packaging/requirements.py,sha256=dgoBeVprPu2YE6Q8nGfwOPTjATHbRa_ZGLyXhFEln6Q,2933 +wheel/vendored/packaging/specifiers.py,sha256=IWSt0SrLSP72heWhAC8UL0eGvas7XIQHjqiViVfmPKE,39778 +wheel/vendored/packaging/tags.py,sha256=fedHXiOHkBxNZTXotXv8uXPmMFU9ae-TKBujgYHigcA,18950 +wheel/vendored/packaging/utils.py,sha256=XgdmP3yx9-wQEFjO7OvMj9RjEf5JlR5HFFR69v7SQ9E,5268 +wheel/vendored/packaging/version.py,sha256=PFJaYZDxBgyxkfYhH3SQw4qfE9ICCWrTmitvq14y3bs,16234 +wheel/vendored/vendor.txt,sha256=Z2ENjB1i5prfez8CdM1Sdr3c6Zxv2rRRolMpLmBncAE,16 +wheel/wheelfile.py,sha256=DtJDWoZMvnBh4leNMDPGOprQU9d_dp6q-MmV0U--4xc,7694 diff --git a/setuptools/_vendor/backports.tarfile-1.0.0.dist-info/REQUESTED b/setuptools/_vendor/wheel-0.43.0.dist-info/REQUESTED similarity index 100% rename from setuptools/_vendor/backports.tarfile-1.0.0.dist-info/REQUESTED rename to setuptools/_vendor/wheel-0.43.0.dist-info/REQUESTED diff --git a/setuptools/_vendor/wheel-0.43.0.dist-info/WHEEL b/setuptools/_vendor/wheel-0.43.0.dist-info/WHEEL new file mode 100644 index 0000000000..3b5e64b5e6 --- /dev/null +++ b/setuptools/_vendor/wheel-0.43.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: flit 3.9.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/setuptools/_vendor/wheel-0.43.0.dist-info/entry_points.txt b/setuptools/_vendor/wheel-0.43.0.dist-info/entry_points.txt new file mode 100644 index 0000000000..06c9f69deb --- /dev/null +++ b/setuptools/_vendor/wheel-0.43.0.dist-info/entry_points.txt @@ -0,0 +1,6 @@ +[console_scripts] +wheel=wheel.cli:main + +[distutils.commands] +bdist_wheel=wheel.bdist_wheel:bdist_wheel + diff --git a/setuptools/_vendor/wheel/__init__.py b/setuptools/_vendor/wheel/__init__.py new file mode 100644 index 0000000000..a773bbbcd7 --- /dev/null +++ b/setuptools/_vendor/wheel/__init__.py @@ -0,0 +1,3 @@ +from __future__ import annotations + +__version__ = "0.43.0" diff --git a/setuptools/_vendor/wheel/macosx_libfile.py b/setuptools/_vendor/wheel/macosx_libfile.py new file mode 100644 index 0000000000..8953c3f805 --- /dev/null +++ b/setuptools/_vendor/wheel/macosx_libfile.py @@ -0,0 +1,469 @@ +""" +This module contains function to analyse dynamic library +headers to extract system information + +Currently only for MacOSX + +Library file on macosx system starts with Mach-O or Fat field. +This can be distinguish by first 32 bites and it is called magic number. +Proper value of magic number is with suffix _MAGIC. Suffix _CIGAM means +reversed bytes order. +Both fields can occur in two types: 32 and 64 bytes. + +FAT field inform that this library contains few version of library +(typically for different types version). It contains +information where Mach-O headers starts. + +Each section started with Mach-O header contains one library +(So if file starts with this field it contains only one version). + +After filed Mach-O there are section fields. +Each of them starts with two fields: +cmd - magic number for this command +cmdsize - total size occupied by this section information. + +In this case only sections LC_VERSION_MIN_MACOSX (for macosx 10.13 and earlier) +and LC_BUILD_VERSION (for macosx 10.14 and newer) are interesting, +because them contains information about minimal system version. + +Important remarks: +- For fat files this implementation looks for maximum number version. + It not check if it is 32 or 64 and do not compare it with currently built package. + So it is possible to false report higher version that needed. +- All structures signatures are taken form macosx header files. +- I think that binary format will be more stable than `otool` output. + and if apple introduce some changes both implementation will need to be updated. +- The system compile will set the deployment target no lower than + 11.0 for arm64 builds. For "Universal 2" builds use the x86_64 deployment + target when the arm64 target is 11.0. +""" + +from __future__ import annotations + +import ctypes +import os +import sys + +"""here the needed const and struct from mach-o header files""" + +FAT_MAGIC = 0xCAFEBABE +FAT_CIGAM = 0xBEBAFECA +FAT_MAGIC_64 = 0xCAFEBABF +FAT_CIGAM_64 = 0xBFBAFECA +MH_MAGIC = 0xFEEDFACE +MH_CIGAM = 0xCEFAEDFE +MH_MAGIC_64 = 0xFEEDFACF +MH_CIGAM_64 = 0xCFFAEDFE + +LC_VERSION_MIN_MACOSX = 0x24 +LC_BUILD_VERSION = 0x32 + +CPU_TYPE_ARM64 = 0x0100000C + +mach_header_fields = [ + ("magic", ctypes.c_uint32), + ("cputype", ctypes.c_int), + ("cpusubtype", ctypes.c_int), + ("filetype", ctypes.c_uint32), + ("ncmds", ctypes.c_uint32), + ("sizeofcmds", ctypes.c_uint32), + ("flags", ctypes.c_uint32), +] +""" +struct mach_header { + uint32_t magic; /* mach magic number identifier */ + cpu_type_t cputype; /* cpu specifier */ + cpu_subtype_t cpusubtype; /* machine specifier */ + uint32_t filetype; /* type of file */ + uint32_t ncmds; /* number of load commands */ + uint32_t sizeofcmds; /* the size of all the load commands */ + uint32_t flags; /* flags */ +}; +typedef integer_t cpu_type_t; +typedef integer_t cpu_subtype_t; +""" + +mach_header_fields_64 = mach_header_fields + [("reserved", ctypes.c_uint32)] +""" +struct mach_header_64 { + uint32_t magic; /* mach magic number identifier */ + cpu_type_t cputype; /* cpu specifier */ + cpu_subtype_t cpusubtype; /* machine specifier */ + uint32_t filetype; /* type of file */ + uint32_t ncmds; /* number of load commands */ + uint32_t sizeofcmds; /* the size of all the load commands */ + uint32_t flags; /* flags */ + uint32_t reserved; /* reserved */ +}; +""" + +fat_header_fields = [("magic", ctypes.c_uint32), ("nfat_arch", ctypes.c_uint32)] +""" +struct fat_header { + uint32_t magic; /* FAT_MAGIC or FAT_MAGIC_64 */ + uint32_t nfat_arch; /* number of structs that follow */ +}; +""" + +fat_arch_fields = [ + ("cputype", ctypes.c_int), + ("cpusubtype", ctypes.c_int), + ("offset", ctypes.c_uint32), + ("size", ctypes.c_uint32), + ("align", ctypes.c_uint32), +] +""" +struct fat_arch { + cpu_type_t cputype; /* cpu specifier (int) */ + cpu_subtype_t cpusubtype; /* machine specifier (int) */ + uint32_t offset; /* file offset to this object file */ + uint32_t size; /* size of this object file */ + uint32_t align; /* alignment as a power of 2 */ +}; +""" + +fat_arch_64_fields = [ + ("cputype", ctypes.c_int), + ("cpusubtype", ctypes.c_int), + ("offset", ctypes.c_uint64), + ("size", ctypes.c_uint64), + ("align", ctypes.c_uint32), + ("reserved", ctypes.c_uint32), +] +""" +struct fat_arch_64 { + cpu_type_t cputype; /* cpu specifier (int) */ + cpu_subtype_t cpusubtype; /* machine specifier (int) */ + uint64_t offset; /* file offset to this object file */ + uint64_t size; /* size of this object file */ + uint32_t align; /* alignment as a power of 2 */ + uint32_t reserved; /* reserved */ +}; +""" + +segment_base_fields = [("cmd", ctypes.c_uint32), ("cmdsize", ctypes.c_uint32)] +"""base for reading segment info""" + +segment_command_fields = [ + ("cmd", ctypes.c_uint32), + ("cmdsize", ctypes.c_uint32), + ("segname", ctypes.c_char * 16), + ("vmaddr", ctypes.c_uint32), + ("vmsize", ctypes.c_uint32), + ("fileoff", ctypes.c_uint32), + ("filesize", ctypes.c_uint32), + ("maxprot", ctypes.c_int), + ("initprot", ctypes.c_int), + ("nsects", ctypes.c_uint32), + ("flags", ctypes.c_uint32), +] +""" +struct segment_command { /* for 32-bit architectures */ + uint32_t cmd; /* LC_SEGMENT */ + uint32_t cmdsize; /* includes sizeof section structs */ + char segname[16]; /* segment name */ + uint32_t vmaddr; /* memory address of this segment */ + uint32_t vmsize; /* memory size of this segment */ + uint32_t fileoff; /* file offset of this segment */ + uint32_t filesize; /* amount to map from the file */ + vm_prot_t maxprot; /* maximum VM protection */ + vm_prot_t initprot; /* initial VM protection */ + uint32_t nsects; /* number of sections in segment */ + uint32_t flags; /* flags */ +}; +typedef int vm_prot_t; +""" + +segment_command_fields_64 = [ + ("cmd", ctypes.c_uint32), + ("cmdsize", ctypes.c_uint32), + ("segname", ctypes.c_char * 16), + ("vmaddr", ctypes.c_uint64), + ("vmsize", ctypes.c_uint64), + ("fileoff", ctypes.c_uint64), + ("filesize", ctypes.c_uint64), + ("maxprot", ctypes.c_int), + ("initprot", ctypes.c_int), + ("nsects", ctypes.c_uint32), + ("flags", ctypes.c_uint32), +] +""" +struct segment_command_64 { /* for 64-bit architectures */ + uint32_t cmd; /* LC_SEGMENT_64 */ + uint32_t cmdsize; /* includes sizeof section_64 structs */ + char segname[16]; /* segment name */ + uint64_t vmaddr; /* memory address of this segment */ + uint64_t vmsize; /* memory size of this segment */ + uint64_t fileoff; /* file offset of this segment */ + uint64_t filesize; /* amount to map from the file */ + vm_prot_t maxprot; /* maximum VM protection */ + vm_prot_t initprot; /* initial VM protection */ + uint32_t nsects; /* number of sections in segment */ + uint32_t flags; /* flags */ +}; +""" + +version_min_command_fields = segment_base_fields + [ + ("version", ctypes.c_uint32), + ("sdk", ctypes.c_uint32), +] +""" +struct version_min_command { + uint32_t cmd; /* LC_VERSION_MIN_MACOSX or + LC_VERSION_MIN_IPHONEOS or + LC_VERSION_MIN_WATCHOS or + LC_VERSION_MIN_TVOS */ + uint32_t cmdsize; /* sizeof(struct min_version_command) */ + uint32_t version; /* X.Y.Z is encoded in nibbles xxxx.yy.zz */ + uint32_t sdk; /* X.Y.Z is encoded in nibbles xxxx.yy.zz */ +}; +""" + +build_version_command_fields = segment_base_fields + [ + ("platform", ctypes.c_uint32), + ("minos", ctypes.c_uint32), + ("sdk", ctypes.c_uint32), + ("ntools", ctypes.c_uint32), +] +""" +struct build_version_command { + uint32_t cmd; /* LC_BUILD_VERSION */ + uint32_t cmdsize; /* sizeof(struct build_version_command) plus */ + /* ntools * sizeof(struct build_tool_version) */ + uint32_t platform; /* platform */ + uint32_t minos; /* X.Y.Z is encoded in nibbles xxxx.yy.zz */ + uint32_t sdk; /* X.Y.Z is encoded in nibbles xxxx.yy.zz */ + uint32_t ntools; /* number of tool entries following this */ +}; +""" + + +def swap32(x): + return ( + ((x << 24) & 0xFF000000) + | ((x << 8) & 0x00FF0000) + | ((x >> 8) & 0x0000FF00) + | ((x >> 24) & 0x000000FF) + ) + + +def get_base_class_and_magic_number(lib_file, seek=None): + if seek is None: + seek = lib_file.tell() + else: + lib_file.seek(seek) + magic_number = ctypes.c_uint32.from_buffer_copy( + lib_file.read(ctypes.sizeof(ctypes.c_uint32)) + ).value + + # Handle wrong byte order + if magic_number in [FAT_CIGAM, FAT_CIGAM_64, MH_CIGAM, MH_CIGAM_64]: + if sys.byteorder == "little": + BaseClass = ctypes.BigEndianStructure + else: + BaseClass = ctypes.LittleEndianStructure + + magic_number = swap32(magic_number) + else: + BaseClass = ctypes.Structure + + lib_file.seek(seek) + return BaseClass, magic_number + + +def read_data(struct_class, lib_file): + return struct_class.from_buffer_copy(lib_file.read(ctypes.sizeof(struct_class))) + + +def extract_macosx_min_system_version(path_to_lib): + with open(path_to_lib, "rb") as lib_file: + BaseClass, magic_number = get_base_class_and_magic_number(lib_file, 0) + if magic_number not in [FAT_MAGIC, FAT_MAGIC_64, MH_MAGIC, MH_MAGIC_64]: + return + + if magic_number in [FAT_MAGIC, FAT_CIGAM_64]: + + class FatHeader(BaseClass): + _fields_ = fat_header_fields + + fat_header = read_data(FatHeader, lib_file) + if magic_number == FAT_MAGIC: + + class FatArch(BaseClass): + _fields_ = fat_arch_fields + + else: + + class FatArch(BaseClass): + _fields_ = fat_arch_64_fields + + fat_arch_list = [ + read_data(FatArch, lib_file) for _ in range(fat_header.nfat_arch) + ] + + versions_list = [] + for el in fat_arch_list: + try: + version = read_mach_header(lib_file, el.offset) + if version is not None: + if el.cputype == CPU_TYPE_ARM64 and len(fat_arch_list) != 1: + # Xcode will not set the deployment target below 11.0.0 + # for the arm64 architecture. Ignore the arm64 deployment + # in fat binaries when the target is 11.0.0, that way + # the other architectures can select a lower deployment + # target. + # This is safe because there is no arm64 variant for + # macOS 10.15 or earlier. + if version == (11, 0, 0): + continue + versions_list.append(version) + except ValueError: + pass + + if len(versions_list) > 0: + return max(versions_list) + else: + return None + + else: + try: + return read_mach_header(lib_file, 0) + except ValueError: + """when some error during read library files""" + return None + + +def read_mach_header(lib_file, seek=None): + """ + This function parses a Mach-O header and extracts + information about the minimal macOS version. + + :param lib_file: reference to opened library file with pointer + """ + base_class, magic_number = get_base_class_and_magic_number(lib_file, seek) + arch = "32" if magic_number == MH_MAGIC else "64" + + class SegmentBase(base_class): + _fields_ = segment_base_fields + + if arch == "32": + + class MachHeader(base_class): + _fields_ = mach_header_fields + + else: + + class MachHeader(base_class): + _fields_ = mach_header_fields_64 + + mach_header = read_data(MachHeader, lib_file) + for _i in range(mach_header.ncmds): + pos = lib_file.tell() + segment_base = read_data(SegmentBase, lib_file) + lib_file.seek(pos) + if segment_base.cmd == LC_VERSION_MIN_MACOSX: + + class VersionMinCommand(base_class): + _fields_ = version_min_command_fields + + version_info = read_data(VersionMinCommand, lib_file) + return parse_version(version_info.version) + elif segment_base.cmd == LC_BUILD_VERSION: + + class VersionBuild(base_class): + _fields_ = build_version_command_fields + + version_info = read_data(VersionBuild, lib_file) + return parse_version(version_info.minos) + else: + lib_file.seek(pos + segment_base.cmdsize) + continue + + +def parse_version(version): + x = (version & 0xFFFF0000) >> 16 + y = (version & 0x0000FF00) >> 8 + z = version & 0x000000FF + return x, y, z + + +def calculate_macosx_platform_tag(archive_root, platform_tag): + """ + Calculate proper macosx platform tag basing on files which are included to wheel + + Example platform tag `macosx-10.14-x86_64` + """ + prefix, base_version, suffix = platform_tag.split("-") + base_version = tuple(int(x) for x in base_version.split(".")) + base_version = base_version[:2] + if base_version[0] > 10: + base_version = (base_version[0], 0) + assert len(base_version) == 2 + if "MACOSX_DEPLOYMENT_TARGET" in os.environ: + deploy_target = tuple( + int(x) for x in os.environ["MACOSX_DEPLOYMENT_TARGET"].split(".") + ) + deploy_target = deploy_target[:2] + if deploy_target[0] > 10: + deploy_target = (deploy_target[0], 0) + if deploy_target < base_version: + sys.stderr.write( + "[WARNING] MACOSX_DEPLOYMENT_TARGET is set to a lower value ({}) than " + "the version on which the Python interpreter was compiled ({}), and " + "will be ignored.\n".format( + ".".join(str(x) for x in deploy_target), + ".".join(str(x) for x in base_version), + ) + ) + else: + base_version = deploy_target + + assert len(base_version) == 2 + start_version = base_version + versions_dict = {} + for dirpath, _dirnames, filenames in os.walk(archive_root): + for filename in filenames: + if filename.endswith(".dylib") or filename.endswith(".so"): + lib_path = os.path.join(dirpath, filename) + min_ver = extract_macosx_min_system_version(lib_path) + if min_ver is not None: + min_ver = min_ver[0:2] + if min_ver[0] > 10: + min_ver = (min_ver[0], 0) + versions_dict[lib_path] = min_ver + + if len(versions_dict) > 0: + base_version = max(base_version, max(versions_dict.values())) + + # macosx platform tag do not support minor bugfix release + fin_base_version = "_".join([str(x) for x in base_version]) + if start_version < base_version: + problematic_files = [k for k, v in versions_dict.items() if v > start_version] + problematic_files = "\n".join(problematic_files) + if len(problematic_files) == 1: + files_form = "this file" + else: + files_form = "these files" + error_message = ( + "[WARNING] This wheel needs a higher macOS version than {} " + "To silence this warning, set MACOSX_DEPLOYMENT_TARGET to at least " + + fin_base_version + + " or recreate " + + files_form + + " with lower " + "MACOSX_DEPLOYMENT_TARGET: \n" + problematic_files + ) + + if "MACOSX_DEPLOYMENT_TARGET" in os.environ: + error_message = error_message.format( + "is set in MACOSX_DEPLOYMENT_TARGET variable." + ) + else: + error_message = error_message.format( + "the version your Python interpreter is compiled against." + ) + + sys.stderr.write(error_message) + + platform_tag = prefix + "_" + fin_base_version + "_" + suffix + return platform_tag diff --git a/setuptools/_vendor/wheel/metadata.py b/setuptools/_vendor/wheel/metadata.py new file mode 100644 index 0000000000..341f614ceb --- /dev/null +++ b/setuptools/_vendor/wheel/metadata.py @@ -0,0 +1,180 @@ +""" +Tools for converting old- to new-style metadata. +""" + +from __future__ import annotations + +import functools +import itertools +import os.path +import re +import textwrap +from email.message import Message +from email.parser import Parser +from typing import Iterator + +from ..packaging.requirements import Requirement + + +def _nonblank(str): + return str and not str.startswith("#") + + +@functools.singledispatch +def yield_lines(iterable): + r""" + Yield valid lines of a string or iterable. + >>> list(yield_lines('')) + [] + >>> list(yield_lines(['foo', 'bar'])) + ['foo', 'bar'] + >>> list(yield_lines('foo\nbar')) + ['foo', 'bar'] + >>> list(yield_lines('\nfoo\n#bar\nbaz #comment')) + ['foo', 'baz #comment'] + >>> list(yield_lines(['foo\nbar', 'baz', 'bing\n\n\n'])) + ['foo', 'bar', 'baz', 'bing'] + """ + return itertools.chain.from_iterable(map(yield_lines, iterable)) + + +@yield_lines.register(str) +def _(text): + return filter(_nonblank, map(str.strip, text.splitlines())) + + +def split_sections(s): + """Split a string or iterable thereof into (section, content) pairs + Each ``section`` is a stripped version of the section header ("[section]") + and each ``content`` is a list of stripped lines excluding blank lines and + comment-only lines. If there are any such lines before the first section + header, they're returned in a first ``section`` of ``None``. + """ + section = None + content = [] + for line in yield_lines(s): + if line.startswith("["): + if line.endswith("]"): + if section or content: + yield section, content + section = line[1:-1].strip() + content = [] + else: + raise ValueError("Invalid section heading", line) + else: + content.append(line) + + # wrap up last segment + yield section, content + + +def safe_extra(extra): + """Convert an arbitrary string to a standard 'extra' name + Any runs of non-alphanumeric characters are replaced with a single '_', + and the result is always lowercased. + """ + return re.sub("[^A-Za-z0-9.-]+", "_", extra).lower() + + +def safe_name(name): + """Convert an arbitrary string to a standard distribution name + Any runs of non-alphanumeric/. characters are replaced with a single '-'. + """ + return re.sub("[^A-Za-z0-9.]+", "-", name) + + +def requires_to_requires_dist(requirement: Requirement) -> str: + """Return the version specifier for a requirement in PEP 345/566 fashion.""" + if getattr(requirement, "url", None): + return " @ " + requirement.url + + requires_dist = [] + for spec in requirement.specifier: + requires_dist.append(spec.operator + spec.version) + + if requires_dist: + return " " + ",".join(sorted(requires_dist)) + else: + return "" + + +def convert_requirements(requirements: list[str]) -> Iterator[str]: + """Yield Requires-Dist: strings for parsed requirements strings.""" + for req in requirements: + parsed_requirement = Requirement(req) + spec = requires_to_requires_dist(parsed_requirement) + extras = ",".join(sorted(safe_extra(e) for e in parsed_requirement.extras)) + if extras: + extras = f"[{extras}]" + + yield safe_name(parsed_requirement.name) + extras + spec + + +def generate_requirements( + extras_require: dict[str, list[str]], +) -> Iterator[tuple[str, str]]: + """ + Convert requirements from a setup()-style dictionary to + ('Requires-Dist', 'requirement') and ('Provides-Extra', 'extra') tuples. + + extras_require is a dictionary of {extra: [requirements]} as passed to setup(), + using the empty extra {'': [requirements]} to hold install_requires. + """ + for extra, depends in extras_require.items(): + condition = "" + extra = extra or "" + if ":" in extra: # setuptools extra:condition syntax + extra, condition = extra.split(":", 1) + + extra = safe_extra(extra) + if extra: + yield "Provides-Extra", extra + if condition: + condition = "(" + condition + ") and " + condition += "extra == '%s'" % extra + + if condition: + condition = " ; " + condition + + for new_req in convert_requirements(depends): + yield "Requires-Dist", new_req + condition + + +def pkginfo_to_metadata(egg_info_path: str, pkginfo_path: str) -> Message: + """ + Convert .egg-info directory with PKG-INFO to the Metadata 2.1 format + """ + with open(pkginfo_path, encoding="utf-8") as headers: + pkg_info = Parser().parse(headers) + + pkg_info.replace_header("Metadata-Version", "2.1") + # Those will be regenerated from `requires.txt`. + del pkg_info["Provides-Extra"] + del pkg_info["Requires-Dist"] + requires_path = os.path.join(egg_info_path, "requires.txt") + if os.path.exists(requires_path): + with open(requires_path, encoding="utf-8") as requires_file: + requires = requires_file.read() + + parsed_requirements = sorted(split_sections(requires), key=lambda x: x[0] or "") + for extra, reqs in parsed_requirements: + for key, value in generate_requirements({extra: reqs}): + if (key, value) not in pkg_info.items(): + pkg_info[key] = value + + description = pkg_info["Description"] + if description: + description_lines = pkg_info["Description"].splitlines() + dedented_description = "\n".join( + # if the first line of long_description is blank, + # the first line here will be indented. + ( + description_lines[0].lstrip(), + textwrap.dedent("\n".join(description_lines[1:])), + "\n", + ) + ) + pkg_info.set_payload(dedented_description) + del pkg_info["Description"] + + return pkg_info diff --git a/setuptools/_vendor/wheel/util.py b/setuptools/_vendor/wheel/util.py new file mode 100644 index 0000000000..d98d98cb52 --- /dev/null +++ b/setuptools/_vendor/wheel/util.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +import base64 +import logging + +log = logging.getLogger("wheel") + +# ensure Python logging is configured +try: + __import__("setuptools.logging") +except ImportError: + # setuptools < ?? + from . import _setuptools_logging + + _setuptools_logging.configure() + + +def urlsafe_b64encode(data: bytes) -> bytes: + """urlsafe_b64encode without padding""" + return base64.urlsafe_b64encode(data).rstrip(b"=") + + +def urlsafe_b64decode(data: bytes) -> bytes: + """urlsafe_b64decode without padding""" + pad = b"=" * (4 - (len(data) & 3)) + return base64.urlsafe_b64decode(data + pad) diff --git a/setuptools/_vendor/wheel/wheelfile.py b/setuptools/_vendor/wheel/wheelfile.py new file mode 100644 index 0000000000..83a31772bd --- /dev/null +++ b/setuptools/_vendor/wheel/wheelfile.py @@ -0,0 +1,199 @@ +from __future__ import annotations + +import csv +import hashlib +import os.path +import re +import stat +import time +from io import StringIO, TextIOWrapper +from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo + +from .util import log, urlsafe_b64decode, urlsafe_b64encode + +# Non-greedy matching of an optional build number may be too clever (more +# invalid wheel filenames will match). Separate regex for .dist-info? +WHEEL_INFO_RE = re.compile( + r"""^(?P(?P[^\s-]+?)-(?P[^\s-]+?))(-(?P\d[^\s-]*))? + -(?P[^\s-]+?)-(?P[^\s-]+?)-(?P\S+)\.whl$""", + re.VERBOSE, +) +MINIMUM_TIMESTAMP = 315532800 # 1980-01-01 00:00:00 UTC + + +def get_zipinfo_datetime(timestamp=None): + # Some applications need reproducible .whl files, but they can't do this without + # forcing the timestamp of the individual ZipInfo objects. See issue #143. + timestamp = int(os.environ.get("SOURCE_DATE_EPOCH", timestamp or time.time())) + timestamp = max(timestamp, MINIMUM_TIMESTAMP) + return time.gmtime(timestamp)[0:6] + + +class WheelFile(ZipFile): + """A ZipFile derivative class that also reads SHA-256 hashes from + .dist-info/RECORD and checks any read files against those. + """ + + _default_algorithm = hashlib.sha256 + + def __init__(self, file, mode="r", compression=ZIP_DEFLATED): + basename = os.path.basename(file) + self.parsed_filename = WHEEL_INFO_RE.match(basename) + if not basename.endswith(".whl") or self.parsed_filename is None: + raise WheelError(f"Bad wheel filename {basename!r}") + + ZipFile.__init__(self, file, mode, compression=compression, allowZip64=True) + + self.dist_info_path = "{}.dist-info".format( + self.parsed_filename.group("namever") + ) + self.record_path = self.dist_info_path + "/RECORD" + self._file_hashes = {} + self._file_sizes = {} + if mode == "r": + # Ignore RECORD and any embedded wheel signatures + self._file_hashes[self.record_path] = None, None + self._file_hashes[self.record_path + ".jws"] = None, None + self._file_hashes[self.record_path + ".p7s"] = None, None + + # Fill in the expected hashes by reading them from RECORD + try: + record = self.open(self.record_path) + except KeyError: + raise WheelError(f"Missing {self.record_path} file") from None + + with record: + for line in csv.reader( + TextIOWrapper(record, newline="", encoding="utf-8") + ): + path, hash_sum, size = line + if not hash_sum: + continue + + algorithm, hash_sum = hash_sum.split("=") + try: + hashlib.new(algorithm) + except ValueError: + raise WheelError( + f"Unsupported hash algorithm: {algorithm}" + ) from None + + if algorithm.lower() in {"md5", "sha1"}: + raise WheelError( + f"Weak hash algorithm ({algorithm}) is not permitted by " + f"PEP 427" + ) + + self._file_hashes[path] = ( + algorithm, + urlsafe_b64decode(hash_sum.encode("ascii")), + ) + + def open(self, name_or_info, mode="r", pwd=None): + def _update_crc(newdata): + eof = ef._eof + update_crc_orig(newdata) + running_hash.update(newdata) + if eof and running_hash.digest() != expected_hash: + raise WheelError(f"Hash mismatch for file '{ef_name}'") + + ef_name = ( + name_or_info.filename if isinstance(name_or_info, ZipInfo) else name_or_info + ) + if ( + mode == "r" + and not ef_name.endswith("/") + and ef_name not in self._file_hashes + ): + raise WheelError(f"No hash found for file '{ef_name}'") + + ef = ZipFile.open(self, name_or_info, mode, pwd) + if mode == "r" and not ef_name.endswith("/"): + algorithm, expected_hash = self._file_hashes[ef_name] + if expected_hash is not None: + # Monkey patch the _update_crc method to also check for the hash from + # RECORD + running_hash = hashlib.new(algorithm) + update_crc_orig, ef._update_crc = ef._update_crc, _update_crc + + return ef + + def write_files(self, base_dir): + log.info(f"creating '{self.filename}' and adding '{base_dir}' to it") + deferred = [] + for root, dirnames, filenames in os.walk(base_dir): + # Sort the directory names so that `os.walk` will walk them in a + # defined order on the next iteration. + dirnames.sort() + for name in sorted(filenames): + path = os.path.normpath(os.path.join(root, name)) + if os.path.isfile(path): + arcname = os.path.relpath(path, base_dir).replace(os.path.sep, "/") + if arcname == self.record_path: + pass + elif root.endswith(".dist-info"): + deferred.append((path, arcname)) + else: + self.write(path, arcname) + + deferred.sort() + for path, arcname in deferred: + self.write(path, arcname) + + def write(self, filename, arcname=None, compress_type=None): + with open(filename, "rb") as f: + st = os.fstat(f.fileno()) + data = f.read() + + zinfo = ZipInfo( + arcname or filename, date_time=get_zipinfo_datetime(st.st_mtime) + ) + zinfo.external_attr = (stat.S_IMODE(st.st_mode) | stat.S_IFMT(st.st_mode)) << 16 + zinfo.compress_type = compress_type or self.compression + self.writestr(zinfo, data, compress_type) + + def writestr(self, zinfo_or_arcname, data, compress_type=None): + if isinstance(zinfo_or_arcname, str): + zinfo_or_arcname = ZipInfo( + zinfo_or_arcname, date_time=get_zipinfo_datetime() + ) + zinfo_or_arcname.compress_type = self.compression + zinfo_or_arcname.external_attr = (0o664 | stat.S_IFREG) << 16 + + if isinstance(data, str): + data = data.encode("utf-8") + + ZipFile.writestr(self, zinfo_or_arcname, data, compress_type) + fname = ( + zinfo_or_arcname.filename + if isinstance(zinfo_or_arcname, ZipInfo) + else zinfo_or_arcname + ) + log.info(f"adding '{fname}'") + if fname != self.record_path: + hash_ = self._default_algorithm(data) + self._file_hashes[fname] = ( + hash_.name, + urlsafe_b64encode(hash_.digest()).decode("ascii"), + ) + self._file_sizes[fname] = len(data) + + def close(self): + # Write RECORD + if self.fp is not None and self.mode == "w" and self._file_hashes: + data = StringIO() + writer = csv.writer(data, delimiter=",", quotechar='"', lineterminator="\n") + writer.writerows( + ( + (fname, algorithm + "=" + hash_, self._file_sizes[fname]) + for fname, (algorithm, hash_) in self._file_hashes.items() + ) + ) + writer.writerow((format(self.record_path), "", "")) + self.writestr(self.record_path, data.getvalue()) + + ZipFile.close(self) + + +class WheelError(Exception): + pass diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index be2742d73d..5799c06ed6 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -322,7 +322,7 @@ def run_setup(self, setup_script='setup.py'): ) def get_requires_for_build_wheel(self, config_settings=None): - return self._get_build_requires(config_settings, requirements=['wheel']) + return self._get_build_requires(config_settings, requirements=[]) def get_requires_for_build_sdist(self, config_settings=None): return self._get_build_requires(config_settings, requirements=[]) diff --git a/setuptools/command/bdist_wheel.py b/setuptools/command/bdist_wheel.py new file mode 100644 index 0000000000..ad34539eb8 --- /dev/null +++ b/setuptools/command/bdist_wheel.py @@ -0,0 +1,599 @@ +""" +Create a wheel (.whl) distribution. + +A wheel is a built archive format. +""" + +from __future__ import annotations + +import os +import re +import shutil +import stat +import struct +import sys +import sysconfig +import warnings +from email.generator import BytesGenerator, Generator +from email.policy import EmailPolicy +from distutils import log +from glob import iglob +from shutil import rmtree +from typing import TYPE_CHECKING, Callable, Iterable, Literal, Sequence, cast +from zipfile import ZIP_DEFLATED, ZIP_STORED + +from .. import Command, __version__ +from ..extern.wheel.metadata import pkginfo_to_metadata +from ..extern.packaging import tags +from ..extern.packaging import version as _packaging_version +from ..extern.wheel.wheelfile import WheelFile + +if TYPE_CHECKING: + import types + + +def safe_name(name: str) -> str: + """Convert an arbitrary string to a standard distribution name + Any runs of non-alphanumeric/. characters are replaced with a single '-'. + """ + return re.sub("[^A-Za-z0-9.]+", "-", name) + + +def safe_version(version: str) -> str: + """ + Convert an arbitrary string to a standard version string + """ + try: + # normalize the version + return str(_packaging_version.Version(version)) + except _packaging_version.InvalidVersion: + version = version.replace(" ", ".") + return re.sub("[^A-Za-z0-9.]+", "-", version) + + +setuptools_major_version = int(__version__.split(".")[0]) + +PY_LIMITED_API_PATTERN = r"cp3\d" + + +def _is_32bit_interpreter() -> bool: + return struct.calcsize("P") == 4 + + +def python_tag() -> str: + return f"py{sys.version_info[0]}" + + +def get_platform(archive_root: str | None) -> str: + """Return our platform name 'win32', 'linux_x86_64'""" + result = sysconfig.get_platform() + if result.startswith("macosx") and archive_root is not None: + from ..extern.wheel.macosx_libfile import calculate_macosx_platform_tag + + result = calculate_macosx_platform_tag(archive_root, result) + elif _is_32bit_interpreter(): + if result == "linux-x86_64": + # pip pull request #3497 + result = "linux-i686" + elif result == "linux-aarch64": + # packaging pull request #234 + # TODO armv8l, packaging pull request #690 => this did not land + # in pip/packaging yet + result = "linux-armv7l" + + return result.replace("-", "_") + + +def get_flag( + var: str, fallback: bool, expected: bool = True, warn: bool = True +) -> bool: + """Use a fallback value for determining SOABI flags if the needed config + var is unset or unavailable.""" + val = sysconfig.get_config_var(var) + if val is None: + if warn: + warnings.warn( + f"Config variable '{var}' is unset, Python ABI tag may be incorrect", + RuntimeWarning, + stacklevel=2, + ) + return fallback + return val == expected + + +def get_abi_tag() -> str | None: + """Return the ABI tag based on SOABI (if available) or emulate SOABI (PyPy2).""" + soabi: str = sysconfig.get_config_var("SOABI") + impl = tags.interpreter_name() + if not soabi and impl in ("cp", "pp") and hasattr(sys, "maxunicode"): + d = "" + m = "" + u = "" + if get_flag("Py_DEBUG", hasattr(sys, "gettotalrefcount"), warn=(impl == "cp")): + d = "d" + + if get_flag( + "WITH_PYMALLOC", + impl == "cp", + warn=(impl == "cp" and sys.version_info < (3, 8)), + ) and sys.version_info < (3, 8): + m = "m" + + abi = f"{impl}{tags.interpreter_version()}{d}{m}{u}" + elif soabi and impl == "cp" and soabi.startswith("cpython"): + # non-Windows + abi = "cp" + soabi.split("-")[1] + elif soabi and impl == "cp" and soabi.startswith("cp"): + # Windows + abi = soabi.split("-")[0] + elif soabi and impl == "pp": + # we want something like pypy36-pp73 + abi = "-".join(soabi.split("-")[:2]) + abi = abi.replace(".", "_").replace("-", "_") + elif soabi and impl == "graalpy": + abi = "-".join(soabi.split("-")[:3]) + abi = abi.replace(".", "_").replace("-", "_") + elif soabi: + abi = soabi.replace(".", "_").replace("-", "_") + else: + abi = None + + return abi + + +def safer_name(name: str) -> str: + return safe_name(name).replace("-", "_") + + +def safer_version(version: str) -> str: + return safe_version(version).replace("-", "_") + + +def remove_readonly( + func: Callable[..., object], + path: str, + excinfo: tuple[type[Exception], Exception, types.TracebackType], +) -> None: + remove_readonly_exc(func, path, excinfo[1]) + + +def remove_readonly_exc(func: Callable[..., object], path: str, exc: Exception) -> None: + os.chmod(path, stat.S_IWRITE) + func(path) + + +class bdist_wheel(Command): + description = "create a wheel distribution" + + supported_compressions = { + "stored": ZIP_STORED, + "deflated": ZIP_DEFLATED, + } + + user_options = [ + ("bdist-dir=", "b", "temporary directory for creating the distribution"), + ( + "plat-name=", + "p", + "platform name to embed in generated filenames " + f"(default: {get_platform(None)})", + ), + ( + "keep-temp", + "k", + "keep the pseudo-installation tree around after " + "creating the distribution archive", + ), + ("dist-dir=", "d", "directory to put final built distributions in"), + ("skip-build", None, "skip rebuilding everything (for testing/debugging)"), + ( + "relative", + None, + "build the archive using relative paths (default: false)", + ), + ( + "owner=", + "u", + "Owner name used when creating a tar file [default: current user]", + ), + ( + "group=", + "g", + "Group name used when creating a tar file [default: current group]", + ), + ("universal", None, "make a universal wheel (default: false)"), + ( + "compression=", + None, + "zipfile compression (one of: {}) (default: 'deflated')".format( + ", ".join(supported_compressions) + ), + ), + ( + "python-tag=", + None, + f"Python implementation compatibility tag (default: '{python_tag()}')", + ), + ( + "build-number=", + None, + "Build number for this particular version. " + "As specified in PEP-0427, this must start with a digit. " + "[default: None]", + ), + ( + "py-limited-api=", + None, + "Python tag (cp32|cp33|cpNN) for abi3 wheel tag (default: false)", + ), + ] + + boolean_options = ["keep-temp", "skip-build", "relative", "universal"] + + def initialize_options(self) -> None: + self.bdist_dir: str | None = None + self.data_dir = None + self.plat_name: str | None = None + self.plat_tag = None + self.format = "zip" + self.keep_temp = False + self.dist_dir: str | None = None + self.egginfo_dir = None + self.root_is_pure: bool | None = None + self.skip_build = None + self.relative = False + self.owner = None + self.group = None + self.universal: bool = False + self.compression: str | int = "deflated" + self.python_tag: str = python_tag() + self.build_number: str | None = None + self.py_limited_api: str | Literal[False] = False + self.plat_name_supplied = False + + def finalize_options(self): + if self.bdist_dir is None: + bdist_base = self.get_finalized_command("bdist").bdist_base + self.bdist_dir = os.path.join(bdist_base, "wheel") + + egg_info = self.distribution.get_command_obj("egg_info") + egg_info.ensure_finalized() # needed for correct `wheel_dist_name` + + self.data_dir = self.wheel_dist_name + ".data" + self.plat_name_supplied = self.plat_name is not None + + try: + self.compression = self.supported_compressions[self.compression] + except KeyError: + raise ValueError(f"Unsupported compression: {self.compression}") from None + + need_options = ("dist_dir", "plat_name", "skip_build") + + self.set_undefined_options("bdist", *zip(need_options, need_options)) + + self.root_is_pure = not ( + self.distribution.has_ext_modules() or self.distribution.has_c_libraries() + ) + + if self.py_limited_api and not re.match( + PY_LIMITED_API_PATTERN, self.py_limited_api + ): + raise ValueError(f"py-limited-api must match '{PY_LIMITED_API_PATTERN}'") + + # Support legacy [wheel] section for setting universal + wheel = self.distribution.get_option_dict("wheel") + if "universal" in wheel: + # please don't define this in your global configs + log.warning( + "The [wheel] section is deprecated. Use [bdist_wheel] instead.", + ) + val = wheel["universal"][1].strip() + if val.lower() in ("1", "true", "yes"): + self.universal = True + + if self.build_number is not None and not self.build_number[:1].isdigit(): + raise ValueError("Build tag (build-number) must start with a digit.") + + @property + def wheel_dist_name(self): + """Return distribution full name with - replaced with _""" + components = ( + safer_name(self.distribution.get_name()), + safer_version(self.distribution.get_version()), + ) + if self.build_number: + components += (self.build_number,) + return "-".join(components) + + def get_tag(self) -> tuple[str, str, str]: + # bdist sets self.plat_name if unset, we should only use it for purepy + # wheels if the user supplied it. + if self.plat_name_supplied: + plat_name = cast(str, self.plat_name) + elif self.root_is_pure: + plat_name = "any" + else: + # macosx contains system version in platform name so need special handle + if self.plat_name and not self.plat_name.startswith("macosx"): + plat_name = self.plat_name + else: + # on macosx always limit the platform name to comply with any + # c-extension modules in bdist_dir, since the user can specify + # a higher MACOSX_DEPLOYMENT_TARGET via tools like CMake + + # on other platforms, and on macosx if there are no c-extension + # modules, use the default platform name. + plat_name = get_platform(self.bdist_dir) + + if _is_32bit_interpreter(): + if plat_name in ("linux-x86_64", "linux_x86_64"): + plat_name = "linux_i686" + if plat_name in ("linux-aarch64", "linux_aarch64"): + # TODO armv8l, packaging pull request #690 => this did not land + # in pip/packaging yet + plat_name = "linux_armv7l" + + plat_name = ( + plat_name.lower().replace("-", "_").replace(".", "_").replace(" ", "_") + ) + + if self.root_is_pure: + if self.universal: + impl = "py2.py3" + else: + impl = self.python_tag + tag = (impl, "none", plat_name) + else: + impl_name = tags.interpreter_name() + impl_ver = tags.interpreter_version() + impl = impl_name + impl_ver + # We don't work on CPython 3.1, 3.0. + if self.py_limited_api and (impl_name + impl_ver).startswith("cp3"): + impl = self.py_limited_api + abi_tag = "abi3" + else: + abi_tag = str(get_abi_tag()).lower() + tag = (impl, abi_tag, plat_name) + # issue gh-374: allow overriding plat_name + supported_tags = [ + (t.interpreter, t.abi, plat_name) for t in tags.sys_tags() + ] + assert ( + tag in supported_tags + ), f"would build wheel with unsupported tag {tag}" + return tag + + def run(self): + build_scripts = self.reinitialize_command("build_scripts") + build_scripts.executable = "python" + build_scripts.force = True + + build_ext = self.reinitialize_command("build_ext") + build_ext.inplace = False + + if not self.skip_build: + self.run_command("build") + + install = self.reinitialize_command("install", reinit_subcommands=True) + install.root = self.bdist_dir + install.compile = False + install.skip_build = self.skip_build + install.warn_dir = False + + # A wheel without setuptools scripts is more cross-platform. + # Use the (undocumented) `no_ep` option to setuptools' + # install_scripts command to avoid creating entry point scripts. + install_scripts = self.reinitialize_command("install_scripts") + install_scripts.no_ep = True + + # Use a custom scheme for the archive, because we have to decide + # at installation time which scheme to use. + for key in ("headers", "scripts", "data", "purelib", "platlib"): + setattr(install, "install_" + key, os.path.join(self.data_dir, key)) + + basedir_observed = "" + + if os.name == "nt": + # win32 barfs if any of these are ''; could be '.'? + # (distutils.command.install:change_roots bug) + basedir_observed = os.path.normpath(os.path.join(self.data_dir, "..")) + self.install_libbase = self.install_lib = basedir_observed + + setattr( + install, + "install_purelib" if self.root_is_pure else "install_platlib", + basedir_observed, + ) + + log.info(f"installing to {self.bdist_dir}") + + self.run_command("install") + + impl_tag, abi_tag, plat_tag = self.get_tag() + archive_basename = f"{self.wheel_dist_name}-{impl_tag}-{abi_tag}-{plat_tag}" + if not self.relative: + archive_root = self.bdist_dir + else: + archive_root = os.path.join( + self.bdist_dir, self._ensure_relative(install.install_base) + ) + + self.set_undefined_options("install_egg_info", ("target", "egginfo_dir")) + distinfo_dirname = ( + f"{safer_name(self.distribution.get_name())}-" + f"{safer_version(self.distribution.get_version())}.dist-info" + ) + distinfo_dir = os.path.join(self.bdist_dir, distinfo_dirname) + self.egg2dist(self.egginfo_dir, distinfo_dir) + + self.write_wheelfile(distinfo_dir) + + # Make the archive + if not os.path.exists(self.dist_dir): + os.makedirs(self.dist_dir) + + wheel_path = os.path.join(self.dist_dir, archive_basename + ".whl") + with WheelFile(wheel_path, "w", self.compression) as wf: + wf.write_files(archive_root) + + # Add to 'Distribution.dist_files' so that the "upload" command works + getattr(self.distribution, "dist_files", []).append(( + "bdist_wheel", + "{}.{}".format(*sys.version_info[:2]), # like 3.7 + wheel_path, + )) + + if not self.keep_temp: + log.info(f"removing {self.bdist_dir}") + if not self.dry_run: + if sys.version_info < (3, 12): + rmtree(self.bdist_dir, onerror=remove_readonly) + else: + rmtree(self.bdist_dir, onexc=remove_readonly_exc) + + def write_wheelfile( + self, wheelfile_base: str, generator: str = f"setuptools ({__version__})" + ): + from email.message import Message + + msg = Message() + msg["Wheel-Version"] = "1.0" # of the spec + msg["Generator"] = generator + msg["Root-Is-Purelib"] = str(self.root_is_pure).lower() + if self.build_number is not None: + msg["Build"] = self.build_number + + # Doesn't work for bdist_wininst + impl_tag, abi_tag, plat_tag = self.get_tag() + for impl in impl_tag.split("."): + for abi in abi_tag.split("."): + for plat in plat_tag.split("."): + msg["Tag"] = "-".join((impl, abi, plat)) + + wheelfile_path = os.path.join(wheelfile_base, "WHEEL") + log.info(f"creating {wheelfile_path}") + with open(wheelfile_path, "wb") as f: + BytesGenerator(f, maxheaderlen=0).flatten(msg) + + def _ensure_relative(self, path: str) -> str: + # copied from dir_util, deleted + drive, path = os.path.splitdrive(path) + if path[0:1] == os.sep: + path = drive + path[1:] + return path + + @property + def license_paths(self) -> Iterable[str]: + if setuptools_major_version >= 57: + # Setuptools has resolved any patterns to actual file names + return self.distribution.metadata.license_files or () + + files: set[str] = set() + metadata = self.distribution.get_option_dict("metadata") + if setuptools_major_version >= 42: + # Setuptools recognizes the license_files option but does not do globbing + patterns = cast(Sequence[str], self.distribution.metadata.license_files) + else: + # Prior to those, wheel is entirely responsible for handling license files + if "license_files" in metadata: + patterns = metadata["license_files"][1].split() + else: + patterns = () + + if "license_file" in metadata: + warnings.warn( + 'The "license_file" option is deprecated. Use "license_files" instead.', + DeprecationWarning, + stacklevel=2, + ) + files.add(metadata["license_file"][1]) + + if not files and not patterns and not isinstance(patterns, list): + patterns = ("LICEN[CS]E*", "COPYING*", "NOTICE*", "AUTHORS*") + + for pattern in patterns: + for path in iglob(pattern): + if path.endswith("~"): + log.debug( + f'ignoring license file "{path}" as it looks like a backup' + ) + continue + + if path not in files and os.path.isfile(path): + log.info( + f'adding license file "{path}" (matched pattern "{pattern}")' + ) + files.add(path) + + return files + + def egg2dist(self, egginfo_path: str, distinfo_path: str): + """Convert an .egg-info directory into a .dist-info directory""" + + def adios(p: str) -> None: + """Appropriately delete directory, file or link.""" + if os.path.exists(p) and not os.path.islink(p) and os.path.isdir(p): + shutil.rmtree(p) + elif os.path.exists(p): + os.unlink(p) + + adios(distinfo_path) + + if not os.path.exists(egginfo_path): + # There is no egg-info. This is probably because the egg-info + # file/directory is not named matching the distribution name used + # to name the archive file. Check for this case and report + # accordingly. + import glob + + pat = os.path.join(os.path.dirname(egginfo_path), "*.egg-info") + possible = glob.glob(pat) + err = f"Egg metadata expected at {egginfo_path} but not found" + if possible: + alt = os.path.basename(possible[0]) + err += f" ({alt} found - possible misnamed archive file?)" + + raise ValueError(err) + + if os.path.isfile(egginfo_path): + # .egg-info is a single file + pkg_info = pkginfo_to_metadata(egginfo_path, egginfo_path) + os.mkdir(distinfo_path) + else: + # .egg-info is a directory + pkginfo_path = os.path.join(egginfo_path, "PKG-INFO") + pkg_info = pkginfo_to_metadata(egginfo_path, pkginfo_path) + + # ignore common egg metadata that is useless to wheel + shutil.copytree( + egginfo_path, + distinfo_path, + ignore=lambda x, y: { + "PKG-INFO", + "requires.txt", + "SOURCES.txt", + "not-zip-safe", + }, + ) + + # delete dependency_links if it is only whitespace + dependency_links_path = os.path.join(distinfo_path, "dependency_links.txt") + with open(dependency_links_path, encoding="utf-8") as dependency_links_file: + dependency_links = dependency_links_file.read().strip() + if not dependency_links: + adios(dependency_links_path) + + pkg_info_path = os.path.join(distinfo_path, "METADATA") + serialization_policy = EmailPolicy( + utf8=True, + mangle_from_=False, + max_line_length=0, + ) + with open(pkg_info_path, "w", encoding="utf-8") as out: + Generator(out, policy=serialization_policy).flatten(pkg_info) + + for license_path in self.license_paths: + filename = os.path.basename(license_path) + shutil.copy(license_path, os.path.join(distinfo_path, filename)) + + adios(egginfo_path) diff --git a/setuptools/command/editable_wheel.py b/setuptools/command/editable_wheel.py index b8ed84750a..a835a8194b 100644 --- a/setuptools/command/editable_wheel.py +++ b/setuptools/command/editable_wheel.py @@ -59,7 +59,7 @@ from .install_scripts import install_scripts as install_scripts_cls if TYPE_CHECKING: - from wheel.wheelfile import WheelFile # type:ignore[import-untyped] # noqa + from .._vendor.wheel.wheelfile import WheelFile _P = TypeVar("_P", bound=StrPath) _logger = logging.getLogger(__name__) @@ -335,7 +335,7 @@ def _safely_run(self, cmd_name: str): ) def _create_wheel_file(self, bdist_wheel): - from wheel.wheelfile import WheelFile + from ..extern.wheel.wheelfile import WheelFile dist_info = self.get_finalized_command("dist_info") dist_name = dist_info.name diff --git a/setuptools/dist.py b/setuptools/dist.py index 4ccb915902..80ae589d4f 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -710,6 +710,12 @@ def get_command_class(self, command): if command in self.cmdclass: return self.cmdclass[command] + # Special case bdist_wheel so it's never loaded from "wheel" + if command == 'bdist_wheel': + from .command.bdist_wheel import bdist_wheel + + return bdist_wheel + eps = metadata.entry_points(group='distutils.commands', name=command) for ep in eps: self.cmdclass[command] = cmdclass = ep.load() diff --git a/setuptools/extern/__init__.py b/setuptools/extern/__init__.py index 8eb02ac6d3..5ad7169e3b 100644 --- a/setuptools/extern/__init__.py +++ b/setuptools/extern/__init__.py @@ -85,6 +85,7 @@ def install(self): 'ordered_set', 'packaging', 'tomli', + 'wheel', 'zipp', ) # [[[end]]] diff --git a/setuptools/tests/bdist_wheel_testdata/abi3extension.dist/extension.c b/setuptools/tests/bdist_wheel_testdata/abi3extension.dist/extension.c new file mode 100644 index 0000000000..a37c3fa2dc --- /dev/null +++ b/setuptools/tests/bdist_wheel_testdata/abi3extension.dist/extension.c @@ -0,0 +1,2 @@ +#define Py_LIMITED_API 0x03020000 +#include diff --git a/setuptools/tests/bdist_wheel_testdata/abi3extension.dist/setup.cfg b/setuptools/tests/bdist_wheel_testdata/abi3extension.dist/setup.cfg new file mode 100644 index 0000000000..9f6ff39a0f --- /dev/null +++ b/setuptools/tests/bdist_wheel_testdata/abi3extension.dist/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +py_limited_api=cp32 diff --git a/setuptools/tests/bdist_wheel_testdata/abi3extension.dist/setup.py b/setuptools/tests/bdist_wheel_testdata/abi3extension.dist/setup.py new file mode 100644 index 0000000000..5962bd1552 --- /dev/null +++ b/setuptools/tests/bdist_wheel_testdata/abi3extension.dist/setup.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +from setuptools import Extension, setup + +setup( + name="extension.dist", + version="0.1", + description="A testing distribution \N{SNOWMAN}", + ext_modules=[ + Extension(name="extension", sources=["extension.c"], py_limited_api=True) + ], +) diff --git a/setuptools/tests/bdist_wheel_testdata/commasinfilenames.dist/mypackage/__init__.py b/setuptools/tests/bdist_wheel_testdata/commasinfilenames.dist/mypackage/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/setuptools/tests/bdist_wheel_testdata/commasinfilenames.dist/mypackage/data/1,2,3.txt b/setuptools/tests/bdist_wheel_testdata/commasinfilenames.dist/mypackage/data/1,2,3.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/setuptools/tests/bdist_wheel_testdata/commasinfilenames.dist/mypackage/data/__init__.py b/setuptools/tests/bdist_wheel_testdata/commasinfilenames.dist/mypackage/data/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/setuptools/tests/bdist_wheel_testdata/commasinfilenames.dist/setup.py b/setuptools/tests/bdist_wheel_testdata/commasinfilenames.dist/setup.py new file mode 100644 index 0000000000..a2783a3b62 --- /dev/null +++ b/setuptools/tests/bdist_wheel_testdata/commasinfilenames.dist/setup.py @@ -0,0 +1,12 @@ +from __future__ import annotations + +from setuptools import setup + +setup( + name="testrepo", + version="0.1", + packages=["mypackage"], + description="A test package with commas in file names", + include_package_data=True, + package_data={"mypackage.data": ["*"]}, +) diff --git a/setuptools/tests/bdist_wheel_testdata/commasinfilenames.dist/testrepo-0.1.0/mypackage/__init__.py b/setuptools/tests/bdist_wheel_testdata/commasinfilenames.dist/testrepo-0.1.0/mypackage/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/setuptools/tests/bdist_wheel_testdata/complex-dist/complexdist/__init__.py b/setuptools/tests/bdist_wheel_testdata/complex-dist/complexdist/__init__.py new file mode 100644 index 0000000000..88aa7b76a4 --- /dev/null +++ b/setuptools/tests/bdist_wheel_testdata/complex-dist/complexdist/__init__.py @@ -0,0 +1,5 @@ +from __future__ import annotations + + +def main(): + return diff --git a/setuptools/tests/bdist_wheel_testdata/complex-dist/setup.py b/setuptools/tests/bdist_wheel_testdata/complex-dist/setup.py new file mode 100644 index 0000000000..e0439d9ef4 --- /dev/null +++ b/setuptools/tests/bdist_wheel_testdata/complex-dist/setup.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from setuptools import setup + +setup( + name="complex-dist", + version="0.1", + description="Another testing distribution \N{SNOWMAN}", + long_description="Another testing distribution \N{SNOWMAN}", + author="Illustrious Author", + author_email="illustrious@example.org", + url="http://example.org/exemplary", + packages=["complexdist"], + setup_requires=["wheel", "setuptools"], + install_requires=["quux", "splort"], + extras_require={"simple": ["simple.dist"]}, + tests_require=["foo", "bar>=10.0.0"], + entry_points={ + "console_scripts": [ + "complex-dist=complexdist:main", + "complex-dist2=complexdist:main", + ], + }, +) diff --git a/setuptools/tests/bdist_wheel_testdata/extension.dist/extension.abi3.so b/setuptools/tests/bdist_wheel_testdata/extension.dist/extension.abi3.so new file mode 100644 index 0000000000..cf9e0b0a49 Binary files /dev/null and b/setuptools/tests/bdist_wheel_testdata/extension.dist/extension.abi3.so differ diff --git a/setuptools/tests/bdist_wheel_testdata/extension.dist/extension.c b/setuptools/tests/bdist_wheel_testdata/extension.dist/extension.c new file mode 100644 index 0000000000..26403efa82 --- /dev/null +++ b/setuptools/tests/bdist_wheel_testdata/extension.dist/extension.c @@ -0,0 +1,17 @@ +#include + +static PyMethodDef methods[] = { + { NULL, NULL, 0, NULL } +}; + +static struct PyModuleDef module_def = { + PyModuleDef_HEAD_INIT, + "extension", + "Dummy extension module", + -1, + methods +}; + +PyMODINIT_FUNC PyInit_extension(void) { + return PyModule_Create(&module_def); +} diff --git a/setuptools/tests/bdist_wheel_testdata/extension.dist/setup.py b/setuptools/tests/bdist_wheel_testdata/extension.dist/setup.py new file mode 100644 index 0000000000..9a6eed8cfd --- /dev/null +++ b/setuptools/tests/bdist_wheel_testdata/extension.dist/setup.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from setuptools import Extension, setup + +setup( + name="extension.dist", + version="0.1", + description="A testing distribution \N{SNOWMAN}", + ext_modules=[Extension(name="extension", sources=["extension.c"])], +) diff --git a/setuptools/tests/bdist_wheel_testdata/headers.dist/header.h b/setuptools/tests/bdist_wheel_testdata/headers.dist/header.h new file mode 100644 index 0000000000..e69de29bb2 diff --git a/setuptools/tests/bdist_wheel_testdata/headers.dist/headersdist.py b/setuptools/tests/bdist_wheel_testdata/headers.dist/headersdist.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/setuptools/tests/bdist_wheel_testdata/headers.dist/setup.cfg b/setuptools/tests/bdist_wheel_testdata/headers.dist/setup.cfg new file mode 100644 index 0000000000..3c6e79cf31 --- /dev/null +++ b/setuptools/tests/bdist_wheel_testdata/headers.dist/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal=1 diff --git a/setuptools/tests/bdist_wheel_testdata/headers.dist/setup.py b/setuptools/tests/bdist_wheel_testdata/headers.dist/setup.py new file mode 100644 index 0000000000..6cf9b46faf --- /dev/null +++ b/setuptools/tests/bdist_wheel_testdata/headers.dist/setup.py @@ -0,0 +1,10 @@ +from __future__ import annotations + +from setuptools import setup + +setup( + name="headers.dist", + version="0.1", + description="A distribution with headers", + headers=["header.h"], +) diff --git a/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/libb.dylib b/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/libb.dylib new file mode 100644 index 0000000000..25c954656b Binary files /dev/null and b/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/libb.dylib differ diff --git a/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib.c b/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib.c new file mode 100644 index 0000000000..dfa2268167 --- /dev/null +++ b/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib.c @@ -0,0 +1,13 @@ +int num_of_letters(char* text){ + int num = 0; + char * lett = text; + while (lett != 0){ + if (*lett >= 'a' && *lett <= 'z'){ + num += 1; + } else if (*lett >= 'A' && *lett <= 'Z'){ + num += 1; + } + lett += 1; + } + return num; +} diff --git a/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_10.dylib b/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_10.dylib new file mode 100644 index 0000000000..eaf1a94e5f Binary files /dev/null and b/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_10.dylib differ diff --git a/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_10_10.dylib b/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_10_10.dylib new file mode 100644 index 0000000000..229d115f8f Binary files /dev/null and b/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_10_10.dylib differ diff --git a/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_10_386.dylib b/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_10_386.dylib new file mode 100644 index 0000000000..8f543875d6 Binary files /dev/null and b/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_10_386.dylib differ diff --git a/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_10_fat.dylib b/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_10_fat.dylib new file mode 100644 index 0000000000..6c095127da Binary files /dev/null and b/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_10_fat.dylib differ diff --git a/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_14.dylib b/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_14.dylib new file mode 100644 index 0000000000..c9024ccc4a Binary files /dev/null and b/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_14.dylib differ diff --git a/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_14_386.dylib b/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_14_386.dylib new file mode 100644 index 0000000000..c85b71691e Binary files /dev/null and b/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_14_386.dylib differ diff --git a/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_14_fat.dylib b/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_14_fat.dylib new file mode 100644 index 0000000000..4bb0940739 Binary files /dev/null and b/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_14_fat.dylib differ diff --git a/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_6.dylib b/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_6.dylib new file mode 100644 index 0000000000..80401eed54 Binary files /dev/null and b/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_6.dylib differ diff --git a/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_6_386.dylib b/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_6_386.dylib new file mode 100644 index 0000000000..1e48cd8532 Binary files /dev/null and b/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_6_386.dylib differ diff --git a/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_6_fat.dylib b/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_6_fat.dylib new file mode 100644 index 0000000000..f4ffaeec25 Binary files /dev/null and b/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_6_fat.dylib differ diff --git a/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_9_universal2.dylib b/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_9_universal2.dylib new file mode 100755 index 0000000000..26ab109c99 Binary files /dev/null and b/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_10_9_universal2.dylib differ diff --git a/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_11.dylib b/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_11.dylib new file mode 100644 index 0000000000..80202c11ba Binary files /dev/null and b/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_11.dylib differ diff --git a/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_multiple_fat.dylib b/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_multiple_fat.dylib new file mode 100644 index 0000000000..5f7fd5091e Binary files /dev/null and b/setuptools/tests/bdist_wheel_testdata/macosx_minimal_system_version/test_lib_multiple_fat.dylib differ diff --git a/setuptools/tests/bdist_wheel_testdata/simple.dist/setup.py b/setuptools/tests/bdist_wheel_testdata/simple.dist/setup.py new file mode 100644 index 0000000000..1e7a78a224 --- /dev/null +++ b/setuptools/tests/bdist_wheel_testdata/simple.dist/setup.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +from setuptools import setup + +setup( + name="simple.dist", + version="0.1", + description="A testing distribution \N{SNOWMAN}", + packages=["simpledist"], + extras_require={"voting": ["beaglevote"]}, +) diff --git a/setuptools/tests/bdist_wheel_testdata/simple.dist/simpledist/__init__.py b/setuptools/tests/bdist_wheel_testdata/simple.dist/simpledist/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/setuptools/tests/bdist_wheel_testdata/unicode.dist/setup.py b/setuptools/tests/bdist_wheel_testdata/unicode.dist/setup.py new file mode 100644 index 0000000000..ec66d1e6af --- /dev/null +++ b/setuptools/tests/bdist_wheel_testdata/unicode.dist/setup.py @@ -0,0 +1,11 @@ +from __future__ import annotations + +from setuptools import setup + +setup( + name="unicode.dist", + version="0.1", + description="A testing distribution \N{SNOWMAN}", + packages=["unicodedist"], + zip_safe=True, +) diff --git a/setuptools/tests/bdist_wheel_testdata/unicode.dist/unicodedist/__init__.py b/setuptools/tests/bdist_wheel_testdata/unicode.dist/unicodedist/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git "a/setuptools/tests/bdist_wheel_testdata/unicode.dist/unicodedist/\303\245\303\244\303\266_\346\227\245\346\234\254\350\252\236.py" "b/setuptools/tests/bdist_wheel_testdata/unicode.dist/unicodedist/\303\245\303\244\303\266_\346\227\245\346\234\254\350\252\236.py" new file mode 100644 index 0000000000..e69de29bb2 diff --git a/setuptools/tests/test_bdist_wheel.py b/setuptools/tests/test_bdist_wheel.py new file mode 100644 index 0000000000..5d28368c88 --- /dev/null +++ b/setuptools/tests/test_bdist_wheel.py @@ -0,0 +1,498 @@ +from __future__ import annotations + +import builtins +import importlib +import os.path +import platform +import shutil +import stat +import struct +import subprocess +import sys +import sysconfig +from contextlib import suppress +from functools import partial +from inspect import cleandoc +from unittest.mock import Mock +from zipfile import ZipFile + +import pytest +import setuptools +from setuptools.command.bdist_wheel import ( + bdist_wheel, + get_abi_tag, + remove_readonly, + remove_readonly_exc, +) +from setuptools.extern.packaging import tags +from setuptools.extern.wheel.wheelfile import WheelFile + +DEFAULT_FILES = { + "dummy_dist-1.0.dist-info/top_level.txt", + "dummy_dist-1.0.dist-info/METADATA", + "dummy_dist-1.0.dist-info/WHEEL", + "dummy_dist-1.0.dist-info/RECORD", +} +DEFAULT_LICENSE_FILES = { + "LICENSE", + "LICENSE.txt", + "LICENCE", + "LICENCE.txt", + "COPYING", + "COPYING.md", + "NOTICE", + "NOTICE.rst", + "AUTHORS", + "AUTHORS.txt", +} +OTHER_IGNORED_FILES = { + "LICENSE~", + "AUTHORS~", +} +SETUPPY_EXAMPLE = """\ +from setuptools import setup + +setup( + name='dummy_dist', + version='1.0', +) +""" + + +@pytest.fixture(scope="module") +def wheel_paths(request, tmp_path_factory): + test_distributions = ( + "complex-dist", + "simple.dist", + "headers.dist", + "commasinfilenames.dist", + "unicode.dist", + ) + + if sys.platform != "win32": + # ABI3 extensions don't really work on Windows + test_distributions += ("abi3extension.dist",) + + pwd = os.path.abspath(os.curdir) + request.addfinalizer(partial(os.chdir, pwd)) + this_dir = os.path.dirname(__file__) + build_dir = tmp_path_factory.mktemp("build") + dist_dir = tmp_path_factory.mktemp("dist") + for dist in test_distributions: + os.chdir(os.path.join(this_dir, "bdist_wheel_testdata", dist)) + subprocess.check_call([ + sys.executable, + "setup.py", + "bdist_wheel", + "-b", + str(build_dir), + "-d", + str(dist_dir), + ]) + + return sorted(str(fname) for fname in dist_dir.iterdir() if fname.suffix == ".whl") + + +@pytest.fixture +def dummy_dist(tmp_path_factory): + basedir = tmp_path_factory.mktemp("dummy_dist") + basedir.joinpath("setup.py").write_text(SETUPPY_EXAMPLE, encoding="utf-8") + for fname in DEFAULT_LICENSE_FILES | OTHER_IGNORED_FILES: + basedir.joinpath(fname).write_text("", encoding="utf-8") + + licensedir = basedir.joinpath("licenses") + licensedir.mkdir() + licensedir.joinpath("DUMMYFILE").write_text("", encoding="utf-8") + return basedir + + +def test_no_scripts(wheel_paths): + """Make sure entry point scripts are not generated.""" + path = next(path for path in wheel_paths if "complex_dist" in path) + for entry in ZipFile(path).infolist(): + assert ".data/scripts/" not in entry.filename + + +def test_unicode_record(wheel_paths): + path = next(path for path in wheel_paths if "unicode.dist" in path) + with ZipFile(path) as zf: + record = zf.read("unicode.dist-0.1.dist-info/RECORD") + + assert "åäö_日本語.py".encode() in record + + +UTF8_PKG_INFO = """\ +Metadata-Version: 2.1 +Name: helloworld +Version: 42 +Author-email: "John X. Ãørçeč" , Γαμα קּ 東 + + +UTF-8 描述 説明 +""" + + +def test_preserve_unicode_metadata(monkeypatch, tmp_path): + monkeypatch.chdir(tmp_path) + egginfo = tmp_path / "dummy_dist.egg-info" + distinfo = tmp_path / "dummy_dist.dist-info" + + egginfo.mkdir() + (egginfo / "PKG-INFO").write_text(UTF8_PKG_INFO, encoding="utf-8") + (egginfo / "dependency_links.txt").touch() + + class simpler_bdist_wheel(bdist_wheel): + """Avoid messing with setuptools/distutils internals""" + + def __init__(self): + pass + + @property + def license_paths(self): + return [] + + cmd_obj = simpler_bdist_wheel() + cmd_obj.egg2dist(egginfo, distinfo) + + metadata = (distinfo / "METADATA").read_text(encoding="utf-8") + assert 'Author-email: "John X. Ãørçeč"' in metadata + assert "Γαμα קּ 東 " in metadata + assert "UTF-8 描述 説明" in metadata + + +def test_licenses_default(dummy_dist, monkeypatch, tmp_path): + monkeypatch.chdir(dummy_dist) + subprocess.check_call([ + sys.executable, + "setup.py", + "bdist_wheel", + "-b", + str(tmp_path), + "--universal", + ]) + with WheelFile("dist/dummy_dist-1.0-py2.py3-none-any.whl") as wf: + license_files = { + "dummy_dist-1.0.dist-info/" + fname for fname in DEFAULT_LICENSE_FILES + } + assert set(wf.namelist()) == DEFAULT_FILES | license_files + + +def test_licenses_deprecated(dummy_dist, monkeypatch, tmp_path): + dummy_dist.joinpath("setup.cfg").write_text( + "[metadata]\nlicense_file=licenses/DUMMYFILE", encoding="utf-8" + ) + monkeypatch.chdir(dummy_dist) + subprocess.check_call([ + sys.executable, + "setup.py", + "bdist_wheel", + "-b", + str(tmp_path), + "--universal", + ]) + with WheelFile("dist/dummy_dist-1.0-py2.py3-none-any.whl") as wf: + license_files = {"dummy_dist-1.0.dist-info/DUMMYFILE"} + assert set(wf.namelist()) == DEFAULT_FILES | license_files + + +@pytest.mark.parametrize( + "config_file, config", + [ + ("setup.cfg", "[metadata]\nlicense_files=licenses/*\n LICENSE"), + ("setup.cfg", "[metadata]\nlicense_files=licenses/*, LICENSE"), + ( + "setup.py", + SETUPPY_EXAMPLE.replace( + ")", " license_files=['licenses/DUMMYFILE', 'LICENSE'])" + ), + ), + ], +) +def test_licenses_override(dummy_dist, monkeypatch, tmp_path, config_file, config): + dummy_dist.joinpath(config_file).write_text(config, encoding="utf-8") + monkeypatch.chdir(dummy_dist) + subprocess.check_call([ + sys.executable, + "setup.py", + "bdist_wheel", + "-b", + str(tmp_path), + "--universal", + ]) + with WheelFile("dist/dummy_dist-1.0-py2.py3-none-any.whl") as wf: + license_files = { + "dummy_dist-1.0.dist-info/" + fname for fname in {"DUMMYFILE", "LICENSE"} + } + assert set(wf.namelist()) == DEFAULT_FILES | license_files + + +def test_licenses_disabled(dummy_dist, monkeypatch, tmp_path): + dummy_dist.joinpath("setup.cfg").write_text( + "[metadata]\nlicense_files=\n", encoding="utf-8" + ) + monkeypatch.chdir(dummy_dist) + subprocess.check_call([ + sys.executable, + "setup.py", + "bdist_wheel", + "-b", + str(tmp_path), + "--universal", + ]) + with WheelFile("dist/dummy_dist-1.0-py2.py3-none-any.whl") as wf: + assert set(wf.namelist()) == DEFAULT_FILES + + +def test_build_number(dummy_dist, monkeypatch, tmp_path): + monkeypatch.chdir(dummy_dist) + subprocess.check_call([ + sys.executable, + "setup.py", + "bdist_wheel", + "-b", + str(tmp_path), + "--universal", + "--build-number=2", + ]) + with WheelFile("dist/dummy_dist-1.0-2-py2.py3-none-any.whl") as wf: + filenames = set(wf.namelist()) + assert "dummy_dist-1.0.dist-info/RECORD" in filenames + assert "dummy_dist-1.0.dist-info/METADATA" in filenames + + +def test_limited_abi(monkeypatch, tmp_path): + """Test that building a binary wheel with the limited ABI works.""" + this_dir = os.path.dirname(__file__) + source_dir = os.path.join(this_dir, "bdist_wheel_testdata", "extension.dist") + build_dir = tmp_path.joinpath("build") + dist_dir = tmp_path.joinpath("dist") + monkeypatch.chdir(source_dir) + subprocess.check_call([ + sys.executable, + "setup.py", + "bdist_wheel", + "-b", + str(build_dir), + "-d", + str(dist_dir), + ]) + + +def test_build_from_readonly_tree(dummy_dist, monkeypatch, tmp_path): + basedir = str(tmp_path.joinpath("dummy")) + shutil.copytree(str(dummy_dist), basedir) + monkeypatch.chdir(basedir) + + # Make the tree read-only + for root, _dirs, files in os.walk(basedir): + for fname in files: + os.chmod(os.path.join(root, fname), stat.S_IREAD) + + subprocess.check_call([sys.executable, "setup.py", "bdist_wheel"]) + + +@pytest.mark.parametrize( + "option, compress_type", + list(bdist_wheel.supported_compressions.items()), + ids=list(bdist_wheel.supported_compressions), +) +def test_compression(dummy_dist, monkeypatch, tmp_path, option, compress_type): + monkeypatch.chdir(dummy_dist) + subprocess.check_call([ + sys.executable, + "setup.py", + "bdist_wheel", + "-b", + str(tmp_path), + "--universal", + f"--compression={option}", + ]) + with WheelFile("dist/dummy_dist-1.0-py2.py3-none-any.whl") as wf: + filenames = set(wf.namelist()) + assert "dummy_dist-1.0.dist-info/RECORD" in filenames + assert "dummy_dist-1.0.dist-info/METADATA" in filenames + for zinfo in wf.filelist: + assert zinfo.compress_type == compress_type + + +def test_wheelfile_line_endings(wheel_paths): + for path in wheel_paths: + with WheelFile(path) as wf: + wheelfile = next(fn for fn in wf.filelist if fn.filename.endswith("WHEEL")) + wheelfile_contents = wf.read(wheelfile) + assert b"\r" not in wheelfile_contents + + +def test_unix_epoch_timestamps(dummy_dist, monkeypatch, tmp_path): + monkeypatch.setenv("SOURCE_DATE_EPOCH", "0") + monkeypatch.chdir(dummy_dist) + subprocess.check_call([ + sys.executable, + "setup.py", + "bdist_wheel", + "-b", + str(tmp_path), + "--universal", + "--build-number=2", + ]) + + +def test_get_abi_tag_windows(monkeypatch): + monkeypatch.setattr(tags, "interpreter_name", lambda: "cp") + monkeypatch.setattr(sysconfig, "get_config_var", lambda x: "cp313-win_amd64") + assert get_abi_tag() == "cp313" + + +def test_get_abi_tag_pypy_old(monkeypatch): + monkeypatch.setattr(tags, "interpreter_name", lambda: "pp") + monkeypatch.setattr(sysconfig, "get_config_var", lambda x: "pypy36-pp73") + assert get_abi_tag() == "pypy36_pp73" + + +def test_get_abi_tag_pypy_new(monkeypatch): + monkeypatch.setattr(sysconfig, "get_config_var", lambda x: "pypy37-pp73-darwin") + monkeypatch.setattr(tags, "interpreter_name", lambda: "pp") + assert get_abi_tag() == "pypy37_pp73" + + +def test_get_abi_tag_graalpy(monkeypatch): + monkeypatch.setattr( + sysconfig, "get_config_var", lambda x: "graalpy231-310-native-x86_64-linux" + ) + monkeypatch.setattr(tags, "interpreter_name", lambda: "graalpy") + assert get_abi_tag() == "graalpy231_310_native" + + +def test_get_abi_tag_fallback(monkeypatch): + monkeypatch.setattr(sysconfig, "get_config_var", lambda x: "unknown-python-310") + monkeypatch.setattr(tags, "interpreter_name", lambda: "unknown-python") + assert get_abi_tag() == "unknown_python_310" + + +def test_platform_with_space(dummy_dist, monkeypatch): + """Ensure building on platforms with a space in the name succeed.""" + monkeypatch.chdir(dummy_dist) + subprocess.check_call([ + sys.executable, + "setup.py", + "bdist_wheel", + "--plat-name", + "isilon onefs", + ]) + + +def test_rmtree_readonly(monkeypatch, tmp_path): + """Verify onerr works as expected""" + + bdist_dir = tmp_path / "with_readonly" + bdist_dir.mkdir() + some_file = bdist_dir.joinpath("file.txt") + some_file.touch() + some_file.chmod(stat.S_IREAD) + + expected_count = 1 if sys.platform.startswith("win") else 0 + + if sys.version_info < (3, 12): + count_remove_readonly = Mock(side_effect=remove_readonly) + shutil.rmtree(bdist_dir, onerror=count_remove_readonly) + assert count_remove_readonly.call_count == expected_count + else: + count_remove_readonly_exc = Mock(side_effect=remove_readonly_exc) + shutil.rmtree(bdist_dir, onexc=count_remove_readonly_exc) + assert count_remove_readonly_exc.call_count == expected_count + + assert not bdist_dir.is_dir() + + +def test_data_dir_with_tag_build(monkeypatch, tmp_path): + """ + Setuptools allow authors to set PEP 440's local version segments + using ``egg_info.tag_build``. This should be reflected not only in the + ``.whl`` file name, but also in the ``.dist-info`` and ``.data`` dirs. + See pypa/setuptools#3997. + """ + monkeypatch.chdir(tmp_path) + files = { + "setup.py": """ + from setuptools import setup + setup(headers=["hello.h"]) + """, + "setup.cfg": """ + [metadata] + name = test + version = 1.0 + + [options.data_files] + hello/world = file.txt + + [egg_info] + tag_build = +what + tag_date = 0 + """, + "file.txt": "", + "hello.h": "", + } + for file, content in files.items(): + with open(file, "w", encoding="utf-8") as fh: + fh.write(cleandoc(content)) + + subprocess.check_call([sys.executable, "setup.py", "bdist_wheel"]) + + # Ensure .whl, .dist-info and .data contain the local segment + wheel_path = "dist/test-1.0+what-py3-none-any.whl" + assert os.path.exists(wheel_path) + entries = set(ZipFile(wheel_path).namelist()) + for expected in ( + "test-1.0+what.data/headers/hello.h", + "test-1.0+what.data/data/hello/world/file.txt", + "test-1.0+what.dist-info/METADATA", + "test-1.0+what.dist-info/WHEEL", + ): + assert expected in entries + + for not_expected in ( + "test.data/headers/hello.h", + "test-1.0.data/data/hello/world/file.txt", + "test.dist-info/METADATA", + "test-1.0.dist-info/WHEEL", + ): + assert not_expected not in entries + + +@pytest.mark.parametrize( + "reported,expected", + [("linux-x86_64", "linux_i686"), ("linux-aarch64", "linux_armv7l")], +) +@pytest.mark.skipif( + platform.system() != "Linux", reason="Only makes sense to test on Linux" +) +def test_platform_linux32(reported, expected, monkeypatch): + monkeypatch.setattr(struct, "calcsize", lambda x: 4) + dist = setuptools.Distribution() + cmd = bdist_wheel(dist) + cmd.plat_name = reported + cmd.root_is_pure = False + _, _, actual = cmd.get_tag() + assert actual == expected + + +def test_no_ctypes(monkeypatch) -> None: + def _fake_import(name: str, *args, **kwargs): + if name == "ctypes": + raise ModuleNotFoundError(f"No module named {name}") + + return importlib.__import__(name, *args, **kwargs) + + with suppress(KeyError): + monkeypatch.delitem(sys.modules, "setuptools.extern.wheel.macosx_libfile") + + # Install an importer shim that refuses to load ctypes + monkeypatch.setattr(builtins, "__import__", _fake_import) + with pytest.raises(ModuleNotFoundError, match="No module named ctypes"): + import setuptools.extern.wheel.macosx_libfile + + # Unload and reimport the bdist_wheel command module to make sure it won't try to + # import ctypes + monkeypatch.delitem(sys.modules, "setuptools.command.bdist_wheel") + + import setuptools.command.bdist_wheel # noqa: F401 diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index cc996b4255..ecb1dcfd87 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -232,7 +232,7 @@ def build_backend(self, tmpdir, request): def test_get_requires_for_build_wheel(self, build_backend): actual = build_backend.get_requires_for_build_wheel() - expected = ['six', 'wheel'] + expected = ['six'] assert sorted(actual) == sorted(expected) def test_get_requires_for_build_sdist(self, build_backend): @@ -783,14 +783,12 @@ def run(): build_backend = self.get_build_backend() if use_wheel: - base_requirements = ['wheel'] get_requires = build_backend.get_requires_for_build_wheel else: - base_requirements = [] get_requires = build_backend.get_requires_for_build_sdist # Ensure that the build requirements are properly parsed - expected = sorted(base_requirements + requirements) + expected = sorted(requirements) actual = get_requires() assert expected == sorted(actual) @@ -821,7 +819,7 @@ def test_setup_requires_with_auto_discovery(self, tmpdir_cwd): path.build(files) build_backend = self.get_build_backend() setup_requires = build_backend.get_requires_for_build_wheel() - assert setup_requires == ["wheel", "foo"] + assert setup_requires == ["foo"] def test_dont_install_setup_requires(self, tmpdir_cwd): files = { @@ -963,7 +961,7 @@ def test_sys_exit_0_in_setuppy(monkeypatch, tmp_path): """ (tmp_path / "setup.py").write_text(DALS(setuppy), encoding="utf-8") backend = BuildBackend(backend_name="setuptools.build_meta") - assert backend.get_requires_for_build_wheel() == ["wheel"] + assert backend.get_requires_for_build_wheel() == [] def test_system_exit_in_setuppy(monkeypatch, tmp_path): diff --git a/tools/vendored.py b/tools/vendored.py index 41079e1330..edc9195f3c 100644 --- a/tools/vendored.py +++ b/tools/vendored.py @@ -1,6 +1,8 @@ import re +import shutil import sys import subprocess +from textwrap import dedent from path import Path @@ -102,6 +104,60 @@ def rewrite_more_itertools(pkg_files: Path): more_file.write_text(text) +def rewrite_wheel(pkg_files: Path): + """ + Remove parts of wheel not needed by bdist_wheel, and rewrite imports to use + setuptools's own code or vendored dependencies. + """ + shutil.rmtree(pkg_files / 'cli') + shutil.rmtree(pkg_files / 'vendored') + pkg_files.joinpath('_setuptools_logging.py').unlink() + pkg_files.joinpath('__main__.py').unlink() + pkg_files.joinpath('bdist_wheel.py').unlink() + + # Rewrite vendored imports to use setuptools's own vendored libraries + for path in pkg_files.iterdir(): + if path.suffix == '.py': # type: ignore[attr-defined] + code = path.read_text() + if path.name == 'wheelfile.py': + code = re.sub( + r"^from wheel.util import ", + r"from .util import ", + code, + flags=re.MULTILINE, + ) + + # No need to keep the wheel.cli package just for this trivial exception + code = re.sub( + r"^from wheel.cli import WheelError\n", + r"", + code, + flags=re.MULTILINE, + ) + code += dedent( + """ + + class WheelError(Exception): + pass + """ + ) + else: + code = re.sub( + r"^from \.vendored\.([\w.]+) import ", + r"from ..\1 import ", + code, + flags=re.MULTILINE, + ) + code = re.sub( + r"^from \.util import log$", + r"from distutils import log$", + code, + flags=re.MULTILINE, + ) + + path.write_text(code) # type: ignore[attr-defined] + + def rewrite_platformdirs(pkg_files: Path): """ Replace some absolute imports with relative ones. @@ -163,6 +219,7 @@ def update_setuptools(): rewrite_importlib_resources(vendor / 'importlib_resources', 'setuptools.extern') rewrite_importlib_metadata(vendor / 'importlib_metadata', 'setuptools.extern') rewrite_more_itertools(vendor / "more_itertools") + rewrite_wheel(vendor / "wheel") def yield_top_level(name): diff --git a/tox.ini b/tox.ini index c6d7068907..ecfe01cd18 100644 --- a/tox.ini +++ b/tox.ini @@ -16,6 +16,7 @@ pass_env = PRE_BUILT_SETUPTOOLS_WHEEL PRE_BUILT_SETUPTOOLS_SDIST TIMEOUT_BACKEND_TEST # timeout (in seconds) for test_build_meta + SSH_AUTH_SOCK # for exercise.py if repo was checked out with ssh windir # required for test_pkg_resources # honor git config in pytest-perf HOME