1"""
2Support for installing and building the "wheel" binary package format.
3"""
4from __future__ import absolute_import
5
6import compileall
7import csv
8import errno
9import functools
10import hashlib
11import logging
12import os
13import os.path
14import re
15import shutil
16import stat
17import sys
18import tempfile
19import warnings
20
21from base64 import urlsafe_b64encode
22from email.parser import Parser
23
24from pip9._vendor.six import StringIO
25
26import pip9
27from pip9.compat import expanduser
28from pip9.download import path_to_url, unpack_url
29from pip9.exceptions import (
30    InstallationError, InvalidWheelFilename, UnsupportedWheel)
31from pip9.locations import distutils_scheme, PIP_DELETE_MARKER_FILENAME
32from notpip import pep425tags
33from pip9.utils import (
34    call_subprocess, ensure_dir, captured_stdout, rmtree, read_chunks,
35)
36from pip9.utils.ui import open_spinner
37from pip9.utils.logging import indent_log
38from pip9.utils.setuptools_build import SETUPTOOLS_SHIM
39from pip9._vendor.distlib.scripts import ScriptMaker
40from pip9._vendor import pkg_resources
41from pip9._vendor.packaging.utils import canonicalize_name
42from pip9._vendor.six.moves import configparser
43
44
45wheel_ext = '.whl'
46
47VERSION_COMPATIBLE = (1, 0)
48
49
50logger = logging.getLogger(__name__)
51
52
53class WheelCache(object):
54    """A cache of wheels for future installs."""
55
56    def __init__(self, cache_dir, format_control):
57        """Create a wheel cache.
58
59        :param cache_dir: The root of the cache.
60        :param format_control: A pip9.index.FormatControl object to limit
61            binaries being read from the cache.
62        """
63        self._cache_dir = expanduser(cache_dir) if cache_dir else None
64        self._format_control = format_control
65
66    def cached_wheel(self, link, package_name):
67        return cached_wheel(
68            self._cache_dir, link, self._format_control, package_name)
69
70
71def _cache_for_link(cache_dir, link):
72    """
73    Return a directory to store cached wheels in for link.
74
75    Because there are M wheels for any one sdist, we provide a directory
76    to cache them in, and then consult that directory when looking up
77    cache hits.
78
79    We only insert things into the cache if they have plausible version
80    numbers, so that we don't contaminate the cache with things that were not
81    unique. E.g. ./package might have dozens of installs done for it and build
82    a version of 0.0...and if we built and cached a wheel, we'd end up using
83    the same wheel even if the source has been edited.
84
85    :param cache_dir: The cache_dir being used by pip9.
86    :param link: The link of the sdist for which this will cache wheels.
87    """
88
89    # We want to generate an url to use as our cache key, we don't want to just
90    # re-use the URL because it might have other items in the fragment and we
91    # don't care about those.
92    key_parts = [link.url_without_fragment]
93    if link.hash_name is not None and link.hash is not None:
94        key_parts.append("=".join([link.hash_name, link.hash]))
95    key_url = "#".join(key_parts)
96
97    # Encode our key url with sha224, we'll use this because it has similar
98    # security properties to sha256, but with a shorter total output (and thus
99    # less secure). However the differences don't make a lot of difference for
100    # our use case here.
101    hashed = hashlib.sha224(key_url.encode()).hexdigest()
102
103    # We want to nest the directories some to prevent having a ton of top level
104    # directories where we might run out of sub directories on some FS.
105    parts = [hashed[:2], hashed[2:4], hashed[4:6], hashed[6:]]
106
107    # Inside of the base location for cached wheels, expand our parts and join
108    # them all together.
109    return os.path.join(cache_dir, "wheels", *parts)
110
111
112def cached_wheel(cache_dir, link, format_control, package_name):
113    if not cache_dir:
114        return link
115    if not link:
116        return link
117    if link.is_wheel:
118        return link
119    if not link.is_artifact:
120        return link
121    if not package_name:
122        return link
123    canonical_name = canonicalize_name(package_name)
124    formats = pip9.index.fmt_ctl_formats(format_control, canonical_name)
125    if "binary" not in formats:
126        return link
127    root = _cache_for_link(cache_dir, link)
128    try:
129        wheel_names = os.listdir(root)
130    except OSError as e:
131        if e.errno in (errno.ENOENT, errno.ENOTDIR):
132            return link
133        raise
134    candidates = []
135    for wheel_name in wheel_names:
136        try:
137            wheel = Wheel(wheel_name)
138        except InvalidWheelFilename:
139            continue
140        if not wheel.supported():
141            # Built for a different python/arch/etc
142            continue
143        candidates.append((wheel.support_index_min(), wheel_name))
144    if not candidates:
145        return link
146    candidates.sort()
147    path = os.path.join(root, candidates[0][1])
148    return pip9.index.Link(path_to_url(path))
149
150
151def rehash(path, algo='sha256', blocksize=1 << 20):
152    """Return (hash, length) for path using hashlib.new(algo)"""
153    h = hashlib.new(algo)
154    length = 0
155    with open(path, 'rb') as f:
156        for block in read_chunks(f, size=blocksize):
157            length += len(block)
158            h.update(block)
159    digest = 'sha256=' + urlsafe_b64encode(
160        h.digest()
161    ).decode('latin1').rstrip('=')
162    return (digest, length)
163
164
165def open_for_csv(name, mode):
166    if sys.version_info[0] < 3:
167        nl = {}
168        bin = 'b'
169    else:
170        nl = {'newline': ''}
171        bin = ''
172    return open(name, mode + bin, **nl)
173
174
175def fix_script(path):
176    """Replace #!python with #!/path/to/python
177    Return True if file was changed."""
178    # XXX RECORD hashes will need to be updated
179    if os.path.isfile(path):
180        with open(path, 'rb') as script:
181            firstline = script.readline()
182            if not firstline.startswith(b'#!python'):
183                return False
184            exename = os.environ['PIP_PYTHON_PATH'].encode(sys.getfilesystemencoding())
185            firstline = b'#!' + exename + os.linesep.encode("ascii")
186            rest = script.read()
187        with open(path, 'wb') as script:
188            script.write(firstline)
189            script.write(rest)
190        return True
191
192dist_info_re = re.compile(r"""^(?P<namever>(?P<name>.+?)(-(?P<ver>\d.+?))?)
193                                \.dist-info$""", re.VERBOSE)
194
195
196def root_is_purelib(name, wheeldir):
197    """
198    Return True if the extracted wheel in wheeldir should go into purelib.
199    """
200    name_folded = name.replace("-", "_")
201    for item in os.listdir(wheeldir):
202        match = dist_info_re.match(item)
203        if match and match.group('name') == name_folded:
204            with open(os.path.join(wheeldir, item, 'WHEEL')) as wheel:
205                for line in wheel:
206                    line = line.lower().rstrip()
207                    if line == "root-is-purelib: true":
208                        return True
209    return False
210
211
212def get_entrypoints(filename):
213    if not os.path.exists(filename):
214        return {}, {}
215
216    # This is done because you can pass a string to entry_points wrappers which
217    # means that they may or may not be valid INI files. The attempt here is to
218    # strip leading and trailing whitespace in order to make them valid INI
219    # files.
220    with open(filename) as fp:
221        data = StringIO()
222        for line in fp:
223            data.write(line.strip())
224            data.write("\n")
225        data.seek(0)
226
227    cp = configparser.RawConfigParser()
228    cp.optionxform = lambda option: option
229    cp.readfp(data)
230
231    console = {}
232    gui = {}
233    if cp.has_section('console_scripts'):
234        console = dict(cp.items('console_scripts'))
235    if cp.has_section('gui_scripts'):
236        gui = dict(cp.items('gui_scripts'))
237    return console, gui
238
239
240def move_wheel_files(name, req, wheeldir, user=False, home=None, root=None,
241                     pycompile=True, scheme=None, isolated=False, prefix=None):
242    """Install a wheel"""
243
244    if not scheme:
245        scheme = distutils_scheme(
246            name, user=user, home=home, root=root, isolated=isolated,
247            prefix=prefix,
248        )
249
250    if root_is_purelib(name, wheeldir):
251        lib_dir = scheme['purelib']
252    else:
253        lib_dir = scheme['platlib']
254
255    info_dir = []
256    data_dirs = []
257    source = wheeldir.rstrip(os.path.sep) + os.path.sep
258
259    # Record details of the files moved
260    #   installed = files copied from the wheel to the destination
261    #   changed = files changed while installing (scripts #! line typically)
262    #   generated = files newly generated during the install (script wrappers)
263    installed = {}
264    changed = set()
265    generated = []
266
267    # Compile all of the pyc files that we're going to be installing
268    if pycompile:
269        with captured_stdout() as stdout:
270            with warnings.catch_warnings():
271                warnings.filterwarnings('ignore')
272                compileall.compile_dir(source, force=True, quiet=True)
273        logger.debug(stdout.getvalue())
274
275    def normpath(src, p):
276        return os.path.relpath(src, p).replace(os.path.sep, '/')
277
278    def record_installed(srcfile, destfile, modified=False):
279        """Map archive RECORD paths to installation RECORD paths."""
280        oldpath = normpath(srcfile, wheeldir)
281        newpath = normpath(destfile, lib_dir)
282        installed[oldpath] = newpath
283        if modified:
284            changed.add(destfile)
285
286    def clobber(source, dest, is_base, fixer=None, filter=None):
287        ensure_dir(dest)  # common for the 'include' path
288
289        for dir, subdirs, files in os.walk(source):
290            basedir = dir[len(source):].lstrip(os.path.sep)
291            destdir = os.path.join(dest, basedir)
292            if is_base and basedir.split(os.path.sep, 1)[0].endswith('.data'):
293                continue
294            for s in subdirs:
295                destsubdir = os.path.join(dest, basedir, s)
296                if is_base and basedir == '' and destsubdir.endswith('.data'):
297                    data_dirs.append(s)
298                    continue
299                elif (is_base and
300                        s.endswith('.dist-info') and
301                        canonicalize_name(s).startswith(
302                            canonicalize_name(req.name))):
303                    assert not info_dir, ('Multiple .dist-info directories: ' +
304                                          destsubdir + ', ' +
305                                          ', '.join(info_dir))
306                    info_dir.append(destsubdir)
307            for f in files:
308                # Skip unwanted files
309                if filter and filter(f):
310                    continue
311                srcfile = os.path.join(dir, f)
312                destfile = os.path.join(dest, basedir, f)
313                # directory creation is lazy and after the file filtering above
314                # to ensure we don't install empty dirs; empty dirs can't be
315                # uninstalled.
316                ensure_dir(destdir)
317
318                # We use copyfile (not move, copy, or copy2) to be extra sure
319                # that we are not moving directories over (copyfile fails for
320                # directories) as well as to ensure that we are not copying
321                # over any metadata because we want more control over what
322                # metadata we actually copy over.
323                shutil.copyfile(srcfile, destfile)
324
325                # Copy over the metadata for the file, currently this only
326                # includes the atime and mtime.
327                st = os.stat(srcfile)
328                if hasattr(os, "utime"):
329                    os.utime(destfile, (st.st_atime, st.st_mtime))
330
331                # If our file is executable, then make our destination file
332                # executable.
333                if os.access(srcfile, os.X_OK):
334                    st = os.stat(srcfile)
335                    permissions = (
336                        st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
337                    )
338                    os.chmod(destfile, permissions)
339
340                changed = False
341                if fixer:
342                    changed = fixer(destfile)
343                record_installed(srcfile, destfile, changed)
344
345    clobber(source, lib_dir, True)
346
347    assert info_dir, "%s .dist-info directory not found" % req
348
349    # Get the defined entry points
350    ep_file = os.path.join(info_dir[0], 'entry_points.txt')
351    console, gui = get_entrypoints(ep_file)
352
353    def is_entrypoint_wrapper(name):
354        # EP, EP.exe and EP-script.py are scripts generated for
355        # entry point EP by setuptools
356        if name.lower().endswith('.exe'):
357            matchname = name[:-4]
358        elif name.lower().endswith('-script.py'):
359            matchname = name[:-10]
360        elif name.lower().endswith(".pya"):
361            matchname = name[:-4]
362        else:
363            matchname = name
364        # Ignore setuptools-generated scripts
365        return (matchname in console or matchname in gui)
366
367    for datadir in data_dirs:
368        fixer = None
369        filter = None
370        for subdir in os.listdir(os.path.join(wheeldir, datadir)):
371            fixer = None
372            if subdir == 'scripts':
373                fixer = fix_script
374                filter = is_entrypoint_wrapper
375            source = os.path.join(wheeldir, datadir, subdir)
376            dest = scheme[subdir]
377            clobber(source, dest, False, fixer=fixer, filter=filter)
378
379    maker = ScriptMaker(None, scheme['scripts'])
380
381    # Ensure old scripts are overwritten.
382    # See https://github.com/pypa/pip/issues/1800
383    maker.clobber = True
384
385    # Ensure we don't generate any variants for scripts because this is almost
386    # never what somebody wants.
387    # See https://bitbucket.org/pypa/distlib/issue/35/
388    maker.variants = set(('', ))
389
390    # This is required because otherwise distlib creates scripts that are not
391    # executable.
392    # See https://bitbucket.org/pypa/distlib/issue/32/
393    maker.set_mode = True
394
395    # Simplify the script and fix the fact that the default script swallows
396    # every single stack trace.
397    # See https://bitbucket.org/pypa/distlib/issue/34/
398    # See https://bitbucket.org/pypa/distlib/issue/33/
399    def _get_script_text(entry):
400        if entry.suffix is None:
401            raise InstallationError(
402                "Invalid script entry point: %s for req: %s - A callable "
403                "suffix is required. Cf https://packaging.python.org/en/"
404                "latest/distributing.html#console-scripts for more "
405                "information." % (entry, req)
406            )
407        return maker.script_template % {
408            "module": entry.prefix,
409            "import_name": entry.suffix.split(".")[0],
410            "func": entry.suffix,
411        }
412
413    maker._get_script_text = _get_script_text
414    maker.script_template = """# -*- coding: utf-8 -*-
415import re
416import sys
417
418from %(module)s import %(import_name)s
419
420if __name__ == '__main__':
421    sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0])
422    sys.exit(%(func)s())
423"""
424
425    # Special case pip and setuptools to generate versioned wrappers
426    #
427    # The issue is that some projects (specifically, pip and setuptools) use
428    # code in setup.py to create "versioned" entry points - pip2.7 on Python
429    # 2.7, pip3.3 on Python 3.3, etc. But these entry points are baked into
430    # the wheel metadata at build time, and so if the wheel is installed with
431    # a *different* version of Python the entry points will be wrong. The
432    # correct fix for this is to enhance the metadata to be able to describe
433    # such versioned entry points, but that won't happen till Metadata 2.0 is
434    # available.
435    # In the meantime, projects using versioned entry points will either have
436    # incorrect versioned entry points, or they will not be able to distribute
437    # "universal" wheels (i.e., they will need a wheel per Python version).
438    #
439    # Because setuptools and pip are bundled with _ensurepip and virtualenv,
440    # we need to use universal wheels. So, as a stopgap until Metadata 2.0, we
441    # override the versioned entry points in the wheel and generate the
442    # correct ones. This code is purely a short-term measure until Metadata 2.0
443    # is available.
444    #
445    # To add the level of hack in this section of code, in order to support
446    # ensurepip this code will look for an ``ENSUREPIP_OPTIONS`` environment
447    # variable which will control which version scripts get installed.
448    #
449    # ENSUREPIP_OPTIONS=altinstall
450    #   - Only pipX.Y and easy_install-X.Y will be generated and installed
451    # ENSUREPIP_OPTIONS=install
452    #   - pipX.Y, pipX, easy_install-X.Y will be generated and installed. Note
453    #     that this option is technically if ENSUREPIP_OPTIONS is set and is
454    #     not altinstall
455    # DEFAULT
456    #   - The default behavior is to install pip, pipX, pipX.Y, easy_install
457    #     and easy_install-X.Y.
458    pip_script = console.pop('pip', None)
459    if pip_script:
460        if "ENSUREPIP_OPTIONS" not in os.environ:
461            spec = 'pip = ' + pip_script
462            generated.extend(maker.make(spec))
463
464        if os.environ.get("ENSUREPIP_OPTIONS", "") != "altinstall":
465            spec = 'pip%s = %s' % (sys.version[:1], pip_script)
466            generated.extend(maker.make(spec))
467
468        spec = 'pip%s = %s' % (sys.version[:3], pip_script)
469        generated.extend(maker.make(spec))
470        # Delete any other versioned pip entry points
471        pip_ep = [k for k in console if re.match(r'pip(\d(\.\d)?)?$', k)]
472        for k in pip_ep:
473            del console[k]
474    easy_install_script = console.pop('easy_install', None)
475    if easy_install_script:
476        if "ENSUREPIP_OPTIONS" not in os.environ:
477            spec = 'easy_install = ' + easy_install_script
478            generated.extend(maker.make(spec))
479
480        spec = 'easy_install-%s = %s' % (sys.version[:3], easy_install_script)
481        generated.extend(maker.make(spec))
482        # Delete any other versioned easy_install entry points
483        easy_install_ep = [
484            k for k in console if re.match(r'easy_install(-\d\.\d)?$', k)
485        ]
486        for k in easy_install_ep:
487            del console[k]
488
489    # Generate the console and GUI entry points specified in the wheel
490    if len(console) > 0:
491        generated.extend(
492            maker.make_multiple(['%s = %s' % kv for kv in console.items()])
493        )
494    if len(gui) > 0:
495        generated.extend(
496            maker.make_multiple(
497                ['%s = %s' % kv for kv in gui.items()],
498                {'gui': True}
499            )
500        )
501
502    # Record pip as the installer
503    installer = os.path.join(info_dir[0], 'INSTALLER')
504    temp_installer = os.path.join(info_dir[0], 'INSTALLER.pip')
505    with open(temp_installer, 'wb') as installer_file:
506        installer_file.write(b'pip\n')
507    shutil.move(temp_installer, installer)
508    generated.append(installer)
509
510    # Record details of all files installed
511    record = os.path.join(info_dir[0], 'RECORD')
512    temp_record = os.path.join(info_dir[0], 'RECORD.pip')
513    with open_for_csv(record, 'r') as record_in:
514        with open_for_csv(temp_record, 'w+') as record_out:
515            reader = csv.reader(record_in)
516            writer = csv.writer(record_out)
517            for row in reader:
518                row[0] = installed.pop(row[0], row[0])
519                if row[0] in changed:
520                    row[1], row[2] = rehash(row[0])
521                writer.writerow(row)
522            for f in generated:
523                h, l = rehash(f)
524                writer.writerow((normpath(f, lib_dir), h, l))
525            for f in installed:
526                writer.writerow((installed[f], '', ''))
527    shutil.move(temp_record, record)
528
529
530def _unique(fn):
531    @functools.wraps(fn)
532    def unique(*args, **kw):
533        seen = set()
534        for item in fn(*args, **kw):
535            if item not in seen:
536                seen.add(item)
537                yield item
538    return unique
539
540
541# TODO: this goes somewhere besides the wheel module
542@_unique
543def uninstallation_paths(dist):
544    """
545    Yield all the uninstallation paths for dist based on RECORD-without-.pyc
546
547    Yield paths to all the files in RECORD. For each .py file in RECORD, add
548    the .pyc in the same directory.
549
550    UninstallPathSet.add() takes care of the __pycache__ .pyc.
551    """
552    from pip9.utils import FakeFile  # circular import
553    r = csv.reader(FakeFile(dist.get_metadata_lines('RECORD')))
554    for row in r:
555        path = os.path.join(dist.location, row[0])
556        yield path
557        if path.endswith('.py'):
558            dn, fn = os.path.split(path)
559            base = fn[:-3]
560            path = os.path.join(dn, base + '.pyc')
561            yield path
562
563
564def wheel_version(source_dir):
565    """
566    Return the Wheel-Version of an extracted wheel, if possible.
567
568    Otherwise, return False if we couldn't parse / extract it.
569    """
570    try:
571        dist = [d for d in pkg_resources.find_on_path(None, source_dir)][0]
572
573        wheel_data = dist.get_metadata('WHEEL')
574        wheel_data = Parser().parsestr(wheel_data)
575
576        version = wheel_data['Wheel-Version'].strip()
577        version = tuple(map(int, version.split('.')))
578        return version
579    except:
580        return False
581
582
583def check_compatibility(version, name):
584    """
585    Raises errors or warns if called with an incompatible Wheel-Version.
586
587    Pip should refuse to install a Wheel-Version that's a major series
588    ahead of what it's compatible with (e.g 2.0 > 1.1); and warn when
589    installing a version only minor version ahead (e.g 1.2 > 1.1).
590
591    version: a 2-tuple representing a Wheel-Version (Major, Minor)
592    name: name of wheel or package to raise exception about
593
594    :raises UnsupportedWheel: when an incompatible Wheel-Version is given
595    """
596    if not version:
597        raise UnsupportedWheel(
598            "%s is in an unsupported or invalid wheel" % name
599        )
600    if version[0] > VERSION_COMPATIBLE[0]:
601        raise UnsupportedWheel(
602            "%s's Wheel-Version (%s) is not compatible with this version "
603            "of pip" % (name, '.'.join(map(str, version)))
604        )
605    elif version > VERSION_COMPATIBLE:
606        logger.warning(
607            'Installing from a newer Wheel-Version (%s)',
608            '.'.join(map(str, version)),
609        )
610
611
612class Wheel(object):
613    """A wheel file"""
614
615    # TODO: maybe move the install code into this class
616
617    wheel_file_re = re.compile(
618        r"""^(?P<namever>(?P<name>.+?)-(?P<ver>\d.*?))
619        ((-(?P<build>\d.*?))?-(?P<pyver>.+?)-(?P<abi>.+?)-(?P<plat>.+?)
620        \.whl|\.dist-info)$""",
621        re.VERBOSE
622    )
623
624    def __init__(self, filename):
625        """
626        :raises InvalidWheelFilename: when the filename is invalid for a wheel
627        """
628        wheel_info = self.wheel_file_re.match(filename)
629        if not wheel_info:
630            raise InvalidWheelFilename(
631                "%s is not a valid wheel filename." % filename
632            )
633        self.filename = filename
634        self.name = wheel_info.group('name').replace('_', '-')
635        # we'll assume "_" means "-" due to wheel naming scheme
636        # (https://github.com/pypa/pip/issues/1150)
637        self.version = wheel_info.group('ver').replace('_', '-')
638        self.pyversions = wheel_info.group('pyver').split('.')
639        self.abis = wheel_info.group('abi').split('.')
640        self.plats = wheel_info.group('plat').split('.')
641
642        # All the tag combinations from this file
643        self.file_tags = set(
644            (x, y, z) for x in self.pyversions
645            for y in self.abis for z in self.plats
646        )
647
648    def support_index_min(self, tags=None):
649        """
650        Return the lowest index that one of the wheel's file_tag combinations
651        achieves in the supported_tags list e.g. if there are 8 supported tags,
652        and one of the file tags is first in the list, then return 0.  Returns
653        None is the wheel is not supported.
654        """
655        if tags is None:  # for mock
656            tags = pep425tags.supported_tags
657        indexes = [tags.index(c) for c in self.file_tags if c in tags]
658        return min(indexes) if indexes else None
659
660    def supported(self, tags=None):
661        """Is this wheel supported on this system?"""
662        if tags is None:  # for mock
663            tags = pep425tags.supported_tags
664        return bool(set(tags).intersection(self.file_tags))
665
666
667class WheelBuilder(object):
668    """Build wheels from a RequirementSet."""
669
670    def __init__(self, requirement_set, finder, build_options=None,
671                 global_options=None):
672        self.requirement_set = requirement_set
673        self.finder = finder
674        self._cache_root = requirement_set._wheel_cache._cache_dir
675        self._wheel_dir = requirement_set.wheel_download_dir
676        self.build_options = build_options or []
677        self.global_options = global_options or []
678
679    def _build_one(self, req, output_dir, python_tag=None):
680        """Build one wheel.
681
682        :return: The filename of the built wheel, or None if the build failed.
683        """
684        tempd = tempfile.mkdtemp('pip-wheel-')
685        try:
686            if self.__build_one(req, tempd, python_tag=python_tag):
687                try:
688                    wheel_name = os.listdir(tempd)[0]
689                    wheel_path = os.path.join(output_dir, wheel_name)
690                    shutil.move(os.path.join(tempd, wheel_name), wheel_path)
691                    logger.info('Stored in directory: %s', output_dir)
692                    return wheel_path
693                except:
694                    pass
695            # Ignore return, we can't do anything else useful.
696            self._clean_one(req)
697            return None
698        finally:
699            rmtree(tempd)
700
701    def _base_setup_args(self, req):
702        return [
703            (PIP_PYTHON_PATH or sys.executable), "-u", '-c',
704            SETUPTOOLS_SHIM % req.setup_py
705        ] + list(self.global_options)
706
707    def __build_one(self, req, tempd, python_tag=None):
708        base_args = self._base_setup_args(req)
709
710        spin_message = 'Running setup.py bdist_wheel for %s' % (req.name,)
711        with open_spinner(spin_message) as spinner:
712            logger.debug('Destination directory: %s', tempd)
713            wheel_args = base_args + ['bdist_wheel', '-d', tempd] \
714                + self.build_options
715
716            if python_tag is not None:
717                wheel_args += ["--python-tag", python_tag]
718
719            try:
720                call_subprocess(wheel_args, cwd=req.setup_py_dir,
721                                show_stdout=False, spinner=spinner)
722                return True
723            except:
724                spinner.finish("error")
725                logger.error('Failed building wheel for %s', req.name)
726                return False
727
728    def _clean_one(self, req):
729        base_args = self._base_setup_args(req)
730
731        logger.info('Running setup.py clean for %s', req.name)
732        clean_args = base_args + ['clean', '--all']
733        try:
734            call_subprocess(clean_args, cwd=req.source_dir, show_stdout=False)
735            return True
736        except:
737            logger.error('Failed cleaning build dir for %s', req.name)
738            return False
739
740    def build(self, autobuilding=False):
741        """Build wheels.
742
743        :param unpack: If True, replace the sdist we built from with the
744            newly built wheel, in preparation for installation.
745        :return: True if all the wheels built correctly.
746        """
747        assert self._wheel_dir or (autobuilding and self._cache_root)
748        # unpack sdists and constructs req set
749        self.requirement_set.prepare_files(self.finder)
750
751        reqset = self.requirement_set.requirements.values()
752
753        buildset = []
754        for req in reqset:
755            if req.constraint:
756                continue
757            if req.is_wheel:
758                if not autobuilding:
759                    logger.info(
760                        'Skipping %s, due to already being wheel.', req.name)
761            elif autobuilding and req.editable:
762                pass
763            elif autobuilding and req.link and not req.link.is_artifact:
764                pass
765            elif autobuilding and not req.source_dir:
766                pass
767            else:
768                if autobuilding:
769                    link = req.link
770                    base, ext = link.splitext()
771                    if pip9.index.egg_info_matches(base, None, link) is None:
772                        # Doesn't look like a package - don't autobuild a wheel
773                        # because we'll have no way to lookup the result sanely
774                        continue
775                    if "binary" not in pip9.index.fmt_ctl_formats(
776                            self.finder.format_control,
777                            canonicalize_name(req.name)):
778                        logger.info(
779                            "Skipping bdist_wheel for %s, due to binaries "
780                            "being disabled for it.", req.name)
781                        continue
782                buildset.append(req)
783
784        if not buildset:
785            return True
786
787        # Build the wheels.
788        logger.info(
789            'Building wheels for collected packages: %s',
790            ', '.join([req.name for req in buildset]),
791        )
792        with indent_log():
793            build_success, build_failure = [], []
794            for req in buildset:
795                python_tag = None
796                if autobuilding:
797                    python_tag = pep425tags.implementation_tag
798                    output_dir = _cache_for_link(self._cache_root, req.link)
799                    try:
800                        ensure_dir(output_dir)
801                    except OSError as e:
802                        logger.warning("Building wheel for %s failed: %s",
803                                       req.name, e)
804                        build_failure.append(req)
805                        continue
806                else:
807                    output_dir = self._wheel_dir
808                wheel_file = self._build_one(
809                    req, output_dir,
810                    python_tag=python_tag,
811                )
812                if wheel_file:
813                    build_success.append(req)
814                    if autobuilding:
815                        # XXX: This is mildly duplicative with prepare_files,
816                        # but not close enough to pull out to a single common
817                        # method.
818                        # The code below assumes temporary source dirs -
819                        # prevent it doing bad things.
820                        if req.source_dir and not os.path.exists(os.path.join(
821                                req.source_dir, PIP_DELETE_MARKER_FILENAME)):
822                            raise AssertionError(
823                                "bad source dir - missing marker")
824                        # Delete the source we built the wheel from
825                        req.remove_temporary_source()
826                        # set the build directory again - name is known from
827                        # the work prepare_files did.
828                        req.source_dir = req.build_location(
829                            self.requirement_set.build_dir)
830                        # Update the link for this.
831                        req.link = pip9.index.Link(
832                            path_to_url(wheel_file))
833                        assert req.link.is_wheel
834                        # extract the wheel into the dir
835                        unpack_url(
836                            req.link, req.source_dir, None, False,
837                            session=self.requirement_set.session)
838                else:
839                    build_failure.append(req)
840
841        # notify success/failure
842        if build_success:
843            logger.info(
844                'Successfully built %s',
845                ' '.join([req.name for req in build_success]),
846            )
847        if build_failure:
848            logger.info(
849                'Failed to build %s',
850                ' '.join([req.name for req in build_failure]),
851            )
852        # Return True if all builds were successful
853        return len(build_failure) == 0
854