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