|
| 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 | + |
| 10 | +from ..bdist_wheel import bdist_wheel |
| 11 | +from ..wheelfile import WheelFile |
| 12 | +from . import WheelError, require_pkgresources |
| 13 | + |
| 14 | +egg_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 | + |
| 21 | +class _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 | + |
| 37 | +def 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 | + |
| 91 | +def 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 | + |
| 162 | +def 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 | + |
| 252 | +def 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") |
0 commit comments