Add File class to represent a file to install

Hiding the file-specific implementation we currently use will let us
trade out the implementation for a zip-backed one later. We can also use
this interface to represent the other kinds of files that we have to
generate as part of wheel installation.

We use a Protocol instead of a base class because there's no need for
shared behavior right now, and using Protocol is less verbose.
This commit is contained in:
Chris Hunt 2020-07-05 12:09:25 -04:00
parent 6b26ac911a
commit aa8dd9cecc
1 changed files with 61 additions and 40 deletions

View File

@ -54,6 +54,7 @@ else:
List,
NewType,
Optional,
Protocol,
Sequence,
Set,
Tuple,
@ -69,6 +70,14 @@ else:
RecordPath = NewType('RecordPath', text_type)
InstalledCSVRow = Tuple[RecordPath, str, Union[int, str]]
class File(Protocol):
src_path = None # type: text_type
dest_path = None # type: text_type
def save(self):
# type: () -> None
pass
logger = logging.getLogger(__name__)
@ -388,6 +397,54 @@ def get_console_script_specs(console):
return scripts_to_generate
class DiskFile(object):
def __init__(self, src_path, dest_path):
# type: (text_type, text_type) -> None
self.src_path = src_path
self.dest_path = dest_path
def save(self):
# type: () -> None
# directory creation is lazy and after file filtering
# to ensure we don't install empty dirs; empty dirs can't be
# uninstalled.
parent_dir = os.path.dirname(self.dest_path)
ensure_dir(parent_dir)
# copyfile (called below) truncates the destination if it
# exists and then writes the new contents. This is fine in most
# cases, but can cause a segfault if pip has loaded a shared
# object (e.g. from pyopenssl through its vendored urllib3)
# Since the shared object is mmap'd an attempt to call a
# symbol in it will then cause a segfault. Unlinking the file
# allows writing of new contents while allowing the process to
# continue to use the old copy.
if os.path.exists(self.dest_path):
os.unlink(self.dest_path)
# We use copyfile (not move, copy, or copy2) to be extra sure
# that we are not moving directories over (copyfile fails for
# directories) as well as to ensure that we are not copying
# over any metadata because we want more control over what
# metadata we actually copy over.
shutil.copyfile(self.src_path, self.dest_path)
# Copy over the metadata for the file, currently this only
# includes the atime and mtime.
st = os.stat(self.src_path)
if hasattr(os, "utime"):
os.utime(self.dest_path, (st.st_atime, st.st_mtime))
# If our file is executable, then make our destination file
# executable.
if os.access(self.src_path, os.X_OK):
st = os.stat(self.src_path)
permissions = (
st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
)
os.chmod(self.dest_path, permissions)
class MissingCallableSuffix(Exception):
pass
@ -479,49 +536,13 @@ def install_unpacked_wheel(
continue
srcfile = os.path.join(dir, f)
destfile = os.path.join(dest, basedir, f)
# directory creation is lazy and after the file filtering above
# to ensure we don't install empty dirs; empty dirs can't be
# uninstalled.
parent_dir = os.path.dirname(destfile)
ensure_dir(parent_dir)
# copyfile (called below) truncates the destination if it
# exists and then writes the new contents. This is fine in most
# cases, but can cause a segfault if pip has loaded a shared
# object (e.g. from pyopenssl through its vendored urllib3)
# Since the shared object is mmap'd an attempt to call a
# symbol in it will then cause a segfault. Unlinking the file
# allows writing of new contents while allowing the process to
# continue to use the old copy.
if os.path.exists(destfile):
os.unlink(destfile)
# We use copyfile (not move, copy, or copy2) to be extra sure
# that we are not moving directories over (copyfile fails for
# directories) as well as to ensure that we are not copying
# over any metadata because we want more control over what
# metadata we actually copy over.
shutil.copyfile(srcfile, destfile)
# Copy over the metadata for the file, currently this only
# includes the atime and mtime.
st = os.stat(srcfile)
if hasattr(os, "utime"):
os.utime(destfile, (st.st_atime, st.st_mtime))
# If our file is executable, then make our destination file
# executable.
if os.access(srcfile, os.X_OK):
st = os.stat(srcfile)
permissions = (
st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
)
os.chmod(destfile, permissions)
file = DiskFile(srcfile, destfile)
file.save()
changed = False
if fixer:
changed = fixer(destfile)
record_installed(srcfile, destfile, changed)
changed = fixer(file.dest_path)
record_installed(file.src_path, file.dest_path, changed)
clobber(
ensure_text(source, encoding=sys.getfilesystemencoding()),