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