You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

210 lines
7.9 KiB

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