Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
bdist_wheel.py604 linesDownload Raw Back to command
1"""2Create a wheel (.whl) distribution.3 4A wheel is a built archive format.5"""6 7from __future__ import annotations8 9import os10import re11import shutil12import struct13import sys14import sysconfig15import warnings16from collections.abc import Iterable, Sequence17from email.generator import BytesGenerator18from glob import iglob19from typing import Literal, cast20from zipfile import ZIP_DEFLATED, ZIP_STORED21 22from packaging import tags, version as _packaging_version23from wheel.wheelfile import WheelFile24 25from .. import Command, __version__, _shutil26from .._core_metadata import _safe_license_file27from .._normalization import safer_name28from ..warnings import SetuptoolsDeprecationWarning29from .egg_info import egg_info as egg_info_cls30 31from distutils import log32 33 34def safe_version(version: str) -> str:35    """36    Convert an arbitrary string to a standard version string37    """38    try:39        # normalize the version40        return str(_packaging_version.Version(version))41    except _packaging_version.InvalidVersion:42        version = version.replace(" ", ".")43        return re.sub("[^A-Za-z0-9.]+", "-", version)44 45 46setuptools_major_version = int(__version__.split(".")[0])47 48PY_LIMITED_API_PATTERN = r"cp3\d"49 50 51def _is_32bit_interpreter() -> bool:52    return struct.calcsize("P") == 453 54 55def python_tag() -> str:56    return f"py{sys.version_info.major}"57 58 59def get_platform(archive_root: str | None) -> str:60    """Return our platform name 'win32', 'linux_x86_64'"""61    result = sysconfig.get_platform()62    if result.startswith("macosx") and archive_root is not None:  # pragma: no cover63        from wheel.macosx_libfile import calculate_macosx_platform_tag64 65        result = calculate_macosx_platform_tag(archive_root, result)66    elif _is_32bit_interpreter():67        if result == "linux-x86_64":68            # pip pull request #349769            result = "linux-i686"70        elif result == "linux-aarch64":71            # packaging pull request #23472            # TODO armv8l, packaging pull request #690 => this did not land73            # in pip/packaging yet74            result = "linux-armv7l"75 76    return result.replace("-", "_")77 78 79def get_flag(80    var: str, fallback: bool, expected: bool = True, warn: bool = True81) -> bool:82    """Use a fallback value for determining SOABI flags if the needed config83    var is unset or unavailable."""84    val = sysconfig.get_config_var(var)85    if val is None:86        if warn:87            warnings.warn(88                f"Config variable '{var}' is unset, Python ABI tag may be incorrect",89                RuntimeWarning,90                stacklevel=2,91            )92        return fallback93    return val == expected94 95 96def get_abi_tag() -> str | None:97    """Return the ABI tag based on SOABI (if available) or emulate SOABI (PyPy2)."""98    soabi: str = sysconfig.get_config_var("SOABI")99    impl = tags.interpreter_name()100    if not soabi and impl in ("cp", "pp") and hasattr(sys, "maxunicode"):101        d = ""102        u = ""103        if get_flag("Py_DEBUG", hasattr(sys, "gettotalrefcount"), warn=(impl == "cp")):104            d = "d"105 106        abi = f"{impl}{tags.interpreter_version()}{d}{u}"107    elif soabi and impl == "cp" and soabi.startswith("cpython"):108        # non-Windows109        abi = "cp" + soabi.split("-")[1]110    elif soabi and impl == "cp" and soabi.startswith("cp"):111        # Windows112        abi = soabi.split("-")[0]113        if hasattr(sys, "gettotalrefcount"):114            # using debug build; append "d" flag115            abi += "d"116    elif soabi and impl == "pp":117        # we want something like pypy36-pp73118        abi = "-".join(soabi.split("-")[:2])119        abi = abi.replace(".", "_").replace("-", "_")120    elif soabi and impl == "graalpy":121        abi = "-".join(soabi.split("-")[:3])122        abi = abi.replace(".", "_").replace("-", "_")123    elif soabi:124        abi = soabi.replace(".", "_").replace("-", "_")125    else:126        abi = None127 128    return abi129 130 131def safer_version(version: str) -> str:132    return safe_version(version).replace("-", "_")133 134 135class bdist_wheel(Command):136    description = "create a wheel distribution"137 138    supported_compressions = {139        "stored": ZIP_STORED,140        "deflated": ZIP_DEFLATED,141    }142 143    user_options = [144        ("bdist-dir=", "b", "temporary directory for creating the distribution"),145        (146            "plat-name=",147            "p",148            "platform name to embed in generated filenames "149            f"[default: {get_platform(None)}]",150        ),151        (152            "keep-temp",153            "k",154            "keep the pseudo-installation tree around after "155            "creating the distribution archive",156        ),157        ("dist-dir=", "d", "directory to put final built distributions in"),158        ("skip-build", None, "skip rebuilding everything (for testing/debugging)"),159        (160            "relative",161            None,162            "build the archive using relative paths [default: false]",163        ),164        (165            "owner=",166            "u",167            "Owner name used when creating a tar file [default: current user]",168        ),169        (170            "group=",171            "g",172            "Group name used when creating a tar file [default: current group]",173        ),174        ("universal", None, "*DEPRECATED* make a universal wheel [default: false]"),175        (176            "compression=",177            None,178            f"zipfile compression (one of: {', '.join(supported_compressions)}) [default: 'deflated']",179        ),180        (181            "python-tag=",182            None,183            f"Python implementation compatibility tag [default: '{python_tag()}']",184        ),185        (186            "build-number=",187            None,188            "Build number for this particular version. "189            "As specified in PEP-0427, this must start with a digit. "190            "[default: None]",191        ),192        (193            "py-limited-api=",194            None,195            "Python tag (cp32|cp33|cpNN) for abi3 wheel tag [default: false]",196        ),197        (198            "dist-info-dir=",199            None,200            "directory where a pre-generated dist-info can be found (e.g. as a "201            "result of calling the PEP517 'prepare_metadata_for_build_wheel' "202            "method)",203        ),204    ]205 206    boolean_options = ["keep-temp", "skip-build", "relative", "universal"]207 208    def initialize_options(self) -> None:209        self.bdist_dir: str | None = None210        self.data_dir = ""211        self.plat_name: str | None = None212        self.plat_tag: str | None = None213        self.format = "zip"214        self.keep_temp = False215        self.dist_dir: str | None = None216        self.dist_info_dir = None217        self.egginfo_dir: str | None = None218        self.root_is_pure: bool | None = None219        self.skip_build = False220        self.relative = False221        self.owner = None222        self.group = None223        self.universal = False224        self.compression: str | int = "deflated"225        self.python_tag = python_tag()226        self.build_number: str | None = None227        self.py_limited_api: str | Literal[False] = False228        self.plat_name_supplied = False229 230    def finalize_options(self) -> None:231        if not self.bdist_dir:232            bdist_base = self.get_finalized_command("bdist").bdist_base233            self.bdist_dir = os.path.join(bdist_base, "wheel")234 235        if self.dist_info_dir is None:236            egg_info = cast(egg_info_cls, self.distribution.get_command_obj("egg_info"))237            egg_info.ensure_finalized()  # needed for correct `wheel_dist_name`238 239        self.data_dir = self.wheel_dist_name + ".data"240        self.plat_name_supplied = bool(self.plat_name)241 242        need_options = ("dist_dir", "plat_name", "skip_build")243 244        self.set_undefined_options("bdist", *zip(need_options, need_options))245 246        self.root_is_pure = not (247            self.distribution.has_ext_modules() or self.distribution.has_c_libraries()248        )249 250        self._validate_py_limited_api()251 252        # Support legacy [wheel] section for setting universal253        wheel = self.distribution.get_option_dict("wheel")254        if "universal" in wheel:  # pragma: no cover255            # please don't define this in your global configs256            log.warn("The [wheel] section is deprecated. Use [bdist_wheel] instead.")257            val = wheel["universal"][1].strip()258            if val.lower() in ("1", "true", "yes"):259                self.universal = True260 261        if self.universal:262            SetuptoolsDeprecationWarning.emit(263                "bdist_wheel.universal is deprecated",264                """265                With Python 2.7 end-of-life, support for building universal wheels266                (i.e., wheels that support both Python 2 and Python 3)267                is being obviated.268                Please discontinue using this option, or if you still need it,269                file an issue with pypa/setuptools describing your use case.270                """,271                due_date=(2025, 8, 30),  # Introduced in 2024-08-30272            )273 274        if self.build_number is not None and not self.build_number[:1].isdigit():275            raise ValueError("Build tag (build-number) must start with a digit.")276 277    def _validate_py_limited_api(self) -> None:278        if not self.py_limited_api:279            return280 281        if not re.match(PY_LIMITED_API_PATTERN, self.py_limited_api):282            raise ValueError(f"py-limited-api must match '{PY_LIMITED_API_PATTERN}'")283 284        if sysconfig.get_config_var("Py_GIL_DISABLED"):285            raise ValueError(286                f"`py_limited_api={self.py_limited_api!r}` not supported. "287                "`Py_LIMITED_API` is currently incompatible with "288                "`Py_GIL_DISABLED`. "289                "See https://github.com/python/cpython/issues/111506."290            )291 292    @property293    def wheel_dist_name(self) -> str:294        """Return distribution full name with - replaced with _"""295        components = [296            safer_name(self.distribution.get_name()),297            safer_version(self.distribution.get_version()),298        ]299        if self.build_number:300            components.append(self.build_number)301        return "-".join(components)302 303    def get_tag(self) -> tuple[str, str, str]:304        # bdist sets self.plat_name if unset, we should only use it for purepy305        # wheels if the user supplied it.306        if self.plat_name_supplied and self.plat_name:307            plat_name = self.plat_name308        elif self.root_is_pure:309            plat_name = "any"310        else:311            # macosx contains system version in platform name so need special handle312            if self.plat_name and not self.plat_name.startswith("macosx"):313                plat_name = self.plat_name314            else:315                # on macosx always limit the platform name to comply with any316                # c-extension modules in bdist_dir, since the user can specify317                # a higher MACOSX_DEPLOYMENT_TARGET via tools like CMake318 319                # on other platforms, and on macosx if there are no c-extension320                # modules, use the default platform name.321                plat_name = get_platform(self.bdist_dir)322 323            if _is_32bit_interpreter():324                if plat_name in ("linux-x86_64", "linux_x86_64"):325                    plat_name = "linux_i686"326                if plat_name in ("linux-aarch64", "linux_aarch64"):327                    # TODO armv8l, packaging pull request #690 => this did not land328                    # in pip/packaging yet329                    plat_name = "linux_armv7l"330 331        plat_name = (332            plat_name.lower().replace("-", "_").replace(".", "_").replace(" ", "_")333        )334 335        if self.root_is_pure:336            if self.universal:337                impl = "py2.py3"338            else:339                impl = self.python_tag340            tag = (impl, "none", plat_name)341        else:342            impl_name = tags.interpreter_name()343            impl_ver = tags.interpreter_version()344            impl = impl_name + impl_ver345            # We don't work on CPython 3.1, 3.0.346            if self.py_limited_api and (impl_name + impl_ver).startswith("cp3"):347                impl = self.py_limited_api348                abi_tag = "abi3"349            else:350                abi_tag = str(get_abi_tag()).lower()351            tag = (impl, abi_tag, plat_name)352            # issue gh-374: allow overriding plat_name353            supported_tags = [354                (t.interpreter, t.abi, plat_name) for t in tags.sys_tags()355            ]356            assert tag in supported_tags, (357                f"would build wheel with unsupported tag {tag}"358            )359        return tag360 361    def run(self):362        build_scripts = self.reinitialize_command("build_scripts")363        build_scripts.executable = "python"364        build_scripts.force = True365 366        build_ext = self.reinitialize_command("build_ext")367        build_ext.inplace = False368 369        if not self.skip_build:370            self.run_command("build")371 372        install = self.reinitialize_command("install", reinit_subcommands=True)373        install.root = self.bdist_dir374        install.compile = False375        install.skip_build = self.skip_build376        install.warn_dir = False377 378        # A wheel without setuptools scripts is more cross-platform.379        # Use the (undocumented) `no_ep` option to setuptools'380        # install_scripts command to avoid creating entry point scripts.381        install_scripts = self.reinitialize_command("install_scripts")382        install_scripts.no_ep = True383 384        # Use a custom scheme for the archive, because we have to decide385        # at installation time which scheme to use.386        for key in ("headers", "scripts", "data", "purelib", "platlib"):387            setattr(install, "install_" + key, os.path.join(self.data_dir, key))388 389        basedir_observed = ""390 391        if os.name == "nt":392            # win32 barfs if any of these are ''; could be '.'?393            # (distutils.command.install:change_roots bug)394            basedir_observed = os.path.normpath(os.path.join(self.data_dir, ".."))395            self.install_libbase = self.install_lib = basedir_observed396 397        setattr(398            install,399            "install_purelib" if self.root_is_pure else "install_platlib",400            basedir_observed,401        )402 403        log.info(f"installing to {self.bdist_dir}")404 405        self.run_command("install")406 407        impl_tag, abi_tag, plat_tag = self.get_tag()408        archive_basename = f"{self.wheel_dist_name}-{impl_tag}-{abi_tag}-{plat_tag}"409        if not self.relative:410            archive_root = self.bdist_dir411        else:412            archive_root = os.path.join(413                self.bdist_dir, self._ensure_relative(install.install_base)414            )415 416        self.set_undefined_options("install_egg_info", ("target", "egginfo_dir"))417        distinfo_dirname = (418            f"{safer_name(self.distribution.get_name())}-"419            f"{safer_version(self.distribution.get_version())}.dist-info"420        )421        distinfo_dir = os.path.join(self.bdist_dir, distinfo_dirname)422        if self.dist_info_dir:423            # Use the given dist-info directly.424            log.debug(f"reusing {self.dist_info_dir}")425            shutil.copytree(self.dist_info_dir, distinfo_dir)426            # Egg info is still generated, so remove it now to avoid it getting427            # copied into the wheel.428            _shutil.rmtree(self.egginfo_dir)429        else:430            # Convert the generated egg-info into dist-info.431            self.egg2dist(self.egginfo_dir, distinfo_dir)432 433        self.write_wheelfile(distinfo_dir)434 435        # Make the archive436        if not os.path.exists(self.dist_dir):437            os.makedirs(self.dist_dir)438 439        wheel_path = os.path.join(self.dist_dir, archive_basename + ".whl")440        with WheelFile(wheel_path, "w", self._zip_compression()) as wf:441            wf.write_files(archive_root)442 443        # Add to 'Distribution.dist_files' so that the "upload" command works444        getattr(self.distribution, "dist_files", []).append((445            "bdist_wheel",446            f"{sys.version_info.major}.{sys.version_info.minor}",447            wheel_path,448        ))449 450        if not self.keep_temp:451            log.info(f"removing {self.bdist_dir}")452            _shutil.rmtree(self.bdist_dir)453 454    def write_wheelfile(455        self, wheelfile_base: str, generator: str = f"setuptools ({__version__})"456    ) -> None:457        from email.message import Message458 459        msg = Message()460        msg["Wheel-Version"] = "1.0"  # of the spec461        msg["Generator"] = generator462        msg["Root-Is-Purelib"] = str(self.root_is_pure).lower()463        if self.build_number is not None:464            msg["Build"] = self.build_number465 466        # Doesn't work for bdist_wininst467        impl_tag, abi_tag, plat_tag = self.get_tag()468        for impl in impl_tag.split("."):469            for abi in abi_tag.split("."):470                for plat in plat_tag.split("."):471                    msg["Tag"] = "-".join((impl, abi, plat))472 473        wheelfile_path = os.path.join(wheelfile_base, "WHEEL")474        log.info(f"creating {wheelfile_path}")475        with open(wheelfile_path, "wb") as f:476            BytesGenerator(f, maxheaderlen=0).flatten(msg)477 478    def _ensure_relative(self, path: str) -> str:479        # copied from dir_util, deleted480        drive, path = os.path.splitdrive(path)481        if path[0:1] == os.sep:482            path = drive + path[1:]483        return path484 485    @property486    def license_paths(self) -> Iterable[str]:487        if setuptools_major_version >= 57:488            # Setuptools has resolved any patterns to actual file names489            return self.distribution.metadata.license_files or ()490 491        files = set[str]()492        metadata = self.distribution.get_option_dict("metadata")493        if setuptools_major_version >= 42:494            # Setuptools recognizes the license_files option but does not do globbing495            patterns = cast(Sequence[str], self.distribution.metadata.license_files)496        else:497            # Prior to those, wheel is entirely responsible for handling license files498            if "license_files" in metadata:499                patterns = metadata["license_files"][1].split()500            else:501                patterns = ()502 503        if "license_file" in metadata:504            warnings.warn(505                'The "license_file" option is deprecated. Use "license_files" instead.',506                DeprecationWarning,507                stacklevel=2,508            )509            files.add(metadata["license_file"][1])510 511        if not files and not patterns and not isinstance(patterns, list):512            patterns = ("LICEN[CS]E*", "COPYING*", "NOTICE*", "AUTHORS*")513 514        for pattern in patterns:515            for path in iglob(pattern):516                if path.endswith("~"):517                    log.debug(518                        f'ignoring license file "{path}" as it looks like a backup'519                    )520                    continue521 522                if path not in files and os.path.isfile(path):523                    log.info(524                        f'adding license file "{path}" (matched pattern "{pattern}")'525                    )526                    files.add(path)527 528        return files529 530    def egg2dist(self, egginfo_path: str, distinfo_path: str) -> None:531        """Convert an .egg-info directory into a .dist-info directory"""532 533        def adios(p: str) -> None:534            """Appropriately delete directory, file or link."""535            if os.path.exists(p) and not os.path.islink(p) and os.path.isdir(p):536                _shutil.rmtree(p)537            elif os.path.exists(p):538                os.unlink(p)539 540        adios(distinfo_path)541 542        if not os.path.exists(egginfo_path):543            # There is no egg-info. This is probably because the egg-info544            # file/directory is not named matching the distribution name used545            # to name the archive file. Check for this case and report546            # accordingly.547            import glob548 549            pat = os.path.join(os.path.dirname(egginfo_path), "*.egg-info")550            possible = glob.glob(pat)551            err = f"Egg metadata expected at {egginfo_path} but not found"552            if possible:553                alt = os.path.basename(possible[0])554                err += f" ({alt} found - possible misnamed archive file?)"555 556            raise ValueError(err)557 558        # .egg-info is a directory559        pkginfo_path = os.path.join(egginfo_path, "PKG-INFO")560 561        # ignore common egg metadata that is useless to wheel562        shutil.copytree(563            egginfo_path,564            distinfo_path,565            ignore=lambda x, y: {566                "PKG-INFO",567                "requires.txt",568                "SOURCES.txt",569                "not-zip-safe",570            },571        )572 573        # delete dependency_links if it is only whitespace574        dependency_links_path = os.path.join(distinfo_path, "dependency_links.txt")575        with open(dependency_links_path, encoding="utf-8") as dependency_links_file:576            dependency_links = dependency_links_file.read().strip()577        if not dependency_links:578            adios(dependency_links_path)579 580        metadata_path = os.path.join(distinfo_path, "METADATA")581        shutil.copy(pkginfo_path, metadata_path)582 583        licenses_folder_path = os.path.join(distinfo_path, "licenses")584        for license_path in self.license_paths:585            safe_path = _safe_license_file(license_path)586            dist_info_license_path = os.path.join(licenses_folder_path, safe_path)587            os.makedirs(os.path.dirname(dist_info_license_path), exist_ok=True)588            shutil.copy(license_path, dist_info_license_path)589 590        adios(egginfo_path)591 592    def _zip_compression(self) -> int:593        if (594            isinstance(self.compression, int)595            and self.compression in self.supported_compressions.values()596        ):597            return self.compression598 599        compression = self.supported_compressions.get(str(self.compression))600        if compression is not None:601            return compression602 603        raise ValueError(f"Unsupported compression: {self.compression!r}")604 
codekingpro/portable-devtools · Team Ai