Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
bdist_wheel.py596 linesDownload Raw Back to wheel
1"""2Create a wheel (.whl) distribution.3 4A wheel is a built archive format.5"""6 7from __future__ import annotations8 9import os10import re11import shutil12import stat13import struct14import sys15import sysconfig16import warnings17from email.generator import BytesGenerator, Generator18from email.policy import EmailPolicy19from glob import iglob20from shutil import rmtree21from zipfile import ZIP_DEFLATED, ZIP_STORED22 23import setuptools24from setuptools import Command25 26from . import __version__ as wheel_version27from .macosx_libfile import calculate_macosx_platform_tag28from .metadata import pkginfo_to_metadata29from .util import log30from .vendored.packaging import tags31from .vendored.packaging import version as _packaging_version32from .wheelfile import WheelFile33 34 35def safe_name(name):36    """Convert an arbitrary string to a standard distribution name37    Any runs of non-alphanumeric/. characters are replaced with a single '-'.38    """39    return re.sub("[^A-Za-z0-9.]+", "-", name)40 41 42def safe_version(version):43    """44    Convert an arbitrary string to a standard version string45    """46    try:47        # normalize the version48        return str(_packaging_version.Version(version))49    except _packaging_version.InvalidVersion:50        version = version.replace(" ", ".")51        return re.sub("[^A-Za-z0-9.]+", "-", version)52 53 54setuptools_major_version = int(setuptools.__version__.split(".")[0])55 56PY_LIMITED_API_PATTERN = r"cp3\d"57 58 59def _is_32bit_interpreter():60    return struct.calcsize("P") == 461 62 63def python_tag():64    return f"py{sys.version_info[0]}"65 66 67def get_platform(archive_root):68    """Return our platform name 'win32', 'linux_x86_64'"""69    result = sysconfig.get_platform()70    if result.startswith("macosx") and archive_root is not None:71        result = calculate_macosx_platform_tag(archive_root, result)72    elif _is_32bit_interpreter():73        if result == "linux-x86_64":74            # pip pull request #349775            result = "linux-i686"76        elif result == "linux-aarch64":77            # packaging pull request #23478            # TODO armv8l, packaging pull request #690 => this did not land79            # in pip/packaging yet80            result = "linux-armv7l"81 82    return result.replace("-", "_")83 84 85def get_flag(var, fallback, expected=True, warn=True):86    """Use a fallback value for determining SOABI flags if the needed config87    var is unset or unavailable."""88    val = sysconfig.get_config_var(var)89    if val is None:90        if warn:91            warnings.warn(92                f"Config variable '{var}' is unset, Python ABI tag may " "be incorrect",93                RuntimeWarning,94                stacklevel=2,95            )96        return fallback97    return val == expected98 99 100def get_abi_tag():101    """Return the ABI tag based on SOABI (if available) or emulate SOABI (PyPy2)."""102    soabi = sysconfig.get_config_var("SOABI")103    impl = tags.interpreter_name()104    if not soabi and impl in ("cp", "pp") and hasattr(sys, "maxunicode"):105        d = ""106        m = ""107        u = ""108        if get_flag("Py_DEBUG", hasattr(sys, "gettotalrefcount"), warn=(impl == "cp")):109            d = "d"110 111        if get_flag(112            "WITH_PYMALLOC",113            impl == "cp",114            warn=(impl == "cp" and sys.version_info < (3, 8)),115        ) and sys.version_info < (3, 8):116            m = "m"117 118        abi = f"{impl}{tags.interpreter_version()}{d}{m}{u}"119    elif soabi and impl == "cp" and soabi.startswith("cpython"):120        # non-Windows121        abi = "cp" + soabi.split("-")[1]122    elif soabi and impl == "cp" and soabi.startswith("cp"):123        # Windows124        abi = soabi.split("-")[0]125    elif soabi and impl == "pp":126        # we want something like pypy36-pp73127        abi = "-".join(soabi.split("-")[:2])128        abi = abi.replace(".", "_").replace("-", "_")129    elif soabi and impl == "graalpy":130        abi = "-".join(soabi.split("-")[:3])131        abi = abi.replace(".", "_").replace("-", "_")132    elif soabi:133        abi = soabi.replace(".", "_").replace("-", "_")134    else:135        abi = None136 137    return abi138 139 140def safer_name(name):141    return safe_name(name).replace("-", "_")142 143 144def safer_version(version):145    return safe_version(version).replace("-", "_")146 147 148def remove_readonly(func, path, excinfo):149    remove_readonly_exc(func, path, excinfo[1])150 151 152def remove_readonly_exc(func, path, exc):153    os.chmod(path, stat.S_IWRITE)154    func(path)155 156 157class bdist_wheel(Command):158    description = "create a wheel distribution"159 160    supported_compressions = {161        "stored": ZIP_STORED,162        "deflated": ZIP_DEFLATED,163    }164 165    user_options = [166        ("bdist-dir=", "b", "temporary directory for creating the distribution"),167        (168            "plat-name=",169            "p",170            "platform name to embed in generated filenames "171            "(default: %s)" % get_platform(None),172        ),173        (174            "keep-temp",175            "k",176            "keep the pseudo-installation tree around after "177            "creating the distribution archive",178        ),179        ("dist-dir=", "d", "directory to put final built distributions in"),180        ("skip-build", None, "skip rebuilding everything (for testing/debugging)"),181        (182            "relative",183            None,184            "build the archive using relative paths " "(default: false)",185        ),186        (187            "owner=",188            "u",189            "Owner name used when creating a tar file" " [default: current user]",190        ),191        (192            "group=",193            "g",194            "Group name used when creating a tar file" " [default: current group]",195        ),196        ("universal", None, "make a universal wheel" " (default: false)"),197        (198            "compression=",199            None,200            "zipfile compression (one of: {})" " (default: 'deflated')".format(201                ", ".join(supported_compressions)202            ),203        ),204        (205            "python-tag=",206            None,207            "Python implementation compatibility tag"208            " (default: '%s')" % (python_tag()),209        ),210        (211            "build-number=",212            None,213            "Build number for this particular version. "214            "As specified in PEP-0427, this must start with a digit. "215            "[default: None]",216        ),217        (218            "py-limited-api=",219            None,220            "Python tag (cp32|cp33|cpNN) for abi3 wheel tag" " (default: false)",221        ),222    ]223 224    boolean_options = ["keep-temp", "skip-build", "relative", "universal"]225 226    def initialize_options(self):227        self.bdist_dir = None228        self.data_dir = None229        self.plat_name = None230        self.plat_tag = None231        self.format = "zip"232        self.keep_temp = False233        self.dist_dir = None234        self.egginfo_dir = None235        self.root_is_pure = None236        self.skip_build = None237        self.relative = False238        self.owner = None239        self.group = None240        self.universal = False241        self.compression = "deflated"242        self.python_tag = python_tag()243        self.build_number = None244        self.py_limited_api = False245        self.plat_name_supplied = False246 247    def finalize_options(self):248        if self.bdist_dir is None:249            bdist_base = self.get_finalized_command("bdist").bdist_base250            self.bdist_dir = os.path.join(bdist_base, "wheel")251 252        egg_info = self.distribution.get_command_obj("egg_info")253        egg_info.ensure_finalized()  # needed for correct `wheel_dist_name`254 255        self.data_dir = self.wheel_dist_name + ".data"256        self.plat_name_supplied = self.plat_name is not None257 258        try:259            self.compression = self.supported_compressions[self.compression]260        except KeyError:261            raise ValueError(f"Unsupported compression: {self.compression}") from None262 263        need_options = ("dist_dir", "plat_name", "skip_build")264 265        self.set_undefined_options("bdist", *zip(need_options, need_options))266 267        self.root_is_pure = not (268            self.distribution.has_ext_modules() or self.distribution.has_c_libraries()269        )270 271        if self.py_limited_api and not re.match(272            PY_LIMITED_API_PATTERN, self.py_limited_api273        ):274            raise ValueError("py-limited-api must match '%s'" % PY_LIMITED_API_PATTERN)275 276        # Support legacy [wheel] section for setting universal277        wheel = self.distribution.get_option_dict("wheel")278        if "universal" in wheel:279            # please don't define this in your global configs280            log.warning(281                "The [wheel] section is deprecated. Use [bdist_wheel] instead.",282            )283            val = wheel["universal"][1].strip()284            if val.lower() in ("1", "true", "yes"):285                self.universal = True286 287        if self.build_number is not None and not self.build_number[:1].isdigit():288            raise ValueError("Build tag (build-number) must start with a digit.")289 290    @property291    def wheel_dist_name(self):292        """Return distribution full name with - replaced with _"""293        components = (294            safer_name(self.distribution.get_name()),295            safer_version(self.distribution.get_version()),296        )297        if self.build_number:298            components += (self.build_number,)299        return "-".join(components)300 301    def get_tag(self):302        # bdist sets self.plat_name if unset, we should only use it for purepy303        # wheels if the user supplied it.304        if self.plat_name_supplied:305            plat_name = self.plat_name306        elif self.root_is_pure:307            plat_name = "any"308        else:309            # macosx contains system version in platform name so need special handle310            if self.plat_name and not self.plat_name.startswith("macosx"):311                plat_name = self.plat_name312            else:313                # on macosx always limit the platform name to comply with any314                # c-extension modules in bdist_dir, since the user can specify315                # a higher MACOSX_DEPLOYMENT_TARGET via tools like CMake316 317                # on other platforms, and on macosx if there are no c-extension318                # modules, use the default platform name.319                plat_name = get_platform(self.bdist_dir)320 321            if _is_32bit_interpreter():322                if plat_name in ("linux-x86_64", "linux_x86_64"):323                    plat_name = "linux_i686"324                if plat_name in ("linux-aarch64", "linux_aarch64"):325                    # TODO armv8l, packaging pull request #690 => this did not land326                    # in pip/packaging yet327                    plat_name = "linux_armv7l"328 329        plat_name = (330            plat_name.lower().replace("-", "_").replace(".", "_").replace(" ", "_")331        )332 333        if self.root_is_pure:334            if self.universal:335                impl = "py2.py3"336            else:337                impl = self.python_tag338            tag = (impl, "none", plat_name)339        else:340            impl_name = tags.interpreter_name()341            impl_ver = tags.interpreter_version()342            impl = impl_name + impl_ver343            # We don't work on CPython 3.1, 3.0.344            if self.py_limited_api and (impl_name + impl_ver).startswith("cp3"):345                impl = self.py_limited_api346                abi_tag = "abi3"347            else:348                abi_tag = str(get_abi_tag()).lower()349            tag = (impl, abi_tag, plat_name)350            # issue gh-374: allow overriding plat_name351            supported_tags = [352                (t.interpreter, t.abi, plat_name) for t in tags.sys_tags()353            ]354            assert (355                tag in supported_tags356            ), f"would build wheel with unsupported tag {tag}"357        return tag358 359    def run(self):360        build_scripts = self.reinitialize_command("build_scripts")361        build_scripts.executable = "python"362        build_scripts.force = True363 364        build_ext = self.reinitialize_command("build_ext")365        build_ext.inplace = False366 367        if not self.skip_build:368            self.run_command("build")369 370        install = self.reinitialize_command("install", reinit_subcommands=True)371        install.root = self.bdist_dir372        install.compile = False373        install.skip_build = self.skip_build374        install.warn_dir = False375 376        # A wheel without setuptools scripts is more cross-platform.377        # Use the (undocumented) `no_ep` option to setuptools'378        # install_scripts command to avoid creating entry point scripts.379        install_scripts = self.reinitialize_command("install_scripts")380        install_scripts.no_ep = True381 382        # Use a custom scheme for the archive, because we have to decide383        # at installation time which scheme to use.384        for key in ("headers", "scripts", "data", "purelib", "platlib"):385            setattr(install, "install_" + key, os.path.join(self.data_dir, key))386 387        basedir_observed = ""388 389        if os.name == "nt":390            # win32 barfs if any of these are ''; could be '.'?391            # (distutils.command.install:change_roots bug)392            basedir_observed = os.path.normpath(os.path.join(self.data_dir, ".."))393            self.install_libbase = self.install_lib = basedir_observed394 395        setattr(396            install,397            "install_purelib" if self.root_is_pure else "install_platlib",398            basedir_observed,399        )400 401        log.info(f"installing to {self.bdist_dir}")402 403        self.run_command("install")404 405        impl_tag, abi_tag, plat_tag = self.get_tag()406        archive_basename = f"{self.wheel_dist_name}-{impl_tag}-{abi_tag}-{plat_tag}"407        if not self.relative:408            archive_root = self.bdist_dir409        else:410            archive_root = os.path.join(411                self.bdist_dir, self._ensure_relative(install.install_base)412            )413 414        self.set_undefined_options("install_egg_info", ("target", "egginfo_dir"))415        distinfo_dirname = (416            f"{safer_name(self.distribution.get_name())}-"417            f"{safer_version(self.distribution.get_version())}.dist-info"418        )419        distinfo_dir = os.path.join(self.bdist_dir, distinfo_dirname)420        self.egg2dist(self.egginfo_dir, distinfo_dir)421 422        self.write_wheelfile(distinfo_dir)423 424        # Make the archive425        if not os.path.exists(self.dist_dir):426            os.makedirs(self.dist_dir)427 428        wheel_path = os.path.join(self.dist_dir, archive_basename + ".whl")429        with WheelFile(wheel_path, "w", self.compression) as wf:430            wf.write_files(archive_root)431 432        # Add to 'Distribution.dist_files' so that the "upload" command works433        getattr(self.distribution, "dist_files", []).append(434            (435                "bdist_wheel",436                "{}.{}".format(*sys.version_info[:2]),  # like 3.7437                wheel_path,438            )439        )440 441        if not self.keep_temp:442            log.info(f"removing {self.bdist_dir}")443            if not self.dry_run:444                if sys.version_info < (3, 12):445                    rmtree(self.bdist_dir, onerror=remove_readonly)446                else:447                    rmtree(self.bdist_dir, onexc=remove_readonly_exc)448 449    def write_wheelfile(450        self, wheelfile_base, generator="bdist_wheel (" + wheel_version + ")"451    ):452        from email.message import Message453 454        msg = Message()455        msg["Wheel-Version"] = "1.0"  # of the spec456        msg["Generator"] = generator457        msg["Root-Is-Purelib"] = str(self.root_is_pure).lower()458        if self.build_number is not None:459            msg["Build"] = self.build_number460 461        # Doesn't work for bdist_wininst462        impl_tag, abi_tag, plat_tag = self.get_tag()463        for impl in impl_tag.split("."):464            for abi in abi_tag.split("."):465                for plat in plat_tag.split("."):466                    msg["Tag"] = "-".join((impl, abi, plat))467 468        wheelfile_path = os.path.join(wheelfile_base, "WHEEL")469        log.info(f"creating {wheelfile_path}")470        with open(wheelfile_path, "wb") as f:471            BytesGenerator(f, maxheaderlen=0).flatten(msg)472 473    def _ensure_relative(self, path):474        # copied from dir_util, deleted475        drive, path = os.path.splitdrive(path)476        if path[0:1] == os.sep:477            path = drive + path[1:]478        return path479 480    @property481    def license_paths(self):482        if setuptools_major_version >= 57:483            # Setuptools has resolved any patterns to actual file names484            return self.distribution.metadata.license_files or ()485 486        files = set()487        metadata = self.distribution.get_option_dict("metadata")488        if setuptools_major_version >= 42:489            # Setuptools recognizes the license_files option but does not do globbing490            patterns = self.distribution.metadata.license_files491        else:492            # Prior to those, wheel is entirely responsible for handling license files493            if "license_files" in metadata:494                patterns = metadata["license_files"][1].split()495            else:496                patterns = ()497 498        if "license_file" in metadata:499            warnings.warn(500                'The "license_file" option is deprecated. Use "license_files" instead.',501                DeprecationWarning,502                stacklevel=2,503            )504            files.add(metadata["license_file"][1])505 506        if not files and not patterns and not isinstance(patterns, list):507            patterns = ("LICEN[CS]E*", "COPYING*", "NOTICE*", "AUTHORS*")508 509        for pattern in patterns:510            for path in iglob(pattern):511                if path.endswith("~"):512                    log.debug(513                        f'ignoring license file "{path}" as it looks like a backup'514                    )515                    continue516 517                if path not in files and os.path.isfile(path):518                    log.info(519                        f'adding license file "{path}" (matched pattern "{pattern}")'520                    )521                    files.add(path)522 523        return files524 525    def egg2dist(self, egginfo_path, distinfo_path):526        """Convert an .egg-info directory into a .dist-info directory"""527 528        def adios(p):529            """Appropriately delete directory, file or link."""530            if os.path.exists(p) and not os.path.islink(p) and os.path.isdir(p):531                shutil.rmtree(p)532            elif os.path.exists(p):533                os.unlink(p)534 535        adios(distinfo_path)536 537        if not os.path.exists(egginfo_path):538            # There is no egg-info. This is probably because the egg-info539            # file/directory is not named matching the distribution name used540            # to name the archive file. Check for this case and report541            # accordingly.542            import glob543 544            pat = os.path.join(os.path.dirname(egginfo_path), "*.egg-info")545            possible = glob.glob(pat)546            err = f"Egg metadata expected at {egginfo_path} but not found"547            if possible:548                alt = os.path.basename(possible[0])549                err += f" ({alt} found - possible misnamed archive file?)"550 551            raise ValueError(err)552 553        if os.path.isfile(egginfo_path):554            # .egg-info is a single file555            pkginfo_path = egginfo_path556            pkg_info = pkginfo_to_metadata(egginfo_path, egginfo_path)557            os.mkdir(distinfo_path)558        else:559            # .egg-info is a directory560            pkginfo_path = os.path.join(egginfo_path, "PKG-INFO")561            pkg_info = pkginfo_to_metadata(egginfo_path, pkginfo_path)562 563            # ignore common egg metadata that is useless to wheel564            shutil.copytree(565                egginfo_path,566                distinfo_path,567                ignore=lambda x, y: {568                    "PKG-INFO",569                    "requires.txt",570                    "SOURCES.txt",571                    "not-zip-safe",572                },573            )574 575            # delete dependency_links if it is only whitespace576            dependency_links_path = os.path.join(distinfo_path, "dependency_links.txt")577            with open(dependency_links_path, encoding="utf-8") as dependency_links_file:578                dependency_links = dependency_links_file.read().strip()579            if not dependency_links:580                adios(dependency_links_path)581 582        pkg_info_path = os.path.join(distinfo_path, "METADATA")583        serialization_policy = EmailPolicy(584            utf8=True,585            mangle_from_=False,586            max_line_length=0,587        )588        with open(pkg_info_path, "w", encoding="utf-8") as out:589            Generator(out, policy=serialization_policy).flatten(pkg_info)590 591        for license_path in self.license_paths:592            filename = os.path.basename(license_path)593            shutil.copy(license_path, os.path.join(distinfo_path, filename))594 595        adios(egginfo_path)596 
codekingpro/portable-devtools · Team Ai