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.

269 lines
9.3 KiB

4 years ago
  1. import os.path
  2. import re
  3. import shutil
  4. import sys
  5. import tempfile
  6. import zipfile
  7. from distutils import dist
  8. from glob import iglob
  9. from ..bdist_wheel import bdist_wheel
  10. from ..wheelfile import WheelFile
  11. from . import WheelError, require_pkgresources
  12. egg_info_re = re.compile(r'''
  13. (?P<name>.+?)-(?P<ver>.+?)
  14. (-(?P<pyver>py\d\.\d)
  15. (-(?P<arch>.+?))?
  16. )?.egg$''', re.VERBOSE)
  17. class _bdist_wheel_tag(bdist_wheel):
  18. # allow the client to override the default generated wheel tag
  19. # The default bdist_wheel implementation uses python and abi tags
  20. # of the running python process. This is not suitable for
  21. # generating/repackaging prebuild binaries.
  22. full_tag_supplied = False
  23. full_tag = None # None or a (pytag, soabitag, plattag) triple
  24. def get_tag(self):
  25. if self.full_tag_supplied and self.full_tag is not None:
  26. return self.full_tag
  27. else:
  28. return bdist_wheel.get_tag(self)
  29. def egg2wheel(egg_path, dest_dir):
  30. filename = os.path.basename(egg_path)
  31. match = egg_info_re.match(filename)
  32. if not match:
  33. raise WheelError('Invalid egg file name: {}'.format(filename))
  34. egg_info = match.groupdict()
  35. dir = tempfile.mkdtemp(suffix="_e2w")
  36. if os.path.isfile(egg_path):
  37. # assume we have a bdist_egg otherwise
  38. with zipfile.ZipFile(egg_path) as egg:
  39. egg.extractall(dir)
  40. else:
  41. # support buildout-style installed eggs directories
  42. for pth in os.listdir(egg_path):
  43. src = os.path.join(egg_path, pth)
  44. if os.path.isfile(src):
  45. shutil.copy2(src, dir)
  46. else:
  47. shutil.copytree(src, os.path.join(dir, pth))
  48. pyver = egg_info['pyver']
  49. if pyver:
  50. pyver = egg_info['pyver'] = pyver.replace('.', '')
  51. arch = (egg_info['arch'] or 'any').replace('.', '_').replace('-', '_')
  52. # assume all binary eggs are for CPython
  53. abi = 'cp' + pyver[2:] if arch != 'any' else 'none'
  54. root_is_purelib = egg_info['arch'] is None
  55. if root_is_purelib:
  56. bw = bdist_wheel(dist.Distribution())
  57. else:
  58. bw = _bdist_wheel_tag(dist.Distribution())
  59. bw.root_is_pure = root_is_purelib
  60. bw.python_tag = pyver
  61. bw.plat_name_supplied = True
  62. bw.plat_name = egg_info['arch'] or 'any'
  63. if not root_is_purelib:
  64. bw.full_tag_supplied = True
  65. bw.full_tag = (pyver, abi, arch)
  66. dist_info_dir = os.path.join(dir, '{name}-{ver}.dist-info'.format(**egg_info))
  67. bw.egg2dist(os.path.join(dir, 'EGG-INFO'), dist_info_dir)
  68. bw.write_wheelfile(dist_info_dir, generator='egg2wheel')
  69. wheel_name = '{name}-{ver}-{pyver}-{}-{}.whl'.format(abi, arch, **egg_info)
  70. with WheelFile(os.path.join(dest_dir, wheel_name), 'w') as wf:
  71. wf.write_files(dir)
  72. shutil.rmtree(dir)
  73. def parse_wininst_info(wininfo_name, egginfo_name):
  74. """Extract metadata from filenames.
  75. Extracts the 4 metadataitems needed (name, version, pyversion, arch) from
  76. the installer filename and the name of the egg-info directory embedded in
  77. the zipfile (if any).
  78. The egginfo filename has the format::
  79. name-ver(-pyver)(-arch).egg-info
  80. The installer filename has the format::
  81. name-ver.arch(-pyver).exe
  82. Some things to note:
  83. 1. The installer filename is not definitive. An installer can be renamed
  84. and work perfectly well as an installer. So more reliable data should
  85. be used whenever possible.
  86. 2. The egg-info data should be preferred for the name and version, because
  87. these come straight from the distutils metadata, and are mandatory.
  88. 3. The pyver from the egg-info data should be ignored, as it is
  89. constructed from the version of Python used to build the installer,
  90. which is irrelevant - the installer filename is correct here (even to
  91. the point that when it's not there, any version is implied).
  92. 4. The architecture must be taken from the installer filename, as it is
  93. not included in the egg-info data.
  94. 5. Architecture-neutral installers still have an architecture because the
  95. installer format itself (being executable) is architecture-specific. We
  96. should therefore ignore the architecture if the content is pure-python.
  97. """
  98. egginfo = None
  99. if egginfo_name:
  100. egginfo = egg_info_re.search(egginfo_name)
  101. if not egginfo:
  102. raise ValueError("Egg info filename %s is not valid" % (egginfo_name,))
  103. # Parse the wininst filename
  104. # 1. Distribution name (up to the first '-')
  105. w_name, sep, rest = wininfo_name.partition('-')
  106. if not sep:
  107. raise ValueError("Installer filename %s is not valid" % (wininfo_name,))
  108. # Strip '.exe'
  109. rest = rest[:-4]
  110. # 2. Python version (from the last '-', must start with 'py')
  111. rest2, sep, w_pyver = rest.rpartition('-')
  112. if sep and w_pyver.startswith('py'):
  113. rest = rest2
  114. w_pyver = w_pyver.replace('.', '')
  115. else:
  116. # Not version specific - use py2.py3. While it is possible that
  117. # pure-Python code is not compatible with both Python 2 and 3, there
  118. # is no way of knowing from the wininst format, so we assume the best
  119. # here (the user can always manually rename the wheel to be more
  120. # restrictive if needed).
  121. w_pyver = 'py2.py3'
  122. # 3. Version and architecture
  123. w_ver, sep, w_arch = rest.rpartition('.')
  124. if not sep:
  125. raise ValueError("Installer filename %s is not valid" % (wininfo_name,))
  126. if egginfo:
  127. w_name = egginfo.group('name')
  128. w_ver = egginfo.group('ver')
  129. return {'name': w_name, 'ver': w_ver, 'arch': w_arch, 'pyver': w_pyver}
  130. def wininst2wheel(path, dest_dir):
  131. with zipfile.ZipFile(path) as bdw:
  132. # Search for egg-info in the archive
  133. egginfo_name = None
  134. for filename in bdw.namelist():
  135. if '.egg-info' in filename:
  136. egginfo_name = filename
  137. break
  138. info = parse_wininst_info(os.path.basename(path), egginfo_name)
  139. root_is_purelib = True
  140. for zipinfo in bdw.infolist():
  141. if zipinfo.filename.startswith('PLATLIB'):
  142. root_is_purelib = False
  143. break
  144. if root_is_purelib:
  145. paths = {'purelib': ''}
  146. else:
  147. paths = {'platlib': ''}
  148. dist_info = "%(name)s-%(ver)s" % info
  149. datadir = "%s.data/" % dist_info
  150. # rewrite paths to trick ZipFile into extracting an egg
  151. # XXX grab wininst .ini - between .exe, padding, and first zip file.
  152. members = []
  153. egginfo_name = ''
  154. for zipinfo in bdw.infolist():
  155. key, basename = zipinfo.filename.split('/', 1)
  156. key = key.lower()
  157. basepath = paths.get(key, None)
  158. if basepath is None:
  159. basepath = datadir + key.lower() + '/'
  160. oldname = zipinfo.filename
  161. newname = basepath + basename
  162. zipinfo.filename = newname
  163. del bdw.NameToInfo[oldname]
  164. bdw.NameToInfo[newname] = zipinfo
  165. # Collect member names, but omit '' (from an entry like "PLATLIB/"
  166. if newname:
  167. members.append(newname)
  168. # Remember egg-info name for the egg2dist call below
  169. if not egginfo_name:
  170. if newname.endswith('.egg-info'):
  171. egginfo_name = newname
  172. elif '.egg-info/' in newname:
  173. egginfo_name, sep, _ = newname.rpartition('/')
  174. dir = tempfile.mkdtemp(suffix="_b2w")
  175. bdw.extractall(dir, members)
  176. # egg2wheel
  177. abi = 'none'
  178. pyver = info['pyver']
  179. arch = (info['arch'] or 'any').replace('.', '_').replace('-', '_')
  180. # Wininst installers always have arch even if they are not
  181. # architecture-specific (because the format itself is).
  182. # So, assume the content is architecture-neutral if root is purelib.
  183. if root_is_purelib:
  184. arch = 'any'
  185. # If the installer is architecture-specific, it's almost certainly also
  186. # CPython-specific.
  187. if arch != 'any':
  188. pyver = pyver.replace('py', 'cp')
  189. wheel_name = '-'.join((dist_info, pyver, abi, arch))
  190. if root_is_purelib:
  191. bw = bdist_wheel(dist.Distribution())
  192. else:
  193. bw = _bdist_wheel_tag(dist.Distribution())
  194. bw.root_is_pure = root_is_purelib
  195. bw.python_tag = pyver
  196. bw.plat_name_supplied = True
  197. bw.plat_name = info['arch'] or 'any'
  198. if not root_is_purelib:
  199. bw.full_tag_supplied = True
  200. bw.full_tag = (pyver, abi, arch)
  201. dist_info_dir = os.path.join(dir, '%s.dist-info' % dist_info)
  202. bw.egg2dist(os.path.join(dir, egginfo_name), dist_info_dir)
  203. bw.write_wheelfile(dist_info_dir, generator='wininst2wheel')
  204. wheel_path = os.path.join(dest_dir, wheel_name)
  205. with WheelFile(wheel_path, 'w') as wf:
  206. wf.write_files(dir)
  207. shutil.rmtree(dir)
  208. def convert(files, dest_dir, verbose):
  209. # Only support wheel convert if pkg_resources is present
  210. require_pkgresources('wheel convert')
  211. for pat in files:
  212. for installer in iglob(pat):
  213. if os.path.splitext(installer)[1] == '.egg':
  214. conv = egg2wheel
  215. else:
  216. conv = wininst2wheel
  217. if verbose:
  218. print("{}... ".format(installer))
  219. sys.stdout.flush()
  220. conv(installer, dest_dir)
  221. if verbose:
  222. print("OK")