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.

221 lines
8.3 KiB

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