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.

217 lines
7.6 KiB

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

Powered by TurnKey Linux.