| | """ |
| | 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 glob import iglob |
| | from shutil import rmtree |
| | from zipfile import ZIP_DEFLATED, ZIP_STORED |
| |
|
| | import setuptools |
| | from setuptools import Command |
| |
|
| | from . import __version__ as wheel_version |
| | from .macosx_libfile import calculate_macosx_platform_tag |
| | from .metadata import pkginfo_to_metadata |
| | from .util import log |
| | from .vendored.packaging import tags |
| | from .vendored.packaging import version as _packaging_version |
| | from .wheelfile import WheelFile |
| |
|
| |
|
| | 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 safe_version(version): |
| | """ |
| | Convert an arbitrary string to a standard version string |
| | """ |
| | try: |
| | |
| | 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(setuptools.__version__.split(".")[0]) |
| |
|
| | PY_LIMITED_API_PATTERN = r"cp3\d" |
| |
|
| |
|
| | def _is_32bit_interpreter(): |
| | return struct.calcsize("P") == 4 |
| |
|
| |
|
| | def python_tag(): |
| | return f"py{sys.version_info[0]}" |
| |
|
| |
|
| | def get_platform(archive_root): |
| | """Return our platform name 'win32', 'linux_x86_64'""" |
| | result = sysconfig.get_platform() |
| | if result.startswith("macosx") and archive_root is not None: |
| | result = calculate_macosx_platform_tag(archive_root, result) |
| | elif _is_32bit_interpreter(): |
| | if result == "linux-x86_64": |
| | |
| | result = "linux-i686" |
| | elif result == "linux-aarch64": |
| | |
| | |
| | |
| | result = "linux-armv7l" |
| |
|
| | return result.replace("-", "_") |
| |
|
| |
|
| | def get_flag(var, fallback, expected=True, warn=True): |
| | """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(): |
| | """Return the ABI tag based on SOABI (if available) or emulate SOABI (PyPy2).""" |
| | soabi = 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"): |
| | |
| | abi = "cp" + soabi.split("-")[1] |
| | elif soabi and impl == "cp" and soabi.startswith("cp"): |
| | |
| | abi = soabi.split("-")[0] |
| | elif soabi and impl == "pp": |
| | |
| | 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): |
| | return safe_name(name).replace("-", "_") |
| |
|
| |
|
| | def safer_version(version): |
| | return safe_version(version).replace("-", "_") |
| |
|
| |
|
| | def remove_readonly(func, path, excinfo): |
| | remove_readonly_exc(func, path, excinfo[1]) |
| |
|
| |
|
| | def remove_readonly_exc(func, path, exc): |
| | 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 " |
| | "(default: %s)" % 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, |
| | "Python implementation compatibility tag" |
| | " (default: '%s')" % (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): |
| | self.bdist_dir = None |
| | self.data_dir = None |
| | self.plat_name = None |
| | self.plat_tag = None |
| | self.format = "zip" |
| | self.keep_temp = False |
| | self.dist_dir = None |
| | self.egginfo_dir = None |
| | self.root_is_pure = None |
| | self.skip_build = None |
| | self.relative = False |
| | self.owner = None |
| | self.group = None |
| | self.universal = False |
| | self.compression = "deflated" |
| | self.python_tag = python_tag() |
| | self.build_number = None |
| | self.py_limited_api = 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() |
| |
|
| | 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("py-limited-api must match '%s'" % PY_LIMITED_API_PATTERN) |
| |
|
| | |
| | wheel = self.distribution.get_option_dict("wheel") |
| | if "universal" in wheel: |
| | |
| | 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): |
| | |
| | |
| | if self.plat_name_supplied: |
| | plat_name = self.plat_name |
| | elif self.root_is_pure: |
| | plat_name = "any" |
| | else: |
| | |
| | if self.plat_name and not self.plat_name.startswith("macosx"): |
| | plat_name = self.plat_name |
| | else: |
| | |
| | |
| | |
| |
|
| | |
| | |
| | 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"): |
| | |
| | |
| | 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 |
| | |
| | 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) |
| | |
| | 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 |
| |
|
| | |
| | |
| | |
| | install_scripts = self.reinitialize_command("install_scripts") |
| | install_scripts.no_ep = True |
| |
|
| | |
| | |
| | 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": |
| | |
| | |
| | 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) |
| |
|
| | |
| | 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) |
| |
|
| | |
| | getattr(self.distribution, "dist_files", []).append( |
| | ( |
| | "bdist_wheel", |
| | "{}.{}".format(*sys.version_info[:2]), |
| | 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, generator="bdist_wheel (" + wheel_version + ")" |
| | ): |
| | from email.message import Message |
| |
|
| | msg = Message() |
| | msg["Wheel-Version"] = "1.0" |
| | 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 |
| |
|
| | |
| | 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): |
| | |
| | drive, path = os.path.splitdrive(path) |
| | if path[0:1] == os.sep: |
| | path = drive + path[1:] |
| | return path |
| |
|
| | @property |
| | def license_paths(self): |
| | if setuptools_major_version >= 57: |
| | |
| | return self.distribution.metadata.license_files or () |
| |
|
| | files = set() |
| | metadata = self.distribution.get_option_dict("metadata") |
| | if setuptools_major_version >= 42: |
| | |
| | patterns = self.distribution.metadata.license_files |
| | else: |
| | |
| | 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, distinfo_path): |
| | """Convert an .egg-info directory into a .dist-info directory""" |
| |
|
| | def adios(p): |
| | """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): |
| | |
| | |
| | |
| | |
| | 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): |
| | |
| | pkginfo_path = egginfo_path |
| | pkg_info = pkginfo_to_metadata(egginfo_path, egginfo_path) |
| | os.mkdir(distinfo_path) |
| | else: |
| | |
| | pkginfo_path = os.path.join(egginfo_path, "PKG-INFO") |
| | pkg_info = pkginfo_to_metadata(egginfo_path, pkginfo_path) |
| |
|
| | |
| | shutil.copytree( |
| | egginfo_path, |
| | distinfo_path, |
| | ignore=lambda x, y: { |
| | "PKG-INFO", |
| | "requires.txt", |
| | "SOURCES.txt", |
| | "not-zip-safe", |
| | }, |
| | ) |
| |
|
| | |
| | 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) |
| |
|