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