|
21 | 21 | from pip._vendor import pkg_resources
|
22 | 22 | from pip._vendor.distlib.scripts import ScriptMaker
|
23 | 23 | from pip._vendor.distlib.util import get_export_entry
|
| 24 | +from pip._vendor.packaging.utils import canonicalize_name |
24 | 25 | from pip._vendor.six import (
|
25 | 26 | PY2,
|
26 | 27 | StringIO,
|
@@ -326,6 +327,104 @@ def make(self, specification, options=None):
|
326 | 327 | return super(PipScriptMaker, self).make(specification, options)
|
327 | 328 |
|
328 | 329 |
|
| 330 | +def _get_file_owners( |
| 331 | + lib_dir, # type: str |
| 332 | + top_level, # type: str |
| 333 | + ignore_name, # type: str |
| 334 | +): |
| 335 | + # type: (...) -> Dict[str, List[str]] |
| 336 | + """Return dict mapping distributions to files under a top level directory |
| 337 | +
|
| 338 | + Look through lib_dir for distributions that own files under the |
| 339 | + top_level directory specified. Skip distributions that match the |
| 340 | + ignore_name. Return a dict mapping filenames to the distributions |
| 341 | + that own them. |
| 342 | +
|
| 343 | + """ |
| 344 | + file_owners = {} # type: Dict[str, List[str]] |
| 345 | + existing_env = pkg_resources.Environment([lib_dir]) |
| 346 | + canonical_name = canonicalize_name(ignore_name) |
| 347 | + for existing_dist_name in existing_env: |
| 348 | + for d in existing_env[existing_dist_name]: |
| 349 | + if canonicalize_name(d.project_name) == canonical_name: |
| 350 | + continue |
| 351 | + |
| 352 | + existing_info_dir = os.path.join( |
| 353 | + lib_dir, |
| 354 | + '{}-{}.dist-info'.format(d.project_name, d.version), |
| 355 | + ) |
| 356 | + |
| 357 | + top_level_path = os.path.join(existing_info_dir, 'top_level.txt') |
| 358 | + if not os.path.exists(top_level_path): |
| 359 | + continue |
| 360 | + with open(top_level_path, 'r') as f: |
| 361 | + existing_top_level = f.read().strip() |
| 362 | + if existing_top_level != top_level: |
| 363 | + continue |
| 364 | + |
| 365 | + record_path = os.path.join(existing_info_dir, 'RECORD') |
| 366 | + if not os.path.exists(record_path): |
| 367 | + continue |
| 368 | + with open(record_path, **csv_io_kwargs('r')) as record_file: |
| 369 | + for row in csv.reader(record_file): |
| 370 | + # Normalize the path before saving the owners, |
| 371 | + # since the record always contains forward |
| 372 | + # slashes, but when we look for a path later we |
| 373 | + # will be using a value with the native path |
| 374 | + # separator. |
| 375 | + o = file_owners.setdefault(os.path.normpath(row[0]), []) |
| 376 | + o.append(str(d)) |
| 377 | + |
| 378 | + return file_owners |
| 379 | + |
| 380 | + |
| 381 | +def _report_file_owner_conflicts( |
| 382 | + lib_dir, # type: str |
| 383 | + name, # type: str |
| 384 | + source_dir, # type: str |
| 385 | + info_dir, # type: str |
| 386 | +): |
| 387 | + # type: (...) -> None |
| 388 | + """Report files owned by other distributions that are being overwritten. |
| 389 | +
|
| 390 | + Scan the lib_dir for distributions that own files under the same |
| 391 | + top level directory as the wheel being installed and report any |
| 392 | + files owned by those other distributions that are going to be |
| 393 | + overwritten. |
| 394 | +
|
| 395 | + """ |
| 396 | + installing_top_level_path = os.path.join( |
| 397 | + source_dir, info_dir, 'top_level.txt') |
| 398 | + if not os.path.exists(installing_top_level_path): |
| 399 | + # We cannot determine the top level directory, so there is no |
| 400 | + # point in continuing. |
| 401 | + return |
| 402 | + |
| 403 | + with open(installing_top_level_path, 'r') as fd: |
| 404 | + installing_top_level = fd.read().strip() |
| 405 | + files_from_other_owners = _get_file_owners( |
| 406 | + lib_dir, installing_top_level, name) |
| 407 | + if not files_from_other_owners: |
| 408 | + # Nothing else owns files under this top level directory, so |
| 409 | + # we don't need to scan the source. |
| 410 | + return |
| 411 | + |
| 412 | + for dir, subdirs, files in os.walk(source_dir): |
| 413 | + basedir = dir[len(source_dir):].lstrip(os.path.sep) |
| 414 | + for f in files: |
| 415 | + partial_src = os.path.join(basedir, f) |
| 416 | + if partial_src not in files_from_other_owners: |
| 417 | + # There are no other owners for this file. |
| 418 | + continue |
| 419 | + destfile = os.path.join(lib_dir, basedir, f) |
| 420 | + for owner in files_from_other_owners[partial_src]: |
| 421 | + warnings.warn( |
| 422 | + 'Overwriting or removing {} for {} ' |
| 423 | + 'which is also owned by {}'.format( |
| 424 | + destfile, name, owner), |
| 425 | + FutureWarning) |
| 426 | + |
| 427 | + |
329 | 428 | def install_unpacked_wheel(
|
330 | 429 | name, # type: str
|
331 | 430 | wheeldir, # type: str
|
@@ -458,6 +557,12 @@ def clobber(
|
458 | 557 | changed = fixer(destfile)
|
459 | 558 | record_installed(srcfile, destfile, changed)
|
460 | 559 |
|
| 560 | + # Look for packages containing files that are already using the |
| 561 | + # same toplevel directory as the wheel we are installing but that |
| 562 | + # have a different dist name. Do this before calling clobber(), so |
| 563 | + # that when the warning is eventually changed to a hard error no |
| 564 | + # partial installation occurs. |
| 565 | + _report_file_owner_conflicts(lib_dir, name, source, info_dir) |
461 | 566 | clobber(
|
462 | 567 | ensure_text(source, encoding=sys.getfilesystemencoding()),
|
463 | 568 | ensure_text(lib_dir, encoding=sys.getfilesystemencoding()),
|
|
0 commit comments