convert.py 9.2 KB

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