wheel.py 7.9 KB

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