wheel.py 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234
  1. """Wheels support."""
  2. import email
  3. import itertools
  4. import functools
  5. import os
  6. import posixpath
  7. import re
  8. import zipfile
  9. import contextlib
  10. from distutils.util import get_platform
  11. import setuptools
  12. from setuptools.extern.packaging.version import Version as parse_version
  13. from setuptools.extern.packaging.tags import sys_tags
  14. from setuptools.extern.packaging.utils import canonicalize_name
  15. from setuptools.command.egg_info import write_requirements, _egg_basename
  16. from setuptools.archive_util import _unpack_zipfile_obj
  17. WHEEL_NAME = re.compile(
  18. r"""^(?P<project_name>.+?)-(?P<version>\d.*?)
  19. ((-(?P<build>\d.*?))?-(?P<py_version>.+?)-(?P<abi>.+?)-(?P<platform>.+?)
  20. )\.whl$""",
  21. re.VERBOSE,
  22. ).match
  23. NAMESPACE_PACKAGE_INIT = "__import__('pkg_resources').declare_namespace(__name__)\n"
  24. @functools.lru_cache(maxsize=None)
  25. def _get_supported_tags():
  26. # We calculate the supported tags only once, otherwise calling
  27. # this method on thousands of wheels takes seconds instead of
  28. # milliseconds.
  29. return {(t.interpreter, t.abi, t.platform) for t in sys_tags()}
  30. def unpack(src_dir, dst_dir):
  31. '''Move everything under `src_dir` to `dst_dir`, and delete the former.'''
  32. for dirpath, dirnames, filenames in os.walk(src_dir):
  33. subdir = os.path.relpath(dirpath, src_dir)
  34. for f in filenames:
  35. src = os.path.join(dirpath, f)
  36. dst = os.path.join(dst_dir, subdir, f)
  37. os.renames(src, dst)
  38. for n, d in reversed(list(enumerate(dirnames))):
  39. src = os.path.join(dirpath, d)
  40. dst = os.path.join(dst_dir, subdir, d)
  41. if not os.path.exists(dst):
  42. # Directory does not exist in destination,
  43. # rename it and prune it from os.walk list.
  44. os.renames(src, dst)
  45. del dirnames[n]
  46. # Cleanup.
  47. for dirpath, dirnames, filenames in os.walk(src_dir, topdown=True):
  48. assert not filenames
  49. os.rmdir(dirpath)
  50. @contextlib.contextmanager
  51. def disable_info_traces():
  52. """
  53. Temporarily disable info traces.
  54. """
  55. from distutils import log
  56. saved = log.set_threshold(log.WARN)
  57. try:
  58. yield
  59. finally:
  60. log.set_threshold(saved)
  61. class Wheel:
  62. def __init__(self, filename):
  63. match = WHEEL_NAME(os.path.basename(filename))
  64. if match is None:
  65. raise ValueError('invalid wheel name: %r' % filename)
  66. self.filename = filename
  67. for k, v in match.groupdict().items():
  68. setattr(self, k, v)
  69. def tags(self):
  70. '''List tags (py_version, abi, platform) supported by this wheel.'''
  71. return itertools.product(
  72. self.py_version.split('.'),
  73. self.abi.split('.'),
  74. self.platform.split('.'),
  75. )
  76. def is_compatible(self):
  77. '''Is the wheel compatible with the current platform?'''
  78. return next((True for t in self.tags() if t in _get_supported_tags()), False)
  79. def egg_name(self):
  80. return (
  81. _egg_basename(
  82. self.project_name,
  83. self.version,
  84. platform=(None if self.platform == 'any' else get_platform()),
  85. )
  86. + ".egg"
  87. )
  88. def get_dist_info(self, zf):
  89. # find the correct name of the .dist-info dir in the wheel file
  90. for member in zf.namelist():
  91. dirname = posixpath.dirname(member)
  92. if dirname.endswith('.dist-info') and canonicalize_name(dirname).startswith(
  93. canonicalize_name(self.project_name)
  94. ):
  95. return dirname
  96. raise ValueError("unsupported wheel format. .dist-info not found")
  97. def install_as_egg(self, destination_eggdir):
  98. '''Install wheel as an egg directory.'''
  99. with zipfile.ZipFile(self.filename) as zf:
  100. self._install_as_egg(destination_eggdir, zf)
  101. def _install_as_egg(self, destination_eggdir, zf):
  102. dist_basename = '%s-%s' % (self.project_name, self.version)
  103. dist_info = self.get_dist_info(zf)
  104. dist_data = '%s.data' % dist_basename
  105. egg_info = os.path.join(destination_eggdir, 'EGG-INFO')
  106. self._convert_metadata(zf, destination_eggdir, dist_info, egg_info)
  107. self._move_data_entries(destination_eggdir, dist_data)
  108. self._fix_namespace_packages(egg_info, destination_eggdir)
  109. @staticmethod
  110. def _convert_metadata(zf, destination_eggdir, dist_info, egg_info):
  111. import pkg_resources
  112. def get_metadata(name):
  113. with zf.open(posixpath.join(dist_info, name)) as fp:
  114. value = fp.read().decode('utf-8')
  115. return email.parser.Parser().parsestr(value)
  116. wheel_metadata = get_metadata('WHEEL')
  117. # Check wheel format version is supported.
  118. wheel_version = parse_version(wheel_metadata.get('Wheel-Version'))
  119. wheel_v1 = parse_version('1.0') <= wheel_version < parse_version('2.0dev0')
  120. if not wheel_v1:
  121. raise ValueError('unsupported wheel format version: %s' % wheel_version)
  122. # Extract to target directory.
  123. _unpack_zipfile_obj(zf, destination_eggdir)
  124. # Convert metadata.
  125. dist_info = os.path.join(destination_eggdir, dist_info)
  126. dist = pkg_resources.Distribution.from_location(
  127. destination_eggdir,
  128. dist_info,
  129. metadata=pkg_resources.PathMetadata(destination_eggdir, dist_info),
  130. )
  131. # Note: Evaluate and strip markers now,
  132. # as it's difficult to convert back from the syntax:
  133. # foobar; "linux" in sys_platform and extra == 'test'
  134. def raw_req(req):
  135. req.marker = None
  136. return str(req)
  137. install_requires = list(map(raw_req, dist.requires()))
  138. extras_require = {
  139. extra: [
  140. req
  141. for req in map(raw_req, dist.requires((extra,)))
  142. if req not in install_requires
  143. ]
  144. for extra in dist.extras
  145. }
  146. os.rename(dist_info, egg_info)
  147. os.rename(
  148. os.path.join(egg_info, 'METADATA'),
  149. os.path.join(egg_info, 'PKG-INFO'),
  150. )
  151. setup_dist = setuptools.Distribution(
  152. attrs=dict(
  153. install_requires=install_requires,
  154. extras_require=extras_require,
  155. ),
  156. )
  157. with disable_info_traces():
  158. write_requirements(
  159. setup_dist.get_command_obj('egg_info'),
  160. None,
  161. os.path.join(egg_info, 'requires.txt'),
  162. )
  163. @staticmethod
  164. def _move_data_entries(destination_eggdir, dist_data):
  165. """Move data entries to their correct location."""
  166. dist_data = os.path.join(destination_eggdir, dist_data)
  167. dist_data_scripts = os.path.join(dist_data, 'scripts')
  168. if os.path.exists(dist_data_scripts):
  169. egg_info_scripts = os.path.join(destination_eggdir, 'EGG-INFO', 'scripts')
  170. os.mkdir(egg_info_scripts)
  171. for entry in os.listdir(dist_data_scripts):
  172. # Remove bytecode, as it's not properly handled
  173. # during easy_install scripts install phase.
  174. if entry.endswith('.pyc'):
  175. os.unlink(os.path.join(dist_data_scripts, entry))
  176. else:
  177. os.rename(
  178. os.path.join(dist_data_scripts, entry),
  179. os.path.join(egg_info_scripts, entry),
  180. )
  181. os.rmdir(dist_data_scripts)
  182. for subdir in filter(
  183. os.path.exists,
  184. (
  185. os.path.join(dist_data, d)
  186. for d in ('data', 'headers', 'purelib', 'platlib')
  187. ),
  188. ):
  189. unpack(subdir, destination_eggdir)
  190. if os.path.exists(dist_data):
  191. os.rmdir(dist_data)
  192. @staticmethod
  193. def _fix_namespace_packages(egg_info, destination_eggdir):
  194. namespace_packages = os.path.join(egg_info, 'namespace_packages.txt')
  195. if os.path.exists(namespace_packages):
  196. with open(namespace_packages) as fp:
  197. namespace_packages = fp.read().split()
  198. for mod in namespace_packages:
  199. mod_dir = os.path.join(destination_eggdir, *mod.split('.'))
  200. mod_init = os.path.join(mod_dir, '__init__.py')
  201. if not os.path.exists(mod_dir):
  202. os.mkdir(mod_dir)
  203. if not os.path.exists(mod_init):
  204. with open(mod_init, 'w') as fp:
  205. fp.write(NAMESPACE_PACKAGE_INIT)